feat: error pages + update docs links in admin pages

pull/5698/head
Nicolas Giard 2 years ago
parent 468aebe2a8
commit 82ebac2dd6
No known key found for this signature in database
GPG Key ID: 85061B8F9D55B7C8

@ -6,6 +6,7 @@ const BruteKnex = require('../helpers/brute-knex')
const router = express.Router()
const moment = require('moment')
const _ = require('lodash')
const path = require('path')
const bruteforce = new ExpressBrute(new BruteKnex({
createTable: true,
@ -23,28 +24,16 @@ const bruteforce = new ExpressBrute(new BruteKnex({
* Login form
*/
router.get('/login', async (req, res, next) => {
_.set(res.locals, 'pageMeta.title', 'Login')
if (req.query.legacy || (req.get('user-agent') && req.get('user-agent').indexOf('Trident') >= 0)) {
const { formStrategies, socialStrategies } = await WIKI.models.authentication.getStrategiesForLegacyClient()
res.render('legacy/login', {
err: false,
formStrategies,
socialStrategies
})
} else {
// -> Bypass Login
if (WIKI.config.auth.autoLogin && !req.query.all) {
const stg = await WIKI.models.authentication.query().orderBy('order').first()
const stgInfo = _.find(WIKI.data.authentication, ['key', stg.strategyKey])
if (!stgInfo.useForm) {
return res.redirect(`/login/${stg.key}`)
}
// -> Bypass Login
if (WIKI.config.auth.autoLogin && !req.query.all) {
const stg = await WIKI.models.authentication.query().orderBy('order').first()
const stgInfo = _.find(WIKI.data.authentication, ['key', stg.strategyKey])
if (!stgInfo.useForm) {
return res.redirect(`/login/${stg.key}`)
}
// -> Show Login
const bgUrl = !_.isEmpty(WIKI.config.auth.loginBgUrl) ? WIKI.config.auth.loginBgUrl : '/_assets/img/splash/1.jpg'
res.render('login', { bgUrl, hideLocal: WIKI.config.auth.hideLocal })
}
// -> Show Login
res.sendFile(path.join(WIKI.ROOTPATH, 'assets/index.html'))
})
/**
@ -89,35 +78,6 @@ router.all('/login/:strategy/callback', async (req, res, next) => {
}
})
/**
* LEGACY - Login form handling
*/
router.post('/login', bruteforce.prevent, async (req, res, next) => {
_.set(res.locals, 'pageMeta.title', 'Login')
if (req.query.legacy || req.get('user-agent').indexOf('Trident') >= 0) {
try {
const authResult = await WIKI.models.users.login({
strategy: req.body.strategy,
username: req.body.user,
password: req.body.pass
}, { req, res })
req.brute.reset()
res.cookie('jwt', authResult.jwt, { expires: moment().add(1, 'y').toDate() })
res.redirect('/')
} catch (err) {
const { formStrategies, socialStrategies } = await WIKI.models.authentication.getStrategiesForLegacyClient()
res.render('legacy/login', {
err,
formStrategies,
socialStrategies
})
}
} else {
res.redirect('/login')
}
})
/**
* Logout
*/
@ -135,7 +95,7 @@ router.get('/register', async (req, res, next) => {
_.set(res.locals, 'pageMeta.title', 'Register')
const localStrg = await WIKI.models.authentication.getStrategy('local')
if (localStrg.selfRegistration) {
res.render('register')
res.sendFile(path.join(WIKI.ROOTPATH, 'assets/index.html'))
} else {
next(new WIKI.Error.AuthRegistrationDisabled())
}

@ -34,9 +34,16 @@ router.get('/healthz', (req, res, next) => {
})
/**
* Administration
* New v3 vue app
*/
router.get(['/_admin', '/_admin/*'], (req, res, next) => {
router.get([
'/_admin',
'/_admin/*',
'/_profile',
'/_profile/*',
'/_error',
'/_error/*'
], (req, res, next) => {
res.sendFile(path.join(WIKI.ROOTPATH, 'assets/index.html'))
})
// router.get(['/_admin', '/_admin/*'], (req, res, next) => {
@ -318,18 +325,6 @@ router.get(['/i', '/i/:id'], async (req, res, next) => {
}
})
/**
* Profile
*/
router.get(['/p', '/p/*'], (req, res, next) => {
if (!req.user || req.user.id < 1 || req.user.id === 2) {
return res.render('unauthorized', { action: 'view' })
}
_.set(res.locals, 'pageMeta.title', 'User Profile')
res.render('profile')
})
/**
* Source
*/
@ -453,10 +448,7 @@ router.get('/*', async (req, res, next) => {
if (pageArgs.path === 'home' && req.user.id === 2) {
return res.redirect('/login')
}
_.set(res.locals, 'pageMeta.title', 'Unauthorized')
return res.status(403).render('unauthorized', {
action: 'view'
})
return res.redirect(`/_error/unauthorized?from=${req.path}`)
}
_.set(res, 'locals.siteConfig.lang', pageArgs.locale)
@ -510,52 +502,37 @@ router.get('/*', async (req, res, next) => {
injectCode.body = `${injectCode.body}\n${page.extra.js}`
}
if (req.query.legacy || req.get('user-agent').indexOf('Trident') >= 0) {
// -> Convert page TOC
if (_.isString(page.toc)) {
page.toc = JSON.parse(page.toc)
}
// -> Render legacy view
res.render('legacy/page', {
page,
sidebar,
injectCode,
isAuthenticated: req.user && req.user.id !== 2
})
} else {
// -> Convert page TOC
if (!_.isString(page.toc)) {
page.toc = JSON.stringify(page.toc)
}
// -> Inject comments variables
const commentTmpl = {
codeTemplate: WIKI.data.commentProvider.codeTemplate,
head: WIKI.data.commentProvider.head,
body: WIKI.data.commentProvider.body,
main: WIKI.data.commentProvider.main
}
if (WIKI.config.features.featurePageComments && WIKI.data.commentProvider.codeTemplate) {
[
{ key: 'pageUrl', value: `${WIKI.config.host}/i/${page.id}` },
{ key: 'pageId', value: page.id }
].forEach((cfg) => {
commentTmpl.head = _.replace(commentTmpl.head, new RegExp(`{{${cfg.key}}}`, 'g'), cfg.value)
commentTmpl.body = _.replace(commentTmpl.body, new RegExp(`{{${cfg.key}}}`, 'g'), cfg.value)
commentTmpl.main = _.replace(commentTmpl.main, new RegExp(`{{${cfg.key}}}`, 'g'), cfg.value)
})
}
// -> Render view
res.render('page', {
page,
sidebar,
injectCode,
comments: commentTmpl,
effectivePermissions
// -> Convert page TOC
if (!_.isString(page.toc)) {
page.toc = JSON.stringify(page.toc)
}
// -> Inject comments variables
const commentTmpl = {
codeTemplate: WIKI.data.commentProvider.codeTemplate,
head: WIKI.data.commentProvider.head,
body: WIKI.data.commentProvider.body,
main: WIKI.data.commentProvider.main
}
if (WIKI.config.features.featurePageComments && WIKI.data.commentProvider.codeTemplate) {
[
{ key: 'pageUrl', value: `${WIKI.config.host}/i/${page.id}` },
{ key: 'pageId', value: page.id }
].forEach((cfg) => {
commentTmpl.head = _.replace(commentTmpl.head, new RegExp(`{{${cfg.key}}}`, 'g'), cfg.value)
commentTmpl.body = _.replace(commentTmpl.body, new RegExp(`{{${cfg.key}}}`, 'g'), cfg.value)
commentTmpl.main = _.replace(commentTmpl.main, new RegExp(`{{${cfg.key}}}`, 'g'), cfg.value)
})
}
// -> Render view
res.render('page', {
page,
sidebar,
injectCode,
comments: commentTmpl,
effectivePermissions
})
} else if (pageArgs.path === 'home') {
_.set(res.locals, 'pageMeta.title', 'Welcome')
res.render('welcome', { locale: pageArgs.locale })

@ -95,7 +95,7 @@ module.exports = {
return DateTime.fromISO(WIKI.system.updates.releaseDate).toJSDate()
},
mailConfigured () {
return false // TODO: return true if mail is setup
return WIKI.config?.mail?.host?.length > 2
},
nodeVersion () {
return process.version.substr(1)

@ -1,5 +0,0 @@
extends base.pug
block body
#root
admin

@ -1,9 +0,0 @@
extends base.pug
block body
#root.is-fullscreen
login(
bg-url=bgUrl
hide-local=hideLocal
change-pwd-continuation-token=changePwdContinuationToken
)

@ -1,5 +0,0 @@
extends base.pug
block body
#root.is-fullscreen
not-found

@ -1,5 +0,0 @@
extends base.pug
block body
#root
profile

@ -1,5 +0,0 @@
extends base.pug
block body
#root.is-fullscreen
register

@ -1,5 +0,0 @@
extends base.pug
block body
#root.is-fullscreen
unauthorized(action=action)

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="96px" height="96px"><defs><linearGradient id="YP84gtDqSIMPo~qxagCUGa" x1="16.442" x2="35.628" y1="3.234" y2="55.948" data-name="Безымянный градиент 100" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#21ad64"/><stop offset="1" stop-color="#088242"/></linearGradient></defs><path fill="url(#YP84gtDqSIMPo~qxagCUGa)" d="M42,8V40a2.00591,2.00591,0,0,1-2,2H8a2.00591,2.00591,0,0,1-2-2V15.83a2.00616,2.00616,0,0,1,.59-1.42L14.41,6.59A2.00614,2.00614,0,0,1,15.83,6H40A2.00587,2.00587,0,0,1,42,8Z"/><path fill="#50d18d" d="M12,36V17.41L17.41,12H36V30H18V20.41406L20.41406,18H30v6H28V22a1.07539,1.07539,0,0,0-1-1H24.41418a1,1,0,0,0-.70709.29291l-1.41418,1.41418A1,1,0,0,0,22,23.41418V25a.99974.99974,0,0,0,1,1h8a.99974.99974,0,0,0,1-1V17a.99974.99974,0,0,0-1-1H20a.99928.99928,0,0,0-.707.293l-3,3A1.00012,1.00012,0,0,0,16,20V31a.99974.99974,0,0,0,1,1H37a.99974.99974,0,0,0,1-1V11a1.003,1.003,0,0,0-1-1H17a1.03276,1.03276,0,0,0-.71.29l-6,6A1.03276,1.03276,0,0,0,10,17V37a1.003,1.003,0,0,0,1,1H42V36Z"/></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

@ -638,7 +638,6 @@ const permissions = [
warning: true,
restrictedForSystem: true,
disabled: true
}
]

@ -161,7 +161,7 @@ q-dialog(ref='dialogRef', @hide='onDialogHide')
<script setup>
import gql from 'graphql-tag'
import { cloneDeep, sampleSize } from 'lodash-es'
import { cloneDeep, sample, sampleSize } from 'lodash-es'
import zxcvbn from 'zxcvbn'
import { useI18n } from 'vue-i18n'
import { useDialogPluginComponent, useQuasar } from 'quasar'
@ -287,8 +287,9 @@ async function loadGroups () {
}
function randomizePassword () {
const pwdChars = 'abcdefghkmnpqrstuvwxyzABCDEFHJKLMNPQRSTUVWXYZ23456789_*=?#!()+'
state.userPassword = sampleSize(pwdChars, 16).join('')
const pwdChars = 'abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789' // omit easily confused chars like O,0 or I,1,l
const withSymbols = `${pwdChars}_*=?#!()+-$%&.`
state.userPassword = `${sample(pwdChars)}${sampleSize(withSymbols, 15).join('')}`
}
async function create () {

@ -58,6 +58,8 @@ body::-webkit-scrollbar-thumb {
.bg-dark-5 { background-color: #0d1117; }
.bg-dark-4 { background-color: #161b22; }
.bg-dark-3 { background-color: #1e232a; }
.bg-dark-2 { background-color: #292f39; }
.bg-dark-1 { background-color: #343b48; }
// ------------------------------------------------------------------
// FORMS

@ -30,3 +30,5 @@ $dark-6: #070a0d;
$dark-5: #0d1117;
$dark-4: #161b22;
$dark-3: #1e232a;
$dark-2: #292f39;
$dark-1: #343b48;

@ -498,7 +498,7 @@
"admin.storage.actionsInactiveWarn": "You must enable this storage target and apply changes before you can run actions.",
"admin.storage.addTarget": "Add Storage Target",
"admin.storage.assetDelivery": "Asset Delivery",
"admin.storage.assetDeliveryHint": "Select how uploaded assets should be delivered to the user. Note that not all storage origins support asset delivery and can only be used for backup purposes.",
"admin.storage.assetDeliveryHint": "Select how uploaded assets should be delivered to the user. Note that not all storage origins support asset delivery and some can only be used for backup purposes.",
"admin.storage.assetDirectAccess": "Direct Access",
"admin.storage.assetDirectAccessHint": "Assets are accessed directly by the user using a secure / signed link. When enabled, takes priority over file streaming.",
"admin.storage.assetDirectAccessNotSupported": "Not supported by this storage target.",
@ -1467,5 +1467,13 @@
"admin.api.createSuccess": "API Key created successfully.",
"admin.api.revoked": "Revoked",
"admin.api.revokedHint": "This key has been revoked and can no longer be used.",
"admin.storage.setupRequired": "Setup required"
"admin.storage.setupRequired": "Setup required",
"admin.blocks.title": "Content Blocks",
"common.error.title": "Error",
"common.error.unauthorized.title": "Unauthorized",
"common.error.unauthorized.hint": "You don't have the required permissions to access this page.",
"common.error.generic.title": "Unexpected Error",
"common.error.generic.hint": "Oops, something went wrong...",
"common.error.notfound.title": "Not Found",
"common.error.notfound.hint": "That page doesn't exist or is not available."
}

@ -82,6 +82,10 @@ q-layout.admin(view='hHh Lpr lff')
q-item-section(avatar)
q-icon(name='img:/_assets/icons/fluent-comments.svg')
q-item-section {{ t('admin.comments.title') }}
q-item(:to='`/_admin/` + adminStore.currentSiteId + `/blocks`', v-ripple, active-class='bg-primary text-white', disabled)
q-item-section(avatar)
q-icon(name='img:/_assets/icons/fluent-rfid-tag.svg')
q-item-section {{ t('admin.blocks.title') }}
q-item(:to='`/_admin/` + adminStore.currentSiteId + `/editors`', v-ripple, active-class='bg-primary text-white', disabled)
q-item-section(avatar)
q-icon(name='img:/_assets/icons/fluent-cashbook.svg')

@ -19,7 +19,7 @@ q-page.admin-api
icon='las la-question-circle'
flat
color='grey'
href='https://docs.js.wiki/admin/api'
:href='siteStore.docsBase + `/dev/api`'
target='_blank'
type='a'
)
@ -103,10 +103,18 @@ import { DateTime } from 'luxon'
import ApiKeyCreateDialog from '../components/ApiKeyCreateDialog.vue'
import ApiKeyRevokeDialog from '../components/ApiKeyRevokeDialog.vue'
import { useAdminStore } from 'src/stores/admin'
import { useSiteStore } from 'src/stores/site'
// QUASAR
const $q = useQuasar()
// STORES
const adminStore = useAdminStore()
const siteStore = useSiteStore()
// I18N
const { t } = useI18n()
@ -154,6 +162,7 @@ async function load () {
})
state.keys = cloneDeep(resp?.data?.apiKeys) ?? []
state.enabled = resp?.data?.apiState === true
adminStore.info.isApiEnabled = state.enabled
$q.loading.hide()
state.loading--
}

@ -11,7 +11,7 @@ q-page.admin-mail
icon='las la-question-circle'
flat
color='grey'
href='https://docs.js.wiki/admin/auth'
:href='siteStore.docsBase + `/admin/auth`'
target='_blank'
type='a'
)

@ -11,7 +11,7 @@ q-page.admin-flags
icon='las la-question-circle'
flat
color='grey'
href='https://docs.js.wiki/admin/editors'
:href='siteStore.docsBase + `/admin/editors`'
target='_blank'
type='a'
)
@ -70,7 +70,13 @@ import { useMeta } from 'quasar'
import { useI18n } from 'vue-i18n'
import { defineAsyncComponent, onMounted, reactive, ref, watch } from 'vue'
// STORES / ROUTERS / i18n
import { useSiteStore } from 'src/stores/site'
// STORES
const siteStore = useSiteStore()
// I18N
const { t } = useI18n()

@ -11,7 +11,7 @@ q-page.admin-extensions
icon='las la-question-circle'
flat
color='grey'
href='https://docs.js.wiki/admin/extensions'
:href='siteStore.docsBase + `/system/extensions`'
target='_blank'
type='a'
)

@ -11,7 +11,7 @@ q-page.admin-flags
icon='las la-question-circle'
flat
color='grey'
href='https://docs.js.wiki/admin/flags'
:href='siteStore.docsBase + `/system/flags`'
target='_blank'
type='a'
)
@ -90,11 +90,17 @@ import { transform } from 'lodash-es'
import { useMeta, useQuasar } from 'quasar'
import { useI18n } from 'vue-i18n'
import { useSiteStore } from 'src/stores/site'
// QUASAR
const $q = useQuasar()
// STORES / ROUTERS / i18n
// STORES
const siteStore = useSiteStore()
// I18N
const { t } = useI18n()

@ -11,7 +11,7 @@ q-page.admin-general
icon='las la-question-circle'
flat
color='grey'
href='https://docs.js.wiki/admin/general'
:href='siteStore.docsBase + `/admin/sites`'
target='_blank'
type='a'
)
@ -770,10 +770,17 @@ onMounted(() => {
&-favicontabs {
overflow: hidden;
border-radius: 5px;
background-color: rgba(0,0,0,.1);
display: flex;
padding: 5px 5px 0 12px;
@at-root .body--light & {
background-color: rgba(0,0,0,.1);
}
@at-root .body--dark & {
background-color: rgba(255,255,255,.1);
}
> div {
display: flex;
padding: 4px 12px;

@ -20,7 +20,7 @@ q-page.admin-groups
flat
color='grey'
type='a'
href='https://docs.js.wiki/admin/groups'
:href='siteStore.docsBase + `/admin/groups`'
target='_blank'
)
q-btn.q-mr-sm.acrylic-btn(
@ -100,6 +100,7 @@ import { computed, onBeforeUnmount, onMounted, reactive, watch } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { useAdminStore } from 'src/stores/admin'
import { useSiteStore } from 'src/stores/site'
import GroupCreateDialog from '../components/GroupCreateDialog.vue'
import GroupDeleteDialog from '../components/GroupDeleteDialog.vue'
@ -111,6 +112,7 @@ const $q = useQuasar()
// STORES
const adminStore = useAdminStore()
const siteStore = useSiteStore()
// ROUTER

@ -20,7 +20,7 @@ q-page.admin-locale
icon='las la-question-circle'
flat
color='grey'
href='https://docs.js.wiki/admin/locale'
:href='siteStore.docsBase + `/admin/locale`'
target='_blank'
type='a'
)

@ -11,7 +11,7 @@ q-page.admin-login
icon='las la-question-circle'
flat
color='grey'
href='https://docs.js.wiki/admin/login'
:href='siteStore.docsBase + `/admin/auth`'
target='_blank'
type='a'
)
@ -185,6 +185,7 @@ import { useMeta, useQuasar } from 'quasar'
import { computed, onMounted, reactive, watch } from 'vue'
import { useAdminStore } from 'src/stores/admin'
import { useSiteStore } from 'src/stores/site'
// QUASAR
@ -193,6 +194,7 @@ const $q = useQuasar()
// STORES
const adminStore = useAdminStore()
const siteStore = useSiteStore()
// I18N

@ -11,7 +11,7 @@ q-page.admin-mail
icon='las la-question-circle'
flat
color='grey'
href='https://docs.js.wiki/admin/mail'
:href='siteStore.docsBase + `/system/mail`'
target='_blank'
type='a'
)
@ -380,6 +380,7 @@ async function load () {
throw new Error('Failed to fetch mail config.')
}
state.config = cloneDeep(resp.data.mailConfig)
adminStore.info.isMailConfigured = state.config?.host?.length > 2
} catch (err) {
$q.notify({
type: 'negative',
@ -455,6 +456,7 @@ async function save () {
type: 'positive',
message: t('admin.mail.saveSuccess')
})
adminStore.info.isMailConfigured = state.config?.host?.length > 2
} catch (err) {
$q.notify({
type: 'negative',

@ -11,7 +11,7 @@ q-page.admin-navigation
icon='las la-question-circle'
flat
color='grey'
href='https://docs.requarks.io/admin/navigation'
:href='siteStore.docsBase + `/admin/navigation`'
target='_blank'
type='a'
)

@ -11,7 +11,7 @@ q-page.admin-mail
icon='las la-question-circle'
flat
color='grey'
href='https://docs.js.wiki/admin/security'
:href='siteStore.docsBase + `/system/security`'
target='_blank'
type='a'
)

@ -12,7 +12,7 @@ q-page.admin-locale
flat
color='grey'
type='a'
href='https://docs.js.wiki/admin/sites'
:href='siteStore.docsBase + `/admin/sites`'
target='_blank'
)
q-btn.q-mr-sm.acrylic-btn(
@ -104,6 +104,7 @@ import { nextTick, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { useAdminStore } from '../stores/admin'
import { useSiteStore } from 'src/stores/site'
// COMPONENTS
@ -118,6 +119,7 @@ const $q = useQuasar()
// STORES
const adminStore = useAdminStore()
const siteStore = useSiteStore()
// ROUTER

@ -16,7 +16,10 @@ q-page.admin-storage
v-model='state.displayMode'
push
no-caps
toggle-color='black'
:toggle-color='$q.dark.isActive ? `white` : `black`'
:toggle-text-color='$q.dark.isActive ? `black` : `white`'
:text-color='$q.dark.isActive ? `white` : `black`'
:color='$q.dark.isActive ? `dark-1` : `white`'
:options=`[
{ label: t('admin.storage.targets'), value: 'targets' },
{ label: t('admin.storage.deliveryPaths'), value: 'delivery' }
@ -27,7 +30,7 @@ q-page.admin-storage
icon='las la-question-circle'
flat
color='grey'
href='https://docs.js.wiki/admin/storage'
:href='siteStore.docsBase + `/admin/storage`'
target='_blank'
type='a'
)
@ -512,7 +515,7 @@ q-page.admin-storage
:edges='state.deliveryEdges'
:paths='state.deliveryPaths'
:layouts='state.deliveryLayouts'
style='height: 600px;'
style='height: 600px; background-color: #FFF;'
)
template(#override-node='{ nodeId, scale, config, ...slotProps }')
rect(

@ -11,7 +11,7 @@ q-page.admin-system
icon='las la-question-circle'
flat
color='grey'
href='https://docs.js.wiki/admin/system'
:href='siteStore.docsBase + `/system/`'
target='_blank'
type='a'
)
@ -243,12 +243,18 @@ import { useMeta, useQuasar } from 'quasar'
import { computed, onMounted, reactive, ref, watch } from 'vue'
import ClipboardJS from 'clipboard'
import { useSiteStore } from 'src/stores/site'
import CheckUpdateDialog from '../components/CheckUpdateDialog.vue'
// QUASAR
const $q = useQuasar()
// STORES
const siteStore = useSiteStore()
// I18N
const { t } = useI18n()

@ -12,7 +12,7 @@ q-page.admin-theme
flat
color='grey'
type='a'
href='https://docs.js.wiki/admin/theme'
:href='siteStore.docsBase + `/admin/theme`'
target='_blank'
)
q-btn.q-mr-sm.acrylic-btn(

@ -20,7 +20,7 @@ q-page.admin-groups
flat
color='grey'
type='a'
href='https://docs.js.wiki/admin/users'
:href='siteStore.docsBase + `/admin/groups`'
target='_blank'
)
q-btn.q-mr-sm.acrylic-btn(
@ -114,6 +114,7 @@ import { onBeforeUnmount, onMounted, reactive, watch } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { useAdminStore } from 'src/stores/admin'
import { useSiteStore } from 'src/stores/site'
import UserCreateDialog from '../components/UserCreateDialog.vue'
@ -124,6 +125,7 @@ const $q = useQuasar()
// STORES
const adminStore = useAdminStore()
const siteStore = useSiteStore()
// ROUTER

@ -11,7 +11,7 @@ q-page.admin-utilities
icon='las la-question-circle'
flat
color='grey'
href='https://docs.js.wiki/admin/utilities'
:href='siteStore.docsBase + `/admin/utilities`'
target='_blank'
type='a'
)
@ -104,11 +104,17 @@ import { computed, reactive } from 'vue'
import { useMeta, useQuasar } from 'quasar'
import { useI18n } from 'vue-i18n'
import { useSiteStore } from 'src/stores/site'
// QUASAR
const $q = useQuasar()
// STORES / ROUTERS / i18n
// STORES
const siteStore = useSiteStore()
// I18N
const { t } = useI18n()

@ -11,7 +11,7 @@ q-page.admin-webhooks
icon='las la-question-circle'
flat
color='grey'
href='https://docs.js.wiki/admin/webhooks'
:href='siteStore.docsBase + `/system/webhooks`'
target='_blank'
type='a'
)
@ -99,6 +99,8 @@ import { useI18n } from 'vue-i18n'
import { useMeta, useQuasar } from 'quasar'
import { onMounted, reactive } from 'vue'
import { useSiteStore } from 'src/stores/site'
import WebhookEditDialog from 'src/components/WebhookEditDialog.vue'
import WebhookDeleteDialog from 'src/components/WebhookDeleteDialog.vue'
@ -106,6 +108,10 @@ import WebhookDeleteDialog from 'src/components/WebhookDeleteDialog.vue'
const $q = useQuasar()
// STORES
const siteStore = useSiteStore()
// I18N
const { t } = useI18n()

@ -0,0 +1,137 @@
<template lang='pug'>
.errorpage
.errorpage-bg
.errorpage-content
.errorpage-code {{error.code}}
.errorpage-title {{error.title}}
.errorpage-hint {{error.hint}}
.errorpage-actions
q-btn(
push
color='primary'
label='Go to home'
icon='las la-home'
to='/'
)
q-btn.q-ml-md(
v-if='error.showLoginBtn'
push
color='primary'
label='Login As...'
icon='las la-sign-in-alt'
to='/login'
)
</template>
<script setup>
import { useI18n } from 'vue-i18n'
import { useMeta } from 'quasar'
import { computed } from 'vue'
import { useRoute } from 'vue-router'
const actions = {
unauthorized: {
code: 403,
showLoginBtn: true
},
notfound: {
code: 404
},
generic: {
code: '!?0'
}
}
// ROUTER
const route = useRoute()
// I18N
const { t } = useI18n()
// META
useMeta({
title: t('common.error.title')
})
// COMPUTED
const error = computed(() => {
if (route.params.action && actions[route.params.action]) {
return {
...actions[route.params.action],
title: t(`common.error.${route.params.action}.title`),
hint: t(`common.error.${route.params.action}.hint`)
}
} else {
return {
...actions.generic,
title: t('common.error.generic.title'),
hint: t('common.error.generic.hint')
}
}
})
</script>
<style lang="scss">
.errorpage {
background: $dark-6 radial-gradient(ellipse, $dark-4, $dark-6);
color: #FFF;
height: 100vh;
&-bg {
position: absolute;
top: 50%;
left: 50%;
width: 320px;
height: 320px;
background: linear-gradient(0, transparent 50%, $red-9 50%);
border-radius: 50%;
filter: blur(80px);
transform: translate(-50%, -50%);
visibility: hidden;
}
&-content {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
&-code {
font-size: 12rem;
line-height: 12rem;
font-weight: 700;
background: linear-gradient(45deg, $red-9, $red-3);
background-clip: text;
-webkit-text-fill-color: transparent;
user-select: none;
}
&-title {
font-size: 5rem;
font-weight: 500;
line-height: 5rem;
}
&-hint {
font-size: 1.2rem;
font-weight: 500;
color: $red-3;
line-height: 1.2rem;
margin-top: 1rem;
}
&-actions {
margin-top: 2rem;
}
}
</style>

@ -10,50 +10,54 @@ const routes = [
// },
{
path: '/login',
component: () => import('../layouts/AuthLayout.vue'),
component: () => import('layouts/AuthLayout.vue'),
children: [
{ path: '', component: () => import('../pages/Login.vue') }
{ path: '', component: () => import('pages/Login.vue') }
]
},
{
path: '/_profile',
component: () => import('../layouts/ProfileLayout.vue'),
component: () => import('layouts/ProfileLayout.vue'),
children: [
{ path: '', redirect: '/_profile/info' },
{ path: 'info', component: () => import('../pages/Profile.vue') }
{ path: 'info', component: () => import('pages/Profile.vue') }
]
},
{
path: '/_admin',
component: () => import('../layouts/AdminLayout.vue'),
component: () => import('layouts/AdminLayout.vue'),
children: [
{ path: '', redirect: '/_admin/dashboard' },
{ path: 'dashboard', component: () => import('../pages/AdminDashboard.vue') },
{ path: 'sites', component: () => import('../pages/AdminSites.vue') },
{ path: 'dashboard', component: () => import('pages/AdminDashboard.vue') },
{ path: 'sites', component: () => import('pages/AdminSites.vue') },
// -> Site
{ path: ':siteid/general', component: () => import('../pages/AdminGeneral.vue') },
{ path: ':siteid/editors', component: () => import('../pages/AdminEditors.vue') },
{ path: ':siteid/locale', component: () => import('../pages/AdminLocale.vue') },
{ path: ':siteid/login', component: () => import('../pages/AdminLogin.vue') },
{ path: ':siteid/navigation', component: () => import('../pages/AdminNavigation.vue') },
// { path: ':siteid/rendering', component: () => import('../pages/AdminRendering.vue') },
{ path: ':siteid/storage/:id?', component: () => import('../pages/AdminStorage.vue') },
{ path: ':siteid/theme', component: () => import('../pages/AdminTheme.vue') },
{ path: ':siteid/general', component: () => import('pages/AdminGeneral.vue') },
{ path: ':siteid/editors', component: () => import('pages/AdminEditors.vue') },
{ path: ':siteid/locale', component: () => import('pages/AdminLocale.vue') },
{ path: ':siteid/login', component: () => import('pages/AdminLogin.vue') },
{ path: ':siteid/navigation', component: () => import('pages/AdminNavigation.vue') },
// { path: ':siteid/rendering', component: () => import('pages/AdminRendering.vue') },
{ path: ':siteid/storage/:id?', component: () => import('pages/AdminStorage.vue') },
{ path: ':siteid/theme', component: () => import('pages/AdminTheme.vue') },
// -> Users
{ path: 'auth', component: () => import('../pages/AdminAuth.vue') },
{ path: 'groups/:id?/:section?', component: () => import('../pages/AdminGroups.vue') },
{ path: 'users/:id?/:section?', component: () => import('../pages/AdminUsers.vue') },
{ path: 'auth', component: () => import('pages/AdminAuth.vue') },
{ path: 'groups/:id?/:section?', component: () => import('pages/AdminGroups.vue') },
{ path: 'users/:id?/:section?', component: () => import('pages/AdminUsers.vue') },
// -> System
{ path: 'api', component: () => import('../pages/AdminApi.vue') },
{ path: 'extensions', component: () => import('../pages/AdminExtensions.vue') },
{ path: 'mail', component: () => import('../pages/AdminMail.vue') },
{ path: 'security', component: () => import('../pages/AdminSecurity.vue') },
{ path: 'system', component: () => import('../pages/AdminSystem.vue') },
{ path: 'utilities', component: () => import('../pages/AdminUtilities.vue') },
{ path: 'webhooks', component: () => import('../pages/AdminWebhooks.vue') },
{ path: 'flags', component: () => import('../pages/AdminFlags.vue') }
{ path: 'api', component: () => import('pages/AdminApi.vue') },
{ path: 'extensions', component: () => import('pages/AdminExtensions.vue') },
{ path: 'mail', component: () => import('pages/AdminMail.vue') },
{ path: 'security', component: () => import('pages/AdminSecurity.vue') },
{ path: 'system', component: () => import('pages/AdminSystem.vue') },
{ path: 'utilities', component: () => import('pages/AdminUtilities.vue') },
{ path: 'webhooks', component: () => import('pages/AdminWebhooks.vue') },
{ path: 'flags', component: () => import('pages/AdminFlags.vue') }
]
},
{
path: '/_error/:action?',
component: () => import('pages/ErrorGeneric.vue')
},
// {
// path: '/_unknown-site',
// component: () => import('../pages/UnknownSite.vue')

@ -50,7 +50,8 @@ export const useSiteStore = defineStore('site', {
backgroundColor: '#FAFAFA',
width: '9px',
opacity: 1
}
},
docsBase: 'https://next.js.wiki/docs'
}),
getters: {},
actions: {

Loading…
Cancel
Save