feat: site logo upload + extensions fix

parent 3ebbbc8b
......@@ -19,7 +19,7 @@ npm-debug.log*
# Generated assets
/assets
/assets-legacy
server/views/base.pug
/server/views/base.pug
# Webpack
.webpack-cache
......
......@@ -154,6 +154,7 @@
"pg-tsquery": "8.4.0",
"pug": "3.0.2",
"punycode": "2.1.1",
"puppeteer-core": "17.1.3",
"qr-image": "3.2.0",
"rate-limiter-flexible": "2.3.8",
"remove-markdown": "0.3.0",
......@@ -164,6 +165,7 @@
"scim-query-filter-parser": "2.0.4",
"semver": "7.3.7",
"serve-favicon": "2.5.0",
"sharp": "0.31.0",
"simple-git": "2.21.0",
"socket.io": "4.5.2",
"ssh2": "1.9.0",
......
......@@ -9,6 +9,7 @@ const path = require('path')
/* global WIKI */
const tmplCreateRegex = /^[0-9]+(,[0-9]+)?$/
const siteAssetsPath = path.resolve(WIKI.ROOTPATH, WIKI.config.dataPath, 'assets')
/**
* Robots.txt
......@@ -34,6 +35,29 @@ router.get('/healthz', (req, res, next) => {
})
/**
* Site Asset
*/
router.get('/_site/:siteId?/:resource', async (req, res, next) => {
const site = req.params.siteId ? WIKI.sites[req.params.siteId] : await WIKI.models.sites.getSiteByHostname({ hostname: req.hostname })
if (!site) {
return res.status(404).send('Site Not Found')
}
switch (req.params.resource) {
case 'logo': {
if (site.config.assets.logo) {
res.sendFile(path.join(siteAssetsPath, `logo-${site.id}.${site.config.assets.logoExt}`))
} else {
res.sendFile(path.join(WIKI.ROOTPATH, 'assets/_assets/logo-wikijs.svg'))
}
break
}
default: {
return res.status(404).send('Invalid Site Resource')
}
}
})
/**
* New v3 vue app
*/
router.get([
......
......@@ -6,6 +6,7 @@ const Promise = require('bluebird')
const _ = require('lodash')
const io = require('socket.io')
const { ApolloServerPluginLandingPageGraphQLPlayground, ApolloServerPluginLandingPageProductionDefault } = require('apollo-server-core')
const { graphqlUploadExpress } = require('graphql-upload')
/* global WIKI */
......@@ -132,7 +133,8 @@ module.exports = {
const graphqlSchema = require('../graph')
this.graph = new ApolloServer({
schema: graphqlSchema,
uploads: false,
csrfPrevention: true,
cache: 'bounded',
context: ({ req, res }) => ({ req, res }),
plugins: [
process.env.NODE_ENV === 'development' ? ApolloServerPluginLandingPageGraphQLPlayground({
......@@ -145,6 +147,7 @@ module.exports = {
]
})
await this.graph.start()
WIKI.app.use(graphqlUploadExpress())
this.graph.applyMiddleware({ app: WIKI.app, cors: false, path: '/_graphql' })
},
/**
......
......@@ -56,7 +56,7 @@ exports.up = async knex => {
})
// ASSET DATA --------------------------
.createTable('assetData', table => {
table.uuid('id').notNullable().index()
table.uuid('id').notNullable().primary()
table.binary('data').notNullable()
})
// ASSET FOLDERS -----------------------
......@@ -485,6 +485,11 @@ exports.up = async knex => {
locale: 'en',
localeNamespacing: false,
localeNamespaces: [],
assets: {
logo: false,
logoExt: 'svg',
favicon: false
},
theme: {
dark: false,
colorPrimary: '#1976D2',
......@@ -580,10 +585,18 @@ exports.up = async knex => {
{
id: userGuestId,
email: 'guest@example.com',
auth: {},
name: 'Guest',
isSystem: true,
isActive: true,
isVerified: true,
meta: {},
prefs: {
timezone: 'America/New_York',
dateFormat: 'YYYY-MM-DD',
timeFormat: '12h',
darkMode: false
},
localeCode: 'en'
}
])
......
......@@ -6,7 +6,6 @@ const _ = require('lodash')
module.exports = {
Query: {
async hooks () {
WIKI.logger.warn('Seriously man')
return WIKI.models.hooks.query().orderBy('name')
},
async hookById (obj, args) {
......
......@@ -2,6 +2,8 @@ const graphHelper = require('../../helpers/graph')
const _ = require('lodash')
const CleanCSS = require('clean-css')
const path = require('path')
const fs = require('fs-extra')
const { v4: uuid } = require('uuid')
/* global WIKI */
......@@ -154,24 +156,41 @@ module.exports = {
if (!WIKI.extensions.ext.sharp.isInstalled) {
throw new Error('This feature requires the Sharp extension but it is not installed.')
}
console.info(mimetype)
const destFormat = mimetype.startsWith('image/svg') ? 'svg' : 'png'
const destPath = path.resolve(
const destFolder = path.resolve(
process.cwd(),
WIKI.config.dataPath,
`assets/logo.${destFormat}`
`assets`
)
const destPath = path.join(destFolder, `logo-${args.id}.${destFormat}`)
await fs.ensureDir(destFolder)
// -> Resize
await WIKI.extensions.ext.sharp.resize({
format: destFormat,
inputStream: createReadStream(),
outputPath: destPath,
width: 100
height: 72
})
// -> Save logo meta to DB
const site = await WIKI.models.sites.query().findById(args.id)
if (!site.config.assets.logo) {
site.config.assets.logo = uuid()
}
site.config.assets.logoExt = 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.logo,
data: imgBuffer
}).onConflict('id').merge()
WIKI.logger.info('New site logo processed successfully.')
return {
operation: graphHelper.generateSuccess('Site logo uploaded successfully')
}
} catch (err) {
WIKI.logger.warn(err)
return graphHelper.generateError(err)
}
},
......
......@@ -42,7 +42,7 @@ module.exports = {
await WIKI.extensions.ext[args.key].install()
// TODO: broadcast ext install
return {
status: graphHelper.generateSuccess('Extension installed successfully')
operation: graphHelper.generateSuccess('Extension installed successfully')
}
} catch (err) {
return graphHelper.generateError(err)
......@@ -55,7 +55,7 @@ module.exports = {
await WIKI.configSvc.applyFlags()
await WIKI.configSvc.saveToDb(['flags'])
return {
status: graphHelper.generateSuccess('System Flags applied successfully')
operation: graphHelper.generateSuccess('System Flags applied successfully')
}
},
async updateSystemSecurity (obj, args, context) {
......
......@@ -84,6 +84,11 @@ module.exports = class Site extends Model {
locale: 'en',
localeNamespacing: false,
localeNamespaces: [],
assets: {
logo: false,
logoExt: 'svg',
favicon: false
},
theme: {
dark: false,
colorPrimary: '#1976D2',
......
......@@ -5,6 +5,7 @@ module.exports = {
title: 'Git',
description: 'Distributed version control system. Required for the Git storage module.',
isInstalled: false,
isInstallable: false,
async isCompatible () {
return true
},
......
......@@ -9,6 +9,7 @@ module.exports = {
return os.arch() === 'x64'
},
isInstalled: false,
isInstallable: false,
async check () {
try {
await cmdExists('pandoc')
......
const cmdExists = require('command-exists')
const os = require('os')
const path = require('path')
const util = require('util')
const exec = util.promisify(require('child_process').exec)
const fs = require('fs-extra')
/* global WIKI */
module.exports = {
key: 'puppeteer',
......@@ -9,13 +14,27 @@ module.exports = {
return os.arch() === 'x64'
},
isInstalled: false,
isInstallable: true,
async check () {
try {
await cmdExists('pandoc')
this.isInstalled = true
this.isInstalled = await fs.pathExists(path.join(process.cwd(), 'node_modules/puppeteer-core/.local-chromium'))
} catch (err) {
this.isInstalled = false
}
return this.isInstalled
},
async install () {
try {
const { stdout, stderr } = await exec('node install.js', {
cwd: path.join(process.cwd(), 'node_modules/puppeteer-core'),
timeout: 300000,
windowsHide: true
})
this.isInstalled = true
WIKI.logger.info(stdout)
WIKI.logger.warn(stderr)
} catch (err) {
WIKI.logger.error(err)
}
}
}
const fs = require('fs-extra')
const os = require('os')
const path = require('path')
const util = require('util')
const exec = util.promisify(require('child_process').exec)
const { pipeline } = require('stream/promises')
/* global WIKI */
......@@ -12,8 +15,72 @@ module.exports = {
return os.arch() === 'x64'
},
isInstalled: false,
isInstallable: true,
async check () {
this.isInstalled = await fs.pathExists(path.join(WIKI.ROOTPATH, 'node_modules/sharp'))
this.isInstalled = await fs.pathExists(path.join(process.cwd(), 'node_modules/sharp/wiki_installed.txt'))
return this.isInstalled
},
async install () {
try {
const { stdout, stderr } = await exec('node install/libvips && node install/dll-copy', {
cwd: path.join(process.cwd(), 'node_modules/sharp'),
timeout: 120000,
windowsHide: true
})
await fs.ensureFile(path.join(process.cwd(), 'node_modules/sharp/wiki_installed.txt'))
this.isInstalled = true
WIKI.logger.info(stdout)
WIKI.logger.warn(stderr)
} catch (err) {
WIKI.logger.error(err)
}
},
sharp: null,
async load () {
if (!this.sharp) {
this.sharp = require('sharp')
}
},
/**
* RESIZE IMAGE
*/
async resize ({
format = 'png',
inputStream = null,
inputPath = null,
outputStream = null,
outputPath = null,
width = null,
height = null,
fit = 'cover',
background = { r: 0, g: 0, b: 0, alpha: 0 }
}) {
this.load()
if (inputPath) {
inputStream = fs.createReadStream(inputPath)
}
if (!inputStream) {
throw new Error('Failed to open readable input stream for image resizing.')
}
if (outputPath) {
outputStream = fs.createWriteStream(outputPath)
}
if (!outputStream) {
throw new Error('Failed to open writable output stream for image resizing.')
}
if (format === 'svg') {
return pipeline([inputStream, outputStream])
} else {
const transformer = this.sharp().resize({
width,
height,
fit,
background
}).toFormat(format)
return pipeline([inputStream, transformer, outputStream])
}
}
}
......@@ -54,20 +54,28 @@ html(lang=siteConfig.lang)
//- CSS
link(
type='text/css'
rel='stylesheet'
href='/_assets-legacy/css/app.629ebe3c082227dbee31.css'
)
//- JS
script(
type='text/javascript'
src='/_assets-legacy/js/runtime.js'
src='/_assets-legacy/js/runtime.js?1662846772'
)
script(
type='text/javascript'
src='/_assets-legacy/js/app.js'
src='/_assets-legacy/js/app.js?1662846772'
)
......
......@@ -181,8 +181,7 @@ module.exports = async () => {
lang: currentSite.config.locale,
rtl: false, // TODO: handle RTL
company: currentSite.config.company,
contentLicense: currentSite.config.contentLicense,
logoUrl: currentSite.config.logoUrl
contentLicense: currentSite.config.contentLicense
}
res.locals.langs = await WIKI.models.locales.getNavLocales({ cache: true })
res.locals.analyticsCode = await WIKI.models.analytics.getCode({ cache: true })
......
......@@ -97,7 +97,8 @@ module.exports = configure(function (/* ctx */) {
open: false, // opens browser window automatically
port: 3001,
proxy: {
'/_graphql': 'http://localhost:3000/_graphql'
'/_graphql': 'http://localhost:3000/_graphql',
'/_site': 'http://localhost:3000'
}
},
......
......@@ -9,73 +9,5 @@ q-layout(view='hHh lpr lff')
</script>
<style lang="scss">
.auth {
background-color: #FFF;
display: flex;
@at-root .body--dark & {
background-color: $dark-6;
}
&-content {
flex: 1 0 100%;
width: 100%;
max-width: 500px;
padding: 3rem 4rem;
display: flex;
flex-direction: column;
justify-content: center;
align-items: stretch;
@media (max-width: $breakpoint-xs-max) {
padding: 1rem 2rem;
max-width: 100vw;
}
}
&-logo {
img {
height: 72px;
}
}
&-site-title {
font-size: 1.875rem;
line-height: 2.25rem;
font-weight: 700;
margin: 0;
color: $blue-grey-9;
@at-root .body--dark & {
color: $blue-grey-1;
}
}
&-strategies {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(45%, 1fr));
gap: 10px;
}
&-bg {
flex: 1;
flex-basis: 0;
position: relative;
height: 100vh;
overflow: hidden;
img {
position: relative;
width: 100%;
height: 100%;
object-fit: cover;
top: 0;
bottom: 0;
left: 0;
right: 0;
margin: 0;
padding: 0;
}
}
}
</style>
......@@ -174,7 +174,7 @@ async function install (ext) {
installExtension (
key: $key
) {
status {
operation {
succeeded
message
}
......@@ -185,7 +185,7 @@ async function install (ext) {
key: ext.key
}
})
if (respRaw.data?.installExtension?.status?.succeeded) {
if (respRaw.data?.installExtension?.operation?.succeeded) {
$q.notify({
type: 'positive',
message: t('admin.extensions.installSuccess')
......@@ -193,7 +193,7 @@ async function install (ext) {
ext.isInstalled = true
// this.$forceUpdate()
} else {
throw new Error(respRaw.data?.installExtension?.status?.message || 'An unexpected error occured')
throw new Error(respRaw.data?.installExtension?.operation?.message || 'An unexpected error occured')
}
} catch (err) {
$q.notify({
......
......@@ -255,16 +255,16 @@ q-page.admin-general
dark
style='height: 64px;'
)
q-btn(dense, flat, to='/')
q-btn(dense, flat, v-if='adminStore.currentSiteId')
q-avatar(
v-if='state.config.logoText'
size='34px'
square
)
img(src='/_assets/logo-wikijs.svg')
img(:src='`/_site/` + adminStore.currentSiteId + `/logo?` + state.assetTimestamp')
img(
v-else
src='https://m.media-amazon.com/images/G/01/audibleweb/arya/navigation/audible_logo._V517446980_.svg'
:src='`/_site/` + adminStore.currentSiteId + `/logo?` + state.assetTimestamp'
style='height: 34px;'
)
q-toolbar-title.text-h6(v-if='state.config.logoText') {{state.config.title}}
......@@ -456,6 +456,7 @@ useMeta({
const state = reactive({
loading: 0,
assetTimestamp: (new Date()).toISOString(),
config: {
hostname: '',
title: '',
......@@ -678,7 +679,7 @@ async function uploadLogo () {
id: $id
image: $image
) {
status {
operation {
succeeded
slug
message
......@@ -695,6 +696,7 @@ async function uploadLogo () {
type: 'positive',
message: t('admin.general.logoUploadSuccess')
})
state.assetTimestamp = (new Date()).toISOString()
} catch (err) {
$q.notify({
type: 'negative',
......
......@@ -2,8 +2,8 @@
.auth
.auth-content
.auth-logo
img(src='/_assets/logo-wikijs.svg' :alt='siteStore.title')
h2.auth-site-title {{ siteStore.title }}
img(src='/_site/logo' :alt='siteStore.title')
h2.auth-site-title(v-if='siteStore.logoText') {{ siteStore.title }}
p.text-grey-7 Login to continue
auth-login-panel
.auth-bg(aria-hidden="true")
......@@ -412,5 +412,75 @@ onMounted(() => {
</script>
<style lang="scss">
.auth {
background-color: #FFF;
display: flex;
@at-root .body--dark & {
background-color: $dark-6;
}
&-content {
flex: 1 0 100%;
width: 100%;
max-width: 500px;
padding: 3rem 4rem;
display: flex;
flex-direction: column;
justify-content: center;
align-items: stretch;
@media (max-width: $breakpoint-xs-max) {
padding: 1rem 2rem;
max-width: 100vw;
}
}
&-logo {
margin-bottom: 6px;
img {
height: 72px;
}
}
&-site-title {
font-size: 1.875rem;
line-height: 2.25rem;
font-weight: 700;
margin: 0;
color: $blue-grey-9;
@at-root .body--dark & {
color: $blue-grey-1;
}
}
&-strategies {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(45%, 1fr));
gap: 10px;
}
&-bg {
flex: 1;
flex-basis: 0;
position: relative;
height: 100vh;
overflow: hidden;
img {
position: relative;
width: 100%;
height: 100%;
object-fit: cover;
top: 0;
bottom: 0;
left: 0;
right: 0;
margin: 0;
padding: 0;
}
}
}
</style>
......@@ -13,7 +13,7 @@ export const useSiteStore = defineStore('site', {
dark: false,
title: '',
description: '',
logoUrl: '',
logoText: true,
search: '',
searchIsFocused: false,
searchIsLoading: false,
......@@ -98,7 +98,7 @@ export const useSiteStore = defineStore('site', {
this.hostname = clone(siteInfo.hostname)
this.title = clone(siteInfo.title)
this.description = clone(siteInfo.description)
this.logoUrl = clone(siteInfo.logoUrl)
this.logoText = clone(siteInfo.logoText)
this.company = clone(siteInfo.company)
this.contentLicense = clone(siteInfo.contentLicense)
this.theme = {
......
This diff was suppressed by a .gitattributes entry.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment