Unverified Commit 2a3e1400 authored by NGPixel's avatar NGPixel

fix: add permissions to resolvers

parent 88197c17
......@@ -105,6 +105,8 @@ export default {
* @param {Express Next Callback} next
*/
authenticate (req, res, next) {
req.isAuthenticated = false
WIKI.auth.passport.authenticate('jwt', { session: false }, async (err, user, info) => {
if (err) { return next() }
let mustRevalidate = false
......@@ -170,6 +172,7 @@ export default {
WIKI.auth.guest.cacheExpiration = DateTime.utc().plus({ minutes: 1 })
}
req.user = WIKI.auth.guest
req.isAuthenticated = false
return next()
}
......@@ -203,6 +206,7 @@ export default {
// JWT is valid
req.logIn(user, { session: false }, (errc) => {
if (errc) { return next(errc) }
req.isAuthenticated = true
next()
})
})(req, res, next)
......@@ -223,7 +227,7 @@ export default {
return true
}
// Check Global Permissions
// Check Permissions
if (_.intersection(userPermissions, permissions).length < 1) {
return false
}
......
......@@ -17,6 +17,10 @@ export default {
* List of API Keys
*/
async apiKeys (obj, args, context) {
if (!WIKI.auth.checkAccess(context.req.user, ['read:api', 'manage:api'])) {
throw new Error('ERR_FORBIDDEN')
}
const keys = await WIKI.db.apiKeys.query().orderBy(['isRevoked', 'name'])
return keys.map(k => ({
id: k.id,
......@@ -31,7 +35,11 @@ export default {
/**
* Current API State
*/
apiState () {
apiState (obj, args, context) {
if (!WIKI.auth.checkAccess(context.req.user, ['read:api', 'manage:api', 'read:dashboard'])) {
throw new Error('ERR_FORBIDDEN')
}
return WIKI.config.api.isEnabled
},
/**
......@@ -82,6 +90,10 @@ export default {
*/
async createApiKey (obj, args, context) {
try {
if (!WIKI.auth.checkAccess(context.req.user, ['manage:api'])) {
throw new Error('ERR_FORBIDDEN')
}
const key = await WIKI.db.apiKeys.createNewKey(args)
await WIKI.auth.reloadApiKeys()
WIKI.events.outbound.emit('reloadApiKeys')
......@@ -136,7 +148,7 @@ export default {
try {
const userId = context.req.user?.id
if (!userId) {
throw new Error('ERR_USER_NOT_AUTHENTICATED')
throw new Error('ERR_NOT_AUTHENTICATED')
}
const usr = await WIKI.db.users.query().findById(userId)
......@@ -182,7 +194,7 @@ export default {
try {
const userId = context.req.user?.id
if (!userId) {
throw new Error('ERR_USER_NOT_AUTHENTICATED')
throw new Error('ERR_NOT_AUTHENTICATED')
}
const usr = await WIKI.db.users.query().findById(userId)
......@@ -224,7 +236,7 @@ export default {
try {
const userId = context.req.user?.id
if (!userId) {
throw new Error('ERR_USER_NOT_AUTHENTICATED')
throw new Error('ERR_NOT_AUTHENTICATED')
}
const usr = await WIKI.db.users.query().findById(userId)
......@@ -283,7 +295,7 @@ export default {
try {
const userId = context.req.user?.id
if (!userId) {
throw new Error('ERR_USER_NOT_AUTHENTICATED')
throw new Error('ERR_NOT_AUTHENTICATED')
}
const usr = await WIKI.db.users.query().findById(userId)
......@@ -346,7 +358,7 @@ export default {
try {
const userId = context.req.user?.id
if (!userId) {
throw new Error('ERR_USER_NOT_AUTHENTICATED')
throw new Error('ERR_NOT_AUTHENTICATED')
}
const usr = await WIKI.db.users.query().findById(userId)
......@@ -584,7 +596,7 @@ export default {
*/
async revokeApiKey (obj, args, context) {
try {
if (!WIKI.auth.checkAccess(context.req.user, ['manage:system'])) {
if (!WIKI.auth.checkAccess(context.req.user, ['manage:api'])) {
throw new Error('ERR_FORBIDDEN')
}
......
......@@ -11,6 +11,9 @@ export default {
Mutation: {
async setBlocksState(obj, args, context) {
try {
if (!WIKI.auth.checkAccess(context.req.user, ['manage:blocks'])) {
throw new Error('ERR_FORBIDDEN')
}
// TODO: update blocks state
return {
operation: generateSuccess('Blocks state updated successfully')
......
......@@ -3,10 +3,18 @@ import _ from 'lodash-es'
export default {
Query: {
async hooks () {
async hooks (obj, args, context) {
if (!WIKI.auth.checkAccess(context.req.user, ['read:webhooks', 'write:webhooks', 'manage:webhooks'])) {
throw new Error('ERR_FORBIDDEN')
}
return WIKI.db.hooks.query().orderBy('name')
},
async hookById (obj, args) {
async hookById (obj, args, context) {
if (!WIKI.auth.checkAccess(context.req.user, ['read:webhooks', 'write:webhooks', 'manage:webhooks'])) {
throw new Error('ERR_FORBIDDEN')
}
return WIKI.db.hooks.query().findById(args.id)
}
},
......@@ -14,8 +22,12 @@ export default {
/**
* CREATE HOOK
*/
async createHook (obj, args) {
async createHook (obj, args, context) {
try {
if (!WIKI.auth.checkAccess(context.req.user, ['write:webhooks', 'manage:webhooks'])) {
throw new Error('ERR_FORBIDDEN')
}
// -> Validate inputs
if (!args.name || args.name.length < 1) {
throw new WIKI.Error.Custom('HookCreateInvalidName', 'Invalid Hook Name')
......@@ -41,8 +53,12 @@ export default {
/**
* UPDATE HOOK
*/
async updateHook (obj, args) {
async updateHook (obj, args, context) {
try {
if (!WIKI.auth.checkAccess(context.req.user, ['manage:webhooks'])) {
throw new Error('ERR_FORBIDDEN')
}
// -> Load hook
const hook = await WIKI.db.hooks.query().findById(args.id)
if (!hook) {
......@@ -72,8 +88,12 @@ export default {
/**
* DELETE HOOK
*/
async deleteHook (obj, args) {
async deleteHook (obj, args, context) {
try {
if (!WIKI.auth.checkAccess(context.req.user, ['manage:webhooks'])) {
throw new Error('ERR_FORBIDDEN')
}
await WIKI.db.hooks.deleteHook(args.id)
WIKI.logger.debug(`Hook ${args.id} deleted successfully.`)
return {
......
import { generateError, generateSuccess } from '../../helpers/graph.mjs'
import _ from 'lodash-es'
export default {
Query: {
async locales(obj, args, context, info) {
......@@ -9,45 +6,5 @@ export default {
localeStrings (obj, args, context, info) {
return WIKI.db.locales.getStrings(args.locale)
}
},
Mutation: {
async downloadLocale(obj, args, context) {
try {
const job = await WIKI.scheduler.registerJob({
name: 'fetch-graph-locale',
immediate: true
}, args.locale)
await job.finished
return {
responseResult: generateSuccess('Locale downloaded successfully')
}
} catch (err) {
return generateError(err)
}
},
async updateLocale(obj, args, context) {
try {
WIKI.config.lang.code = args.locale
WIKI.config.lang.autoUpdate = args.autoUpdate
WIKI.config.lang.namespacing = args.namespacing
WIKI.config.lang.namespaces = _.union(args.namespaces, [args.locale])
const newLocale = await WIKI.db.locales.query().select('isRTL').where('code', args.locale).first()
WIKI.config.lang.rtl = newLocale.isRTL
await WIKI.configSvc.saveToDb(['lang'])
await WIKI.lang.setCurrentLocale(args.locale)
await WIKI.lang.refreshNamespaces()
await WIKI.cache.del('nav:locales')
return {
responseResult: generateSuccess('Locale config updated')
}
} catch (err) {
return generateError(err)
}
}
}
}
......@@ -3,7 +3,11 @@ import { generateError, generateSuccess } from '../../helpers/graph.mjs'
export default {
Query: {
async mailConfig(obj, args, context, info) {
async mailConfig(obj, args, context) {
if (!WIKI.auth.checkAccess(context.req.user, ['manage:system'])) {
throw new Error('ERR_FORBIDDEN')
}
return {
...WIKI.config.mail,
pass: WIKI.config.mail.pass.length > 0 ? '********' : ''
......@@ -13,6 +17,10 @@ export default {
Mutation: {
async sendMailTest(obj, args, context) {
try {
if (!WIKI.auth.checkAccess(context.req.user, ['manage:system'])) {
throw new Error('ERR_FORBIDDEN')
}
if (_.isEmpty(args.recipientEmail) || args.recipientEmail.length < 6) {
throw new WIKI.Error.MailInvalidRecipient()
}
......@@ -36,6 +44,10 @@ export default {
},
async updateMailConfig(obj, args, context) {
try {
if (!WIKI.auth.checkAccess(context.req.user, ['manage:system'])) {
throw new Error('ERR_FORBIDDEN')
}
WIKI.config.mail = {
senderName: args.senderName,
senderEmail: args.senderEmail,
......
import _ from 'lodash-es'
import { generateError, generateSuccess } from '../../helpers/graph.mjs'
import { parsePath }from '../../helpers/page.mjs'
import { parsePath } from '../../helpers/page.mjs'
import tsquery from 'pg-tsquery'
const tsq = tsquery()
......@@ -247,12 +247,19 @@ export default {
siteId: args.siteId
})
if (page) {
return {
...page,
...page.config,
scriptCss: page.scripts?.css,
scriptJsLoad: page.scripts?.jsLoad,
scriptJsUnload: page.scripts?.jsUnload
if (WIKI.auth.checkAccess(context.req.user, ['read:pages'], {
path: page.path,
locale: page.locale
})) {
return {
...page,
...page.config,
scriptCss: page.scripts?.css,
scriptJsLoad: page.scripts?.jsLoad,
scriptJsUnload: page.scripts?.jsUnload
}
} else {
throw new Error('ERR_FORBIDDEN')
}
} else {
throw new Error('ERR_PAGE_NOT_FOUND')
......@@ -265,17 +272,17 @@ export default {
async pathFromAlias (obj, args, context, info) {
const alias = args.alias?.trim()
if (!alias) {
throw new Error('ERR_ALIAS_MISSING')
throw new Error('ERR_PAGE_ALIAS_MISSING')
}
if (!WIKI.sites[args.siteId]) {
throw new Error('ERR_INVALID_SITE_ID')
throw new Error('ERR_INVALID_SITE')
}
const page = await WIKI.db.pages.query().findOne({
alias: args.alias,
siteId: args.siteId
}).select('id', 'path', 'locale')
if (!page) {
throw new Error('ERR_ALIAS_NOT_FOUND')
throw new Error('ERR_PAGE_ALIAS_NOT_FOUND')
}
return {
id: page.id,
......@@ -287,7 +294,7 @@ export default {
* FETCH TAGS
*/
async tags (obj, args, context, info) {
if (!args.siteId) { throw new Error('Missing Site ID')}
if (!args.siteId) { throw new Error('Missing Site ID') }
const tags = await WIKI.db.knex('tags').where('siteId', args.siteId).orderBy('tag')
// TODO: check permissions
return tags
......@@ -670,19 +677,29 @@ export default {
}
},
Page: {
icon (obj) {
return obj.icon || 'las la-file-alt'
icon (page) {
return page.icon || 'las la-file-alt'
},
password (page) {
return page.password ? '********' : ''
},
password (obj) {
return obj.password ? '********' : ''
content (page, args, context) {
if (!WIKI.auth.checkAccess(context.req.user, ['read:source', 'write:pages', 'manage:pages'], {
path: page.path,
locale: page.locale
})) {
throw new Error('ERR_FORBIDDEN')
}
return page.content
},
// async tags (obj) {
// return WIKI.db.pages.relatedQuery('tags').for(obj.id)
// async tags (page) {
// return WIKI.db.pages.relatedQuery('tags').for(page.id)
// },
tocDepth (obj) {
tocDepth (page) {
return {
min: obj.extra?.tocDepth?.min ?? 1,
max: obj.extra?.tocDepth?.max ?? 2
min: page.extra?.tocDepth?.min ?? 1,
max: page.extra?.tocDepth?.max ?? 2
}
}
// comments(pg) {
......
......@@ -49,8 +49,12 @@ export default {
/**
* CREATE SITE
*/
async createSite (obj, args) {
async createSite (obj, args, context) {
try {
if (!WIKI.auth.checkAccess(context.req.user, ['write:sites', 'manage:sites'])) {
throw new Error('ERR_FORBIDDEN')
}
// -> Validate inputs
if (!args.hostname || args.hostname.length < 1 || !/^(\\*)|([a-z0-9\-.:]+)$/.test(args.hostname)) {
throw new WIKI.Error.Custom('SiteCreateInvalidHostname', 'Invalid Site Hostname')
......@@ -83,8 +87,12 @@ export default {
/**
* UPDATE SITE
*/
async updateSite (obj, args) {
async updateSite (obj, args, context) {
try {
if (!WIKI.auth.checkAccess(context.req.user, ['manage:sites'])) {
throw new Error('ERR_FORBIDDEN')
}
// -> Load site
const site = await WIKI.db.sites.query().findById(args.id)
if (!site) {
......@@ -127,8 +135,12 @@ export default {
/**
* DELETE SITE
*/
async deleteSite (obj, args) {
async deleteSite (obj, args, context) {
try {
if (!WIKI.auth.checkAccess(context.req.user, ['manage:sites'])) {
throw new Error('ERR_FORBIDDEN')
}
// -> Ensure site isn't last one
const sitesCount = await WIKI.db.sites.query().count('id').first()
if (sitesCount?.count && _.toNumber(sitesCount?.count) <= 1) {
......@@ -149,6 +161,10 @@ export default {
*/
async uploadSiteLogo (obj, args, context) {
try {
if (!WIKI.auth.checkAccess(context.req.user, ['manage:sites'])) {
throw new Error('ERR_FORBIDDEN')
}
const { filename, mimetype, createReadStream } = await args.image
WIKI.logger.info(`Processing site logo ${filename} of type ${mimetype}...`)
if (!WIKI.extensions.ext.sharp.isInstalled) {
......@@ -208,6 +224,10 @@ export default {
*/
async uploadSiteFavicon (obj, args, context) {
try {
if (!WIKI.auth.checkAccess(context.req.user, ['manage:sites'])) {
throw new Error('ERR_FORBIDDEN')
}
const { filename, mimetype, createReadStream } = await args.image
WIKI.logger.info(`Processing site favicon ${filename} of type ${mimetype}...`)
if (!WIKI.extensions.ext.sharp.isInstalled) {
......@@ -268,6 +288,10 @@ export default {
*/
async uploadSiteLoginBg (obj, args, context) {
try {
if (!WIKI.auth.checkAccess(context.req.user, ['manage:sites'])) {
throw new Error('ERR_FORBIDDEN')
}
const { filename, mimetype, createReadStream } = await args.image
WIKI.logger.info(`Processing site login bg ${filename} of type ${mimetype}...`)
if (!WIKI.extensions.ext.sharp.isInstalled) {
......
......@@ -133,7 +133,7 @@ export default {
.first()
if (!folder) {
throw new Error('ERR_FOLDER_NOT_EXIST')
throw new Error('ERR_INVALID_FOLDER')
}
return {
......@@ -158,7 +158,7 @@ export default {
.first()
if (!folder) {
throw new Error('ERR_FOLDER_NOT_EXIST')
throw new Error('ERR_INVALID_FOLDER')
}
return {
......
......@@ -7,19 +7,6 @@ extend type Query {
localeStrings(locale: String!): JSON
}
extend type Mutation {
downloadLocale(
locale: String!
): DefaultResponse
updateLocale(
locale: String!
autoUpdate: Boolean!
namespacing: Boolean!
namespaces: [String]!
): DefaultResponse
}
# -----------------------------------------------
# TYPES
# -----------------------------------------------
......
......@@ -86,6 +86,7 @@ type SystemInfo {
isSchedulerHealthy: Boolean
latestVersion: String
latestVersionReleaseDate: Date
loginsPastDay: Int
nodeVersion: String
operatingSystem: String
pagesTotal: Int
......
......@@ -227,7 +227,7 @@ export class Page extends Model {
static async createPage(opts) {
// -> Validate site
if (!WIKI.sites[opts.siteId]) {
throw new Error('ERR_INVALID_SITE_ID')
throw new Error('ERR_INVALID_SITE')
}
// -> Remove trailing slash
......
......@@ -77,7 +77,7 @@ export class Tree extends Model {
if (id) {
const parent = await WIKI.db.knex('tree').where('id', id).first()
if (!parent) {
throw new Error('ERR_NONEXISTING_FOLDER_ID')
throw new Error('ERR_INVALID_FOLDER')
}
return parent
} else {
......@@ -105,7 +105,7 @@ export class Tree extends Model {
siteId
})
} else {
throw new Error('ERR_NONEXISTING_FOLDER_PATH')
throw new Error('ERR_INVALID_FOLDER')
}
}
}
......@@ -150,7 +150,7 @@ export class Tree extends Model {
siteId,
tags,
meta,
navigationId: siteId,
navigationId: siteId
}).returning('*')
return pageEntry[0]
......@@ -215,12 +215,12 @@ export class Tree extends Model {
static async createFolder ({ parentId, parentPath, pathName, title, locale, siteId }) {
// Validate path name
if (!rePathName.test(pathName)) {
throw new Error('ERR_INVALID_PATH_NAME')
throw new Error('ERR_INVALID_PATH')
}
// Validate title
if (!reTitle.test(title)) {
throw new Error('ERR_INVALID_TITLE')
throw new Error('ERR_FOLDER_TITLE_INVALID')
}
parentPath = encodeTreePath(parentPath)
......@@ -236,7 +236,7 @@ export class Tree extends Model {
if (parentId) {
parent = await WIKI.db.knex('tree').where('id', parentId).first()
if (!parent) {
throw new Error('ERR_NONEXISTING_PARENT_ID')
throw new Error('ERR_FOLDER_PARENT_INVALID')
}
parentPath = parent.folderPath ? `${decodeFolderPath(parent.folderPath)}.${parent.fileName}` : parent.fileName
} else if (parentPath) {
......@@ -254,7 +254,7 @@ export class Tree extends Model {
type: 'folder'
}).first()
if (existingFolder) {
throw new Error('ERR_FOLDER_ALREADY_EXISTS')
throw new Error('ERR_FOLDER_DUPLICATE')
}
// Ensure all ancestors exist
......@@ -338,17 +338,17 @@ export class Tree extends Model {
// Get folder
const folder = await WIKI.db.knex('tree').where('id', folderId).first()
if (!folder) {
throw new Error('ERR_NONEXISTING_FOLDER_ID')
throw new Error('ERR_INVALID_FOLDER')
}
// Validate path name
if (!rePathName.test(pathName)) {
throw new Error('ERR_INVALID_PATH_NAME')
throw new Error('ERR_INVALID_PATH')
}
// Validate title
if (!reTitle.test(title)) {
throw new Error('ERR_INVALID_TITLE')
throw new Error('ERR_FOLDER_TITLE_INVALID')
}
WIKI.logger.debug(`Renaming folder ${folder.id} path to ${pathName}...`)
......@@ -364,7 +364,7 @@ export class Tree extends Model {
type: 'folder'
}).first()
if (existingFolder) {
throw new Error('ERR_FOLDER_ALREADY_EXISTS')
throw new Error('ERR_FOLDER_DUPLICATE')
}
// Build new paths
......@@ -406,7 +406,7 @@ export class Tree extends Model {
// Get folder
const folder = await WIKI.db.knex('tree').where('id', folderId).first()
if (!folder) {
throw new Error('ERR_NONEXISTING_FOLDER_ID')
throw new Error('ERR_INVALID_FOLDER')
}
const folderPath = folder.folderPath ? `${folder.folderPath}.${folder.fileName}` : folder.fileName
WIKI.logger.debug(`Deleting folder ${folder.id} at path ${folderPath}...`)
......
......@@ -451,7 +451,7 @@ export class User extends Model {
})
if (strategyId !== expectedStrategyId) {
throw new Error('ERR_UNEXPECTED_STRATEGY_ID')
throw new Error('ERR_INVALID_STRATEGY')
}
if (user) {
......@@ -461,11 +461,11 @@ export class User extends Model {
}
return WIKI.db.users.afterLoginChecks(user, strategyId, context, { siteId, skipTFA: true })
} else {
throw new Error('ERR_INCORRECT_TFA_TOKEN')
throw new Error('ERR_TFA_INCORRECT_TOKEN')
}
}
}
throw new Error('ERR_INVALID_TFA_REQUEST')
throw new Error('ERR_TFA_INVALID_REQUEST')
}
/**
......@@ -481,7 +481,7 @@ export class User extends Model {
})
if (strategyId !== expectedStrategyId) {
throw new Error('ERR_UNEXPECTED_STRATEGY_ID')
throw new Error('ERR_INVALID_STRATEGY')
}
if (user) {
......@@ -503,12 +503,12 @@ export class User extends Model {
static async changePassword ({ strategyId, siteId, currentPassword, newPassword }, context) {
const userId = context.req.user?.id
if (!userId) {
throw new Error('ERR_USER_NOT_AUTHENTICATED')
throw new Error('ERR_NOT_AUTHENTICATED')
}
const user = await WIKI.db.users.query().findById(userId)
if (!user) {
throw new Error('ERR_USER_NOT_FOUND')
throw new Error('ERR_INVALID_USER')
}
if (!newPassword || newPassword.length < 8) {
......@@ -516,7 +516,7 @@ export class User extends Model {
}
if (!user.auth[strategyId]?.password) {
throw new Error('ERR_UNEXPECTED_STRATEGY_ID')
throw new Error('ERR_INVALID_STRATEGY')
}
if (await bcrypt.compare(currentPassword, user.auth[strategyId].password) !== true) {
......@@ -627,7 +627,7 @@ export class User extends Model {
// Check if email already exists
const usr = await WIKI.db.users.query().findOne({ email })
if (usr) {
throw new Error('ERR_ACCOUNT_ALREADY_EXIST')
throw new Error('ERR_DUPLICATE_ACCOUNT_EMAIL')
}
WIKI.logger.debug(`Creating new user account for ${email}...`)
......
......@@ -21,9 +21,9 @@ export default {
if (user) {
const authStrategyData = user.auth[strategyId]
if (!authStrategyData) {
throw new Error('ERR_INVALID_STRATEGY_ID')
throw new Error('ERR_INVALID_STRATEGY')
} else if (await bcrypt.compare(uPassword, authStrategyData.password) !== true) {
throw new Error('ERR_AUTH_FAILED')
throw new Error('ERR_LOGIN_FAILED')
} else if (!user.isActive) {
throw new Error('ERR_INACTIVE_USER')
} else if (authStrategyData.restrictLogin) {
......@@ -34,7 +34,7 @@ export default {
done(null, user)
}
} else {
throw new Error('ERR_AUTH_FAILED')
throw new Error('ERR_LOGIN_FAILED')
}
} catch (err) {
done(err, null)
......
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="96px" height="96px"><linearGradient id="NyTLzsOvu2hiH2q16GFlAa" x1="-5.326" x2="17.563" y1="96.186" y2="50.024" gradientTransform="translate(20.942 -50.558)" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#0077d2"/><stop offset="1" stop-color="#0b59a2"/></linearGradient><path fill="url(#NyTLzsOvu2hiH2q16GFlAa)" d="M25.524,9.068L9.455,25.137c-1.435,1.435-1.435,3.761,0,5.196l11.212,11.212 c1.435,1.435,3.761,1.435,5.196,0l16.07-16.07C42.616,24.792,43,23.865,43,22.898V11.646C43,9.641,41.359,8,39.354,8H28.103 C27.135,8,26.208,8.384,25.524,9.068z"/><linearGradient id="NyTLzsOvu2hiH2q16GFlAb" x1="10.819" x2="34.706" y1="5.309" y2="29.196" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#32bdef"/><stop offset="1" stop-color="#1ea2e4"/></linearGradient><path fill="url(#NyTLzsOvu2hiH2q16GFlAb)" d="M21.837,6.049L6.057,21.83c-1.409,1.409-1.409,3.694,0,5.103l11.011,11.011 c1.409,1.409,3.694,1.409,5.103,0l15.781-15.781C38.623,21.491,39,20.58,39,19.63V8.581C39,6.611,37.389,5,35.419,5H24.37 C23.42,5,22.509,5.377,21.837,6.049z M33.629,12.351c-0.985,0-1.79-0.806-1.79-1.79s0.806-1.79,1.79-1.79s1.79,0.806,1.79,1.79 S34.614,12.351,33.629,12.351"/></svg>
\ No newline at end of file
......@@ -125,6 +125,10 @@ q-layout.admin(view='hHh Lpr lff')
q-item-section(side)
//- TODO: Reflect site storage status
status-light(:color='true ? `positive` : `warning`', :pulse='false')
q-item(:to='`/_admin/` + adminStore.currentSiteId + `/tags`', v-ripple, active-class='bg-primary text-white', disabled, v-if='flagsStore.experimental && (userStore.can(`manage:sites`))')
q-item-section(avatar)
q-icon(name='img:/_assets/icons/fluent-tag.svg')
q-item-section {{ t('admin.tags.title') }}
q-item(:to='`/_admin/` + adminStore.currentSiteId + `/theme`', v-ripple, active-class='bg-primary text-white', v-if='userStore.can(`manage:sites`) || userStore.can(`manage:theme`)')
q-item-section(avatar)
q-icon(name='img:/_assets/icons/fluent-paint-roller.svg')
......
......@@ -39,7 +39,7 @@ q-page.admin-dashboard
img(src='/_assets/icons/fluent-people.svg')
div
strong {{ t('admin.groups.title') }}
small.text-positive {{adminStore.info.groupsTotal}}
span {{adminStore.info.groupsTotal}}
q-separator
q-card-actions(align='right')
q-btn(
......@@ -88,6 +88,23 @@ q-page.admin-dashboard
.col-12.col-sm-6.col-lg-3
q-card
q-card-section.admin-dashboard-card
img(src='/_assets/icons/fluent-tag.svg')
div
strong {{ t('admin.tags.title') }}
span {{adminStore.info.tagsTotal}}
q-separator
q-card-actions(align='right')
q-btn(
flat
color='primary'
icon='las la-tags'
:label='t(`common.actions.manage`)'
:disable='!userStore.can(`manage:sites`)'
:to='`/_admin/` + adminStore.currentSiteId + `/tags`'
)
.col-12.col-sm-6.col-lg-3
q-card
q-card-section.admin-dashboard-card
img(src='/_assets/icons/fluent-female-working-with-a-laptop.svg')
div
strong Logins
......@@ -102,6 +119,10 @@ q-page.admin-dashboard
:disable='!userStore.can(`manage:sites`)'
:to='`/_admin/` + adminStore.currentSiteId + `/analytics`'
)
.col-12.col-lg-9
q-card
q-card-section ---
.col-12
q-banner.bg-positive.text-white(
:class='adminStore.isVersionLatest ? `bg-positive` : `bg-warning`'
......@@ -123,9 +144,6 @@ q-page.admin-dashboard
:label='t(`admin.system.title`)'
to='/_admin/system'
)
.col-12
q-card
q-card-section ---
//- v-container(fluid, grid-list-lg)
//- v-layout(row, wrap)
......
......@@ -13,6 +13,7 @@ export const useAdminStore = defineStore('admin', {
latestVersion: 'n/a',
groupsTotal: 0,
pagesTotal: 0,
tagsTotal: 0,
usersTotal: 0,
loginsPastDay: 0,
isApiEnabled: false,
......@@ -57,7 +58,9 @@ export const useAdminStore = defineStore('admin', {
apiState
systemInfo {
groupsTotal
tagsTotal
usersTotal
loginsPastDay
currentVersion
latestVersion
isMailConfigured
......@@ -68,7 +71,9 @@ export const useAdminStore = defineStore('admin', {
fetchPolicy: 'network-only'
})
this.info.groupsTotal = clone(resp?.data?.systemInfo?.groupsTotal ?? 0)
this.info.tagsTotal = clone(resp?.data?.systemInfo?.tagsTotal ?? 0)
this.info.usersTotal = clone(resp?.data?.systemInfo?.usersTotal ?? 0)
this.info.loginsPastDay = clone(resp?.data?.systemInfo?.loginsPastDay ?? 0)
this.info.currentVersion = clone(resp?.data?.systemInfo?.currentVersion ?? 'n/a')
this.info.latestVersion = clone(resp?.data?.systemInfo?.latestVersion ?? 'n/a')
this.info.isApiEnabled = clone(resp?.data?.apiState ?? false)
......
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