feat: admin api keys

pull/5698/head
Nicolas Giard 2 years ago
parent 6625267bc9
commit 7fe7fbb371
No known key found for this signature in database
GPG Key ID: 85061B8F9D55B7C8

@ -29,7 +29,7 @@ exports.up = async knex => {
table.uuid('id').notNullable().primary().defaultTo(knex.raw('gen_random_uuid()'))
table.string('name').notNullable()
table.text('key').notNullable()
table.string('expiration').notNullable()
table.timestamp('expiration').notNullable().defaultTo(knex.fn.now())
table.boolean('isRevoked').notNullable().defaultTo(false)
table.timestamp('createdAt').notNullable().defaultTo(knex.fn.now())
table.timestamp('updatedAt').notNullable().defaultTo(knex.fn.now())

@ -12,6 +12,7 @@ module.exports = {
*/
async apiKeys (obj, args, context) {
const keys = await WIKI.models.apiKeys.query().orderBy(['isRevoked', 'name'])
console.info(keys)
return keys.map(k => ({
id: k.id,
name: k.name,
@ -78,9 +79,10 @@ module.exports = {
WIKI.events.outbound.emit('reloadApiKeys')
return {
key,
responseResult: graphHelper.generateSuccess('API Key created successfully')
operation: graphHelper.generateSuccess('API Key created successfully')
}
} catch (err) {
WIKI.logger.warn(err)
return graphHelper.generateError(err)
}
},
@ -165,7 +167,7 @@ module.exports = {
WIKI.config.api.isEnabled = args.enabled
await WIKI.configSvc.saveToDb(['api'])
return {
responseResult: graphHelper.generateSuccess('API State changed successfully')
operation: graphHelper.generateSuccess('API State changed successfully')
}
} catch (err) {
return graphHelper.generateError(err)
@ -182,7 +184,7 @@ module.exports = {
await WIKI.auth.reloadApiKeys()
WIKI.events.outbound.emit('reloadApiKeys')
return {
responseResult: graphHelper.generateSuccess('API Key revoked successfully')
operation: graphHelper.generateSuccess('API Key revoked successfully')
}
} catch (err) {
return graphHelper.generateError(err)

@ -21,8 +21,7 @@ extend type Mutation {
createApiKey(
name: String!
expiration: String!
fullAccess: Boolean!
group: Int
groups: [UUID]!
): AuthenticationCreateApiKeyResponse
login(
@ -53,7 +52,7 @@ extend type Mutation {
): AuthenticationRegisterResponse
revokeApiKey(
id: Int!
id: UUID!
): DefaultResponse
setApiState(
@ -135,13 +134,13 @@ input AuthenticationStrategyInput {
}
type AuthenticationApiKey {
id: Int!
name: String!
keyShort: String!
expiration: Date!
createdAt: Date!
updatedAt: Date!
isRevoked: Boolean!
id: UUID
name: String
keyShort: String
expiration: Date
createdAt: Date
updatedAt: Date
isRevoked: Boolean
}
type AuthenticationCreateApiKeyResponse {

@ -1,7 +1,7 @@
/* global WIKI */
const Model = require('objection').Model
const moment = require('moment')
const { DateTime } = require('luxon')
const ms = require('ms')
const jwt = require('jsonwebtoken')
@ -17,7 +17,7 @@ module.exports = class ApiKey extends Model {
required: ['name', 'key'],
properties: {
id: {type: 'integer'},
id: {type: 'string'},
name: {type: 'string'},
key: {type: 'string'},
expiration: {type: 'string'},
@ -31,29 +31,33 @@ module.exports = class ApiKey extends Model {
async $beforeUpdate(opt, context) {
await super.$beforeUpdate(opt, context)
this.updatedAt = moment.utc().toISOString()
this.updatedAt = new Date().toISOString()
}
async $beforeInsert(context) {
await super.$beforeInsert(context)
this.createdAt = moment.utc().toISOString()
this.updatedAt = moment.utc().toISOString()
this.createdAt = new Date().toISOString()
this.updatedAt = new Date().toISOString()
}
static async createNewKey ({ name, expiration, fullAccess, group }) {
static async createNewKey ({ name, expiration, groups }) {
console.info(DateTime.utc().plus(ms(expiration)).toISO())
const entry = await WIKI.models.apiKeys.query().insert({
name,
key: 'pending',
expiration: moment.utc().add(ms(expiration), 'ms').toISOString(),
expiration: DateTime.utc().plus(ms(expiration)).toISO(),
isRevoked: true
})
console.info(entry)
const key = jwt.sign({
api: entry.id,
grp: fullAccess ? 1 : group
grp: groups
}, {
key: WIKI.config.certs.private,
passphrase: WIKI.config.sessionSecret
key: WIKI.config.auth.certs.private,
passphrase: WIKI.config.auth.secret
}, {
algorithm: 'RS256',
expiresIn: expiration,

@ -35,7 +35,7 @@ module.exports = class Authentication extends Model {
}
static async getStrategies() {
const strategies = await WIKI.models.authentication.query().orderBy('order')
const strategies = await WIKI.models.authentication.query()
return strategies.map(str => ({
...str,
domainWhitelist: _.get(str.domainWhitelist, 'v', []),

@ -32,7 +32,7 @@
"@codemirror/tooltip": "0.19.16",
"@codemirror/view": "6.0.2",
"@lezer/common": "1.0.0",
"@quasar/extras": "1.14.2",
"@quasar/extras": "1.15.0",
"@tiptap/core": "2.0.0-beta.176",
"@tiptap/extension-code-block": "2.0.0-beta.37",
"@tiptap/extension-code-block-lowlight": "2.0.0-beta.68",
@ -57,9 +57,8 @@
"@tiptap/extension-typography": "2.0.0-beta.20",
"@tiptap/starter-kit": "2.0.0-beta.185",
"@tiptap/vue-3": "2.0.0-beta.91",
"@vue/apollo-option": "4.0.0-alpha.17",
"apollo-upload-client": "17.0.0",
"browser-fs-access": "0.30.2",
"browser-fs-access": "0.31.0",
"clipboard": "2.0.11",
"codemirror": "6.0.1",
"filesize": "9.0.11",
@ -69,31 +68,31 @@
"js-cookie": "3.0.1",
"jwt-decode": "3.1.2",
"lodash-es": "4.17.21",
"luxon": "2.5.0",
"pinia": "2.0.14",
"luxon": "3.0.1",
"pinia": "2.0.17",
"pug": "3.0.2",
"quasar": "2.7.5",
"tippy.js": "6.3.7",
"uuid": "8.3.2",
"v-network-graph": "0.6.3",
"v-network-graph": "0.6.5",
"vue": "3.2.37",
"vue-codemirror": "6.0.0",
"vue-codemirror": "6.0.2",
"vue-i18n": "9.1.10",
"vue-router": "4.1.1",
"vue-router": "4.1.3",
"vuedraggable": "4.1.0",
"zxcvbn": "4.4.2"
},
"devDependencies": {
"@intlify/vite-plugin-vue-i18n": "3.4.0",
"@intlify/vite-plugin-vue-i18n": "5.0.1",
"@quasar/app-vite": "1.0.5",
"@types/lodash": "4.14.182",
"browserlist": "latest",
"eslint": "8.19.0",
"eslint": "8.20.0",
"eslint-config-standard": "17.0.0",
"eslint-plugin-import": "2.26.0",
"eslint-plugin-n": "15.2.4",
"eslint-plugin-promise": "6.0.0",
"eslint-plugin-vue": "9.2.0"
"eslint-plugin-vue": "9.3.0"
},
"engines": {
"node": "^18 || ^16",

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="96px" height="96px"><linearGradient id="Gx4Hql1L2mKhvP9291EBWa" x1="19.53" x2="28.032" y1="12.426" y2="32.179" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#32bdef"/><stop offset="1" stop-color="#1ea2e4"/></linearGradient><path fill="url(#Gx4Hql1L2mKhvP9291EBWa)" d="M31.19,22H27v-8c0-0.552-0.448-1-1-1h-4c-0.552,0-1,0.448-1,1v8h-4.19 c-0.72,0-1.08,0.87-0.571,1.379l6.701,6.701c0.586,0.586,1.536,0.586,2.121,0l6.701-6.701C32.271,22.87,31.91,22,31.19,22z"/><linearGradient id="Gx4Hql1L2mKhvP9291EBWb" x1="39.761" x2="43.605" y1="31.57" y2="42.462" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#32bdef"/><stop offset="1" stop-color="#1ea2e4"/></linearGradient><path fill="url(#Gx4Hql1L2mKhvP9291EBWb)" d="M39,33v10l4.828-4.828c0.75-0.75,1.172-1.768,1.172-2.828V33c0-0.552-0.448-1-1-1h-4 C39.448,32,39,32.448,39,33z"/><linearGradient id="Gx4Hql1L2mKhvP9291EBWc" x1="9" x2="39" y1="40" y2="40" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#0362b0"/><stop offset=".112" stop-color="#036abd"/><stop offset=".258" stop-color="#036fc5"/><stop offset=".5" stop-color="#0370c8"/><stop offset=".742" stop-color="#036fc5"/><stop offset=".888" stop-color="#036abd"/><stop offset="1" stop-color="#0362b0"/></linearGradient><rect width="30" height="6" x="9" y="37" fill="url(#Gx4Hql1L2mKhvP9291EBWc)"/><linearGradient id="Gx4Hql1L2mKhvP9291EBWd" x1="332.761" x2="336.605" y1="31.57" y2="42.462" gradientTransform="matrix(-1 0 0 1 341 0)" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#32bdef"/><stop offset="1" stop-color="#1ea2e4"/></linearGradient><path fill="url(#Gx4Hql1L2mKhvP9291EBWd)" d="M9,33v10l-4.828-4.828C3.421,37.421,3,36.404,3,35.343V33c0-0.552,0.448-1,1-1h4 C8.552,32,9,32.448,9,33z"/><linearGradient id="Gx4Hql1L2mKhvP9291EBWe" x1="23.174" x2="24.956" y1="4.081" y2="8.222" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#32bdef"/><stop offset="1" stop-color="#1ea2e4"/></linearGradient><path fill="url(#Gx4Hql1L2mKhvP9291EBWe)" d="M26,7h-4c-0.552,0-1-0.448-1-1v0c0-0.552,0.448-1,1-1h4c0.552,0,1,0.448,1,1v0 C27,6.552,26.552,7,26,7z"/><linearGradient id="Gx4Hql1L2mKhvP9291EBWf" x1="23.174" x2="24.956" y1="8.081" y2="12.222" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#32bdef"/><stop offset="1" stop-color="#1ea2e4"/></linearGradient><path fill="url(#Gx4Hql1L2mKhvP9291EBWf)" d="M26,11h-4c-0.552,0-1-0.448-1-1v0c0-0.552,0.448-1,1-1h4c0.552,0,1,0.448,1,1v0 C27,10.552,26.552,11,26,11z"/></svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="96px" height="96px"><linearGradient id="XED_QU6xiox5XDw0HU_eba" x1="20.958" x2="5.741" y1="26.758" y2="42.622" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#e5a505"/><stop offset=".01" stop-color="#e9a804"/><stop offset=".06" stop-color="#f4b102"/><stop offset=".129" stop-color="#fbb600"/><stop offset=".323" stop-color="#fdb700"/></linearGradient><path fill="url(#XED_QU6xiox5XDw0HU_eba)" d="M12,41.5c0-1.381,1.119-2.5,2.5-2.5c0.156,0,0.307,0.019,0.454,0.046l1.186-1.186 C16.058,37.586,16,37.301,16,37c0-1.657,1.343-3,3-3c0.301,0,0.586,0.058,0.86,0.14L24,30l-6-6L4.586,37.414 C4.211,37.789,4,38.298,4,38.828v1.343c0,0.53,0.211,1.039,0.586,1.414l1.828,1.828C6.789,43.789,7.298,44,7.828,44h1.343 c0.53,0,1.039-0.211,1.414-0.586l1.46-1.46C12.019,41.807,12,41.656,12,41.5z"/><linearGradient id="XED_QU6xiox5XDw0HU_ebb" x1="21.64" x2="36.971" y1="7.073" y2="29.362" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#fede00"/><stop offset="1" stop-color="#ffd000"/></linearGradient><path fill="url(#XED_QU6xiox5XDw0HU_ebb)" d="M29.5,5C22.044,5,16,11.044,16,18.5S22.044,32,29.5,32S43,25.956,43,18.5S36.956,5,29.5,5z M33,19c-2.209,0-4-1.791-4-4s1.791-4,4-4s4,1.791,4,4S35.209,19,33,19z"/></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="96px" height="96px"><linearGradient id="GCWVriy4rQhfclYQVzRmda" x1="9.812" x2="38.361" y1="9.812" y2="38.361" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#f44f5a"/><stop offset=".443" stop-color="#ee3d4a"/><stop offset="1" stop-color="#e52030"/></linearGradient><path fill="url(#GCWVriy4rQhfclYQVzRmda)" d="M24,4C12.955,4,4,12.955,4,24s8.955,20,20,20s20-8.955,20-20C44,12.955,35.045,4,24,4z M24,38 c-7.732,0-14-6.268-14-14s6.268-14,14-14s14,6.268,14,14S31.732,38,24,38z"/><linearGradient id="GCWVriy4rQhfclYQVzRmdb" x1="6.821" x2="41.08" y1="6.321" y2="40.58" gradientTransform="translate(-.146 .354)" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#f44f5a"/><stop offset=".443" stop-color="#ee3d4a"/><stop offset="1" stop-color="#e52030"/></linearGradient><polygon fill="url(#GCWVriy4rQhfclYQVzRmdb)" points="13.371,38.871 9.129,34.629 34.629,9.129 38.871,13.371"/></svg>

After

Width:  |  Height:  |  Size: 969 B

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 40" width="80px" height="80px"><path fill="#fff" d="M6.5 37.5L6.5 2.5 24.793 2.5 33.5 11.207 33.5 37.5z"/><path fill="#4788c7" d="M24.586,3L33,11.414V37H7V3H24.586 M25,2H6v36h28V11L25,2L25,2z"/><path fill="#dff0fe" d="M24.5 11.5L24.5 2.5 24.793 2.5 33.5 11.207 33.5 11.5z"/><path fill="#4788c7" d="M25,3.414L32.586,11H25V3.414 M25,2h-1v10h10v-1L25,2L25,2z"/><path fill="none" stroke="#4788c7" stroke-miterlimit="10" d="M13.5 18.5L14.5 18.5 14.5 23M19.5 22.5L19.5 22.5c-.552 0-1-.448-1-1v-2c0-.552.448-1 1-1h0c.552 0 1 .448 1 1v2C20.5 22.052 20.052 22.5 19.5 22.5zM25.5 22.5L25.5 22.5c-.552 0-1-.448-1-1v-2c0-.552.448-1 1-1h0c.552 0 1 .448 1 1v2C26.5 22.052 26.052 22.5 25.5 22.5zM25.5 25.5L26.5 25.5 26.5 30M20.5 29.5L20.5 29.5c-.552 0-1-.448-1-1v-2c0-.552.448-1 1-1h0c.552 0 1 .448 1 1v2C21.5 29.052 21.052 29.5 20.5 29.5zM14.5 29.5L14.5 29.5c-.552 0-1-.448-1-1v-2c0-.552.448-1 1-1h0c.552 0 1 .448 1 1v2C15.5 29.052 15.052 29.5 14.5 29.5z"/></svg>

After

Width:  |  Height:  |  Size: 1005 B

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 40" width="80px" height="80px"><path fill="#fff" d="M2.5,37.5V8.985C2.5,5.409,5.409,2.5,8.985,2.5H37.5v35H2.5z"/><path fill="#4788c7" d="M37,3v34H3V8.985C3,5.685,5.685,3,8.985,3H37 M38,2H8.985C5.127,2,2,5.127,2,8.985V38h36V2L38,2z"/><path fill="#b6dcfe" d="M2.522,7.5c0.253-2.8,2.613-5,5.478-5h29.5v5H2.522z"/><path fill="#4788c7" d="M37,3v4H3.1C3.565,4.721,5.585,3,8,3H37 M38,2H8C4.686,2,2,4.686,2,8h36V2L38,2z"/><path fill="#b6dcfe" d="M13 14H15V16H13zM21 14H23V16H21zM25 14H27V16H25zM29 14H31V16H29zM9 18H11V20H9zM13 18H15V20H13zM21 18H23V20H21zM25 18H27V20H25zM29 18H31V20H29zM9 26H11V28H9zM13 26H15V28H13zM21 26H23V28H21zM17 14H19V16H17zM17 18H19V20H17zM9 22H11V24H9zM13 22H15V24H13zM21 22H23V24H21zM25 22H27V24H25zM29 22H31V24H29zM17 22H19V24H17zM17 26H19V28H17zM25 26H27V28H25zM3 34H37V37H3z"/><path fill="#98ccfd" d="M12,23.5C5.659,23.5,0.5,18.341,0.5,12S5.659,0.5,12,0.5S23.5,5.659,23.5,12S18.341,23.5,12,23.5z"/><path fill="#4788c7" d="M12,1c6.065,0,11,4.935,11,11s-4.935,11-11,11S1,18.065,1,12S5.935,1,12,1 M12,0 C5.373,0,0,5.373,0,12s5.373,12,12,12s12-5.373,12-12S18.627,0,12,0L12,0z"/><g><path fill="#fff" d="M12 3A9 9 0 1 0 12 21A9 9 0 1 0 12 3Z"/></g><path fill="none" stroke="#4788c7" stroke-linecap="round" stroke-miterlimit="10" d="M14.5 5.5L12 12 15.5 15.5"/><g><path fill="#4788c7" d="M12 10.667A1.333 1.333 0 1 0 12 13.333A1.333 1.333 0 1 0 12 10.667Z"/></g></svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 12 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 9.6 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.6 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 25 KiB

@ -1,5 +1,4 @@
import { boot } from 'quasar/wrappers'
import { createApolloProvider } from '@vue/apollo-option'
import { ApolloClient, InMemoryCache } from '@apollo/client/core'
import { setContext } from '@apollo/client/link/context'
import { createUploadLink } from 'apollo-upload-client'
@ -41,16 +40,9 @@ export default boot(({ app }) => {
ssrForceFetchDelay: 100
})
// Init Vue Apollo
const apolloProvider = createApolloProvider({
defaultClient: client
})
if (import.meta.env.SSR) {
global.APOLLO_CLIENT = client
} else {
window.APOLLO_CLIENT = client
}
app.use(apolloProvider)
})

@ -0,0 +1,63 @@
<template lang="pug">
q-dialog(ref='dialogRef', @hide='onDialogHide', persistent)
q-card(style='min-width: 600px;')
q-card-section.card-header
q-icon(name='img:/_assets/icons/fluent-key-2.svg', left, size='sm')
span {{t(`admin.api.copyKeyTitle`)}}
q-card-section.card-negative
i18n-t(tag='span', keypath='admin.api.newKeyCopyWarn')
template(#bold)
strong {{t('admin.api.newKeyCopyWarnBold')}}
q-form.q-py-sm
q-item
blueprint-icon.self-start(icon='binary-file')
q-item-section
q-input(
type='textarea'
outlined
v-model='props.keyValue'
dense
hide-bottom-space
:label='t(`admin.api.key`)'
:aria-label='t(`admin.api.key`)'
autofocus
)
q-card-actions.card-actions
q-space
q-btn(
unelevated
:label='t(`common.actions.close`)'
color='primary'
padding='xs md'
@click='onDialogOK'
)
</template>
<script setup>
import { useI18n } from 'vue-i18n'
import { useDialogPluginComponent, useQuasar } from 'quasar'
// PROPS
const props = defineProps({
keyValue: {
type: String,
required: true
}
})
// EMITS
defineEmits([
...useDialogPluginComponent.emits
])
// QUASAR
const { dialogRef, onDialogHide, onDialogOK } = useDialogPluginComponent()
const $q = useQuasar()
// I18N
const { t } = useI18n()
</script>

@ -0,0 +1,253 @@
<template lang="pug">
q-dialog(ref='dialogRef', @hide='onDialogHide')
q-card(style='min-width: 650px;')
q-card-section.card-header
q-icon(name='img:/_assets/icons/fluent-plus-plus.svg', left, size='sm')
span {{t(`admin.api.newKeyTitle`)}}
q-form.q-py-sm(ref='createKeyForm', @submit='create')
q-item
blueprint-icon.self-start(icon='grand-master-key')
q-item-section
q-input(
outlined
v-model='state.keyName'
dense
:rules='keyNameValidation'
hide-bottom-space
:label='t(`admin.api.newKeyName`)'
:aria-label='t(`admin.api.newKeyName`)'
:hint='t(`admin.api.newKeyNameHint`)'
lazy-rules='ondemand'
autofocus
ref='iptName'
)
q-item
blueprint-icon.self-start(icon='schedule')
q-item-section
q-select(
outlined
:options='expirations'
v-model='state.keyExpiration'
multiple
map-options
option-value='value'
option-label='text'
emit-value
options-dense
dense
hide-bottom-space
:label='t(`admin.api.newKeyExpiration`)'
:aria-label='t(`admin.api.newKeyExpiration`)'
:hint='t(`admin.api.newKeyExpirationHint`)'
)
q-item
blueprint-icon.self-start(icon='access')
q-item-section
q-select(
outlined
:options='state.groups'
v-model='state.keyGroups'
multiple
map-options
emit-value
option-value='id'
option-label='name'
options-dense
dense
:rules='keyGroupsValidation'
hide-bottom-space
:label='t(`admin.api.permissionGroups`)'
:aria-label='t(`admin.api.permissionGroups`)'
:hint='t(`admin.api.newKeyGroupHint`)'
lazy-rules='ondemand'
:loading='state.loadingGroups'
)
template(v-slot:selected)
.text-caption(v-if='state.keyGroups.length > 1')
i18n-t(keypath='admin.api.groupsSelected')
template(#count)
strong {{ state.keyGroups.length }}
.text-caption(v-else-if='state.keyGroups.length === 1')
i18n-t(keypath='admin.api.groupSelected')
template(#group)
strong {{ selectedGroupName }}
span(v-else)
template(v-slot:option='{ itemProps, opt, selected, toggleOption }')
q-item(
v-bind='itemProps'
)
q-item-section(side)
q-checkbox(
size='sm'
:model-value='selected'
@update:model-value='toggleOption(opt)'
)
q-item-section
q-item-label {{opt.name}}
q-card-actions.card-actions
q-space
q-btn.acrylic-btn(
flat
:label='t(`common.actions.cancel`)'
color='grey'
padding='xs md'
@click='onDialogCancel'
)
q-btn(
unelevated
:label='t(`common.actions.create`)'
color='primary'
padding='xs md'
@click='create'
:loading='state.loading > 0'
)
</template>
<script setup>
import gql from 'graphql-tag'
import { cloneDeep, sampleSize } from 'lodash-es'
import { useI18n } from 'vue-i18n'
import { useDialogPluginComponent, useQuasar } from 'quasar'
import { computed, onMounted, reactive, ref } from 'vue'
import ApiKeyCopyDialog from './ApiKeyCopyDialog.vue'
// EMITS
defineEmits([
...useDialogPluginComponent.emits
])
// QUASAR
const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } = useDialogPluginComponent()
const $q = useQuasar()
// I18N
const { t } = useI18n()
// DATA
const state = reactive({
keyName: '',
keyExpiration: '90d',
keyGroups: [],
groups: [],
loadingGroups: false,
loading: false
})
const expirations = [
{ value: '30d', text: t('admin.api.expiration30d') },
{ value: '90d', text: t('admin.api.expiration90d') },
{ value: '180d', text: t('admin.api.expiration180d') },
{ value: '1y', text: t('admin.api.expiration1y') },
{ value: '3y', text: t('admin.api.expiration3y') }
]
// REFS
const createKeyForm = ref(null)
const iptName = ref(null)
// COMPUTED
const selectedGroupName = computed(() => {
return state.groups.filter(g => g.id === state.keyGroups[0])[0]?.name
})
// VALIDATION RULES
const keyNameValidation = [
val => val.length > 0 || t('admin.api.nameMissing'),
val => /^[^<>"]+$/.test(val) || t('admin.api.nameInvalidChars')
]
const keyGroupsValidation = [
val => val.length > 0 || t('admin.api.groupsMissing')
]
// METHODS
async function loadGroups () {
state.loading++
state.loadingGroups = true
const resp = await APOLLO_CLIENT.query({
query: gql`
query getGroupsForCreateApiKey {
groups {
id
name
}
}
`,
fetchPolicy: 'network-only'
})
state.groups = cloneDeep(resp?.data?.groups?.filter(g => g.id !== '10000000-0000-4000-8000-000000000001') ?? [])
state.loadingGroups = false
state.loading--
}
async function create () {
state.loading++
try {
const isFormValid = await createKeyForm.value.validate(true)
if (!isFormValid) {
throw new Error(t('admin.api.createInvalidData'))
}
const resp = await APOLLO_CLIENT.mutate({
mutation: gql`
mutation createApiKey (
$name: String!
$expiration: String!
$groups: [UUID]!
) {
createApiKey (
name: $name
expiration: $expiration
groups: $groups
) {
operation {
succeeded
message
}
key
}
}
`,
variables: {
name: state.keyName,
expiration: state.keyExpiration,
groups: state.keyGroups
}
})
if (resp?.data?.createApiKey?.operation?.succeeded) {
$q.notify({
type: 'positive',
message: t('admin.api.createSuccess')
})
$q.dialog({
component: ApiKeyCopyDialog,
componentProps: {
keyValue: resp?.data?.createApiKey?.key || 'ERROR'
}
}).onDismiss(() => {
onDialogOK()
})
} else {
throw new Error(resp?.data?.createApiKey?.operation?.message || 'An unexpected error occured.')
}
} catch (err) {
$q.notify({
type: 'negative',
message: err.message
})
}
state.loading--
}
// MOUNTED
onMounted(loadGroups)
</script>

@ -0,0 +1,104 @@
<template lang="pug">
q-dialog(ref='dialogRef', @hide='onDialogHide')
q-card(style='min-width: 350px; max-width: 450px;')
q-card-section.card-header
q-icon(name='img:/_assets/icons/fluent-unavailable.svg', left, size='sm')
span {{t(`admin.api.revokeConfirm`)}}
q-card-section
.text-body2
i18n-t(keypath='admin.api.revokeConfirmText')
template(#name)
strong {{apiKey.name}}
q-card-actions.card-actions
q-space
q-btn.acrylic-btn(
flat
:label='t(`common.actions.cancel`)'
color='grey'
padding='xs md'
@click='onDialogCancel'
)
q-btn(
unelevated
:label='t(`admin.api.revoke`)'
color='negative'
padding='xs md'
@click='confirm'
:loading='state.isLoading'
)
</template>
<script setup>
import gql from 'graphql-tag'
import { useI18n } from 'vue-i18n'
import { useDialogPluginComponent, useQuasar } from 'quasar'
import { reactive } from 'vue'
// PROPS
const props = defineProps({
apiKey: {
type: Object,
required: true
}
})
// EMITS
defineEmits([
...useDialogPluginComponent.emits
])
// QUASAR
const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } = useDialogPluginComponent()
const $q = useQuasar()
// I18N
const { t } = useI18n()
// DATA
const state = reactive({
isLoading: false
})
// METHODS
async function confirm () {
state.isLoading = true
try {
const resp = await APOLLO_CLIENT.mutate({
mutation: gql`
mutation revokeApiKey ($id: UUID!) {
revokeApiKey (id: $id) {
operation {
succeeded
message
}
}
}
`,
variables: {
id: props.apiKey.id
}
})
if (resp?.data?.revokeApiKey?.operation?.succeeded) {
$q.notify({
type: 'positive',
message: t('admin.api.revokeSuccess')
})
onDialogOK()
} else {
throw new Error(resp?.data?.revokeApiKey?.operation?.message || 'An unexpected error occured.')
}
} catch (err) {
$q.notify({
type: 'negative',
message: err.message
})
}
state.isLoading = false
}
</script>

@ -0,0 +1,101 @@
<template lang="pug">
q-dialog(ref='dialogRef', @hide='onDialogHide')
q-card(style='min-width: 350px; max-width: 450px;')
q-card-section.card-header
q-icon(name='img:/_assets/icons/fluent-downloading-updates.svg', left, size='sm')
span {{t(`admin.system.checkingForUpdates`)}}
q-card-section
.q-pa-md.text-center
img(src='/_assets/illustrations/undraw_going_up.svg', style='width: 150px;')
q-linear-progress(
indeterminate
size='lg'
rounded
)
.q-mt-sm.text-center.text-caption Fetching latest version info...
q-card-actions.card-actions
q-space
q-btn.acrylic-btn(
flat
:label='t(`common.actions.cancel`)'
color='grey'
padding='xs md'
@click='onDialogCancel'
)
q-btn(
v-if='state.canUpgrade'
unelevated
:label='t(`admin.system.upgrade`)'
color='primary'
padding='xs md'
@click='upgrade'
:loading='state.isLoading'
)
</template>
<script setup>
import gql from 'graphql-tag'
import { useI18n } from 'vue-i18n'
import { useDialogPluginComponent, useQuasar } from 'quasar'
import { reactive } from 'vue'
// EMITS
defineEmits([
...useDialogPluginComponent.emits
])
// QUASAR
const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } = useDialogPluginComponent()
const $q = useQuasar()
// I18N
const { t } = useI18n()
// DATA
const state = reactive({
isLoading: false,
canUpgrade: false
})
// METHODS
async function upgrade () {
state.isLoading = true
try {
const resp = await APOLLO_CLIENT.mutate({
mutation: gql`
mutation deleteHook ($id: UUID!) {
deleteHook(id: $id) {
operation {
succeeded
message
}
}
}
`,
variables: {
id: 0
}
})
if (resp?.data?.deleteHook?.operation?.succeeded) {
$q.notify({
type: 'positive',
message: t('admin.webhooks.deleteSuccess')
})
onDialogOK()
} else {
throw new Error(resp?.data?.deleteHook?.operation?.message || 'An unexpected error occured.')
}
} catch (err) {
$q.notify({
type: 'negative',
message: err.message
})
}
state.isLoading = false
}
</script>

@ -152,6 +152,27 @@ body::-webkit-scrollbar-thumb {
}
}
.card-negative {
display: flex;
align-items: center;
font-size: .9rem;
@at-root .body--light & {
background-color: $red-1;
background-image: radial-gradient(at bottom center, lighten($red-1, 2%), lighten($red-2, 2%));
border-bottom: 1px solid $red-3;
text-shadow: 0 0 4px #FFF;
color: $red-9;
}
@at-root .body--dark & {
background-color: $red-9;
background-image: radial-gradient(at bottom center, $red-7, $red-9);
border-bottom: 1px solid $red-7;
text-shadow: 0 0 4px darken($red-9, 10%);
color: #FFF;
}
}
.card-actions {
@at-root .body--light & {
background-color: #FAFAFA;

@ -24,7 +24,7 @@
"admin.api.headerRevoke": "Revoke",
"admin.api.newKeyButton": "New API Key",
"admin.api.newKeyCopyWarn": "Copy the key shown below as {bold}",
"admin.api.newKeyCopyWarnBold": "it will NOT be shown again",
"admin.api.newKeyCopyWarnBold": "it will NOT be shown again.",
"admin.api.newKeyExpiration": "Expiration",
"admin.api.newKeyExpirationHint": "You can still revoke a key anytime regardless of the expiration.",
"admin.api.newKeyFullAccess": "Full Access",
@ -1449,5 +1449,22 @@
"admin.auth.enabledHint": "Should this strategy be available to sites for login.",
"admin.auth.vendor": "Vendor",
"admin.auth.vendorWebsite": "Website",
"admin.auth.status": "Status"
"admin.auth.status": "Status",
"admin.system.checkingForUpdates": "Checking for Updates...",
"admin.system.upgrade": "Upgrade",
"admin.system.checkForUpdates": "Check",
"admin.system.checkUpdate": "Check / Upgrade",
"admin.api.none": "There are no API keys yet.",
"admin.api.groupsMissing": "You must select at least 1 group for this key.",
"admin.api.nameInvalidChars": "Key name has invalid characters.",
"admin.api.nameMissing": "Key name is missing.",
"admin.api.permissionGroups": "Group Permissions",
"admin.api.groupSelected": "Use {group} group permissions",
"admin.api.groupsSelected": "Use permissions from {count} groups",
"admin.api.createInvalidData": "Some fields are missing or have invalid data.",
"admin.api.copyKeyTitle": "Copy API Key",
"admin.api.key": "API Key",
"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."
}

@ -9,7 +9,7 @@ q-page.admin-api
.col
.flex.items-center
template(v-if='state.enabled')
q-spinner-rings.q-mr-sm(color='green')
q-spinner-rings.q-mr-sm(color='green', size='md')
.text-caption.text-green {{t('admin.api.enabled')}}
template(v-else)
q-spinner-rings.q-mr-sm(color='red', size='md')
@ -28,7 +28,7 @@ q-page.admin-api
flat
color='secondary'
:loading='state.loading > 0'
@click='load'
@click='refresh'
)
q-btn.q-mr-sm(
unelevated
@ -36,6 +36,7 @@ q-page.admin-api
:label='!state.enabled ? t(`admin.api.enableButton`) : t(`admin.api.disableButton`)'
:color='!state.enabled ? `positive` : `negative`'
@click='globalSwitch'
:loading='state.isToggleLoading'
:disabled='state.loading > 0'
)
q-btn(
@ -48,70 +49,47 @@ q-page.admin-api
)
q-separator(inset)
.row.q-pa-md.q-col-gutter-md
.col-12.col-lg-7
q-card.shadow-1
//- v-container(fluid, grid-list-lg)
//- v-layout(row, wrap)
//- v-flex(xs12)
//- .admin-header
//- img.animated.fadeInUp(src='/_assets/svg/icon-rest-api.svg', alt='API', style='width: 80px;')
//- .admin-header-title
//- .headline.primary--text.animated.fadeInLeft {{$t('admin.api.title')}}
//- .subtitle-1.grey--text.animated.fadeInLeft {{$t('admin.api.subtitle')}}
//- v-spacer
//- template(v-if='enabled')
//- status-indicator.mr-3(positive, pulse)
//- .caption.green--text.animated.fadeInLeft {{$t('admin.api.enabled')}}
//- template(v-else)
//- status-indicator.mr-3(negative, pulse)
//- .caption.red--text.animated.fadeInLeft {{$t('admin.api.disabled')}}
//- v-spacer
//- v-btn.mr-3.animated.fadeInDown.wait-p2s(outlined, color='grey', icon, @click='refresh')
//- v-icon mdi-refresh
//- v-btn.mr-3.animated.fadeInDown.wait-p1s(:color='enabled ? `red` : `green`', depressed, @click='globalSwitch', dark, :loading='isToggleLoading')
//- v-icon(left) mdi-power
//- span(v-if='!enabled') {{$t('admin.api.enableButton')}}
//- span(v-else) {{$t('admin.api.disableButton')}}
//- v-btn.animated.fadeInDown(color='primary', depressed, large, @click='newKey', dark)
//- v-icon(left) mdi-plus
//- span {{$t('admin.api.newKeyButton')}}
//- v-card.mt-3.animated.fadeInUp
//- v-simple-table(v-if='keys && keys.length > 0')
//- template(v-slot:default)
//- thead
//- tr.grey(:class='$vuetify.theme.dark ? `darken-4-d5` : `lighten-5`')
//- th {{$t('admin.api.headerName')}}
//- th {{$t('admin.api.headerKeyEnding')}}
//- th {{$t('admin.api.headerExpiration')}}
//- th {{$t('admin.api.headerCreated')}}
//- th {{$t('admin.api.headerLastUpdated')}}
//- th(width='100') {{$t('admin.api.headerRevoke')}}
//- tbody
//- tr(v-for='key of keys', :key='`key-` + key.id')
//- td
//- strong(:class='key.isRevoked ? `red--text` : ``') {{ key.name }}
//- em.caption.ml-1.red--text(v-if='key.isRevoked') (revoked)
//- td.caption {{ key.keyShort }}
//- td(:style='key.isRevoked ? `text-decoration: line-through;` : ``') {{ key.expiration | moment('LL') }}
//- td {{ key.createdAt | moment('calendar') }}
//- td {{ key.updatedAt | moment('calendar') }}
//- td: v-btn(icon, @click='revoke(key)', :disabled='key.isRevoked'): v-icon(color='error') mdi-cancel
//- v-card-text(v-else)
//- v-alert.mb-0(icon='mdi-information', :value='true', outlined, color='info') {{$t('admin.api.noKeyInfo')}}
//- create-api-key(v-model='isCreateDialogShown', @refresh='refresh(false)')
//- v-dialog(v-model='isRevokeConfirmDialogShown', max-width='500', persistent)
//- v-card
//- .dialog-header.is-red {{$t('admin.api.revokeConfirm')}}
//- v-card-text.pa-4
//- i18next(tag='span', path='admin.api.revokeConfirmText')
//- strong(place='name') {{ current.name }}
//- v-card-actions
//- v-spacer
//- v-btn(text, @click='isRevokeConfirmDialogShown = false', :disabled='revokeLoading') {{$t('common.actions.cancel')}}
//- v-btn(color='red', dark, @click='revokeConfirm', :loading='revokeLoading') {{$t('admin.api.revoke')}}
.col-12(v-if='state.keys.length < 1')
q-card.rounded-borders(
flat
:class='$q.dark.isActive ? `bg-dark-5 text-white` : `bg-grey-3 text-dark`'
)
q-card-section.items-center(horizontal)
q-card-section.col-auto.q-pr-none
q-icon(name='las la-info-circle', size='sm')
q-card-section.text-caption {{ t('admin.api.none') }}
.col-12(v-else)
q-card
q-list(separator)
q-item(v-for='key of state.keys', :key='key.id')
q-item-section(side)
q-icon(name='las la-key', :color='key.isRevoked ? `negative` : `positive`')
q-item-section
q-item-label {{key.name}}
q-item-label(caption) Ending in {{key.keyShort}}
q-item-label(caption) Created On: #[strong {{DateTime.fromISO(key.createdAt).toFormat('fff')}}]
q-item-label(caption) Expiration: #[strong(:style='key.isRevoked ? `text-decoration: line-through;` : ``') {{DateTime.fromISO(key.expiration).toFormat('fff')}}]
q-item-section(
v-if='key.isRevoked'
side
style='flex-direction: row; align-items: center;'
)
q-icon.q-mr-sm(
color='negative'
size='xs'
name='las la-exclamation-triangle'
)
.text-caption.text-negative {{t('admin.api.revoked')}}
q-tooltip(anchor='center left', self='center right') {{t('admin.api.revokedHint')}}
q-separator.q-ml-md(vertical)
q-item-section(side, style='flex-direction: row; align-items: center;')
q-btn.acrylic-btn(
:color='key.isRevoked ? `gray` : `red`'
icon='las la-ban'
flat
@click='revoke(key)'
:disable='key.isRevoked'
)
</template>
<script setup>
@ -120,6 +98,10 @@ import { cloneDeep } from 'lodash-es'
import { useI18n } from 'vue-i18n'
import { useMeta, useQuasar } from 'quasar'
import { computed, onMounted, reactive, watch } from 'vue'
import { DateTime } from 'luxon'
import ApiKeyCreateDialog from '../components/ApiKeyCreateDialog.vue'
import ApiKeyRevokeDialog from '../components/ApiKeyRevokeDialog.vue'
// QUASAR
@ -140,6 +122,7 @@ useMeta({
const state = reactive({
enabled: false,
loading: 0,
isToggleLoading: false,
keys: [],
isCreateDialogShown: false,
isRevokeConfirmDialogShown: false,
@ -154,7 +137,7 @@ async function load () {
$q.loading.show()
const resp = await APOLLO_CLIENT.query({
query: gql`
query getHooks {
query getApiKeys {
apiKeys {
id
name
@ -175,20 +158,24 @@ async function load () {
state.loading--
}
async function refresh () {
await load()
$q.notify({
type: 'positive',
message: t('admin.api.refreshSuccess')
})
}
async function globalSwitch () {
state.isToggleLoading = true
try {
const resp = await APOLLO_CLIENT.mutate({
mutation: gql`
mutation ($enabled: Boolean!) {
authentication {
setApiState (enabled: $enabled) {
responseResult {
succeeded
errorCode
slug
message
}
setApiState (enabled: $enabled) {
operation {
succeeded
message
}
}
}
@ -204,7 +191,7 @@ async function globalSwitch () {
})
await load()
} else {
throw new Error(resp?.data?.setApiState?.operation.message || 'An unexpected error occurred.')
throw new Error(resp?.data?.setApiState?.operation?.message || 'An unexpected error occurred.')
}
} catch (err) {
$q.notify({
@ -217,62 +204,27 @@ async function globalSwitch () {
}
async function newKey () {
state.isCreateDialogShown = true
$q.dialog({
component: ApiKeyCreateDialog
}).onOk(() => {
load()
})
}
function revoke (key) {
state.current = key
state.isRevokeConfirmDialogShown = true
}
async function revokeConfirm () {
state.revokeLoading = true
try {
const resp = await APOLLO_CLIENT.mutate({
mutation: gql`
mutation ($id: Int!) {
authentication {
revokeApiKey (id: $id) {
responseResult {
succeeded
errorCode
slug
message
}
}
}
}
`,
variables: {
id: state.current.id
}
})
// if (_get(resp, 'data.authentication.revokeApiKey.responseResult.succeeded', false)) {
// this.$store.commit('showNotification', {
// style: 'success',
// message: this.$t('admin.api.revokeSuccess'),
// icon: 'check'
// })
// this.load()
// } else {
// this.$store.commit('showNotification', {
// style: 'red',
// message: _get(resp, 'data.authentication.revokeApiKey.responseResult.message', 'An unexpected error occurred.'),
// icon: 'alert'
// })
// }
} catch (err) {
// this.$store.commit('pushGraphError', err)
}
state.isRevokeConfirmDialogShown = false
state.revokeLoading = false
$q.dialog({
component: ApiKeyRevokeDialog,
componentProps: {
apiKey: key
}
}).onOk(() => {
load()
})
}
// MOUNTED
onMounted(() => {
load()
})
onMounted(load)
</script>

@ -1,236 +0,0 @@
<template lang="pug">
div
v-dialog(v-model='isShown', max-width='650', persistent)
v-card
.dialog-header.is-short
v-icon.mr-3(color='white') mdi-plus
span {{$t('admin.api.newKeyTitle')}}
v-card-text.pt-5
v-text-field(
outlined
prepend-icon='mdi-format-title'
v-model='name'
:label='$t(`admin.api.newKeyName`)'
persistent-hint
ref='keyNameInput'
:hint='$t(`admin.api.newKeyNameHint`)'
counter='255'
)
v-select.mt-3(
:items='expirations'
outlined
prepend-icon='mdi-clock'
v-model='expiration'
:label='$t(`admin.api.newKeyExpiration`)'
:hint='$t(`admin.api.newKeyExpirationHint`)'
persistent-hint
)
v-divider.mt-4
v-subheader.pl-2: strong.indigo--text {{$t('admin.api.newKeyPermissionScopes')}}
v-list.pl-8(nav)
v-list-item-group(v-model='fullAccess')
v-list-item(
:value='true'
active-class='indigo--text'
)
template(v-slot:default='{ active, toggle }')
v-list-item-action
v-checkbox(
:input-value='active'
:true-value='true'
color='indigo'
@click='toggle'
)
v-list-item-content
v-list-item-title {{$t('admin.api.newKeyFullAccess')}}
v-divider.mt-3
v-subheader.caption.indigo--text {{$t('admin.api.newKeyGroupPermissions')}}
v-list-item
v-select(
:disabled='fullAccess'
:items='groups'
item-text='name'
item-value='id'
outlined
color='indigo'
v-model='group'
:label='$t(`admin.api.newKeyGroup`)'
:hint='$t(`admin.api.newKeyGroupHint`)'
persistent-hint
)
v-card-chin
v-spacer
v-btn(text, @click='isShown = false', :disabled='loading') {{$t('common.actions.cancel')}}
v-btn.px-3(depressed, color='primary', @click='generate', :loading='loading')
v-icon(left) mdi-chevron-right
span {{$t('common.actions.generate')}}
v-dialog(
v-model='isCopyKeyDialogShown'
max-width='750'
persistent
overlay-color='blue darken-5'
overlay-opacity='.9'
)
v-card
v-toolbar(dense, flat, color='primary', dark) {{$t('admin.api.newKeyTitle')}}
v-card-text.pt-5
.body-2.text-center
i18next(tag='span', path='admin.api.newKeyCopyWarn')
strong(place='bold') {{$t('admin.api.newKeyCopyWarnBold')}}
v-textarea.mt-3(
ref='keyContentsIpt'
filled
no-resize
readonly
v-model='key'
:rows='10'
hide-details
)
v-card-chin
v-spacer
v-btn.px-3(depressed, dark, color='primary', @click='isCopyKeyDialogShown = false') {{$t('common.actions.close')}}
</template>
<script>
import _ from 'lodash'
import gql from 'graphql-tag'
import groupsQuery from 'gql/admin/users/users-query-groups.gql'
export default {
props: {
value: {
type: Boolean,
default: false
}
},
data() {
return {
loading: false,
name: '',
expiration: '1y',
fullAccess: true,
groups: [],
group: null,
isCopyKeyDialogShown: false,
key: ''
}
},
computed: {
isShown: {
get() { return this.value },
set(val) { this.$emit('input', val) }
},
expirations() {
return [
{ value: '30d', text: this.$t('admin.api.expiration30d') },
{ value: '90d', text: this.$t('admin.api.expiration90d') },
{ value: '180d', text: this.$t('admin.api.expiration180d') },
{ value: '1y', text: this.$t('admin.api.expiration1y') },
{ value: '3y', text: this.$t('admin.api.expiration3y') }
]
}
},
watch: {
value (newValue, oldValue) {
if (newValue) {
setTimeout(() => {
this.$refs.keyNameInput.focus()
}, 400)
}
}
},
methods: {
async generate () {
try {
if (_.trim(this.name).length < 2 || this.name.length > 255) {
throw new Error(this.$t('admin.api.newKeyNameError'))
} else if (!this.fullAccess && !this.group) {
throw new Error(this.$t('admin.api.newKeyGroupError'))
} else if (!this.fullAccess && this.group === 2) {
throw new Error(this.$t('admin.api.newKeyGuestGroupError'))
}
} catch (err) {
return this.$store.commit('showNotification', {
style: 'red',
message: err,
icon: 'alert'
})
}
this.loading = true
try {
const resp = await this.$apollo.mutate({
mutation: gql`
mutation ($name: String!, $expiration: String!, $fullAccess: Boolean!, $group: Int) {
authentication {
createApiKey (name: $name, expiration: $expiration, fullAccess: $fullAccess, group: $group) {
key
responseResult {
succeeded
errorCode
slug
message
}
}
}
}
`,
variables: {
name: this.name,
expiration: this.expiration,
fullAccess: (this.fullAccess === true),
group: this.group
},
watchLoading (isLoading) {
this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-api-create')
}
})
if (_.get(resp, 'data.authentication.createApiKey.responseResult.succeeded', false)) {
this.$store.commit('showNotification', {
style: 'success',
message: this.$t('admin.api.newKeySuccess'),
icon: 'check'
})
this.name = ''
this.expiration = '1y'
this.fullAccess = true
this.group = null
this.isShown = false
this.$emit('refresh')
this.key = _.get(resp, 'data.authentication.createApiKey.key', '???')
this.isCopyKeyDialogShown = true
setTimeout(() => {
this.$refs.keyContentsIpt.$refs.input.select()
}, 400)
} else {
this.$store.commit('showNotification', {
style: 'red',
message: _.get(resp, 'data.authentication.createApiKey.responseResult.message', 'An unexpected error occurred.'),
icon: 'alert'
})
}
} catch (err) {
this.$store.commit('pushGraphError', err)
}
this.loading = false
}
},
apollo: {
groups: {
query: groupsQuery,
fetchPolicy: 'network-only',
update: (data) => data.groups.list,
watchLoading (isLoading) {
this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-api-groups-refresh')
}
}
}
}
</script>

@ -162,8 +162,9 @@ q-page.admin-mail
.text-subtitle1 {{state.strategy.strategy.title}}
q-img.q-mt-sm.rounded-borders(
:src='state.strategy.strategy.logo'
fit='cover'
fit='contain'
no-spinner
style='height: 100px;'
)
.text-body2.q-mt-md {{state.strategy.strategy.description}}
q-separator.q-mb-sm(inset)

@ -97,12 +97,13 @@ q-page.admin-dashboard
template(#action)
q-btn(
flat
label='Check'
:label='t(`admin.system.checkForUpdates`)'
@click='checkForUpdates'
)
q-separator.q-mx-sm(vertical, dark)
q-btn(
flat
label='System Info'
:label='t(`admin.system.title`)'
to='/_admin/system'
)
.col-12
@ -224,6 +225,7 @@ import { useAdminStore } from '../stores/admin'
// COMPONENTS
import CheckUpdateDialog from '../components/CheckUpdateDialog.vue'
import SiteCreateDialog from '../components/SiteCreateDialog.vue'
import UserCreateDialog from '../components/UserCreateDialog.vue'
@ -265,6 +267,11 @@ function newUser () {
router.push('/_admin/users')
})
}
function checkForUpdates () {
$q.dialog({
component: CheckUpdateDialog
})
}
</script>

@ -77,6 +77,10 @@ q-page.admin-flags
unchecked-icon='las la-times'
:aria-label='t(`admin.flags.hidedonatebtn.label`)'
)
.col-12.col-lg-5.gt-md
.q-pa-md.text-center
img(src='/_assets/illustrations/undraw_settings.svg', style='width: 80%;')
</template>
<script setup>

@ -117,6 +117,9 @@ q-page.admin-locale
:aria-label='lc.name'
)
.q-pa-md.text-center.gt-md(v-else)
img(src='/_assets/illustrations/undraw_world.svg', style='width: 80%;')
//- q-separator.q-my-sm(inset)
//- q-item
//- blueprint-icon(icon='test-passed')

@ -54,7 +54,16 @@ q-page.admin-system
q-item-label {{ t('admin.system.latestVersion') }}
q-item-label(caption) {{t('admin.system.latestVersionHint')}}
q-item-section
q-item-label.dark-value(caption) {{ state.info.latestVersion }}
.row.q-col-gutter-sm
.col
.dark-value(caption) {{ state.info.latestVersion }}
.col-auto
q-btn.acrylic-btn(
flat
color='purple'
@click='checkForUpdates'
:label='t(`admin.system.checkUpdate`)'
)
//- -----------------------
//- CLIENT
@ -234,6 +243,8 @@ import { useMeta, useQuasar } from 'quasar'
import { computed, onMounted, reactive, ref, watch } from 'vue'
import ClipboardJS from 'clipboard'
import CheckUpdateDialog from '../components/CheckUpdateDialog.vue'
// QUASAR
const $q = useQuasar()
@ -340,6 +351,12 @@ async function load () {
state.loading--
}
function checkForUpdates () {
$q.dialog({
component: CheckUpdateDialog
})
}
// async function performUpgrade () {
// state.isUpgrading = true
// state.isUpgradingStarted = false

@ -570,21 +570,21 @@ __metadata:
languageName: node
linkType: hard
"@intlify/bundle-utils@npm:^2.2.2":
version: 2.2.2
resolution: "@intlify/bundle-utils@npm:2.2.2"
"@intlify/bundle-utils@npm:next":
version: 3.1.0
resolution: "@intlify/bundle-utils@npm:3.1.0"
dependencies:
"@intlify/message-compiler": ^9.1.0
"@intlify/shared": ^9.1.0
"@intlify/message-compiler": next
"@intlify/shared": next
jsonc-eslint-parser: ^1.0.1
source-map: ^0.6.1
source-map: 0.6.1
yaml-eslint-parser: ^0.3.2
peerDependenciesMeta:
petite-vue-i18n:
optional: true
vue-i18n:
optional: true
checksum: fc447a353eb076797f60800e9d5ef451a5dacc758bbb9288306d418e963c06c6aa28973e0901b4924c160b3ed38126cf5697bad03ea11b9c21bf5ec6a9aae4a9
checksum: 708f071736e5ae2f55929b9561b5488c4bb087a46e7e75208e181ef3e6fea66c283a2cd47027df7bb2b2d7dbcc9792bdbe9bd94753400b8f18ea86c057f2bdd2
languageName: node
linkType: hard
@ -622,14 +622,13 @@ __metadata:
languageName: node
linkType: hard
"@intlify/message-compiler@npm:^9.1.0":
version: 9.1.9
resolution: "@intlify/message-compiler@npm:9.1.9"
"@intlify/message-compiler@npm:next":
version: 9.2.0-beta.40
resolution: "@intlify/message-compiler@npm:9.2.0-beta.40"
dependencies:
"@intlify/message-resolver": 9.1.9
"@intlify/shared": 9.1.9
"@intlify/shared": 9.2.0-beta.40
source-map: 0.6.1
checksum: 4677349715199f833a042df9852b4285c4384accf3715182492b6fffdbbd5948bb8527eb611021f9c5184e599882e79ea5935d7d43e4f8ab02a47dc7e7989c3e
checksum: 4f1c47b1a00b98213b8d18b8f458f092850b2a64ef82ba915486ab82b7fa9f0f94cc57e0d7cbfb63cad27d284a7c351300165eac414f383e55013cc7c928c2a5
languageName: node
linkType: hard
@ -640,13 +639,6 @@ __metadata:
languageName: node
linkType: hard
"@intlify/message-resolver@npm:9.1.9":
version: 9.1.9
resolution: "@intlify/message-resolver@npm:9.1.9"
checksum: bd7097927f0a83b3f44d810be0aaa46f4d779e0a70c2a1a71dfc1ad058de5f0b1afa9121cde03dd42732303a70a66ec5c9679efda53c36222945081c1fde9cbb
languageName: node
linkType: hard
"@intlify/runtime@npm:9.1.10":
version: 9.1.10
resolution: "@intlify/runtime@npm:9.1.10"
@ -665,33 +657,35 @@ __metadata:
languageName: node
linkType: hard
"@intlify/shared@npm:9.1.9, @intlify/shared@npm:^9.1.0":
version: 9.1.9
resolution: "@intlify/shared@npm:9.1.9"
checksum: 39c589eefadff0a5b1f6210f9010a382c1e100a79beebddd57f67e74297abb67a501d6e0d7e0dc5ba49ccef4069cd877bc36cc0ea4f712442bad76eaa15eed4a
"@intlify/shared@npm:9.2.0-beta.40, @intlify/shared@npm:next":
version: 9.2.0-beta.40
resolution: "@intlify/shared@npm:9.2.0-beta.40"
checksum: a28f175c8e79136e43bcb4c817e586c859fcff195b11eb79d25e6a9c82f8e2079b8a4447333be93e36533957cafb35d7e0e550659eeb8451aa0841185c1d59db
languageName: node
linkType: hard
"@intlify/vite-plugin-vue-i18n@npm:3.4.0":
version: 3.4.0
resolution: "@intlify/vite-plugin-vue-i18n@npm:3.4.0"
"@intlify/vite-plugin-vue-i18n@npm:5.0.1":
version: 5.0.1
resolution: "@intlify/vite-plugin-vue-i18n@npm:5.0.1"
dependencies:
"@intlify/bundle-utils": ^2.2.2
"@intlify/shared": ^9.1.0
"@rollup/pluginutils": ^4.1.0
"@intlify/bundle-utils": next
"@intlify/shared": next
"@rollup/pluginutils": ^4.2.0
debug: ^4.3.1
fast-glob: ^3.2.5
source-map: 0.6.1
peerDependencies:
petite-vue-i18n: ^9.1.0
vite: ^2.0.0
vue-i18n: ^9.1.0
petite-vue-i18n: "*"
vite: ^2.9.0 || ^3.0.0
vue-i18n: "*"
peerDependenciesMeta:
petite-vue-i18n:
optional: true
vite:
optional: true
vue-i18n:
optional: true
checksum: 5d1c59525e00c25d6b6ee0b3f9ae94ec0f67a0c96f4954051dc28a922da673dff90d79bfac090a830194ffc11e4e523691d56de609d3723142491e3b34c2537e
checksum: ba7561ea1be2cba4cbd738064bf1b2944b7487364b83e8e8af705d8b05e39467ada16008560c7564a532c2d59cf95be83401fe8f3e6e0b3e01ecad11f42d6fa0
languageName: node
linkType: hard
@ -952,10 +946,10 @@ __metadata:
languageName: node
linkType: hard
"@quasar/extras@npm:1.14.2":
version: 1.14.2
resolution: "@quasar/extras@npm:1.14.2"
checksum: 600305d8fb641eca6990ef1527ce544c35835a4af088b58263851b70df444d6890e134dc01cb7da3f256ec8b5ae369ce2d5c29ddadff2452fff6e080e0411905
"@quasar/extras@npm:1.15.0":
version: 1.15.0
resolution: "@quasar/extras@npm:1.15.0"
checksum: 2fb3274f441ef4db6b0925a9d72bc80845793ac3ca07b7929a89978922236e8c09667bf2f633a8103ba197d0535a757c66fc4d5337664138b8c6f35418ae9ea2
languageName: node
linkType: hard
@ -977,7 +971,7 @@ __metadata:
languageName: node
linkType: hard
"@rollup/pluginutils@npm:^4.1.0, @rollup/pluginutils@npm:^4.1.2":
"@rollup/pluginutils@npm:^4.1.2":
version: 4.2.0
resolution: "@rollup/pluginutils@npm:4.2.0"
dependencies:
@ -987,6 +981,16 @@ __metadata:
languageName: node
linkType: hard
"@rollup/pluginutils@npm:^4.2.0":
version: 4.2.1
resolution: "@rollup/pluginutils@npm:4.2.1"
dependencies:
estree-walker: ^2.0.1
picomatch: ^2.2.2
checksum: 6bc41f22b1a0f1efec3043899e4d3b6b1497b3dea4d94292d8f83b4cf07a1073ecbaedd562a22d11913ff7659f459677b01b09e9598a98936e746780ecc93a12
languageName: node
linkType: hard
"@tiptap/core@npm:2.0.0-beta.176, @tiptap/core@npm:^2.0.0-beta.176":
version: 2.0.0-beta.176
resolution: "@tiptap/core@npm:2.0.0-beta.176"
@ -1710,18 +1714,6 @@ __metadata:
languageName: node
linkType: hard
"@vue/apollo-option@npm:4.0.0-alpha.17":
version: 4.0.0-alpha.17
resolution: "@vue/apollo-option@npm:4.0.0-alpha.17"
dependencies:
throttle-debounce: ^3.0.1
peerDependencies:
"@apollo/client": ^3.2.1
vue: ^3.1.0
checksum: f1d1152131a1bfa36497c7bc42fd7c1ca2848a0a98943990b4943ef80d528ce7713be41e6943dbf7755726f413b350731fdb868cece69dee5d37377346b7fe01
languageName: node
linkType: hard
"@vue/compiler-core@npm:3.2.37":
version: 3.2.37
resolution: "@vue/compiler-core@npm:3.2.37"
@ -1779,6 +1771,13 @@ __metadata:
languageName: node
linkType: hard
"@vue/devtools-api@npm:^6.2.1":
version: 6.2.1
resolution: "@vue/devtools-api@npm:6.2.1"
checksum: 34765af0be9b0cc7e3def73b2792b1514e3c348852c5a7503fe07d013f0e907af6c27c0a32c0637dd748caf37c075af8e53ca3220433e0bd34b6f3405f358272
languageName: node
linkType: hard
"@vue/reactivity-transform@npm:3.2.37":
version: 3.2.37
resolution: "@vue/reactivity-transform@npm:3.2.37"
@ -2232,10 +2231,10 @@ __metadata:
languageName: node
linkType: hard
"browser-fs-access@npm:0.30.2":
version: 0.30.2
resolution: "browser-fs-access@npm:0.30.2"
checksum: 4fa53ebe2e5ea5fb2df39f2ce78ae67cf3b0710877f123d57d8a9f7be171e27b8c8413079fe023612aa05d27549c38ff68a94f694750fd766cf9ecd80067d822
"browser-fs-access@npm:0.31.0":
version: 0.31.0
resolution: "browser-fs-access@npm:0.31.0"
checksum: d1b6682415c2ee4c05dc44cd95daaa1a4f3f59d1ec723bde0d384bf44b71c804a665ca24a3805d0a76d7f2626541d4a777df4f45ae4a7439a8b76e20897d301d
languageName: node
linkType: hard
@ -2705,6 +2704,13 @@ __metadata:
languageName: node
linkType: hard
"csstype@npm:^3.1.0":
version: 3.1.0
resolution: "csstype@npm:3.1.0"
checksum: 644e986cefab86525f0b674a06889cfdbb1f117e5b7d1ce0fc55b0423ecc58807a1ea42ecc75c4f18999d14fc42d1d255f84662a45003a52bb5840e977eb2ffd
languageName: node
linkType: hard
"d3-dispatch@npm:1 - 3":
version: 3.0.1
resolution: "d3-dispatch@npm:3.0.1"
@ -3513,9 +3519,9 @@ __metadata:
languageName: node
linkType: hard
"eslint-plugin-vue@npm:9.2.0":
version: 9.2.0
resolution: "eslint-plugin-vue@npm:9.2.0"
"eslint-plugin-vue@npm:9.3.0":
version: 9.3.0
resolution: "eslint-plugin-vue@npm:9.3.0"
dependencies:
eslint-utils: ^3.0.0
natural-compare: ^1.4.0
@ -3526,7 +3532,7 @@ __metadata:
xml-name-validator: ^4.0.0
peerDependencies:
eslint: ^6.2.0 || ^7.0.0 || ^8.0.0
checksum: 008819b12ad50ed62bfdc7f93e9e610575fa16dcccfb62ad9bb4ad27e69b1245419bf76752285742434fa6b9cf076f6fc173324084639e10209b2af2992630df
checksum: 3a1819749fb9a617f3647a58ee21595e811a9bcbf2a5eb1c2105da24c41c7b91a58e4473d751d6ab78cc0bb140dc7e11aac09337fc2c7d44401564a11ec92100
languageName: node
linkType: hard
@ -3581,9 +3587,9 @@ __metadata:
languageName: node
linkType: hard
"eslint@npm:8.19.0":
version: 8.19.0
resolution: "eslint@npm:8.19.0"
"eslint@npm:8.20.0":
version: 8.20.0
resolution: "eslint@npm:8.20.0"
dependencies:
"@eslint/eslintrc": ^1.3.0
"@humanwhocodes/config-array": ^0.9.2
@ -3622,7 +3628,7 @@ __metadata:
v8-compile-cache: ^2.0.3
bin:
eslint: bin/eslint.js
checksum: 0bc9df1a3a09dcd5a781ec728f280aa8af3ab19c2d1f14e2668b5ee5b8b1fb0e72dde5c3acf738e7f4281685fb24ec149b6154255470b06cf41de76350bca7a4
checksum: a31adf390d71d916925586bc8467b48f620e93dd0416bc1e897d99265af88b48d4eba3985b5ff4653ae5cc46311a360d373574002277e159bb38a4363abf9228
languageName: node
linkType: hard
@ -4905,10 +4911,10 @@ __metadata:
languageName: node
linkType: hard
"luxon@npm:2.5.0":
version: 2.5.0
resolution: "luxon@npm:2.5.0"
checksum: 2fccce6bbdfc8f13c5a8c148ff045ab3b10f4f80cac28dd92575588fffce9b2d7197096d7fedcc61a6245b59f4233507797f530e63f22b9ae4c425dff2909ae3
"luxon@npm:3.0.1":
version: 3.0.1
resolution: "luxon@npm:3.0.1"
checksum: aa966eb919bf95b1bd819cda784d1f6f66e3fb65bd9ec7bf68b6a978eeb4e3e14f7e2275021b473f93b15b6b7ba2e5a30471e53add3929a7e695fcfd6dd40ec8
languageName: node
linkType: hard
@ -5518,11 +5524,11 @@ __metadata:
languageName: node
linkType: hard
"pinia@npm:2.0.14":
version: 2.0.14
resolution: "pinia@npm:2.0.14"
"pinia@npm:2.0.17":
version: 2.0.17
resolution: "pinia@npm:2.0.17"
dependencies:
"@vue/devtools-api": ^6.1.4
"@vue/devtools-api": ^6.2.1
vue-demi: "*"
peerDependencies:
"@vue/composition-api": ^1.4.0
@ -5533,7 +5539,7 @@ __metadata:
optional: true
typescript:
optional: true
checksum: d07ed55b53e92da0971c3fcc0a1bd520b26cd50d266f46c8bab24fed87788461782a2c75088cf97c79810e7a4ceb28381f5d77daf280883ad3340c41962d0934
checksum: d308c6358570242b6c8126d990d756a224d1a77dfa375db5069da7f1c350372bf4e07feb4efce0d924dfc1b39f39d533234cdecf3594c4a2876de30477f6f978
languageName: node
linkType: hard
@ -6591,13 +6597,6 @@ __metadata:
languageName: node
linkType: hard
"throttle-debounce@npm:^3.0.1":
version: 3.0.1
resolution: "throttle-debounce@npm:3.0.1"
checksum: e34ef638e8df3a9154249101b68afcbf2652a139c803415ef8a2f6a8bc577bcd4d79e4bb914ad3cd206523ac78b9fb7e80885bfa049f64fbb1927f99d98b5736
languageName: node
linkType: hard
"through@npm:^2.3.6":
version: 2.3.8
resolution: "through@npm:2.3.8"
@ -6837,10 +6836,10 @@ __metadata:
"@codemirror/state": 6.0.1
"@codemirror/tooltip": 0.19.16
"@codemirror/view": 6.0.2
"@intlify/vite-plugin-vue-i18n": 3.4.0
"@intlify/vite-plugin-vue-i18n": 5.0.1
"@lezer/common": 1.0.0
"@quasar/app-vite": 1.0.5
"@quasar/extras": 1.14.2
"@quasar/extras": 1.15.0
"@tiptap/core": 2.0.0-beta.176
"@tiptap/extension-code-block": 2.0.0-beta.37
"@tiptap/extension-code-block-lowlight": 2.0.0-beta.68
@ -6866,18 +6865,17 @@ __metadata:
"@tiptap/starter-kit": 2.0.0-beta.185
"@tiptap/vue-3": 2.0.0-beta.91
"@types/lodash": 4.14.182
"@vue/apollo-option": 4.0.0-alpha.17
apollo-upload-client: 17.0.0
browser-fs-access: 0.30.2
browser-fs-access: 0.31.0
browserlist: latest
clipboard: 2.0.11
codemirror: 6.0.1
eslint: 8.19.0
eslint: 8.20.0
eslint-config-standard: 17.0.0
eslint-plugin-import: 2.26.0
eslint-plugin-n: 15.2.4
eslint-plugin-promise: 6.0.0
eslint-plugin-vue: 9.2.0
eslint-plugin-vue: 9.3.0
filesize: 9.0.11
filesize-parser: 1.5.0
graphql: 16.5.0
@ -6885,32 +6883,32 @@ __metadata:
js-cookie: 3.0.1
jwt-decode: 3.1.2
lodash-es: 4.17.21
luxon: 2.5.0
pinia: 2.0.14
luxon: 3.0.1
pinia: 2.0.17
pug: 3.0.2
quasar: 2.7.5
tippy.js: 6.3.7
uuid: 8.3.2
v-network-graph: 0.6.3
v-network-graph: 0.6.5
vue: 3.2.37
vue-codemirror: 6.0.0
vue-codemirror: 6.0.2
vue-i18n: 9.1.10
vue-router: 4.1.1
vue-router: 4.1.3
vuedraggable: 4.1.0
zxcvbn: 4.4.2
languageName: unknown
linkType: soft
"v-network-graph@npm:0.6.3":
version: 0.6.3
resolution: "v-network-graph@npm:0.6.3"
"v-network-graph@npm:0.6.5":
version: 0.6.5
resolution: "v-network-graph@npm:0.6.5"
dependencies:
"@dash14/svg-pan-zoom": ^3.6.8
mitt: ^3.0.0
peerDependencies:
d3-force: ^3.0.0
vue: ^3.2.31
checksum: cecae746aaf6fd0f480b0a251b4d50dab89a16e88420c7ffd2de0fe5c5fb32a72cb19f94c3b6a19a4e83110fa0a136635c148b5231988c29e807dadb877f691a
checksum: a6312014cd424cc6747dafd8b05eef5bfd3f1a1a01ca67363ca1d17553d1c493b4a80f5d3d0bbd9c45b709ff78618f596f3a12673c9518086b303b39c6e7b5dd
languageName: node
linkType: hard
@ -6993,19 +6991,19 @@ __metadata:
languageName: node
linkType: hard
"vue-codemirror@npm:6.0.0":
version: 6.0.0
resolution: "vue-codemirror@npm:6.0.0"
"vue-codemirror@npm:6.0.2":
version: 6.0.2
resolution: "vue-codemirror@npm:6.0.2"
dependencies:
"@codemirror/commands": 6.x
"@codemirror/language": 6.x
"@codemirror/state": 6.x
"@codemirror/view": 6.x
csstype: ^2.6.8
csstype: ^3.1.0
peerDependencies:
codemirror: 6.x
vue: 3.x
checksum: aa762ebfe8b8f9f43c98b76ba822842c79af2fc16be1b2639142d3c85d86f73376c1467d0372bdae1b5e73fd4b76405f23b0266d6e5c72505408677164505e0e
checksum: 77620c05c1d23ef8d5ab287fc6b28246a1b0c4317a5f23d457da742d7906890d2995e68d592439c552c9163ca2e63e34b6d077ba6059c0d2717a6ad9f57eac92
languageName: node
linkType: hard
@ -7056,14 +7054,14 @@ __metadata:
languageName: node
linkType: hard
"vue-router@npm:4.1.1":
version: 4.1.1
resolution: "vue-router@npm:4.1.1"
"vue-router@npm:4.1.3":
version: 4.1.3
resolution: "vue-router@npm:4.1.3"
dependencies:
"@vue/devtools-api": ^6.1.4
peerDependencies:
vue: ^3.2.0
checksum: a521d9c8e225497ba3f0759b00fdb48fdbf5ed7a81b7f05558733aaecbc539e94180c0bbf336063c65b1c2cf5e3e93fced2ec79718dbadac082fa8150bbfb924
checksum: 92a827a05e3afd5cc6ff6a4d033abaefae098b60e4d128b402bebb88473ad8ca485e675a6f7df2a5598ba4a8a23a2652c0f7c55c2c57c4ac433ce75fb4672222
languageName: node
linkType: hard

Loading…
Cancel
Save