diff --git a/server/controllers/common.js b/server/controllers/common.js index 805d2537..4fd60365 100644 --- a/server/controllers/common.js +++ b/server/controllers/common.js @@ -51,6 +51,14 @@ router.get('/_site/:siteId?/:resource', async (req, res, next) => { } break } + case 'favicon': { + if (site.config.assets.favicon) { + res.sendFile(path.join(siteAssetsPath, `favicon-${site.id}.${site.config.assets.faviconExt}`)) + } else { + res.sendFile(path.join(WIKI.ROOTPATH, 'assets/_assets/logo-wikijs.svg')) + } + break + } default: { return res.status(404).send('Invalid Site Resource') } diff --git a/server/db/migrations/3.0.0.js b/server/db/migrations/3.0.0.js index f23ef345..14a428ba 100644 --- a/server/db/migrations/3.0.0.js +++ b/server/db/migrations/3.0.0.js @@ -488,7 +488,8 @@ exports.up = async knex => { assets: { logo: false, logoExt: 'svg', - favicon: false + favicon: false, + faviconExt: 'svg' }, theme: { dark: false, diff --git a/server/graph/resolvers/site.js b/server/graph/resolvers/site.js index 7e8affbb..ead5de3f 100644 --- a/server/graph/resolvers/site.js +++ b/server/graph/resolvers/site.js @@ -156,6 +156,9 @@ module.exports = { if (!WIKI.extensions.ext.sharp.isInstalled) { throw new Error('This feature requires the Sharp extension but it is not installed.') } + if (!['.svg', '.png', '.jpg', 'webp', '.gif'].some(s => filename.endsWith(s))) { + throw new Error('Invalid File Extension. Must be svg, png, jpg, webp or gif.') + } const destFormat = mimetype.startsWith('image/svg') ? 'svg' : 'png' const destFolder = path.resolve( process.cwd(), @@ -198,10 +201,52 @@ module.exports = { * UPLOAD FAVICON */ async uploadSiteFavicon (obj, args) { - const { filename, mimetype, createReadStream } = await args.image - console.info(filename, mimetype) - return { - operation: graphHelper.generateSuccess('Site favicon uploaded successfully') + try { + const { filename, mimetype, createReadStream } = await args.image + WIKI.logger.info(`Processing site favicon ${filename} of type ${mimetype}...`) + if (!WIKI.extensions.ext.sharp.isInstalled) { + throw new Error('This feature requires the Sharp extension but it is not installed.') + } + if (!['.svg', '.png', '.jpg', '.webp', '.gif'].some(s => filename.endsWith(s))) { + throw new Error('Invalid File Extension. Must be svg, png, jpg, webp or gif.') + } + const destFormat = mimetype.startsWith('image/svg') ? 'svg' : 'png' + const destFolder = path.resolve( + process.cwd(), + WIKI.config.dataPath, + `assets` + ) + const destPath = path.join(destFolder, `favicon-${args.id}.${destFormat}`) + await fs.ensureDir(destFolder) + // -> Resize + await WIKI.extensions.ext.sharp.resize({ + format: destFormat, + inputStream: createReadStream(), + outputPath: destPath, + width: 64, + height: 64 + }) + // -> Save favicon meta to DB + const site = await WIKI.models.sites.query().findById(args.id) + if (!site.config.assets.favicon) { + site.config.assets.favicon = uuid() + } + site.config.assets.faviconExt = destFormat + await WIKI.models.sites.query().findById(args.id).patch({ config: site.config }) + await WIKI.models.sites.reloadCache() + // -> Save image data to DB + const imgBuffer = await fs.readFile(destPath) + await WIKI.models.knex('assetData').insert({ + id: site.config.assets.favicon, + data: imgBuffer + }).onConflict('id').merge() + WIKI.logger.info('New site favicon processed successfully.') + return { + operation: graphHelper.generateSuccess('Site favicon uploaded successfully') + } + } catch (err) { + WIKI.logger.warn(err) + return graphHelper.generateError(err) } } } diff --git a/ux/index.html b/ux/index.html index 0694a120..7fc9a727 100644 --- a/ux/index.html +++ b/ux/index.html @@ -2,7 +2,7 @@ - + diff --git a/ux/quasar.config.js b/ux/quasar.config.js index 270a0a35..a78a2c1b 100644 --- a/ux/quasar.config.js +++ b/ux/quasar.config.js @@ -77,6 +77,12 @@ module.exports = configure(function (/* ctx */) { extendViteConf (viteConf) { viteConf.build.assetsDir = '_assets' + viteConf.build.rollupOptions = { + ...viteConf.build.rollupOptions ?? {}, + external: [ + /^\/_site\// + ] + } }, // viteVuePluginOptions: {}, diff --git a/ux/src/i18n/locales/en.json b/ux/src/i18n/locales/en.json index 76991e51..4538b0c6 100644 --- a/ux/src/i18n/locales/en.json +++ b/ux/src/i18n/locales/en.json @@ -173,14 +173,14 @@ "admin.general.displaySiteTitle": "Display Site Title", "admin.general.displaySiteTitleHint": "Should the site title be displayed next to the logo? If your logo isn't square and contain your brand name, turn this option off.", "admin.general.favicon": "Favicon", - "admin.general.faviconHint": "Favicon image file, in SVG, PNG, ICO or GIF format. Must be a square image.", + "admin.general.faviconHint": "Favicon image file, in SVG, PNG, JPG, WEBP or GIF format. Must be a square image.", "admin.general.faviconUploadSuccess": "Site Favicon uploaded successfully.", "admin.general.features": "Features", "admin.general.footerCopyright": "Footer / Copyright", "admin.general.general": "General", "admin.general.logo": "Logo", "admin.general.logoUpl": "Site Logo", - "admin.general.logoUplHint": "Logo image file, in SVG, PNG, JPG or GIF format.", + "admin.general.logoUplHint": "Logo image file, in SVG, PNG, JPG, WEBP or GIF format.", "admin.general.logoUploadSuccess": "Site logo uploaded successfully.", "admin.general.ratingsOff": "Off", "admin.general.ratingsStars": "Stars", diff --git a/ux/src/pages/AdminGeneral.vue b/ux/src/pages/AdminGeneral.vue index 16e00055..4713fe26 100644 --- a/ux/src/pages/AdminGeneral.vue +++ b/ux/src/pages/AdminGeneral.vue @@ -302,10 +302,11 @@ q-page.admin-general .admin-general-favicontabs.q-mt-md div q-avatar( + v-if='adminStore.currentSiteId' size='24px' square ) - img(src='/_assets/logo-wikijs.svg') + img(:src='`/_site/` + adminStore.currentSiteId + `/favicon?` + state.assetTimestamp') .text-caption.q-ml-sm {{state.config.title}} div q-icon(name='las la-otter', size='24px', color='grey') @@ -669,7 +670,7 @@ async function uploadLogo () { input.onchange = async e => { state.loading++ try { - await APOLLO_CLIENT.mutate({ + const resp = await APOLLO_CLIENT.mutate({ mutation: gql` mutation uploadLogo ( $id: UUID! @@ -681,7 +682,6 @@ async function uploadLogo () { ) { operation { succeeded - slug message } } @@ -692,11 +692,15 @@ async function uploadLogo () { image: e.target.files[0] } }) - $q.notify({ - type: 'positive', - message: t('admin.general.logoUploadSuccess') - }) - state.assetTimestamp = (new Date()).toISOString() + if (resp?.data?.uploadSiteLogo?.operation?.succeeded) { + $q.notify({ + type: 'positive', + message: t('admin.general.logoUploadSuccess') + }) + state.assetTimestamp = (new Date()).toISOString() + } else { + throw new Error(resp?.data?.uploadSiteLogo?.operation?.message || 'An unexpected error occured.') + } } catch (err) { $q.notify({ type: 'negative', @@ -717,7 +721,7 @@ async function uploadFavicon () { input.onchange = async e => { state.loading++ try { - await APOLLO_CLIENT.mutate({ + const resp = await APOLLO_CLIENT.mutate({ mutation: gql` mutation uploadFavicon ( $id: UUID! @@ -727,9 +731,8 @@ async function uploadFavicon () { id: $id image: $image ) { - status { + operation { succeeded - slug message } } @@ -740,10 +743,15 @@ async function uploadFavicon () { image: e.target.files[0] } }) - $q.notify({ - type: 'positive', - message: t('admin.general.faviconUploadSuccess') - }) + if (resp?.data?.uploadSiteFavicon?.operation?.succeeded) { + $q.notify({ + type: 'positive', + message: t('admin.general.faviconUploadSuccess') + }) + state.assetTimestamp = (new Date()).toISOString() + } else { + throw new Error(resp?.data?.uploadSiteFavicon?.operation?.message || 'An unexpected error occured.') + } } catch (err) { $q.notify({ type: 'negative', diff --git a/ux/src/pages/AdminTerminal.vue b/ux/src/pages/AdminTerminal.vue index 5e224896..351c1c8f 100644 --- a/ux/src/pages/AdminTerminal.vue +++ b/ux/src/pages/AdminTerminal.vue @@ -119,7 +119,7 @@ onMounted(() => { state.connecting = true // socket = io(window.location.host, { - socket = io('localhost:3000', { + socket = io(window.location.host, { path: '/_ws/', auth: { token: 'TEST' // TODO: Use active token