feat: login screen UI + server code cleanup

parent 1a720c91
...@@ -8,7 +8,7 @@ trim_trailing_whitespace = true ...@@ -8,7 +8,7 @@ trim_trailing_whitespace = true
end_of_line = lf end_of_line = lf
insert_final_newline = true insert_final_newline = true
[*.{jade,pug,md}] [*.{pug,md}]
trim_trailing_whitespace = false trim_trailing_whitespace = false
[Makefile] [Makefile]
......
...@@ -47,7 +47,7 @@ store.commit('user/REFRESH_AUTH') ...@@ -47,7 +47,7 @@ store.commit('user/REFRESH_AUTH')
// Initialize Apollo Client (GraphQL) // Initialize Apollo Client (GraphQL)
// ==================================== // ====================================
const graphQLEndpoint = window.location.protocol + '//' + window.location.host + '/graphql' const graphQLEndpoint = window.location.protocol + '//' + window.location.host + '/_graphql'
const graphQLLink = ApolloLink.from([ const graphQLLink = ApolloLink.from([
new ErrorLink(({ graphQLErrors, networkError }) => { new ErrorLink(({ graphQLErrors, networkError }) => {
......
/* eslint-disable import/first */
import Vue from 'vue'
import Vuetify from 'vuetify/lib'
import boot from './modules/boot'
/* eslint-enable import/first */
window.WIKI = null
window.boot = boot
Vue.use(Vuetify)
Vue.component('setup', () => import(/* webpackMode: "eager" */ './components/setup.vue'))
let bootstrap = () => {
window.WIKI = new Vue({
el: '#root',
vuetify: new Vuetify()
})
}
window.boot.onDOMReady(bootstrap)
...@@ -662,26 +662,34 @@ export default { ...@@ -662,26 +662,34 @@ export default {
apollo: { apollo: {
strategies: { strategies: {
query: gql` query: gql`
{ query loginFetchSiteStrategies(
authentication { $siteId: UUID
activeStrategies(enabledOnly: true) { ) {
authStrategies(
siteId: $siteId
enabledOnly: true
) {
key
strategy {
key key
strategy { logo
key color
logo icon
color useForm
icon usernameType
useForm
usernameType
}
displayName
order
selfRegistration
} }
displayName
order
selfRegistration
} }
} }
`, `,
update: (data) => _.sortBy(data.authentication.activeStrategies, ['order']), variables () {
return {
siteId: siteConfig.id
}
},
update: (data) => _.sortBy(data.authStrategies, ['order']),
watchLoading (isLoading) { watchLoading (isLoading) {
this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'login-strategies-refresh') this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'login-strategies-refresh')
} }
......
require('./scss/legacy.scss')
require('./scss/fonts/default.scss')
window.WIKI = null
require('core-js/stable')
require('regenerator-runtime/runtime')
/* eslint-disable no-unused-expressions */
require('./scss/app.scss')
import(/* webpackChunkName: "mdi" */ '@mdi/font/css/materialdesignicons.css')
require('./helpers/compatibility.js')
require('./client-setup.js')
...@@ -60,7 +60,7 @@ const init = { ...@@ -60,7 +60,7 @@ const init = {
}, },
async reload() { async reload() {
console.warn(chalk.yellow('--- Gracefully stopping server...')) console.warn(chalk.yellow('--- Gracefully stopping server...'))
await global.WIKI.kernel.shutdown() await global.WIKI.kernel.shutdown(true)
console.warn(chalk.yellow('--- Purging node modules cache...')) console.warn(chalk.yellow('--- Purging node modules cache...'))
......
...@@ -29,6 +29,7 @@ html(lang=siteConfig.lang) ...@@ -29,6 +29,7 @@ html(lang=siteConfig.lang)
//- Site Properties //- Site Properties
script. script.
var siteId = "!{siteId}"
var siteConfig = !{JSON.stringify(siteConfig)} var siteConfig = !{JSON.stringify(siteConfig)}
var siteLangs = !{JSON.stringify(langs)} var siteLangs = !{JSON.stringify(langs)}
......
doctype html
html
head
meta(http-equiv='X-UA-Compatible', content='IE=edge')
meta(charset='UTF-8')
meta(name='viewport', content='user-scalable=yes, width=device-width, initial-scale=1, maximum-scale=5')
meta(name='theme-color', content='#1976d2')
meta(name='msapplication-TileColor', content='#1976d2')
meta(name='msapplication-TileImage', content='/_assets/favicons/mstile-150x150.png')
title Wiki.js Setup
//- Favicon
link(rel='apple-touch-icon', sizes='180x180', href='/_assets/favicons/apple-touch-icon.png')
link(rel='icon', type='image/png', sizes='192x192', href='/_assets/favicons/android-chrome-192x192.png')
link(rel='icon', type='image/png', sizes='32x32', href='/_assets/favicons/favicon-32x32.png')
link(rel='icon', type='image/png', sizes='16x16', href='/_assets/favicons/favicon-16x16.png')
link(rel='mask-icon', href='/_assets/favicons/safari-pinned-tab.svg', color='#1976d2')
link(rel='manifest', href='/_assets/manifest.json')
//- Site Lang
script.
var siteConfig = !{JSON.stringify({ title: config.title })}
//- Dev Mode Warning
if devMode
script.
siteConfig.devMode = true
//- CSS
<% for (var index in htmlWebpackPlugin.files.css) { %>
link(
type='text/css'
rel='stylesheet'
href='<%= htmlWebpackPlugin.files.css[index] %>'
)
<% } %>
//- JS
<% for (var index in htmlWebpackPlugin.files.js) { %>
script(
type='text/javascript'
src='<%= htmlWebpackPlugin.files.js[index] %>'
)
<% } %>
body
#root
setup(telemetry-id=telemetryClientID, wiki-version=packageObj.version)
...@@ -24,9 +24,7 @@ fs.emptyDirSync(path.join(process.cwd(), 'assets-legacy')) ...@@ -24,9 +24,7 @@ fs.emptyDirSync(path.join(process.cwd(), 'assets-legacy'))
module.exports = { module.exports = {
mode: 'development', mode: 'development',
entry: { entry: {
app: ['./client/index-app.js', 'webpack-hot-middleware/client'], app: ['./client/index-app.js', 'webpack-hot-middleware/client']
legacy: ['./client/index-legacy.js', 'webpack-hot-middleware/client'],
setup: ['./client/index-setup.js', 'webpack-hot-middleware/client']
}, },
output: { output: {
path: path.join(process.cwd(), 'assets-legacy'), path: path.join(process.cwd(), 'assets-legacy'),
...@@ -197,25 +195,10 @@ module.exports = { ...@@ -197,25 +195,10 @@ module.exports = {
] ]
}), }),
new HtmlWebpackPlugin({ new HtmlWebpackPlugin({
template: 'dev/templates/master.pug', template: 'dev/templates/base.pug',
filename: '../server/views/master.pug', filename: '../server/views/base.pug',
hash: false, hash: false,
inject: false, inject: false
excludeChunks: ['setup', 'legacy']
}),
new HtmlWebpackPlugin({
template: 'dev/templates/legacy.pug',
filename: '../server/views/legacy/master.pug',
hash: false,
inject: false,
excludeChunks: ['setup', 'app']
}),
new HtmlWebpackPlugin({
template: 'dev/templates/setup.pug',
filename: '../server/views/setup.pug',
hash: false,
inject: false,
excludeChunks: ['app', 'legacy']
}), }),
new HtmlWebpackPugPlugin(), new HtmlWebpackPugPlugin(),
new WebpackBarPlugin({ new WebpackBarPlugin({
......
...@@ -29,9 +29,7 @@ fs.emptyDirSync(path.join(process.cwd(), 'assets-legacy')) ...@@ -29,9 +29,7 @@ fs.emptyDirSync(path.join(process.cwd(), 'assets-legacy'))
module.exports = { module.exports = {
mode: 'production', mode: 'production',
entry: { entry: {
app: './client/index-app.js', app: './client/index-app.js'
legacy: './client/index-legacy.js',
setup: './client/index-setup.js'
}, },
output: { output: {
path: path.join(process.cwd(), 'assets-legacy'), path: path.join(process.cwd(), 'assets-legacy'),
...@@ -208,25 +206,10 @@ module.exports = { ...@@ -208,25 +206,10 @@ module.exports = {
chunkFilename: 'css/[name].[chunkhash].css' chunkFilename: 'css/[name].[chunkhash].css'
}), }),
new HtmlWebpackPlugin({ new HtmlWebpackPlugin({
template: 'dev/templates/master.pug', template: 'dev/templates/base.pug',
filename: '../server/views/master.pug', filename: '../server/views/base.pug',
hash: false, hash: false,
inject: false, inject: false
excludeChunks: ['setup', 'legacy']
}),
new HtmlWebpackPlugin({
template: 'dev/templates/legacy.pug',
filename: '../server/views/legacy/master.pug',
hash: false,
inject: false,
excludeChunks: ['setup', 'app']
}),
new HtmlWebpackPlugin({
template: 'dev/templates/setup.pug',
filename: '../server/views/setup.pug',
hash: false,
inject: false,
excludeChunks: ['app', 'legacy']
}), }),
new HtmlWebpackPugPlugin(), new HtmlWebpackPugPlugin(),
new ScriptExtHtmlWebpackPlugin({ new ScriptExtHtmlWebpackPlugin({
......
...@@ -8,7 +8,7 @@ ...@@ -8,7 +8,7 @@
"scripts": { "scripts": {
"start": "node server", "start": "node server",
"dev": "nodemon server", "dev": "nodemon server",
"legacy:dev": "node dev", "legacy:dev": "NODE_OPTIONS=--openssl-legacy-provider node dev",
"legacy:build": "NODE_OPTIONS=--openssl-legacy-provider webpack --profile --config dev/webpack/webpack.prod.js", "legacy:build": "NODE_OPTIONS=--openssl-legacy-provider webpack --profile --config dev/webpack/webpack.prod.js",
"test": "eslint --format codeframe --ext .js,.vue . && pug-lint server/views && jest", "test": "eslint --format codeframe --ext .js,.vue . && pug-lint server/views && jest",
"cypress:open": "cypress open" "cypress:open": "cypress open"
......
...@@ -24,14 +24,13 @@ module.exports = { ...@@ -24,14 +24,13 @@ module.exports = {
process.exit(1) process.exit(1)
} }
this.bootMaster() this.bootWeb()
}, },
/** /**
* Pre-Master Boot Sequence * Pre-Web Boot Sequence
*/ */
async preBootMaster() { async preBootWeb() {
try { try {
await this.initTelemetry()
WIKI.sideloader = await require('./sideloader').init() WIKI.sideloader = await require('./sideloader').init()
WIKI.cache = require('./cache').init() WIKI.cache = require('./cache').init()
WIKI.scheduler = require('./scheduler').init() WIKI.scheduler = require('./scheduler').init()
...@@ -48,22 +47,22 @@ module.exports = { ...@@ -48,22 +47,22 @@ module.exports = {
} }
}, },
/** /**
* Boot Master Process * Boot Web Process
*/ */
async bootMaster() { async bootWeb() {
try { try {
await this.preBootMaster() await this.preBootWeb()
await require('../master')() await require('../web')()
this.postBootMaster() this.postBootWeb()
} catch (err) { } catch (err) {
WIKI.logger.error(err) WIKI.logger.error(err)
process.exit(1) process.exit(1)
} }
}, },
/** /**
* Post-Master Boot Sequence * Post-Web Boot Sequence
*/ */
async postBootMaster() { async postBootWeb() {
await WIKI.models.analytics.refreshProvidersFromDisk() await WIKI.models.analytics.refreshProvidersFromDisk()
await WIKI.models.authentication.refreshStrategiesFromDisk() await WIKI.models.authentication.refreshStrategiesFromDisk()
await WIKI.models.commentProviders.refreshProvidersFromDisk() await WIKI.models.commentProviders.refreshProvidersFromDisk()
...@@ -74,30 +73,16 @@ module.exports = { ...@@ -74,30 +73,16 @@ module.exports = {
await WIKI.auth.activateStrategies() await WIKI.auth.activateStrategies()
await WIKI.models.commentProviders.initProvider() await WIKI.models.commentProviders.initProvider()
await WIKI.models.sites.reloadCache()
await WIKI.models.storage.initTargets() await WIKI.models.storage.initTargets()
// WIKI.scheduler.start() // WIKI.scheduler.start()
await WIKI.models.subscribeToNotifications() await WIKI.models.subscribeToNotifications()
}, },
/** /**
* Init Telemetry
*/
async initTelemetry() {
require('./telemetry').init()
process.on('unhandledRejection', (err) => {
WIKI.logger.warn(err)
WIKI.telemetry.sendError(err)
})
process.on('uncaughtException', (err) => {
WIKI.logger.warn(err)
WIKI.telemetry.sendError(err)
})
},
/**
* Graceful shutdown * Graceful shutdown
*/ */
async shutdown () { async shutdown (devMode = false) {
if (WIKI.servers) { if (WIKI.servers) {
await WIKI.servers.stopServers() await WIKI.servers.stopServers()
} }
...@@ -113,6 +98,8 @@ module.exports = { ...@@ -113,6 +98,8 @@ module.exports = {
if (WIKI.asar) { if (WIKI.asar) {
await WIKI.asar.unload() await WIKI.asar.unload()
} }
process.exit(0) if (!devMode) {
process.exit(0)
}
} }
} }
...@@ -11,7 +11,7 @@ module.exports = { ...@@ -11,7 +11,7 @@ module.exports = {
minimumVersionRequired: '3.0.0-beta.0', minimumVersionRequired: '3.0.0-beta.0',
minimumNodeRequired: '18.0.0' minimumNodeRequired: '18.0.0'
}, },
init() { init () {
// Clear content cache // Clear content cache
fs.emptyDir(path.resolve(WIKI.ROOTPATH, WIKI.config.dataPath, 'cache')) fs.emptyDir(path.resolve(WIKI.ROOTPATH, WIKI.config.dataPath, 'cache'))
......
const _ = require('lodash')
const { createApolloFetch } = require('apollo-fetch')
const { v4: uuid } = require('uuid')
const os = require('os')
const fs = require('fs-extra')
/* global WIKI */
module.exports = {
enabled: false,
init() {
WIKI.telemetry = this
if (_.get(WIKI.config, 'telemetry.isEnabled', false) === true && WIKI.config.offline !== true) {
this.enabled = true
this.sendInstanceEvent('STARTUP')
}
},
sendError(err) {
// TODO
},
sendEvent(eventCategory, eventAction, eventLabel) {
// TODO
},
async sendInstanceEvent(eventType) {
if (WIKI.devMode || !this.enabled) { return }
try {
const apollo = createApolloFetch({
uri: WIKI.config.graphEndpoint
})
// Platform detection
let platform = 'LINUX'
let isDockerized = false
let osname = `${os.type()} ${os.release()}`
switch (os.platform()) {
case 'win32':
platform = 'WINDOWS'
break
case 'darwin':
platform = 'MACOS'
break
default:
platform = 'LINUX'
isDockerized = await fs.pathExists('/.dockerenv')
if (isDockerized) {
osname = 'Docker'
}
break
}
// DB Version detection
let dbVersion = 'Unknown'
switch (WIKI.config.db.type) {
case 'mariadb':
case 'mysql':
const resultMYSQL = await WIKI.models.knex.raw('SELECT VERSION() as version;')
dbVersion = _.get(resultMYSQL, '[0][0].version', 'Unknown')
break
case 'mssql':
const resultMSSQL = await WIKI.models.knex.raw('SELECT @@VERSION as version;')
dbVersion = _.get(resultMSSQL, '[0].version', 'Unknown')
break
case 'postgres':
dbVersion = _.get(WIKI.models, 'knex.client.version', 'Unknown')
break
case 'sqlite':
dbVersion = _.get(WIKI.models, 'knex.client.driver.VERSION', 'Unknown')
break
}
let arch = os.arch().toUpperCase()
if (['ARM', 'ARM64', 'X32', 'X64'].indexOf(arch) < 0) {
arch = 'OTHER'
}
// Send Event
const respStrings = await apollo({
query: `mutation (
$version: String!
$platform: TelemetryPlatform!
$os: String!
$architecture: TelemetryArchitecture!
$dbType: TelemetryDBType!
$dbVersion: String!
$nodeVersion: String!
$cpuCores: Int!
$ramMBytes: Int!,
$clientId: String!,
$event: TelemetryInstanceEvent!
) {
telemetry {
instance(
version: $version
platform: $platform
os: $os
architecture: $architecture
dbType: $dbType
dbVersion: $dbVersion
nodeVersion: $nodeVersion
cpuCores: $cpuCores
ramMBytes: $ramMBytes
clientId: $clientId
event: $event
) {
responseResult {
succeeded
errorCode
slug
message
}
}
}
}`,
variables: {
version: WIKI.version,
platform,
os: osname,
architecture: arch,
dbType: WIKI.config.db.type.toUpperCase(),
dbVersion,
nodeVersion: process.version.substr(1),
cpuCores: os.cpus().length,
ramMBytes: Math.round(os.totalmem() / 1024 / 1024),
clientId: WIKI.config.telemetry.clientId,
event: eventType
}
})
const telemetryResponse = _.get(respStrings, 'data.telemetry.instance.responseResult', { succeeded: false, message: 'Unexpected Error' })
if (!telemetryResponse.succeeded) {
WIKI.logger.warn('Failed to send instance telemetry: ' + telemetryResponse.message)
} else {
WIKI.logger.info('Telemetry is active: [ OK ]')
}
} catch (err) {
WIKI.logger.warn(err)
}
},
generateClientId() {
_.set(WIKI.config, 'telemetry.clientId', uuid())
return WIKI.config.telemetry.clientId
}
}
...@@ -32,6 +32,7 @@ module.exports = { ...@@ -32,6 +32,7 @@ module.exports = {
// }), ['title', 'key']) // }), ['title', 'key'])
return _.sortBy(WIKI.storage.defs.map(md => { return _.sortBy(WIKI.storage.defs.map(md => {
const dbTarget = dbTargets.find(tg => tg.module === md.key) const dbTarget = dbTargets.find(tg => tg.module === md.key)
console.info(md.actions)
return { return {
id: dbTarget?.id ?? uuid(), id: dbTarget?.id ?? uuid(),
isEnabled: dbTarget?.isEnabled ?? false, isEnabled: dbTarget?.isEnabled ?? false,
...@@ -62,12 +63,12 @@ module.exports = { ...@@ -62,12 +63,12 @@ module.exports = {
setup: { setup: {
handler: md?.setup?.handler, handler: md?.setup?.handler,
state: dbTarget?.state?.setup ?? 'notconfigured', state: dbTarget?.state?.setup ?? 'notconfigured',
values: md.setup?.handler values: md.setup?.handler ?
? _.transform(md.setup.defaultValues, _.transform(md.setup.defaultValues,
(r, v, k) => { (r, v, k) => {
r[k] = dbTarget?.config?.[k] ?? v r[k] = dbTarget?.config?.[k] ?? v
}, {}) }, {}) :
: {} {}
}, },
config: _.transform(md.props, (r, v, k) => { config: _.transform(md.props, (r, v, k) => {
const cfValue = dbTarget?.config?.[k] ?? v.default const cfValue = dbTarget?.config?.[k] ?? v.default
......
...@@ -8,6 +8,7 @@ extend type Query { ...@@ -8,6 +8,7 @@ extend type Query {
apiState: Boolean apiState: Boolean
authStrategies( authStrategies(
siteId: UUID
enabledOnly: Boolean enabledOnly: Boolean
): [AuthenticationStrategy] ): [AuthenticationStrategy]
} }
......
...@@ -22,6 +22,8 @@ let WIKI = { ...@@ -22,6 +22,8 @@ let WIKI = {
Error: require('./helpers/error'), Error: require('./helpers/error'),
configSvc: require('./core/config'), configSvc: require('./core/config'),
kernel: require('./core/kernel'), kernel: require('./core/kernel'),
sites: {},
sitesMappings: {},
startedAt: DateTime.utc(), startedAt: DateTime.utc(),
storage: { storage: {
defs: [], defs: [],
......
...@@ -28,6 +28,28 @@ module.exports = class Site extends Model { ...@@ -28,6 +28,28 @@ module.exports = class Site extends Model {
return ['config'] return ['config']
} }
static async getSiteByHostname ({ hostname, forceReload = false }) {
if (forceReload) {
await WIKI.models.sites.reloadCache()
}
const siteId = WIKI.sitesMappings[hostname] || WIKI.sitesMappings['*']
if (siteId) {
return WIKI.sites[siteId]
}
return null
}
static async reloadCache () {
WIKI.logger.info('Reloading site configurations...')
const sites = await WIKI.models.sites.query().orderBy('id')
WIKI.sites = _.keyBy(sites, 'id')
WIKI.sitesMappings = {}
for (const site of sites) {
WIKI.sitesMappings[site.hostname] = site.id
}
WIKI.logger.info(`Loaded ${sites.length} site configurations [ OK ]`)
}
static async createSite (hostname, config) { static async createSite (hostname, config) {
const newSite = await WIKI.models.sites.query().insertAndFetch({ const newSite = await WIKI.models.sites.query().insertAndFetch({
hostname, hostname,
......
const path = require('path')
const { v4: uuid } = require('uuid')
const bodyParser = require('body-parser')
const compression = require('compression')
const express = require('express')
const favicon = require('serve-favicon')
const http = require('http')
const Promise = require('bluebird')
const fs = require('fs-extra')
const _ = require('lodash')
const crypto = Promise.promisifyAll(require('crypto'))
const pem2jwk = require('pem-jwk').pem2jwk
const semver = require('semver')
/* global WIKI */
module.exports = () => {
WIKI.config.site = {
path: '',
title: 'Wiki.js'
}
WIKI.system = require('./core/system')
// ----------------------------------------
// Define Express App
// ----------------------------------------
let app = express()
app.use(compression())
// ----------------------------------------
// Public Assets
// ----------------------------------------
app.use(favicon(path.join(WIKI.ROOTPATH, 'assets', 'favicon.ico')))
app.use('/_assets', express.static(path.join(WIKI.ROOTPATH, 'assets')))
// ----------------------------------------
// View Engine Setup
// ----------------------------------------
app.set('views', path.join(WIKI.SERVERPATH, 'views'))
app.set('view engine', 'pug')
app.use(bodyParser.json())
app.use(bodyParser.urlencoded({ extended: false }))
app.locals.config = WIKI.config
app.locals.data = WIKI.data
app.locals._ = require('lodash')
app.locals.devMode = WIKI.devMode
// ----------------------------------------
// HMR (Dev Mode Only)
// ----------------------------------------
if (global.DEV) {
app.use(global.WP_DEV.devMiddleware)
app.use(global.WP_DEV.hotMiddleware)
}
// ----------------------------------------
// Controllers
// ----------------------------------------
app.get('*', async (req, res) => {
let packageObj = await fs.readJson(path.join(WIKI.ROOTPATH, 'package.json'))
res.render('setup', { packageObj })
})
/**
* Finalize
*/
app.post('/finalize', async (req, res) => {
try {
// Set config
_.set(WIKI.config, 'auth', {
audience: 'urn:wiki.js',
tokenExpiration: '30m',
tokenRenewal: '14d'
})
_.set(WIKI.config, 'company', '')
_.set(WIKI.config, 'features', {
featurePageRatings: true,
featurePageComments: true,
featurePersonalWikis: true
})
_.set(WIKI.config, 'graphEndpoint', 'https://graph.requarks.io')
_.set(WIKI.config, 'host', req.body.siteUrl)
_.set(WIKI.config, 'lang', {
code: 'en',
autoUpdate: true,
namespacing: false,
namespaces: []
})
_.set(WIKI.config, 'logo', {
hasLogo: false,
logoIsSquare: false
})
_.set(WIKI.config, 'mail', {
senderName: '',
senderEmail: '',
host: '',
port: 465,
secure: true,
verifySSL: true,
user: '',
pass: '',
useDKIM: false,
dkimDomainName: '',
dkimKeySelector: '',
dkimPrivateKey: ''
})
_.set(WIKI.config, 'seo', {
description: '',
robots: ['index', 'follow'],
analyticsService: '',
analyticsId: ''
})
_.set(WIKI.config, 'sessionSecret', (await crypto.randomBytesAsync(32)).toString('hex'))
_.set(WIKI.config, 'telemetry', {
isEnabled: req.body.telemetry === true,
clientId: uuid()
})
_.set(WIKI.config, 'theming', {
theme: 'default',
darkMode: false,
iconset: 'mdi',
injectCSS: '',
injectHead: '',
injectBody: ''
})
_.set(WIKI.config, 'title', 'Wiki.js')
// Init Telemetry
WIKI.kernel.initTelemetry()
// WIKI.telemetry.sendEvent('setup', 'install-start')
// Basic checks
if (!semver.satisfies(process.version, '>=10.12')) {
throw new Error('Node.js 10.12.x or later required!')
}
// Create directory structure
WIKI.logger.info('Creating data directories...')
await fs.ensureDir(path.resolve(WIKI.ROOTPATH, WIKI.config.dataPath))
await fs.emptyDir(path.resolve(WIKI.ROOTPATH, WIKI.config.dataPath, 'cache'))
await fs.ensureDir(path.resolve(WIKI.ROOTPATH, WIKI.config.dataPath, 'uploads'))
// Generate certificates
WIKI.logger.info('Generating certificates...')
const certs = crypto.generateKeyPairSync('rsa', {
modulusLength: 2048,
publicKeyEncoding: {
type: 'pkcs1',
format: 'pem'
},
privateKeyEncoding: {
type: 'pkcs1',
format: 'pem',
cipher: 'aes-256-cbc',
passphrase: WIKI.config.sessionSecret
}
})
_.set(WIKI.config, 'certs', {
jwk: pem2jwk(certs.publicKey),
public: certs.publicKey,
private: certs.privateKey
})
// Save config to DB
WIKI.logger.info('Persisting config to DB...')
await WIKI.configSvc.saveToDb([
'auth',
'certs',
'company',
'features',
'graphEndpoint',
'host',
'lang',
'logo',
'mail',
'seo',
'sessionSecret',
'telemetry',
'theming',
'uploads',
'title'
], false)
// Truncate tables (reset from previous failed install)
await WIKI.models.locales.query().where('code', '!=', 'x').del()
await WIKI.models.navigation.query().truncate()
switch (WIKI.config.db.type) {
case 'postgres':
await WIKI.models.knex.raw('TRUNCATE groups, users CASCADE')
break
case 'mysql':
case 'mariadb':
await WIKI.models.groups.query().where('id', '>', 0).del()
await WIKI.models.users.query().where('id', '>', 0).del()
await WIKI.models.knex.raw('ALTER TABLE `groups` AUTO_INCREMENT = 1')
await WIKI.models.knex.raw('ALTER TABLE `users` AUTO_INCREMENT = 1')
break
case 'mssql':
await WIKI.models.groups.query().del()
await WIKI.models.users.query().del()
await WIKI.models.knex.raw(`
IF EXISTS (SELECT * FROM sys.identity_columns WHERE OBJECT_NAME(OBJECT_ID) = 'groups' AND last_value IS NOT NULL)
DBCC CHECKIDENT ([groups], RESEED, 0)
`)
await WIKI.models.knex.raw(`
IF EXISTS (SELECT * FROM sys.identity_columns WHERE OBJECT_NAME(OBJECT_ID) = 'users' AND last_value IS NOT NULL)
DBCC CHECKIDENT ([users], RESEED, 0)
`)
break
case 'sqlite':
await WIKI.models.groups.query().truncate()
await WIKI.models.users.query().truncate()
break
}
// Create default locale
WIKI.logger.info('Installing default locale...')
await WIKI.models.locales.query().insert({
code: 'en',
strings: {},
isRTL: false,
name: 'English',
nativeName: 'English'
})
// Create default groups
WIKI.logger.info('Creating default groups...')
const adminGroup = await WIKI.models.groups.query().insert({
name: 'Administrators',
permissions: JSON.stringify(['manage:system']),
pageRules: JSON.stringify([]),
isSystem: true
})
const guestGroup = await WIKI.models.groups.query().insert({
name: 'Guests',
permissions: JSON.stringify(['read:pages', 'read:assets', 'read:comments']),
pageRules: JSON.stringify([
{ id: 'guest', roles: ['read:pages', 'read:assets', 'read:comments'], match: 'START', deny: false, path: '', locales: [] }
]),
isSystem: true
})
if (adminGroup.id !== 1 || guestGroup.id !== 2) {
throw new Error('Incorrect groups auto-increment configuration! Should start at 0 and increment by 1. Contact your database administrator.')
}
// Load local authentication strategy
await WIKI.models.authentication.query().insert({
key: 'local',
config: {},
selfRegistration: false,
isEnabled: true,
domainWhitelist: {v: []},
autoEnrollGroups: {v: []},
order: 0,
strategyKey: 'local',
displayName: 'Local'
})
// Load editors + enable default
await WIKI.models.editors.refreshEditorsFromDisk()
await WIKI.models.editors.query().patch({ isEnabled: true }).where('key', 'markdown')
// Load loggers
await WIKI.models.loggers.refreshLoggersFromDisk()
// Load renderers
await WIKI.models.renderers.refreshRenderersFromDisk()
// Load search engines + enable default
await WIKI.models.searchEngines.refreshSearchEnginesFromDisk()
await WIKI.models.searchEngines.query().patch({ isEnabled: true }).where('key', 'db')
// WIKI.telemetry.sendEvent('setup', 'install-loadedmodules')
// Load storage targets
await WIKI.models.storage.refreshTargetsFromDisk()
// Create root administrator
WIKI.logger.info('Creating root administrator...')
const adminUser = await WIKI.models.users.query().insert({
email: req.body.adminEmail.toLowerCase(),
provider: 'local',
password: req.body.adminPassword,
name: 'Administrator',
locale: 'en',
defaultEditor: 'markdown',
tfaIsActive: false,
isActive: true,
isVerified: true
})
await adminUser.$relatedQuery('groups').relate(adminGroup.id)
// Create Guest account
WIKI.logger.info('Creating guest account...')
const guestUser = await WIKI.models.users.query().insert({
provider: 'local',
email: 'guest@example.com',
name: 'Guest',
password: '',
locale: 'en',
defaultEditor: 'markdown',
tfaIsActive: false,
isSystem: true,
isActive: true,
isVerified: true
})
await guestUser.$relatedQuery('groups').relate(guestGroup.id)
if (adminUser.id !== 1 || guestUser.id !== 2) {
throw new Error('Incorrect users auto-increment configuration! Should start at 0 and increment by 1. Contact your database administrator.')
}
// Create site nav
WIKI.logger.info('Creating default site navigation')
await WIKI.models.navigation.query().insert({
key: 'site',
config: [
{
locale: 'en',
items: [
{
id: uuid(),
icon: 'mdi-home',
kind: 'link',
label: 'Home',
target: '/',
targetType: 'home',
visibilityMode: 'all',
visibilityGroups: null
}
]
}
]
})
WIKI.logger.info('Setup is complete!')
// WIKI.telemetry.sendEvent('setup', 'install-completed')
res.json({
ok: true,
redirectPath: '/',
redirectPort: WIKI.config.port
}).end()
if (WIKI.config.telemetry.isEnabled) {
await WIKI.telemetry.sendInstanceEvent('INSTALL')
}
WIKI.config.setup = false
WIKI.logger.info('Stopping Setup...')
WIKI.server.destroy(() => {
WIKI.logger.info('Setup stopped. Starting Wiki.js...')
_.delay(() => {
WIKI.kernel.bootMaster()
}, 1000)
})
} catch (err) {
try {
await WIKI.models.knex('settings').truncate()
} catch (err) {}
WIKI.telemetry.sendError(err)
res.json({ ok: false, error: err.message })
}
})
// ----------------------------------------
// Error handling
// ----------------------------------------
app.use(function (req, res, next) {
const err = new Error('Not Found')
err.status = 404
next(err)
})
app.use(function (err, req, res, next) {
res.status(err.status || 500)
res.send({
message: err.message,
error: WIKI.IS_DEBUG ? err : {}
})
WIKI.logger.error(err.message)
WIKI.telemetry.sendError(err)
})
// ----------------------------------------
// Start HTTP server
// ----------------------------------------
WIKI.logger.info(`Starting HTTP server on port ${WIKI.config.port}...`)
app.set('port', WIKI.config.port)
WIKI.logger.info(`HTTP Server on port: [ ${WIKI.config.port} ]`)
WIKI.server = http.createServer(app)
WIKI.server.listen(WIKI.config.port, WIKI.config.bindIP)
var openConnections = []
WIKI.server.on('connection', (conn) => {
let key = conn.remoteAddress + ':' + conn.remotePort
openConnections[key] = conn
conn.on('close', () => {
openConnections.splice(key, 1)
})
})
WIKI.server.destroy = (cb) => {
WIKI.server.close(cb)
for (let key in openConnections) {
openConnections[key].destroy()
}
}
WIKI.server.on('error', (error) => {
if (error.syscall !== 'listen') {
throw error
}
switch (error.code) {
case 'EACCES':
WIKI.logger.error('Listening on port ' + WIKI.config.port + ' requires elevated privileges!')
return process.exit(1)
case 'EADDRINUSE':
WIKI.logger.error('Port ' + WIKI.config.port + ' is already in use!')
return process.exit(1)
default:
throw error
}
})
WIKI.server.on('listening', () => {
WIKI.logger.info('HTTP Server: [ RUNNING ]')
WIKI.logger.info('🔻🔻🔻🔻🔻🔻🔻🔻🔻🔻🔻🔻🔻🔻🔻🔻🔻🔻🔻🔻🔻🔻🔻🔻🔻🔻🔻🔻🔻')
WIKI.logger.info('')
WIKI.logger.info(`Browse to http://YOUR-SERVER-IP:${WIKI.config.port}/ to complete setup!`)
WIKI.logger.info('')
WIKI.logger.info('🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺')
})
}
name: Default
author: requarks.io
site: https://wiki.requarks.io/
version: 1.0.0
requirements:
minimum: '>= 2.0.0'
maximum: '< 3.0.0'
props:
accentColor:
type: String
title: Accent Color
hint: Color used in the sidebar navigation and other elements.
order: 1
default: blue darken-2
control: color-material
tocPosition:
type: String
title: Table of Contents Position
hint: Select whether the table of contents is shown on the left, right or not at all.
order: 2
default: left
enum:
- left
- right
- hidden
This diff was suppressed by a .gitattributes entry.
extends master.pug extends base.pug
block body block body
#root #root
......
doctype html doctype html
html html(lang=siteConfig.lang)
head head
meta(http-equiv='X-UA-Compatible', content='IE=edge') meta(http-equiv='X-UA-Compatible', content='IE=edge')
meta(charset='UTF-8') meta(charset='UTF-8')
...@@ -21,12 +21,23 @@ html ...@@ -21,12 +21,23 @@ html
//- Favicon //- Favicon
link(rel='apple-touch-icon', sizes='180x180', href='/_assets/favicons/apple-touch-icon.png') link(rel='apple-touch-icon', sizes='180x180', href='/_assets/favicons/apple-touch-icon.png')
link(rel='icon', type='image/png', sizes='192x192', href='/_assets/favicons/android-icon-192x192.png') link(rel='icon', type='image/png', sizes='192x192', href='/_assets/favicons/android-chrome-192x192.png')
link(rel='icon', type='image/png', sizes='32x32', href='/_assets/favicons/favicon-32x32.png') link(rel='icon', type='image/png', sizes='32x32', href='/_assets/favicons/favicon-32x32.png')
link(rel='icon', type='image/png', sizes='16x16', href='/_assets/favicons/favicon-16x16.png') link(rel='icon', type='image/png', sizes='16x16', href='/_assets/favicons/favicon-16x16.png')
link(rel='mask-icon', href='/_assets/favicons/safari-pinned-tab.svg', color='#1976d2') link(rel='mask-icon', href='/_assets/favicons/safari-pinned-tab.svg', color='#1976d2')
link(rel='manifest', href='/_assets/manifest.json') link(rel='manifest', href='/_assets/manifest.json')
//- Site Properties
script.
var siteId = "!{siteId}"
var siteConfig = !{JSON.stringify(siteConfig)}
var siteLangs = !{JSON.stringify(langs)}
//- Dev Mode Warning
if devMode
script.
siteConfig.devMode = true
//- Icon Set //- Icon Set
if config.theming.iconset === 'fa' if config.theming.iconset === 'fa'
link( link(
...@@ -42,26 +53,24 @@ html ...@@ -42,26 +53,24 @@ html
) )
//- CSS //- CSS
<% for (var index in htmlWebpackPlugin.files.css) { %>
link(
type='text/css'
rel='stylesheet'
href='<%= htmlWebpackPlugin.files.css[index] %>'
)
<% } %>
script(
crossorigin='anonymous'
src='https://polyfill.io/v3/polyfill.min.js?features=EventSource'
)
//- JS //- JS
<% for (var index in htmlWebpackPlugin.files.js) { %>
script(
type='text/javascript'
src='/_assets-legacy/js/runtime.js'
)
script( script(
type='text/javascript' type='text/javascript'
src='<%= htmlWebpackPlugin.files.js[index] %>' src='/_assets-legacy/js/app.js'
) )
<% } %>
!= analyticsCode.head != analyticsCode.head
......
extends master.pug extends base.pug
block head block head
if injectCode.css if injectCode.css
......
extends master.pug extends base.pug
block body block body
#root.is-fullscreen #root.is-fullscreen
......
extends master.pug extends base.pug
block head block head
......
extends master.pug
block body
#root
.login-deprecated!= t('outdatedBrowserWarning', { modernBrowser: '<a href="https://bestvpn.org/outdatedbrowser/en" rel="nofollow">' + t('modernBrowser') + '</a>', interpolation: { escapeValue: false } })
.login
.login-dialog
if err
.login-error= err.message
form(method='post', action='/login')
h1= config.title
select(name='strategy')
each str in formStrategies
option(value=str.key, selected)= str.title
input(type='text', name='user', placeholder=t('auth:fields.emailUser'))
input(type='password', name='pass', placeholder=t('auth:fields.password'))
button(type='submit')= t('auth:actions.login')
if socialStrategies.length
.login-social
h2= t('auth:orLoginUsingStrategy')
each str in socialStrategies
a.login-social-icon(href='/login/' + str.key, class=str.color)
!= str.icon
extends master.pug
block head
if injectCode.css
style(type='text/css')!= injectCode.css
if injectCode.head
!= injectCode.head
block body
#root
.header
span.header-title= siteConfig.title
span.header-deprecated!= t('outdatedBrowserWarning', { modernBrowser: '<a href="https://bestvpn.org/outdatedbrowser/en" rel="nofollow">' + t('modernBrowser') + '</a>', interpolation: { escapeValue: false } })
span.header-login
if !isAuthenticated
a(href='/login', title='Login')
i.mdi.mdi-account-circle
else
a(href='/logout', title='Logout')
i.mdi.mdi-logout
.main
.sidebar
each navItem in sidebar
if navItem.k === 'link'
a.sidebar-link(href=navItem.t)
i.mdi(class=navItem.c)
span= navItem.l
else if navItem.k === 'divider'
.sidebar-divider
else if navItem.k === 'header'
.sidebar-title= navItem.l
.main-container
.page-header
.page-header-left
h1= page.title
h2= page.description
//- .page-header-right
//- .page-header-right-title Last edited by
//- .page-header-right-author= page.authorName
//- .page-header-right-updated= page.updatedAt
.page-contents.v-content
.contents
div!= page.render
if page.toc.length
.toc
.toc-title= t('page.toc')
each tocItem, tocIdx in page.toc
a.toc-tile(href=tocItem.anchor)
i.mdi.mdi-chevron-right
span= tocItem.title
if tocIdx < page.toc.length - 1 || tocItem.children.length
.toc-divider
each tocSubItem in tocItem.children
a.toc-tile.inset(href=tocSubItem.anchor)
i.mdi.mdi-chevron-right
span= tocSubItem.title
if tocIdx < page.toc.length - 1
.toc-divider.inset
if injectCode.body
!= injectCode.body
extends master.pug extends base.pug
block body block body
#root.is-fullscreen #root.is-fullscreen
......
extends master.pug extends base.pug
block body block body
#root.is-fullscreen #root.is-fullscreen
......
extends master.pug extends base.pug
block body block body
#root.is-fullscreen #root.is-fullscreen
......
extends master.pug extends base.pug
block head block head
if injectCode.css if injectCode.css
......
extends master.pug extends base.pug
block body block body
#root #root
......
extends master.pug extends base.pug
block body block body
#root.is-fullscreen #root.is-fullscreen
......
extends master.pug extends base.pug
block head block head
......
extends master.pug extends base.pug
block body block body
#root #root
......
extends master.pug extends base.pug
block body block body
#root.is-fullscreen #root.is-fullscreen
......
extends master.pug extends base.pug
block body block body
#root.is-fullscreen #root.is-fullscreen
......
...@@ -149,15 +149,20 @@ module.exports = async () => { ...@@ -149,15 +149,20 @@ module.exports = async () => {
// ---------------------------------------- // ----------------------------------------
app.use(async (req, res, next) => { app.use(async (req, res, next) => {
const currentSite = await WIKI.models.sites.getSiteByHostname({ hostname: req.hostname })
if (!currentSite) {
return res.status(404).send('Site Not Found')
}
res.locals.siteConfig = { res.locals.siteConfig = {
title: WIKI.config.title, id: currentSite.id,
theme: WIKI.config.theming.theme, title: currentSite.config.title,
darkMode: WIKI.config.theming.darkMode, darkMode: currentSite.config.theme.dark,
lang: WIKI.config.lang.code, lang: currentSite.config.locale,
rtl: WIKI.config.lang.rtl, rtl: false, // TODO: handle RTL
company: WIKI.config.company, company: currentSite.config.company,
contentLicense: WIKI.config.contentLicense, contentLicense: currentSite.config.contentLicense,
logoUrl: WIKI.config.logoUrl logoUrl: currentSite.config.logoUrl
} }
res.locals.langs = await WIKI.models.locales.getNavLocales({ cache: true }) res.locals.langs = await WIKI.models.locales.getNavLocales({ cache: true })
res.locals.analyticsCode = await WIKI.models.analytics.getCode({ cache: true }) res.locals.analyticsCode = await WIKI.models.analytics.getCode({ cache: true })
......
...@@ -68,6 +68,7 @@ module.exports = { ...@@ -68,6 +68,7 @@ module.exports = {
'prefer-promise-reject-errors': 'off', 'prefer-promise-reject-errors': 'off',
'no-unused-vars': 'off', 'no-unused-vars': 'off',
'vue/multi-word-component-names': 'off',
// allow debugger during development only // allow debugger during development only
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off' 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off'
......
...@@ -8,6 +8,9 @@ ...@@ -8,6 +8,9 @@
<meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width"> <meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width">
<title>Wiki.js</title> <title>Wiki.js</title>
<!--preload-links--> <!--preload-links-->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Rubik:wght@300..900&display=swap" rel="stylesheet">
<style type="text/css"> <style type="text/css">
@keyframes initspinner { @keyframes initspinner {
to { transform: rotate(360deg); } to { transform: rotate(360deg); }
......
...@@ -11,28 +11,28 @@ ...@@ -11,28 +11,28 @@
"lint": "eslint --ext .js,.vue ./" "lint": "eslint --ext .js,.vue ./"
}, },
"dependencies": { "dependencies": {
"@apollo/client": "3.6.8", "@apollo/client": "3.6.9",
"@codemirror/autocomplete": "6.0.2", "@codemirror/autocomplete": "6.0.2",
"@codemirror/basic-setup": "0.20.0", "@codemirror/basic-setup": "0.20.0",
"@codemirror/closebrackets": "0.19.2", "@codemirror/closebrackets": "0.19.2",
"@codemirror/commands": "6.0.0", "@codemirror/commands": "6.0.1",
"@codemirror/comment": "0.19.1", "@codemirror/comment": "0.19.1",
"@codemirror/fold": "0.19.4", "@codemirror/fold": "0.19.4",
"@codemirror/gutter": "0.19.9", "@codemirror/gutter": "0.19.9",
"@codemirror/highlight": "0.19.8", "@codemirror/highlight": "0.19.8",
"@codemirror/history": "0.19.2", "@codemirror/history": "0.19.2",
"@codemirror/lang-css": "6.0.0", "@codemirror/lang-css": "6.0.0",
"@codemirror/lang-html": "6.0.0", "@codemirror/lang-html": "6.1.0",
"@codemirror/lang-javascript": "6.0.0", "@codemirror/lang-javascript": "6.0.1",
"@codemirror/lang-json": "6.0.0", "@codemirror/lang-json": "6.0.0",
"@codemirror/lang-markdown": "6.0.0", "@codemirror/lang-markdown": "6.0.0",
"@codemirror/matchbrackets": "0.19.4", "@codemirror/matchbrackets": "0.19.4",
"@codemirror/search": "6.0.0", "@codemirror/search": "6.0.0",
"@codemirror/state": "6.0.1", "@codemirror/state": "6.0.1",
"@codemirror/tooltip": "0.19.16", "@codemirror/tooltip": "0.19.16",
"@codemirror/view": "6.0.1", "@codemirror/view": "6.0.2",
"@lezer/common": "1.0.0", "@lezer/common": "1.0.0",
"@quasar/extras": "1.14.0", "@quasar/extras": "1.14.2",
"@tiptap/core": "2.0.0-beta.176", "@tiptap/core": "2.0.0-beta.176",
"@tiptap/extension-code-block": "2.0.0-beta.37", "@tiptap/extension-code-block": "2.0.0-beta.37",
"@tiptap/extension-code-block-lowlight": "2.0.0-beta.68", "@tiptap/extension-code-block-lowlight": "2.0.0-beta.68",
...@@ -59,24 +59,23 @@ ...@@ -59,24 +59,23 @@
"@tiptap/vue-3": "2.0.0-beta.91", "@tiptap/vue-3": "2.0.0-beta.91",
"@vue/apollo-option": "4.0.0-alpha.17", "@vue/apollo-option": "4.0.0-alpha.17",
"apollo-upload-client": "17.0.0", "apollo-upload-client": "17.0.0",
"browser-fs-access": "0.29.6", "browser-fs-access": "0.30.2",
"clipboard": "2.0.11", "clipboard": "2.0.11",
"codemirror": "6.0.0", "codemirror": "6.0.1",
"filesize": "9.0.9", "filesize": "9.0.11",
"filesize-parser": "1.5.0", "filesize-parser": "1.5.0",
"graphql": "16.5.0", "graphql": "16.5.0",
"graphql-tag": "2.12.6", "graphql-tag": "2.12.6",
"js-cookie": "3.0.1", "js-cookie": "3.0.1",
"jwt-decode": "3.1.2", "jwt-decode": "3.1.2",
"lodash": "4.17.21",
"lodash-es": "4.17.21", "lodash-es": "4.17.21",
"luxon": "2.4.0", "luxon": "2.4.0",
"pinia": "2.0.14", "pinia": "2.0.14",
"pug": "3.0.2", "pug": "3.0.2",
"quasar": "2.7.3", "quasar": "2.7.4",
"tippy.js": "6.3.7", "tippy.js": "6.3.7",
"uuid": "8.3.2", "uuid": "8.3.2",
"v-network-graph": "0.5.19", "v-network-graph": "0.6.3",
"vue": "3.2.37", "vue": "3.2.37",
"vue-codemirror": "6.0.0", "vue-codemirror": "6.0.0",
"vue-i18n": "9.1.10", "vue-i18n": "9.1.10",
...@@ -86,10 +85,10 @@ ...@@ -86,10 +85,10 @@
}, },
"devDependencies": { "devDependencies": {
"@intlify/vite-plugin-vue-i18n": "3.4.0", "@intlify/vite-plugin-vue-i18n": "3.4.0",
"@quasar/app-vite": "1.0.2", "@quasar/app-vite": "1.0.4",
"@types/lodash": "4.14.182", "@types/lodash": "4.14.182",
"autoprefixer": "10.4.7", "browserlist": "latest",
"eslint": "8.18.0", "eslint": "8.19.0",
"eslint-config-standard": "17.0.0", "eslint-config-standard": "17.0.0",
"eslint-plugin-import": "2.26.0", "eslint-plugin-import": "2.26.0",
"eslint-plugin-n": "15.2.3", "eslint-plugin-n": "15.2.3",
......
...@@ -63,7 +63,7 @@ module.exports = configure(function (/* ctx */) { ...@@ -63,7 +63,7 @@ module.exports = configure(function (/* ctx */) {
vueRouterMode: 'history', // available values: 'hash', 'history' vueRouterMode: 'history', // available values: 'hash', 'history'
// vueRouterBase, // vueRouterBase,
// vueDevtools, // vueDevtools,
vueOptionsAPI: true, // vueOptionsAPI: true,
rebuildCache: true, // rebuilds Vite/linter/etc cache on startup rebuildCache: true, // rebuilds Vite/linter/etc cache on startup
......
<template> <template lang="pug">
<router-view /> router-view
</template> </template>
<script> <script setup>
import { defineComponent, nextTick, onMounted } from 'vue' import { nextTick, onMounted } from 'vue'
import { useSiteStore } from 'src/stores/site'
export default defineComponent({
name: 'App', /* global siteConfig */
setup () {
onMounted(() => { // STORES
nextTick(() => {
document.querySelector('.init-loading').remove() const siteStore = useSiteStore()
})
}) // INIT SITE STORE
}
if (typeof siteConfig !== 'undefined') {
siteStore.$patch({
id: siteConfig.id,
title: siteConfig.title
})
} else {
siteStore.loadSite(window.location.hostname)
}
// MOUNTED
onMounted(async () => {
nextTick(() => {
document.querySelector('.init-loading').remove()
})
}) })
</script> </script>
...@@ -494,9 +494,8 @@ q-layout(view='hHh lpR fFf', container) ...@@ -494,9 +494,8 @@ q-layout(view='hHh lpR fFf', container)
<script setup> <script setup>
import gql from 'graphql-tag' import gql from 'graphql-tag'
import { DateTime } from 'luxon' import { DateTime } from 'luxon'
import cloneDeep from 'lodash/cloneDeep'
import some from 'lodash/some'
import { v4 as uuid } from 'uuid' import { v4 as uuid } from 'uuid'
import { cloneDeep, some } from 'lodash-es'
import { fileOpen } from 'browser-fs-access' import { fileOpen } from 'browser-fs-access'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
......
...@@ -119,7 +119,7 @@ q-card.icon-picker(flat, style='width: 400px;') ...@@ -119,7 +119,7 @@ q-card.icon-picker(flat, style='width: 400px;')
</template> </template>
<script> <script>
import find from 'lodash/find' import { find } from 'lodash-es'
export default { export default {
props: { props: {
......
...@@ -161,8 +161,7 @@ q-card.page-datatmpl-dialog(style='width: 1100px; max-width: 1100px;') ...@@ -161,8 +161,7 @@ q-card.page-datatmpl-dialog(style='width: 1100px; max-width: 1100px;')
<script> <script>
import { get, sync } from 'vuex-pathify' import { get, sync } from 'vuex-pathify'
import { v4 as uuid } from 'uuid' import { v4 as uuid } from 'uuid'
import cloneDeep from 'lodash/cloneDeep' import { cloneDeep, sortBy } from 'lodash-es'
import sortBy from 'lodash/sortBy'
import draggable from 'vuedraggable' import draggable from 'vuedraggable'
export default { export default {
......
...@@ -114,8 +114,7 @@ q-card.page-relation-dialog(style='width: 500px;') ...@@ -114,8 +114,7 @@ q-card.page-relation-dialog(style='width: 500px;')
<script> <script>
import { v4 as uuid } from 'uuid' import { v4 as uuid } from 'uuid'
import find from 'lodash/find' import { cloneDeep, find } from 'lodash-es'
import cloneDeep from 'lodash/cloneDeep'
import IconPickerDialog from './IconPickerDialog.vue' import IconPickerDialog from './IconPickerDialog.vue'
......
...@@ -30,7 +30,7 @@ q-dialog(ref='dialogRef', @hide='onDialogHide') ...@@ -30,7 +30,7 @@ q-dialog(ref='dialogRef', @hide='onDialogHide')
<script setup> <script setup>
import gql from 'graphql-tag' import gql from 'graphql-tag'
import cloneDeep from 'lodash/cloneDeep' import { cloneDeep } from 'lodash-es'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { useDialogPluginComponent, useQuasar } from 'quasar' import { useDialogPluginComponent, useQuasar } from 'quasar'
import { reactive, ref } from 'vue' import { reactive, ref } from 'vue'
......
...@@ -69,7 +69,7 @@ q-dialog(ref='dialogRef', @hide='onDialogHide') ...@@ -69,7 +69,7 @@ q-dialog(ref='dialogRef', @hide='onDialogHide')
<script setup> <script setup>
import gql from 'graphql-tag' import gql from 'graphql-tag'
import sampleSize from 'lodash/sampleSize' import { sampleSize } from 'lodash-es'
import zxcvbn from 'zxcvbn' import zxcvbn from 'zxcvbn'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
......
...@@ -161,9 +161,8 @@ q-dialog(ref='dialogRef', @hide='onDialogHide') ...@@ -161,9 +161,8 @@ q-dialog(ref='dialogRef', @hide='onDialogHide')
<script setup> <script setup>
import gql from 'graphql-tag' import gql from 'graphql-tag'
import sampleSize from 'lodash/sampleSize' import { cloneDeep, sampleSize } from 'lodash-es'
import zxcvbn from 'zxcvbn' import zxcvbn from 'zxcvbn'
import cloneDeep from 'lodash/cloneDeep'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { useDialogPluginComponent, useQuasar } from 'quasar' import { useDialogPluginComponent, useQuasar } from 'quasar'
import { computed, onMounted, reactive, ref } from 'vue' import { computed, onMounted, reactive, ref } from 'vue'
......
...@@ -488,12 +488,7 @@ q-layout(view='hHh lpR fFf', container) ...@@ -488,12 +488,7 @@ q-layout(view='hHh lpR fFf', container)
<script setup> <script setup>
import gql from 'graphql-tag' import gql from 'graphql-tag'
import cloneDeep from 'lodash/cloneDeep' import { cloneDeep, find, findKey, map, some } from 'lodash-es'
import some from 'lodash/some'
import find from 'lodash/find'
import findKey from 'lodash/findKey'
import _get from 'lodash/get'
import map from 'lodash/map'
import { DateTime } from 'luxon' import { DateTime } from 'luxon'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
......
...@@ -194,7 +194,7 @@ q-dialog(ref='dialogRef', @hide='onDialogHide') ...@@ -194,7 +194,7 @@ q-dialog(ref='dialogRef', @hide='onDialogHide')
<script setup> <script setup>
import gql from 'graphql-tag' import gql from 'graphql-tag'
import cloneDeep from 'lodash/cloneDeep' import { cloneDeep } from 'lodash-es'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { useDialogPluginComponent, useQuasar } from 'quasar' import { useDialogPluginComponent, useQuasar } from 'quasar'
import { computed, onMounted, reactive, ref } from 'vue' import { computed, onMounted, reactive, ref } from 'vue'
......
...@@ -33,7 +33,7 @@ body::-webkit-scrollbar-thumb { ...@@ -33,7 +33,7 @@ body::-webkit-scrollbar-thumb {
} }
.font-poppins { .font-poppins {
font-family: 'Poppins', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; font-family: $font-poppins;
} }
@font-face { @font-face {
...@@ -117,7 +117,7 @@ body::-webkit-scrollbar-thumb { ...@@ -117,7 +117,7 @@ body::-webkit-scrollbar-thumb {
// BUTTONS // BUTTONS
// ------------------------------------------------------------------ // ------------------------------------------------------------------
body.desktop .acrylic-btn { #app .acrylic-btn {
.q-focus-helper { .q-focus-helper {
background-color: currentColor; background-color: currentColor;
opacity: .1; opacity: .1;
......
...@@ -30,3 +30,7 @@ $dark-6: #070a0d; ...@@ -30,3 +30,7 @@ $dark-6: #070a0d;
$dark-5: #0d1117; $dark-5: #0d1117;
$dark-4: #161b22; $dark-4: #161b22;
$dark-3: #1e232a; $dark-3: #1e232a;
// -- FONTS --
$font-poppins: 'Poppins', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
...@@ -300,7 +300,7 @@ ...@@ -300,7 +300,7 @@
"admin.login.logoutRedirect": "Logout Redirect", "admin.login.logoutRedirect": "Logout Redirect",
"admin.login.logoutRedirectHint": "Optionally redirect the user to a specific page when he/she logouts. This can be overridden at the group level.", "admin.login.logoutRedirectHint": "Optionally redirect the user to a specific page when he/she logouts. This can be overridden at the group level.",
"admin.login.providers": "Login Providers", "admin.login.providers": "Login Providers",
"admin.login.providersVisbleWarning": "Note that you can always temporarily show all hidden providers by adding ?all to the url. This is useful to login as local admin while hiding it from normal users.", "admin.login.providersVisbleWarning": "Note that you can always temporarily show all hidden providers by adding ?all=1 to the url. This is useful to login as local admin while hiding it from normal users.",
"admin.login.subtitle": "Configure the login user experience of your wiki site", "admin.login.subtitle": "Configure the login user experience of your wiki site",
"admin.login.title": "Login", "admin.login.title": "Login",
"admin.login.welcomeRedirect": "First-time Login Redirect", "admin.login.welcomeRedirect": "First-time Login Redirect",
...@@ -671,7 +671,7 @@ ...@@ -671,7 +671,7 @@
"admin.theme.bodyHtmlInjectionHint": "HTML code to be injected just before the closing body tag.", "admin.theme.bodyHtmlInjectionHint": "HTML code to be injected just before the closing body tag.",
"admin.theme.codeInjection": "Code Injection", "admin.theme.codeInjection": "Code Injection",
"admin.theme.cssOverride": "CSS Override", "admin.theme.cssOverride": "CSS Override",
"admin.theme.cssOverrideHint": "CSS code to inject after system default CSS. Consider using custom themes if you have a large amount of css code. Injecting too much CSS code will result in poor page load performance! CSS will automatically be minified.", "admin.theme.cssOverrideHint": "CSS code to inject after system default CSS. Injecting too much CSS code can result in poor page load performance! CSS will automatically be minified.",
"admin.theme.cssOverrideWarning": "{caution} When adding styles for page content, you must scope them to the {cssClass} class. Omitting this could break the layout of the editor!", "admin.theme.cssOverrideWarning": "{caution} When adding styles for page content, you must scope them to the {cssClass} class. Omitting this could break the layout of the editor!",
"admin.theme.cssOverrideWarningCaution": "CAUTION:", "admin.theme.cssOverrideWarningCaution": "CAUTION:",
"admin.theme.darkMode": "Dark Mode", "admin.theme.darkMode": "Dark Mode",
......
...@@ -79,7 +79,7 @@ q-layout.admin(view='hHh Lpr lff') ...@@ -79,7 +79,7 @@ q-layout.admin(view='hHh Lpr lff')
q-item-section(avatar) q-item-section(avatar)
q-icon(name='img:/_assets/icons/fluent-comments.svg') q-icon(name='img:/_assets/icons/fluent-comments.svg')
q-item-section {{ t('admin.comments.title') }} q-item-section {{ t('admin.comments.title') }}
q-item(:to='`/_admin/` + adminStore.currentSiteId + `/editors`', v-ripple, active-class='bg-primary text-white') q-item(:to='`/_admin/` + adminStore.currentSiteId + `/editors`', v-ripple, active-class='bg-primary text-white', disabled)
q-item-section(avatar) q-item-section(avatar)
q-icon(name='img:/_assets/icons/fluent-cashbook.svg') q-icon(name='img:/_assets/icons/fluent-cashbook.svg')
q-item-section {{ t('admin.editors.title') }} q-item-section {{ t('admin.editors.title') }}
......
...@@ -4,45 +4,78 @@ q-layout(view='hHh lpr lff') ...@@ -4,45 +4,78 @@ q-layout(view='hHh lpr lff')
router-view router-view
</template> </template>
<script> <script setup>
export default {
name: 'AuthLayout',
data () {
return {
bgUrl: '_assets/bg/login-v3.jpg'
// bgUrl: 'https://docs.requarks.io/_assets/img/splash/1.jpg'
}
}
}
</script> </script>
<style lang="scss"> <style lang="scss">
.auth { .auth {
background-color: #FFF; background-color: #FFF;
background-size: cover;
background-position: center center;
height: 100vh;
display: flex; display: flex;
justify-content: center; font-family: 'Rubik', sans-serif;
align-items: center;
@at-root .body--dark & {
&-box { background-color: $dark-6;
background-color: rgba(255,255,255,.25); }
backdrop-filter: blur(5px);
-webkit-backdrop-filter: blur(5px); &-content {
padding: 32px; flex: 1 0 100%;
border-radius: 8px; width: 100%;
width: 500px; max-width: 500px;
max-width: 95vw; padding: 3rem 4rem;
display: flex;
@at-root .no-backdropfilter & { flex-direction: column;
background-color: rgba(255,255,255,.95); 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;
@media (max-width: $breakpoint-md-max) { img {
margin-left: 0; position: relative;
width: 100%; width: 100%;
height: 100%;
object-fit: cover;
top: 0;
bottom: 0;
left: 0;
right: 0;
margin: 0;
padding: 0;
} }
} }
} }
......
...@@ -32,23 +32,30 @@ q-page.admin-navigation ...@@ -32,23 +32,30 @@ q-page.admin-navigation
) )
q-separator(inset) q-separator(inset)
.row.q-pa-md.q-col-gutter-md .row.q-pa-md.q-col-gutter-md
.col-6 .col-auto
//- v-container(fluid, grid-list-lg) q-card.q-mt-sm {{t('admin.navigation.mode')}}
//- v-layout(row wrap)
//- v-flex(xs12) q-card.bg-dark.q-mt-sm
//- .admin-header q-list(
//- img.animated.fadeInUp(src='/_assets/svg/icon-triangle-arrow.svg', alt='Navigation', style='width: 80px;') style='min-width: 350px;'
//- .admin-header-title padding
//- .headline.primary--text.animated.fadeInLeft {{$t('navigation.title')}} dark
//- .subtitle-1.grey--text.animated.fadeInLeft.wait-p4s {{$t('navigation.subtitle')}} )
//- v-spacer q-item
//- v-btn.animated.fadeInDown.wait-p3s(icon, outlined, color='grey', href='https://docs.requarks.io/navigation', target='_blank') q-item-section
//- v-icon mdi-help-circle q-select(
//- v-btn.mx-3.animated.fadeInDown.wait-p2s.mr-3(icon, outlined, color='grey', @click='refresh') dark
//- v-icon mdi-refresh outlined
//- v-btn.animated.fadeInDown(color='success', depressed, @click='save', large) option-value='value'
//- v-icon(left) mdi-check option-label='text'
//- span {{$t('common.actions.apply')}} emit-value
map-options
dense
options-dense
:label='t(`admin.navigation.mode`)'
:aria-label='t(`admin.navigation.mode`)'
)
//- v-container.pa-0.mt-3(fluid, grid-list-lg) //- v-container.pa-0.mt-3(fluid, grid-list-lg)
//- v-row(dense) //- v-row(dense)
//- v-col(cols='3') //- v-col(cols='3')
......
...@@ -165,14 +165,7 @@ q-page.admin-mail ...@@ -165,14 +165,7 @@ q-page.admin-mail
</template> </template>
<script> <script>
import cloneDeep from 'lodash/cloneDeep' import { cloneDeep, concat, filter, find, findIndex, reduce, reverse, sortBy } from 'lodash-es'
import concat from 'lodash/concat'
import filter from 'lodash/filter'
import find from 'lodash/find'
import findIndex from 'lodash/findIndex'
import reduce from 'lodash/reduce'
import reverse from 'lodash/reverse'
import sortBy from 'lodash/sortBy'
import { DepGraph } from 'dependency-graph' import { DepGraph } from 'dependency-graph'
import gql from 'graphql-tag' import gql from 'graphql-tag'
......
<template lang='pug'> <template lang='pug'>
.auth(:style='`background-image: url(` + bgUrl + `);`') .auth
.auth-box .auth-content
.flex.mb-5 .auth-logo
.auth-login-logo img(src='/_assets/logo-wikijs.svg' :alt='siteStore.title')
q-avatar(square, size='34px') h2.auth-site-title {{ siteStore.title }}
q-img(:src='logoUrl') p.text-grey-7 Login to continue
.auth-login-title template(v-if='state.strategies?.length > 1')
.text-h6.text-grey-9 {{ siteTitle }} p Sign in with
.auth-strategies
q-banner.bg-red-7.text-white.q-mt-md( q-btn(
v-if='errorShown' label='GitHub'
transition='slide-y-reverse-transition' icon='lab la-github'
dense push
no-caps
:color='$q.dark.isActive ? `blue-grey-9` : `grey-1`'
:text-color='$q.dark.isActive ? `white` : `blue-grey-9`'
)
q-btn(
label='Google'
icon='lab la-google-plus'
push
no-caps
:color='$q.dark.isActive ? `blue-grey-9` : `grey-1`'
:text-color='$q.dark.isActive ? `white` : `blue-grey-9`'
)
q-btn(
label='Twitter'
icon='lab la-twitter'
push
no-caps
:color='$q.dark.isActive ? `blue-grey-9` : `grey-1`'
:text-color='$q.dark.isActive ? `white` : `blue-grey-9`'
)
q-btn(
label='Local'
icon='las la-seedling'
push
color='primary'
no-caps
)
q-form.q-mt-md
q-input(
outlined
label='Email Address'
autocomplete='email'
)
template(#prepend)
i.las.la-user
q-input.q-mt-sm(
outlined
label='Password'
type='password'
autocomplete='current-password'
)
template(#prepend)
i.las.la-key
q-btn.full-width.q-mt-sm(
push
color='primary'
label='Login'
no-caps
icon='las la-sign-in-alt'
) )
template(v-slot:avatar) q-separator.q-my-md
q-icon.q-pl-sm(name='las la-exclamation-triangle', size='sm') q-btn.acrylic-btn.full-width(
span {{errorMessage}} flat
//------------------------------------------------- color='primary'
//- PROVIDERS LIST label='Create an Account'
//------------------------------------------------- no-caps
template(v-if='screen === `login` && strategies.length > 1') icon='las la-user-plus'
.auth-login-subtitle )
.text-subtitle1 {{$t('auth.selectAuthProvider')}} q-btn.acrylic-btn.full-width.q-mt-sm(
.auth-login-list flat
q-list.bg-white.shadow-2.rounded-borders.q-pa-sm(separator) color='primary'
q-item.rounded-borders( label='Forgot Password'
clickable no-caps
v-ripple icon='las la-life-ring'
v-for='(stg, idx) of filteredStrategies' )
:key='stg.key' .auth-bg(aria-hidden="true")
@click='selectedStrategyKey = stg.key' img(src='https://docs.requarks.io/_assets/img/splash/1.jpg' alt='')
:class='stg.key === selectedStrategyKey ? `bg-primary text-white` : ``'
)
q-item-section(avatar)
q-avatar.mr-3(:color='stg.strategy.color', rounded, size='32px')
div(v-html='stg.strategy.icon')
q-item-section
span.text-none {{stg.displayName}}
//-------------------------------------------------
//- LOGIN FORM
//-------------------------------------------------
template(v-if='screen === `login` && selectedStrategy.strategy.useForm')
.auth-login-subtitle
.text-subtitle1 {{$t('auth.enterCredentials')}}
.auth-login-form
q-input.text-black(
outlined
bg-color='white'
ref='iptEmail'
v-model='username'
:label='isUsernameEmail ? $t(`auth.fields.email`) : $t(`auth.fields.username`)'
:type='isUsernameEmail ? `email` : `text`'
:autocomplete='isUsernameEmail ? `email` : `username`'
)
template(v-slot:prepend)
q-icon(name='las la-user-circle', color='primary')
q-input.q-mt-sm(
outlined
bg-color='white'
ref='iptPassword'
v-model='password'
:type='hidePassword ? "password" : "text"'
:label='$t("auth:fields.password")'
autocomplete='current-password'
@keyup.enter='login'
)
template(v-slot:prepend)
q-icon(name='las la-key', color='primary')
template(v-slot:append)
q-icon.cursor-pointer(
:name='hidePassword ? "las la-eye-slash" : "las la-eye"'
@click='() => (hidePassword = !hidePassword)'
)
q-btn.q-mt-sm.q-py-xs.full-width(
no-caps
color='blue-7'
push
@click='login'
:loading='isLoading'
:label='$t(`auth.actions.login`)'
icon='las la-arrow-right'
)
.text-center.q-mt-lg
q-btn(
flat
no-caps
rounded
color='grey-8'
@click.stop.prevent='forgotPassword'
href='#forgot'
): .text-caption {{ $t('auth.forgotPasswordLink') }}
q-btn(
v-if='selectedStrategyKey === `local` && selectedStrategy.selfRegistration'
color='indigo darken-2'
flat
no-caps
rounded
href='/register'
): .text-caption {{ $t('auth.switchToRegister.link') }}
</template> </template>
<script> <script setup>
import { get } from 'vuex-pathify'
import gql from 'graphql-tag' import gql from 'graphql-tag'
import find from 'lodash/find' import { find, has, head, reject, sortBy } from 'lodash-es'
import _get from 'lodash/get'
import has from 'lodash/has'
import head from 'lodash/head'
import reject from 'lodash/reject'
import sortBy from 'lodash/sortBy'
import Cookies from 'js-cookie' import Cookies from 'js-cookie'
export default { import { useI18n } from 'vue-i18n'
name: 'PageLogin', import { useMeta, useQuasar } from 'quasar'
i18nOptions: { namespaces: 'auth' }, import { onMounted, reactive, watch } from 'vue'
data () {
return { import { useSiteStore } from 'src/stores/site'
error: false, import { useDataStore } from 'src/stores/data'
strategies: [],
selectedStrategyKey: 'unselected', // QUASAR
selectedStrategy: { key: 'unselected', strategy: { useForm: false, usernameType: 'email' } },
screen: 'login', const $q = useQuasar()
username: '',
password: '', // STORES
hidePassword: true,
securityCode: '', const siteStore = useSiteStore()
continuationToken: '', const dataStore = useDataStore()
isLoading: false,
loaderColor: 'grey darken-4', // I18N
loaderTitle: 'Working...',
isShown: false, const { t } = useI18n()
newPassword: '',
newPasswordVerify: '', // META
isTFAShown: false,
isTFASetupShown: false, useMeta({
tfaQRImage: '', title: t('auth.login.title')
errorShown: false, })
errorMessage: '',
bgUrl: '_assets/bg/login-v3.jpg' // DATA
}
}, const state = reactive({
computed: { error: false,
logoUrl: get('site/logoUrl', false), strategies: [],
siteTitle: get('site/title', false), selectedStrategyKey: 'unselected',
isSocialShown () { selectedStrategy: { key: 'unselected', strategy: { useForm: false, usernameType: 'email' } },
return this.strategies.length > 1 screen: 'login',
}, username: '',
filteredStrategies () { password: '',
const qParams = new URLSearchParams(!import.meta.env.SSR ? window.location.search : '') hidePassword: true,
if (this.hideLocal && !qParams.has('all')) { securityCode: '',
return reject(this.strategies, ['key', 'local']) continuationToken: '',
} else { isLoading: false,
return this.strategies loaderColor: 'grey darken-4',
} loaderTitle: 'Working...',
}, isShown: false,
isUsernameEmail () { newPassword: '',
return this.selectedStrategy.strategy.usernameType === 'email' newPasswordVerify: '',
} isTFAShown: false,
}, isTFASetupShown: false,
watch: { tfaQRImage: '',
filteredStrategies (newValue, oldValue) { errorShown: false,
if (head(newValue).strategy.useForm) { errorMessage: '',
this.selectedStrategyKey = head(newValue).key bgUrl: '_assets/bg/login-v3.jpg'
} })
},
selectedStrategyKey (newValue, oldValue) { // isSocialShown () {
this.selectedStrategy = find(this.strategies, ['key', newValue]) // return this.strategies.length > 1
if (this.screen === 'changePwd') { // }
return // filteredStrategies () {
} // const qParams = new URLSearchParams(!import.meta.env.SSR ? window.location.search : '')
this.screen = 'login' // if (this.hideLocal && !qParams.has('all')) {
if (!this.selectedStrategy.strategy.useForm) { // return reject(this.strategies, ['key', 'local'])
this.isLoading = true // } else {
window.location.assign('/login/' + newValue) // return this.strategies
} else { // }
this.$nextTick(() => { // }
this.$refs.iptEmail.focus() // isUsernameEmail () {
}) // return this.selectedStrategy.strategy.usernameType === 'email'
// }
// filteredStrategies (newValue, oldValue) {
// if (head(newValue).strategy.useForm) {
// this.selectedStrategyKey = head(newValue).key
// }
// }
// selectedStrategyKey (newValue, oldValue) {
// this.selectedStrategy = find(this.strategies, ['key', newValue])
// if (this.screen === 'changePwd') {
// return
// }
// this.screen = 'login'
// if (!this.selectedStrategy.strategy.useForm) {
// this.isLoading = true
// window.location.assign('/login/' + newValue)
// } else {
// this.$nextTick(() => {
// this.$refs.iptEmail.focus()
// })
// }
// }
// mounted () {
// this.isShown = true
// if (this.changePwdContinuationToken) {
// this.screen = 'changePwd'
// this.continuationToken = this.changePwdContinuationToken
// }
// }
// METHODS
async function fetchStrategies () {
const resp = await APOLLO_CLIENT.query({
query: gql`
query loginFetchSiteStrategies(
$siteId: UUID
) {
authStrategies(
siteId: $siteId
enabledOnly: true
) {
key
strategy {
key
logo
color
icon
useForm
usernameType
}
displayName
order
selfRegistration
}
} }
`,
variables: {
siteId: siteStore.id
} }
}, })
mounted () { }
this.isShown = true
if (this.changePwdContinuationToken) { /**
this.screen = 'changePwd' * LOGIN
this.continuationToken = this.changePwdContinuationToken */
} async function login () {
}, this.errorShown = false
methods: { if (this.username.length < 2) {
/** this.errorMessage = t('auth.invalidEmailUsername')
* LOGIN this.errorShown = true
*/ this.$refs.iptEmail.focus()
async login () { } else if (this.password.length < 2) {
this.errorShown = false this.errorMessage = t('auth.invalidPassword')
if (this.username.length < 2) { this.errorShown = true
this.errorMessage = this.$t('auth.invalidEmailUsername') this.$refs.iptPassword.focus()
this.errorShown = true } else {
this.$refs.iptEmail.focus() this.loaderColor = 'grey darken-4'
} else if (this.password.length < 2) { this.loaderTitle = t('auth.signingIn')
this.errorMessage = this.$t('auth.invalidPassword') this.isLoading = true
this.errorShown = true try {
this.$refs.iptPassword.focus() const resp = await this.$apollo.mutate({
} else { mutation: gql`
this.loaderColor = 'grey darken-4' mutation($username: String!, $password: String!, $strategy: String!) {
this.loaderTitle = this.$t('auth.signingIn') authentication {
this.isLoading = true login(username: $username, password: $password, strategy: $strategy) {
try { responseResult {
const resp = await this.$apollo.mutate({ succeeded
mutation: gql` errorCode
mutation($username: String!, $password: String!, $strategy: String!) { slug
authentication { message
login(username: $username, password: $password, strategy: $strategy) {
responseResult {
succeeded
errorCode
slug
message
}
jwt
mustChangePwd
mustProvideTFA
mustSetupTFA
continuationToken
redirect
tfaQRImage
}
} }
jwt
mustChangePwd
mustProvideTFA
mustSetupTFA
continuationToken
redirect
tfaQRImage
} }
`,
variables: {
username: this.username,
password: this.password,
strategy: this.selectedStrategy.key
}
})
if (has(resp, 'data.authentication.login')) {
const respObj = _get(resp, 'data.authentication.login', {})
if (respObj.responseResult.succeeded === true) {
this.handleLoginResponse(respObj)
} else {
throw new Error(respObj.responseResult.message)
} }
} else {
throw new Error(this.$t('auth.genericError'))
} }
} catch (err) { `,
console.error(err) variables: {
this.$q.notify({ username: this.username,
type: 'negative', password: this.password,
message: err.message strategy: this.selectedStrategy.key
})
this.isLoading = false
} }
} })
}, if (has(resp, 'data.authentication.login')) {
/** const respObj = resp?.data?.authentication?.login ?? {}
* VERIFY TFA CODE if (respObj.responseResult.succeeded === true) {
*/ this.handleLoginResponse(respObj)
async verifySecurityCode (setup = false) {
if (this.securityCode.length !== 6) {
this.$store.commit('showNotification', {
style: 'red',
message: 'Enter a valid security code.',
icon: 'alert'
})
if (setup) {
this.$refs.iptTFASetup.focus()
} else { } else {
this.$refs.iptTFA.focus() throw new Error(respObj.responseResult.message)
} }
} else { } else {
this.loaderColor = 'grey darken-4' throw new Error(t('auth.genericError'))
this.loaderTitle = this.$t('auth.signingIn')
this.isLoading = true
try {
const resp = await this.$apollo.mutate({
mutation: gql`
mutation(
$continuationToken: String!
$securityCode: String!
$setup: Boolean
) {
authentication {
loginTFA(
continuationToken: $continuationToken
securityCode: $securityCode
setup: $setup
) {
responseResult {
succeeded
errorCode
slug
message
}
jwt
mustChangePwd
continuationToken
redirect
}
}
}
`,
variables: {
continuationToken: this.continuationToken,
securityCode: this.securityCode,
setup
}
})
if (has(resp, 'data.authentication.loginTFA')) {
const respObj = _get(resp, 'data.authentication.loginTFA', {})
if (respObj.responseResult.succeeded === true) {
this.handleLoginResponse(respObj)
} else {
if (!setup) {
this.isTFAShown = false
}
throw new Error(respObj.responseResult.message)
}
} else {
throw new Error(this.$t('auth.genericError'))
}
} catch (err) {
console.error(err)
this.$q.notify({
type: 'negative',
message: err.message
})
this.isLoading = false
}
} }
}, } catch (err) {
/** console.error(err)
* CHANGE PASSWORD this.$q.notify({
*/ type: 'negative',
async changePassword () { message: err.message
this.loaderColor = 'grey darken-4' })
this.loaderTitle = this.$t('auth.changePwd.loading') this.isLoading = false
this.isLoading = true }
try { }
const resp = await this.$apollo.mutate({ }
mutation: gql`
mutation ( /**
$continuationToken: String! * VERIFY TFA CODE
$newPassword: String! */
async function verifySecurityCode (setup = false) {
if (this.securityCode.length !== 6) {
this.$store.commit('showNotification', {
style: 'red',
message: 'Enter a valid security code.',
icon: 'alert'
})
if (setup) {
this.$refs.iptTFASetup.focus()
} else {
this.$refs.iptTFA.focus()
}
} else {
this.loaderColor = 'grey darken-4'
this.loaderTitle = t('auth.signingIn')
this.isLoading = true
try {
const resp = await this.$apollo.mutate({
mutation: gql`
mutation(
$continuationToken: String!
$securityCode: String!
$setup: Boolean
) { ) {
authentication { authentication {
loginChangePassword ( loginTFA(
continuationToken: $continuationToken continuationToken: $continuationToken
newPassword: $newPassword securityCode: $securityCode
setup: $setup
) { ) {
responseResult { responseResult {
succeeded succeeded
errorCode errorCode
slug slug
message message
}
jwt
continuationToken
redirect
} }
jwt
mustChangePwd
continuationToken
redirect
} }
} }
`,
variables: {
continuationToken: this.continuationToken,
newPassword: this.newPassword
}
})
if (has(resp, 'data.authentication.loginChangePassword')) {
const respObj = _get(resp, 'data.authentication.loginChangePassword', {})
if (respObj.responseResult.succeeded === true) {
this.handleLoginResponse(respObj)
} else {
throw new Error(respObj.responseResult.message)
} }
`,
variables: {
continuationToken: this.continuationToken,
securityCode: this.securityCode,
setup
}
})
if (has(resp, 'data.authentication.loginTFA')) {
const respObj = resp?.data?.authentication?.loginTFA ?? {}
if (respObj.responseResult.succeeded === true) {
this.handleLoginResponse(respObj)
} else { } else {
throw new Error(this.$t('auth.genericError')) if (!setup) {
this.isTFAShown = false
}
throw new Error(respObj.responseResult.message)
} }
} catch (err) { } else {
console.error(err) throw new Error(t('auth.genericError'))
this.$store.commit('showNotification', {
style: 'red',
message: err.message,
icon: 'alert'
})
this.isLoading = false
} }
}, } catch (err) {
/** console.error(err)
* SWITCH TO FORGOT PASSWORD SCREEN this.$q.notify({
*/ type: 'negative',
forgotPassword () { message: err.message
this.screen = 'forgot'
this.$nextTick(() => {
this.$refs.iptForgotPwdEmail.focus()
}) })
}, this.isLoading = false
/** }
* FORGOT PASSWORD SUBMIT }
*/ }
async forgotPasswordSubmit () {
this.loaderColor = 'grey darken-4' /**
this.loaderTitle = this.$t('auth.forgotPasswordLoading') * CHANGE PASSWORD
this.isLoading = true */
try { async function changePassword () {
const resp = await this.$apollo.mutate({ this.loaderColor = 'grey darken-4'
mutation: gql` this.loaderTitle = t('auth.changePwd.loading')
mutation ( this.isLoading = true
$email: String! try {
const resp = await this.$apollo.mutate({
mutation: gql`
mutation (
$continuationToken: String!
$newPassword: String!
) {
authentication {
loginChangePassword (
continuationToken: $continuationToken
newPassword: $newPassword
) { ) {
authentication { responseResult {
forgotPassword ( succeeded
email: $email errorCode
) { slug
responseResult { message
succeeded
errorCode
slug
message
}
}
} }
jwt
continuationToken
redirect
} }
`,
variables: {
email: this.username
} }
})
if (has(resp, 'data.authentication.forgotPassword.responseResult')) {
const respObj = _get(resp, 'data.authentication.forgotPassword.responseResult', {})
if (respObj.succeeded === true) {
this.$store.commit('showNotification', {
style: 'success',
message: this.$t('auth.forgotPasswordSuccess'),
icon: 'email'
})
this.screen = 'login'
} else {
throw new Error(respObj.message)
}
} else {
throw new Error(this.$t('auth.genericError'))
} }
} catch (err) { `,
console.error(err) variables: {
this.$store.commit('showNotification', { continuationToken: this.continuationToken,
style: 'red', newPassword: this.newPassword
message: err.message,
icon: 'alert'
})
} }
this.isLoading = false })
}, if (has(resp, 'data.authentication.loginChangePassword')) {
handleLoginResponse (respObj) { const respObj = resp?.data?.authentication?.loginChangePassword ?? {}
this.continuationToken = respObj.continuationToken if (respObj.responseResult.succeeded === true) {
if (respObj.mustChangePwd === true) { this.handleLoginResponse(respObj)
this.screen = 'changePwd'
this.$nextTick(() => {
this.$refs.iptNewPassword.focus()
})
this.isLoading = false
} else if (respObj.mustProvideTFA === true) {
this.securityCode = ''
this.isTFAShown = true
setTimeout(() => {
this.$refs.iptTFA.focus()
}, 500)
this.isLoading = false
} else if (respObj.mustSetupTFA === true) {
this.securityCode = ''
this.isTFASetupShown = true
this.tfaQRImage = respObj.tfaQRImage
setTimeout(() => {
this.$refs.iptTFASetup.focus()
}, 500)
this.isLoading = false
} else { } else {
this.loaderColor = 'green darken-1' throw new Error(respObj.responseResult.message)
this.loaderTitle = this.$t('auth.loginSuccess')
Cookies.set('jwt', respObj.jwt, { expires: 365 })
setTimeout(() => {
const loginRedirect = Cookies.get('loginRedirect')
if (loginRedirect === '/' && respObj.redirect) {
Cookies.remove('loginRedirect')
window.location.replace(respObj.redirect)
} else if (loginRedirect) {
Cookies.remove('loginRedirect')
window.location.replace(loginRedirect)
} else if (respObj.redirect) {
window.location.replace(respObj.redirect)
} else {
window.location.replace('/')
}
}, 1000)
} }
} else {
throw new Error(t('auth.genericError'))
} }
}, } catch (err) {
apollo: { console.error(err)
strategies: { this.$store.commit('showNotification', {
prefetch: false, style: 'red',
query: gql` message: err.message,
{ icon: 'alert'
})
this.isLoading = false
}
}
/**
* SWITCH TO FORGOT PASSWORD SCREEN
*/
function forgotPassword () {
this.screen = 'forgot'
this.$nextTick(() => {
this.$refs.iptForgotPwdEmail.focus()
})
}
/**
* FORGOT PASSWORD SUBMIT
*/
async function forgotPasswordSubmit () {
this.loaderColor = 'grey darken-4'
this.loaderTitle = t('auth.forgotPasswordLoading')
this.isLoading = true
try {
const resp = await this.$apollo.mutate({
mutation: gql`
mutation (
$email: String!
) {
authentication { authentication {
activeStrategies(enabledOnly: true) { forgotPassword (
key email: $email
strategy { ) {
key responseResult {
logo succeeded
color errorCode
icon slug
useForm message
usernameType
} }
displayName
order
selfRegistration
} }
} }
} }
`, `,
update: (data) => sortBy(data.authentication.activeStrategies, ['order']), variables: {
watchLoading (isLoading) { email: this.username
this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'login-strategies-refresh') }
})
if (has(resp, 'data.authentication.forgotPassword.responseResult')) {
const respObj = resp?.data?.authentication?.forgotPassword?.responseResult ?? {}
if (respObj.succeeded === true) {
this.$store.commit('showNotification', {
style: 'success',
message: t('auth.forgotPasswordSuccess'),
icon: 'email'
})
this.screen = 'login'
} else {
throw new Error(respObj.message)
} }
} else {
throw new Error(t('auth.genericError'))
} }
} catch (err) {
console.error(err)
this.$store.commit('showNotification', {
style: 'red',
message: err.message,
icon: 'alert'
})
} }
this.isLoading = false
} }
</script>
<style lang="scss">
.auth-login {
&-logo {
padding: 12px 0 0 12px;
width: 58px;
height: 58px;
background-color: #000;
margin-left: 12px;
border-radius: 7px;
}
&-title {
height: 58px;
padding-left: 12px;
display: flex;
align-items: center;
text-shadow: .5px .5px #FFF;
}
&-subtitle {
padding: 24px 12px 12px 12px;
color: #111;
font-weight: 500;
text-shadow: 1px 1px rgba(255,255,255,.5);
background-image: linear-gradient(to bottom, rgba(0,0,0,0), rgba(0,0,0,.15));
text-align: center;
border-bottom: 1px solid rgba(0,0,0,.3);
}
&-info {
border-top: 1px solid rgba(255,255,255,.85);
background-color: rgba(255,255,255,.15);
border-bottom: 1px solid rgba(0,0,0,.15);
padding: 12px;
font-size: 13px;
text-align: center;
color: mc('grey', '900');
}
&-list {
border-top: 1px solid rgba(255,255,255,.85);
padding: 12px;
}
&-form { function handleLoginResponse (respObj) {
padding: 12px; this.continuationToken = respObj.continuationToken
border-top: 1px solid rgba(255,255,255,.85); if (respObj.mustChangePwd === true) {
} this.screen = 'changePwd'
this.$nextTick(() => {
&-main { this.$refs.iptNewPassword.focus()
flex: 1 0 100vw; })
height: 100vh; this.isLoading = false
} } else if (respObj.mustProvideTFA === true) {
this.securityCode = ''
&-tfa { this.isTFAShown = true
background-color: #EEE; setTimeout(() => {
border: 7px solid #FFF; this.$refs.iptTFA.focus()
}, 500)
&-field input { this.isLoading = false
text-align: center; } else if (respObj.mustSetupTFA === true) {
} this.securityCode = ''
this.isTFASetupShown = true
&-qr { this.tfaQRImage = respObj.tfaQRImage
background-color: #FFF; setTimeout(() => {
padding: 5px; this.$refs.iptTFASetup.focus()
border-radius: 5px; }, 500)
width: 200px; this.isLoading = false
height: 200px; } else {
margin: 0 auto; this.loaderColor = 'green darken-1'
this.loaderTitle = t('auth.loginSuccess')
Cookies.set('jwt', respObj.jwt, { expires: 365 })
setTimeout(() => {
const loginRedirect = Cookies.get('loginRedirect')
if (loginRedirect === '/' && respObj.redirect) {
Cookies.remove('loginRedirect')
window.location.replace(respObj.redirect)
} else if (loginRedirect) {
Cookies.remove('loginRedirect')
window.location.replace(loginRedirect)
} else if (respObj.redirect) {
window.location.replace(respObj.redirect)
} else {
window.location.replace('/')
} }
} }, 1000)
} }
}
onMounted(() => {
fetchStrategies()
})
</script>
<style lang="scss">
</style> </style>
...@@ -8,13 +8,13 @@ const routes = [ ...@@ -8,13 +8,13 @@ const routes = [
// { path: 'n/:editor?', component: () => import('../pages/Index.vue') } // { path: 'n/:editor?', component: () => import('../pages/Index.vue') }
// ] // ]
// }, // },
// { {
// path: '/login', path: '/login',
// component: () => import('../layouts/AuthLayout.vue'), component: () => import('../layouts/AuthLayout.vue'),
// children: [ children: [
// { path: '', component: () => import('../pages/Login.vue') } { path: '', component: () => import('../pages/Login.vue') }
// ] ]
// }, },
// { // {
// path: '/p', // path: '/p',
// component: () => import('../layouts/ProfileLayout.vue'), // component: () => import('../layouts/ProfileLayout.vue'),
......
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import gql from 'graphql-tag' import gql from 'graphql-tag'
import cloneDeep from 'lodash/cloneDeep' import { cloneDeep } from 'lodash-es'
/* global APOLLO_CLIENT */ /* global APOLLO_CLIENT */
......
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import gql from 'graphql-tag' import gql from 'graphql-tag'
import clone from 'lodash/clone' import { clone } from 'lodash-es'
export const useSiteStore = defineStore('site', { export const useSiteStore = defineStore('site', {
state: () => ({ state: () => ({
......
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