diff --git a/api/config/custom-environment-variables.cjs b/api/config/custom-environment-variables.cjs index 980362d9..4d99ef9f 100644 --- a/api/config/custom-environment-variables.cjs +++ b/api/config/custom-environment-variables.cjs @@ -86,8 +86,9 @@ module.exports = { theme: { logo: 'THEME_LOGO', favicon: 'THEME_FAVICON', - dark: 'THEME_DARK', colors: { + background: 'THEME_BACKGROUND', + surface: 'THEME_SURFACE', primary: 'THEME_PRIMARY', secondary: 'THEME_SECONDARY', accent: 'THEME_ACCENT', @@ -95,18 +96,7 @@ module.exports = { info: 'THEME_INFO', success: 'THEME_SUCCESS', warning: 'THEME_WARNING' - }, - darkColors: { - primary: 'THEME_DARK_PRIMARY', - secondary: 'THEME_DARK_SECONDARY', - accent: 'THEME_DARK_ACCENT', - error: 'THEME_DARK_ERROR', - info: 'THEME_DARK_INFO', - success: 'THEME_DARK_SUCCESS', - warning: 'THEME_DARK_WARNING' - }, - cssUrl: 'THEME_CSS_URL', - cssText: 'THEME_CSS_TEXT' + } }, i18n: { defaultLocale: 'I18N_DEFAULT_LOCALE', diff --git a/api/config/default.cjs b/api/config/default.cjs index 6c73e243..e1f15b04 100644 --- a/api/config/default.cjs +++ b/api/config/default.cjs @@ -122,10 +122,10 @@ module.exports = { analytics: {}, // a "modules" definition for @koumoul/vue-multianalytics theme: { logo: undefined, - favicon: undefined, - dark: false, colors: { // standard vuetify colors + background: '#FFFFFF', + surface: '#FFFFFF', primary: '#1E88E5', // blue.darken1 secondary: '#42A5F5', // blue.lighten1, accent: '#FF9800', // orange.base @@ -134,13 +134,7 @@ module.exports = { success: '#4CAF50', // green.base warning: '#E91E63', // pink.base admin: '#E53935' // red.darken1 - }, - darkColors: { - primary: '#2196F3', // blue.base - success: '#00E676' // green.accent3 - }, - cssUrl: undefined, - cssText: '' + } }, i18n: { defaultLocale: 'fr', diff --git a/api/config/type/schema.json b/api/config/type/schema.json index 3f571ca4..2e0edfe5 100644 --- a/api/config/type/schema.json +++ b/api/config/type/schema.json @@ -122,16 +122,14 @@ }, "theme": { "type": "object", - "required": [], + "required": ["colors"], "properties": { "logo": { "type": "string" }, - "notificationIcon": { - "type": "string" - }, - "notificationBadge": { - "type": "string" + "colors": { + "type": "object", + "$ref": "#/$defs/colors" } } }, @@ -673,6 +671,42 @@ "type": "string" } } + }, + "colors": { + "type": "object", + "required": ["background", "surface", "primary", "secondary", "accent", "error", "info", "success", "warning", "admin"], + "properties": { + "background": { + "type": "string" + }, + "surface": { + "type": "string" + }, + "primary": { + "type": "string" + }, + "secondary": { + "type": "string" + }, + "accent": { + "type": "string" + }, + "error": { + "type": "string" + }, + "info": { + "type": "string" + }, + "success": { + "type": "string" + }, + "warning": { + "type": "string" + }, + "admin": { + "type": "string" + } + } } } } \ No newline at end of file diff --git a/api/src/invitations/router.ts b/api/src/invitations/router.ts index df9c91bf..be63df5b 100644 --- a/api/src/invitations/router.ts +++ b/api/src/invitations/router.ts @@ -1,4 +1,4 @@ -import { type UserWritable, type Invitation } from '#types' +import { type UserWritable, type Invitation, type ActionPayload, type ShortenedInvitation } from '#types' import { Router } from 'express' import config from '#config' import { assertAccountRole, reqUser, reqSession, reqSiteUrl, session, httpError, reqUserAuthenticated } from '@data-fair/lib-express' @@ -173,7 +173,7 @@ router.get('/_accept', async (req, res, next) => { // if the token was once valid, but deprecated we accept it partially // meaning that we will not perform writes based on it // but we accept to check the user's existence and create the best redirect for him - invit = unshortenInvit(decodeToken(req.query.invit_token)) + invit = unshortenInvit(decodeToken(req.query.invit_token) as ShortenedInvitation) verified = false } debug('accept invitation', invit, verified) @@ -206,7 +206,7 @@ router.get('/_accept', async (req, res, next) => { throw new Error('missing password verification implementation') } else { if (!await storage.getPassword(invit.email) && !config.passwordless) { - const payload = { id: existingUser.id, email: existingUser.email, action: 'changePassword' } + const payload: ActionPayload = { id: existingUser.id, email: existingUser.email, action: 'changePassword' } const token = await signToken(payload, config.jwtDurations.initialToken) const reboundRedirect = redirectUrl.href redirectUrl = new URL(`${reqSiteUrl(req) + '/simple-directory'}/login`) diff --git a/api/src/sites/router.ts b/api/src/sites/router.ts index 03bccd7c..1128bdc7 100644 --- a/api/src/sites/router.ts +++ b/api/src/sites/router.ts @@ -1,7 +1,7 @@ import { type SitePublic } from '#types' import { Router, type Request } from 'express' import config from '#config' -import { reqUser, reqUserAuthenticated, reqSiteUrl, httpError, reqSessionAuthenticated } from '@data-fair/lib-express' +import { reqUser, reqUserAuthenticated, reqSiteUrl, httpError, reqSessionAuthenticated, reqHost } from '@data-fair/lib-express' import { nanoid } from 'nanoid' import { findAllSites, findOwnerSites, patchSite, deleteSite } from './service.ts' import { reqSite } from '#services' @@ -63,17 +63,22 @@ router.delete('/:id', async (req, res, next) => { router.get('/_public', async (req, res, next) => { const site = await reqSite(req) if (!site) { - // TODO: return information for main site too ? - return res.status(204).send() - } - const sitePublic: SitePublic = { - host: site.host, - theme: site.theme, - logo: site.logo || `${reqSiteUrl(req) + '/simple-directory'}/api/avatars/${site.owner.type}/${site.owner.id}/avatar.png`, - reducedPersonalInfoAtCreation: site.reducedPersonalInfoAtCreation, - tosMessage: site.tosMessage, - authMode: site.authMode ?? 'onlyBackOffice', - authOnlyOtherSite: site.authOnlyOtherSite + const sitePublic: SitePublic = { + main: true, + host: reqHost(req), + colors: config.theme.colors, + authMode: 'onlyLocal' + } + res.send(sitePublic) + } else { + const colors = { ...config.theme.colors } + if (site.theme?.primaryColor) colors.primary = site.theme?.primaryColor + const sitePublic: SitePublic = { + host: site.host, + logo: site.logo || `${reqSiteUrl(req) + '/simple-directory'}/api/avatars/${site.owner.type}/${site.owner.id}/avatar.png`, + colors, + authMode: site.authMode ?? 'onlyBackOffice' + } + res.send(sitePublic) } - res.send(sitePublic) }) diff --git a/api/src/tokens/keys-manager.ts b/api/src/tokens/keys-manager.ts index e16d5be7..d2128314 100644 --- a/api/src/tokens/keys-manager.ts +++ b/api/src/tokens/keys-manager.ts @@ -39,7 +39,7 @@ export const start = async () => { while (!stopped) { if (await locks.acquire('signature-keys-rotation')) { const signatureKeys = await getSignatureKeys() - if (dayjs().diff(dayjs(signatureKeys.lastUpdate), 'day') > 20) { + if (dayjs().diff(dayjs(signatureKeys.lastUpdate), 'day') > 30) { rotatePromise = rotateKeys() await rotatePromise } diff --git a/api/types/site-public/schema.json b/api/types/site-public/schema.json index 6d3b050b..22bb2ca0 100644 --- a/api/types/site-public/schema.json +++ b/api/types/site-public/schema.json @@ -6,16 +6,18 @@ "additionalProperties":false, "required":[ "host", - "theme", - "logo", + "colors", "authMode" ], "properties":{ + "main": { + "type": "boolean" + }, "host": { "$ref": "https://github.com/data-fair/simple-directory/site#/properties/host" }, - "theme": { - "$ref": "https://github.com/data-fair/simple-directory/site#/properties/theme" + "colors": { + "$ref": "https://github.com/data-fair/simple-directory/api/config#/$defs/colors" }, "logo": { "$ref": "https://github.com/data-fair/simple-directory/site#/properties/logo" diff --git a/package-lock.json b/package-lock.json index 9e2373c4..60eb4e75 100644 --- a/package-lock.json +++ b/package-lock.json @@ -841,9 +841,9 @@ "license": "MIT" }, "node_modules/@data-fair/lib-vue": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@data-fair/lib-vue/-/lib-vue-1.8.0.tgz", - "integrity": "sha512-fyBNaJdfiL7HGwFTLYlVjRKpIL8Yvp1hoefeAL35ntNwK71BPX5x5YJDvRQ9Gyc/xk3hpO3mMnGwjaOCF6ddJQ==", + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@data-fair/lib-vue/-/lib-vue-1.9.1.tgz", + "integrity": "sha512-MWrU+7wlVfrjtLewvXWY/6pjMANwAGgtGpxDe/FW3N0T1PpPE7cgHg5Ke0XbnCHE7HSYOYMUCnwRherL6/dPog==", "license": "MIT", "dependencies": { "@data-fair/lib-common-types": "^1.1.0", @@ -867,16 +867,16 @@ } }, "node_modules/@data-fair/lib-vuetify": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@data-fair/lib-vuetify/-/lib-vuetify-1.3.0.tgz", - "integrity": "sha512-+hRJqHv/kQIgiOHDYJAuBpmDF9HBoGT5R+twm/Kd/vTxm/zC9KHkpf6DRDucboO15Lb/RqDtdkUT9SgYh+8y/g==", + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@data-fair/lib-vuetify/-/lib-vuetify-1.4.1.tgz", + "integrity": "sha512-Cn6710l+r8jgA44ed9pYzsBiGcu1efkRA1cpZGb7pQNOnkzN9Xp4soaNWLm3OfVXMpXUC94ffi8PFXduArUAaA==", "license": "MIT", "dependencies": { "@data-fair/lib-common-types": "^1.1.0", "@mdi/js": "^7.4.47" }, "peerDependencies": { - "@data-fair/lib-vue": "^1.2.0", + "@data-fair/lib-vue": "^1.9.0", "ofetch": "1", "vue-i18n": "10", "vuetify": "3" @@ -12104,8 +12104,8 @@ "ui": { "version": "0.0.0", "dependencies": { - "@data-fair/lib-vue": "^1.8.0", - "@data-fair/lib-vuetify": "^1.3.0", + "@data-fair/lib-vue": "^1.9.1", + "@data-fair/lib-vuetify": "^1.4.1", "@intlify/unplugin-vue-i18n": "^5.2.0", "@koumoul/v-iframe": "^2.4.4", "@mdi/js": "^7.4.47", diff --git a/ui/package.json b/ui/package.json index dc332189..2615bd8a 100644 --- a/ui/package.json +++ b/ui/package.json @@ -12,8 +12,8 @@ "lint-fix": "eslint --fix ." }, "dependencies": { - "@data-fair/lib-vue": "^1.8.0", - "@data-fair/lib-vuetify": "^1.3.0", + "@data-fair/lib-vue": "^1.9.1", + "@data-fair/lib-vuetify": "^1.4.1", "@intlify/unplugin-vue-i18n": "^5.2.0", "@koumoul/v-iframe": "^2.4.4", "@mdi/js": "^7.4.47", diff --git a/ui/src/main.ts b/ui/src/main.ts index e0ada506..db8e075a 100644 --- a/ui/src/main.ts +++ b/ui/src/main.ts @@ -3,7 +3,7 @@ import { createRouter, createWebHistory } from 'vue-router' import { routes } from 'vue-router/auto-routes' import { createVuetify } from 'vuetify' import { aliases, mdi } from 'vuetify/iconsets/mdi-svg' -import { defaultOptions } from '@data-fair/lib-vuetify' +import { vuetifySessionOptions } from '@data-fair/lib-vuetify' import '@data-fair/lib-vuetify/default.scss' import { createReactiveSearchParams } from '@data-fair/lib-vue/reactive-search-params.js' import { createLocaleDayjs } from '@data-fair/lib-vue/locale-dayjs.js' @@ -24,7 +24,7 @@ import 'iframe-resizer/js/iframeResizer.contentWindow.js' const localeDayjs = createLocaleDayjs(session.state.lang) const uiNotif = createUiNotif() const vuetify = createVuetify({ - ...defaultOptions(reactiveSearchParams.state, session.state.dark), + ...vuetifySessionOptions(session), icons: { defaultSet: 'mdi', aliases, sets: { mdi, } } }) const i18n = createI18n({ locale: session.state.lang, messages: $uiConfig.publicMessages }) diff --git a/ui/src/store/style.js b/ui/src/store/style.js index 60d66b66..fe292561 100644 --- a/ui/src/store/style.js +++ b/ui/src/store/style.js @@ -1,12 +1,12 @@ import tinycolor from 'tinycolor2' -const isDark = (color) => tinycolor(color).getLuminance() < 0.4 +export const isDark = (color) => tinycolor(color).getLuminance() < 0.4 // calculate a variant of a color with guaranteed readability // default background is #FAFAFA the light grey background // TODO: deprecate this, instead we rely on warnings showed to admins when the colors they chose don't have a sufficient contrast const contrastColorCache = {} -const contrastColor = (color1, color2 = '#FAFAFA', color3) => { +export const contrastColor = (color1, color2 = '#FAFAFA', color3) => { if (!color1) return const cacheKey = JSON.stringify([color1, color2, color3]) if (contrastColorCache[cacheKey]) return contrastColorCache[cacheKey] @@ -28,9 +28,6 @@ export default () => ({ customPrimaryColor: null }, getters: { - contrastColor () { - return (color1, color2, color3) => contrastColor(color1, color2, color2) - }, readablePrimaryColor (state, getters, rootState) { return state.customPrimaryColor && contrastColor(state.customPrimaryColor) },