feat: profile (avatar, auth and groups) pages + various fixes

pull/6078/head
Nicolas Giard 2 years ago
parent acc3b7369f
commit c9504ed6f4
No known key found for this signature in database
GPG Key ID: 85061B8F9D55B7C8

@ -400,14 +400,14 @@ router.get(['/t', '/t/*'], (req, res, next) => {
/**
* User Avatar
*/
router.get('/_userav/:uid', async (req, res, next) => {
router.get('/_user/:uid/avatar', async (req, res, next) => {
if (!WIKI.auth.checkAccess(req.user, ['read:pages'])) {
return res.sendStatus(403)
}
const av = await WIKI.db.users.getUserAvatarData(req.params.uid)
if (av) {
res.set('Content-Type', 'image/jpeg')
res.send(av)
return res.send(av)
}
return res.sendStatus(404)

@ -300,7 +300,7 @@ exports.up = async knex => {
table.jsonb('auth').notNullable().defaultTo('{}')
table.jsonb('meta').notNullable().defaultTo('{}')
table.jsonb('prefs').notNullable().defaultTo('{}')
table.string('pictureUrl')
table.boolean('hasAvatar').notNullable().defaultTo(false)
table.boolean('isSystem').notNullable().defaultTo(false)
table.boolean('isActive').notNullable().defaultTo(false)
table.boolean('isVerified').notNullable().defaultTo(false)

@ -1,5 +1,7 @@
const graphHelper = require('../../helpers/graph')
const _ = require('lodash')
const path = require('node:path')
const fs = require('fs-extra')
module.exports = {
Query: {
@ -273,9 +275,88 @@ module.exports = {
} catch (err) {
return graphHelper.generateError(err)
}
},
/**
* UPLOAD USER AVATAR
*/
async uploadUserAvatar (obj, args) {
try {
const { filename, mimetype, createReadStream } = await args.image
WIKI.logger.debug(`Processing user ${args.id} avatar ${filename} of type ${mimetype}...`)
if (!WIKI.extensions.ext.sharp.isInstalled) {
throw new Error('This feature requires the Sharp extension but it is not installed.')
}
if (!['.png', '.jpg', '.webp', '.gif'].some(s => filename.endsWith(s))) {
throw new Error('Invalid File Extension. Must be png, jpg, webp or gif.')
}
const destFolder = path.resolve(
process.cwd(),
WIKI.config.dataPath,
`assets`
)
const destPath = path.join(destFolder, `userav-${args.id}.jpg`)
await fs.ensureDir(destFolder)
// -> Resize
await WIKI.extensions.ext.sharp.resize({
format: 'jpg',
inputStream: createReadStream(),
outputPath: destPath,
width: 180,
height: 180
})
// -> Set avatar flag for this user in the DB
await WIKI.db.users.query().findById(args.id).patch({ hasAvatar: true })
// -> Save image data to DB
const imgBuffer = await fs.readFile(destPath)
await WIKI.db.knex('userAvatars').insert({
id: args.id,
data: imgBuffer
}).onConflict('id').merge()
WIKI.logger.debug(`Processed user ${args.id} avatar successfully.`)
return {
operation: graphHelper.generateSuccess('User avatar uploaded successfully')
}
} catch (err) {
WIKI.logger.warn(err)
return graphHelper.generateError(err)
}
},
/**
* CLEAR USER AVATAR
*/
async clearUserAvatar (obj, args) {
try {
WIKI.logger.debug(`Clearing user ${args.id} avatar...`)
await WIKI.db.users.query().findById(args.id).patch({ hasAvatar: false })
await WIKI.db.knex('userAvatars').where({ id: args.id }).del()
WIKI.logger.debug(`Cleared user ${args.id} avatar successfully.`)
return {
operation: graphHelper.generateSuccess('User avatar cleared successfully')
}
} catch (err) {
WIKI.logger.warn(err)
return graphHelper.generateError(err)
}
}
},
User: {
async auth (usr) {
const authStrategies = await WIKI.db.authentication.getStrategies({ enabledOnly: true })
return _.transform(usr.auth, (result, value, key) => {
const authStrategy = _.find(authStrategies, ['id', key])
const authModule = _.find(WIKI.data.authentication, ['key', authStrategy.module])
if (!authStrategy || !authModule) { return }
result.push({
authId: key,
authName: authStrategy.displayName,
strategyKey: authStrategy.module,
strategyIcon: authModule.icon,
config: authStrategy.module === 'local' ? {
isTfaSetup: value.tfaSecret?.length > 0
} : {}
})
}, [])
},
groups (usr) {
return usr.$relatedQuery('groups')
}

@ -73,6 +73,15 @@ extend type Mutation {
timeFormat: String
appearance: UserSiteAppearance
): DefaultResponse
uploadUserAvatar(
id: UUID!
image: Upload!
): DefaultResponse
clearUserAvatar(
id: UUID!
): DefaultResponse
}
# -----------------------------------------------
@ -104,19 +113,27 @@ type User {
id: UUID
name: String
email: String
auth: JSON
auth: [UserAuth]
hasAvatar: Boolean
isSystem: Boolean
isActive: Boolean
isVerified: Boolean
meta: JSON
prefs: JSON
pictureUrl: String
createdAt: Date
updatedAt: Date
lastLoginAt: Date
groups: [Group]
}
type UserAuth {
authId: UUID
authName: String
strategyKey: String
strategyIcon: String
config: JSON
}
type UserTokenResponse {
operation: Operation
jwt: String

@ -105,7 +105,8 @@ module.exports = configure(function (/* ctx */) {
port: 3001,
proxy: {
'/_graphql': 'http://localhost:3000/_graphql',
'/_site': 'http://localhost:3000'
'/_site': 'http://localhost:3000',
'/_user': 'http://localhost:3000'
}
},

@ -1,8 +1,14 @@
<template lang='pug'>
q-btn.q-ml-md(flat, round, dense, color='grey')
q-icon(v-if='!userStore.authenticated || !userStore.pictureUrl', name='las la-user-circle')
q-avatar(v-else)
img(:src='userStore.pictureUrl')
q-icon(
v-if='!userStore.authenticated || !userStore.hasAvatar'
name='las la-user-circle'
)
q-avatar(
v-else
size='32px'
)
img(:src='`/_user/` + userStore.id + `/avatar`')
q-menu(auto-close)
q-card(flat, style='width: 300px;', :dark='false')
q-card-section(align='center')
@ -12,24 +18,32 @@ q-btn.q-ml-md(flat, round, dense, color='grey')
q-card-actions(align='center')
q-btn(
flat
label='Profile'
:label='t(`common.header.profile`)'
icon='las la-user-alt'
color='primary'
to='/_profile'
no-caps
)
q-btn(flat
label='Logout'
:label='t(`common.header.logout`)'
icon='las la-sign-out-alt'
color='red'
href='/logout'
no-caps
)
q-tooltip Account
q-tooltip {{ t('common.header.account') }}
</template>
<script setup>
import { useI18n } from 'vue-i18n'
import { useUserStore } from 'src/stores/user'
// STORES
const userStore = useUserStore()
// I18N
const { t } = useI18n()
</script>

@ -50,6 +50,7 @@ const { t } = useI18n()
// METHODS
function create (editor) {
pageStore.pageCreate({ editor })
window.location.assign('/_edit/new')
// pageStore.pageCreate({ editor })
}
</script>

@ -318,13 +318,17 @@ const quickaccess = [
{ key: 'refCardVisibility', icon: 'las la-eye', label: t('editor.props.visibility') }
]
// REFS
const iptPagePassword = ref(null)
// WATCHERS
watch(() => state.requirePassword, (newValue) => {
if (newValue) {
nextTick(() => {
this.$refs.iptPagePassword.focus()
this.$refs.iptPagePassword.$el.scrollIntoView({
iptPagePassword.value.focus()
iptPagePassword.value.$el.scrollIntoView({
behavior: 'smooth'
})
})

@ -7,6 +7,9 @@
"admin.analytics.saveSuccess": "Analytics configuration saved successfully",
"admin.analytics.subtitle": "Add analytics and tracking tools to your wiki",
"admin.analytics.title": "Analytics",
"admin.api.copyKeyTitle": "Copy API Key",
"admin.api.createInvalidData": "Some fields are missing or have invalid data.",
"admin.api.createSuccess": "API Key created successfully.",
"admin.api.disableButton": "Disable API",
"admin.api.disabled": "API Disabled",
"admin.api.enableButton": "Enable API",
@ -16,12 +19,18 @@
"admin.api.expiration30d": "30 days",
"admin.api.expiration3y": "3 years",
"admin.api.expiration90d": "90 days",
"admin.api.groupSelected": "Use {group} group permissions",
"admin.api.groupsMissing": "You must select at least 1 group for this key.",
"admin.api.groupsSelected": "Use permissions from {count} groups",
"admin.api.headerCreated": "Created",
"admin.api.headerExpiration": "Expiration",
"admin.api.headerKeyEnding": "Key Ending",
"admin.api.headerLastUpdated": "Last Updated",
"admin.api.headerName": "Name",
"admin.api.headerRevoke": "Revoke",
"admin.api.key": "API Key",
"admin.api.nameInvalidChars": "Key name has invalid characters.",
"admin.api.nameMissing": "Key name is missing.",
"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.",
@ -40,10 +49,14 @@
"admin.api.newKeySuccess": "API key created successfully.",
"admin.api.newKeyTitle": "New API Key",
"admin.api.noKeyInfo": "No API keys have been generated yet.",
"admin.api.none": "There are no API keys yet.",
"admin.api.permissionGroups": "Group Permissions",
"admin.api.refreshSuccess": "List of API keys has been refreshed.",
"admin.api.revoke": "Revoke",
"admin.api.revokeConfirm": "Revoke API Key?",
"admin.api.revokeConfirmText": "Are you sure you want to revoke key {name}? This action cannot be undone!",
"admin.api.revoked": "Revoked",
"admin.api.revokedHint": "This key has been revoked and can no longer be used.",
"admin.api.revokeSuccess": "The key has been revoked successfully.",
"admin.api.subtitle": "Manage keys to access the API",
"admin.api.title": "API Access",
@ -63,6 +76,9 @@
"admin.auth.displayNameHint": "The title shown to the end user for this authentication strategy.",
"admin.auth.domainsWhitelist": "Limit to specific email domains",
"admin.auth.domainsWhitelistHint": "A list of domains authorized to register. The user email address domain must match one of these to gain access.",
"admin.auth.enabled": "Enabled",
"admin.auth.enabledForced": "This strategy cannot be disabled.",
"admin.auth.enabledHint": "Should this strategy be available to sites for login.",
"admin.auth.force2fa": "Force all users to use Two-Factor Authentication (2FA)",
"admin.auth.force2faHint": "Users will be required to setup 2FA the first time they login and cannot be disabled by the user.",
"admin.auth.globalAdvSettings": "Global Advanced Settings",
@ -75,6 +91,7 @@
"admin.auth.selfRegistration": "Allow self-registration",
"admin.auth.selfRegistrationHint": "Allow any user successfully authorized by the strategy to access the wiki.",
"admin.auth.siteUrlNotSetup": "You must set a valid {siteUrl} first! Click on {general} in the left sidebar.",
"admin.auth.status": "Status",
"admin.auth.strategies": "Strategies",
"admin.auth.strategyConfiguration": "Strategy Configuration",
"admin.auth.strategyIsEnabled": "Active",
@ -86,6 +103,9 @@
"admin.auth.strategyStateLocked": "and cannot be disabled.",
"admin.auth.subtitle": "Configure the authentication settings of your wiki",
"admin.auth.title": "Authentication",
"admin.auth.vendor": "Vendor",
"admin.auth.vendorWebsite": "Website",
"admin.blocks.title": "Content Blocks",
"admin.comments.provider": "Provider",
"admin.comments.providerConfig": "Provider Configuration",
"admin.comments.providerNoConfig": "This provider has no configuration options you can modify.",
@ -126,11 +146,11 @@
"admin.editors.wysiwygName": "Visual Editor",
"admin.extensions.incompatible": "not compatible",
"admin.extensions.install": "Install",
"admin.extensions.installFailed": "Failed to install extension.",
"admin.extensions.installSuccess": "Extension installed successfully.",
"admin.extensions.installed": "Installed",
"admin.extensions.installFailed": "Failed to install extension.",
"admin.extensions.installing": "Installing extension...",
"admin.extensions.installingHint": "This may take a while depending on your server.",
"admin.extensions.installSuccess": "Extension installed successfully.",
"admin.extensions.instructions": "Instructions",
"admin.extensions.instructionsHint": "Must be installed manually",
"admin.extensions.reinstall": "Reinstall",
@ -163,13 +183,13 @@
"admin.general.contentLicenseHint": "License shown in the footer of all content pages.",
"admin.general.defaultDateFormat": "Default Date Format",
"admin.general.defaultDateFormatHint": "The default date format for new users.",
"admin.general.defaults": "Site Defaults",
"admin.general.defaultTimeFormat": "Default Time Format",
"admin.general.defaultTimeFormat12h": "12 hour",
"admin.general.defaultTimeFormat24h": "24 hour",
"admin.general.defaultTimeFormatHint": "The default time format for new users.",
"admin.general.defaultTimezone": "Default Timezone",
"admin.general.defaultTimezoneHint": "The default timezone for new users.",
"admin.general.defaults": "Site Defaults",
"admin.general.displaySiteTitle": "Display Site Title",
"admin.general.displaySiteTitleHint": "Should the site title be displayed next to the logo? If your logo isn't square and contain your brand name, turn this option off.",
"admin.general.favicon": "Favicon",
@ -177,11 +197,15 @@
"admin.general.faviconUploadSuccess": "Site Favicon uploaded successfully.",
"admin.general.features": "Features",
"admin.general.footerCopyright": "Footer / Copyright",
"admin.general.footerExtra": "Additional Footer Text",
"admin.general.footerExtraHint": "Optionally add more content to the footer, such as additional copyright terms or mandatory regulatory info.",
"admin.general.general": "General",
"admin.general.logo": "Logo",
"admin.general.logoUpl": "Site Logo",
"admin.general.logoUplHint": "Logo image file, in SVG, PNG, JPG, WEBP or GIF format.",
"admin.general.logoUploadSuccess": "Site logo uploaded successfully.",
"admin.general.pageExtensions": "Page Extensions",
"admin.general.pageExtensionsHint": "A comma-separated list of URL extensions that will be treated as pages. For example, adding md will treat /foobar.md the same as /foobar.",
"admin.general.ratingsOff": "Off",
"admin.general.ratingsStars": "Stars",
"admin.general.ratingsThumbs": "Thumbs",
@ -200,6 +224,7 @@
"admin.general.siteHostnameInvalid": "Invalid Hostname",
"admin.general.siteInfo": "Site Info",
"admin.general.sitemap": "Allow Sitemap",
"admin.general.sitemapHint": "Make a sitemap.xml available to search engines with all pages accessible to guests.",
"admin.general.siteTitle": "Site Title",
"admin.general.siteTitleHint": "Displayed in the top bar and appended to all pages meta title.",
"admin.general.siteTitleInvalidChars": "Site Title contains invalid characters.",
@ -209,6 +234,7 @@
"admin.general.uploadLogo": "Upload Logo",
"admin.general.uploadSizeHint": "An image of {size} pixels is recommended for best results.",
"admin.general.uploadTypesHint": "{typeList} or {lastType} files only",
"admin.general.urlHandling": "URL Handling",
"admin.groups.assignUser": "Assign User",
"admin.groups.authBehaviors": "Authentication Behaviors",
"admin.groups.create": "New Group",
@ -251,10 +277,10 @@
"admin.groups.ruleMatchStart": "Path Starts With...",
"admin.groups.ruleMatchTag": "Has Any Tag...",
"admin.groups.ruleMatchTagAll": "Has All Tags...",
"admin.groups.ruleSites": "Site(s)",
"admin.groups.ruleUntitled": "Untitled Rule",
"admin.groups.rules": "Rules",
"admin.groups.ruleSites": "Site(s)",
"admin.groups.rulesNone": "This group doesn't have any rules yet.",
"admin.groups.ruleUntitled": "Untitled Rule",
"admin.groups.selectedLocales": "Any Locale | {locale} locale only | {count} locales selected",
"admin.groups.selectedSites": "Any Site | 1 site selected | {count} sites selected",
"admin.groups.subtitle": "Manage user groups and permissions",
@ -262,6 +288,13 @@
"admin.groups.userCount": "User Count",
"admin.groups.users": "Users",
"admin.groups.usersCount": "0 user | 1 user | {count} users",
"admin.groups.usersNone": "This group doesn't have any user yet.",
"admin.instances.activeConnections": "Active Connections",
"admin.instances.activeListeners": "Active Listeners",
"admin.instances.firstSeen": "First Seen",
"admin.instances.lastSeen": "Last Seen",
"admin.instances.subtitle": "View a list of active instances",
"admin.instances.title": "Instances",
"admin.locale.activeNamespaces": "Active Namespaces",
"admin.locale.autoUpdate.hint": "Automatically download updates to this locale as they become available.",
"admin.locale.autoUpdate.hintWithNS": "Automatically download updates to all namespaced locales enabled below.",
@ -290,6 +323,7 @@
"admin.logging.title": "Logging",
"admin.login.background": "Background Image",
"admin.login.backgroundHint": "Specify an image to use as the login background. PNG and JPG are supported, 1920x1080 recommended. Leave empty for default.",
"admin.login.bgUploadSuccess": "Login background image uploaded successfully.",
"admin.login.bypassScreen": "Bypass Login Screen",
"admin.login.bypassScreenHint": "Should the user be redirected automatically to the first authentication provider. Has no effect if the first provider is a username/password provider type.",
"admin.login.bypassUnauthorized": "Bypass Unauthorized Screen",
@ -301,6 +335,7 @@
"admin.login.logoutRedirectHint": "Optionally redirect the user to a specific page when he/she logouts. This can be overridden at the group level.",
"admin.login.providers": "Login Providers",
"admin.login.providersVisbleWarning": "Note that you can always temporarily show all hidden providers by adding ?all=1 to the url. This is useful to login as local admin while hiding it from normal users.",
"admin.login.saveSuccess": "Login configuration saved successfully.",
"admin.login.subtitle": "Configure the login user experience of your wiki site",
"admin.login.title": "Login",
"admin.login.welcomeRedirect": "First-time Login Redirect",
@ -317,13 +352,15 @@
"admin.mail.dkimUse": "Use DKIM",
"admin.mail.dkimUseHint": "Should DKIM be used when sending emails.",
"admin.mail.saveSuccess": "Configuration saved successfully.",
"admin.mail.sendTestSuccess": "A test email was sent successfully.",
"admin.mail.sender": "Sender",
"admin.mail.senderEmail": "Sender Email",
"admin.mail.senderName": "Sender Name",
"admin.mail.sendTestSuccess": "A test email was sent successfully.",
"admin.mail.smtp": "SMTP Settings",
"admin.mail.smtpHost": "Host",
"admin.mail.smtpHostHint": "Hostname or IP address of the SMTP server.",
"admin.mail.smtpName": "Client Identifying Name",
"admin.mail.smtpNameHint": "An optional name to send to the SMTP server to identify your mailer. Leave empty to use server hostname. For Google Workspace customers, this should be your main domain name.",
"admin.mail.smtpPort": "Port",
"admin.mail.smtpPortHint": "Usually 465 (recommended), 587 or 25.",
"admin.mail.smtpPwd": "Password",
@ -336,8 +373,8 @@
"admin.mail.smtpVerifySSLHint": "Some hosts requires SSL certificate checking to be disabled. Leave enabled for proper security.",
"admin.mail.subtitle": "Configure mail settings",
"admin.mail.templateResetPwd": "Password Reset Email",
"admin.mail.templateWelcome": "Welcome Email",
"admin.mail.templates": "Mail Templates",
"admin.mail.templateWelcome": "Welcome Email",
"admin.mail.test": "Send a test email",
"admin.mail.testHint": "Send a test email to ensure your SMTP configuration is working.",
"admin.mail.testRecipient": "Recipient Email Address",
@ -388,6 +425,38 @@
"admin.pages.title": "Pages",
"admin.rendering.subtitle": "Configure the page rendering pipeline",
"admin.rendering.title": "Rendering",
"admin.scheduler.active": "Active",
"admin.scheduler.activeNone": "There are no active jobs at the moment.",
"admin.scheduler.attempt": "Attempt",
"admin.scheduler.cancelJob": "Cancel Job",
"admin.scheduler.cancelJobSuccess": "Job cancelled successfully.",
"admin.scheduler.completed": "Completed",
"admin.scheduler.completedIn": "Completed in {duration}",
"admin.scheduler.completedNone": "There are no recently completed job to display.",
"admin.scheduler.createdAt": "Created",
"admin.scheduler.createdBy": "by instance {instance}",
"admin.scheduler.cron": "Cron",
"admin.scheduler.error": "Error",
"admin.scheduler.failed": "Failed",
"admin.scheduler.failedNone": "There are no recently failed job to display.",
"admin.scheduler.interrupted": "Interrupted",
"admin.scheduler.pending": "Pending",
"admin.scheduler.result": "Result",
"admin.scheduler.retryJob": "Retry Job",
"admin.scheduler.retryJobSuccess": "Job has been rescheduled and will execute shortly.",
"admin.scheduler.schedule": "Schedule",
"admin.scheduler.scheduled": "Scheduled",
"admin.scheduler.scheduledNone": "There are no scheduled jobs at the moment.",
"admin.scheduler.startedAt": "Started",
"admin.scheduler.state": "State",
"admin.scheduler.subtitle": "View scheduled and completed jobs",
"admin.scheduler.title": "Scheduler",
"admin.scheduler.type": "Type",
"admin.scheduler.upcoming": "Upcoming",
"admin.scheduler.upcomingNone": "There are no upcoming job for the moment.",
"admin.scheduler.updatedAt": "Last Updated",
"admin.scheduler.useWorker": "Execution Mode",
"admin.scheduler.waitUntil": "Start",
"admin.search.configSaveSuccess": "Search engine configuration saved successfully.",
"admin.search.engineConfig": "Engine Configuration",
"admin.search.engineNoConfig": "This engine has no configuration options you can modify.",
@ -502,10 +571,10 @@
"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.",
"admin.storage.assetsOnly": "Assets Only",
"admin.storage.assetStreaming": "File Streaming",
"admin.storage.assetStreamingHint": "Assets will be streamed from the storage target, through the server, to the user.",
"admin.storage.assetStreamingNotSupported": "Not supported by this storage target.",
"admin.storage.assetsOnly": "Assets Only",
"admin.storage.cancelSetup": "Cancel",
"admin.storage.config": "Configuration",
"admin.storage.contentTypeDocuments": "Documents",
@ -576,27 +645,28 @@
"admin.storage.setup": "Setup",
"admin.storage.setupConfiguredHint": "This module is already configured. You can uninstall this module to start over.",
"admin.storage.setupHint": "This module requires a setup process to be completed in order to use it. Follow the instructions below to get started.",
"admin.storage.setupRequired": "Setup required",
"admin.storage.startSetup": "Start Setup",
"admin.storage.status": "Status",
"admin.storage.subtitle": "Set backup and sync targets for your content",
"admin.storage.sync": "Synchronization",
"admin.storage.syncDirBi": "Bi-directional",
"admin.storage.syncDirBiHint": "In bi-directional mode, content is first pulled from the storage target. Any newer content overwrites local content. New content since last sync is then pushed to the storage target, overwriting any content on target if present.",
"admin.storage.syncDirection": "Sync Direction",
"admin.storage.syncDirectionSubtitle": "Choose how content synchronization is handled for this storage target.",
"admin.storage.syncDirPull": "Pull from target",
"admin.storage.syncDirPullHint": "Content is always pulled from the storage target, overwriting any local content which already exists. This choice is usually reserved for single-use content import. Caution with this option as any local content will always be overwritten!",
"admin.storage.syncDirPush": "Push to target",
"admin.storage.syncDirPushHint": "Content is always pushed to the storage target, overwriting any existing content. This is safest choice for backup scenarios.",
"admin.storage.syncDirection": "Sync Direction",
"admin.storage.syncDirectionSubtitle": "Choose how content synchronization is handled for this storage target.",
"admin.storage.syncSchedule": "Sync Schedule",
"admin.storage.syncScheduleCurrent": "Currently set to every {schedule}.",
"admin.storage.syncScheduleDefault": "The default is every {schedule}.",
"admin.storage.syncScheduleHint": "For performance reasons, this storage target synchronize changes on an interval-based schedule, instead of on every change. Define at which interval should the synchronization occur.",
"admin.storage.targetConfig": "Target Configuration",
"admin.storage.targets": "Targets",
"admin.storage.targetState": "This storage target is {state}",
"admin.storage.targetStateActive": "active",
"admin.storage.targetStateInactive": "inactive",
"admin.storage.targets": "Targets",
"admin.storage.title": "Storage",
"admin.storage.uninstall": "Uninstall",
"admin.storage.unsupported": "Unsupported",
@ -610,6 +680,9 @@
"admin.storage.versioningNotSupported": "Not supported by this storage target.",
"admin.system.browser": "Browser",
"admin.system.browserHint": "The browser name and version.",
"admin.system.checkForUpdates": "Check",
"admin.system.checkingForUpdates": "Checking for Updates...",
"admin.system.checkUpdate": "Check / Upgrade",
"admin.system.client": "Client",
"admin.system.clientCookies": "Cookies Support",
"admin.system.clientCookiesHint": "Whether cookies are enabled on this browser.",
@ -646,6 +719,7 @@
"admin.system.title": "System Info",
"admin.system.totalRAM": "Total RAM",
"admin.system.totalRAMHint": "The total amount of RAM available to Wiki.js.",
"admin.system.upgrade": "Upgrade",
"admin.system.workingDirectory": "Working Directory",
"admin.system.workingDirectoryHint": "The directory path where Wiki.js is currently running from.",
"admin.tags.date": "Created {created} and last updated {updated}.",
@ -665,11 +739,29 @@
"admin.tags.tag": "Tag",
"admin.tags.title": "Tags",
"admin.tags.viewLinkedPages": "View Linked Pages",
"admin.terminal.clear": "Clear",
"admin.terminal.command": "Command",
"admin.terminal.connect": "Connect",
"admin.terminal.connected": "Connected.",
"admin.terminal.connectError": "Connection Error:",
"admin.terminal.connecting": "Connecting to server...",
"admin.terminal.disconnect": "Disconnect",
"admin.terminal.disconnected": "Disconnected.",
"admin.terminal.logs": "Logs",
"admin.terminal.subtitle": "View process logs in real-time",
"admin.terminal.title": "Terminal",
"admin.theme.accentColor": "Accent Color",
"admin.theme.accentColorHint": "The accent color for elements that need to stand out or grab the user attention.",
"admin.theme.appearance": "Appearance",
"admin.theme.baseFont": "Base Font",
"admin.theme.baseFontHint": "The font used across the site for the interface.",
"admin.theme.bodyHtmlInjection": "Body HTML Injection",
"admin.theme.bodyHtmlInjectionHint": "HTML code to be injected just before the closing body tag.",
"admin.theme.codeInjection": "Code Injection",
"admin.theme.contentFont": "Content Font",
"admin.theme.contentFontHint": "The font used specifically for page content.",
"admin.theme.contentWidth": "Content Width",
"admin.theme.contentWidthHint": "Should the content use all available viewport space or stay centered.",
"admin.theme.cssOverride": "CSS Override",
"admin.theme.cssOverrideHint": "CSS code to inject after system default CSS. Injecting too much CSS code can result in poor page load performance! CSS will automatically be minified.",
"admin.theme.cssOverrideWarning": "{caution} When adding styles for page content, you must scope them to the {cssClass} class. Omitting this could break the layout of the editor!",
@ -680,10 +772,11 @@
"admin.theme.downloadDownload": "Download",
"admin.theme.downloadName": "Name",
"admin.theme.downloadThemes": "Download Themes",
"admin.theme.headHtmlInjection": "Head HTML Injection",
"admin.theme.headHtmlInjectionHint": "HTML code to be injected just before the closing head tag. Usually for script tags.",
"admin.theme.fonts": "Fonts",
"admin.theme.headerColor": "Header Color",
"admin.theme.headerColorHint": "The background color for the site top header. Does not apply to the administration area.",
"admin.theme.headHtmlInjection": "Head HTML Injection",
"admin.theme.headHtmlInjectionHint": "HTML code to be injected just before the closing head tag. Usually for script tags.",
"admin.theme.iconset": "Icon Set",
"admin.theme.iconsetHint": "Set of icons to use for the sidebar navigation.",
"admin.theme.layout": "Layout",
@ -712,11 +805,12 @@
"admin.theme.tocPositionHint": "On which side should the Table of Contents sidebar be displayed for content pages.",
"admin.users.active": "Active",
"admin.users.activity": "Activity",
"admin.users.appearance": "Site Appearance",
"admin.users.assignGroup": "Assign Group",
"admin.users.auth": "Authentication",
"admin.users.authentication": "Authentication",
"admin.users.authProvider": "Provider",
"admin.users.authProviderId": "Provider Id",
"admin.users.authentication": "Authentication",
"admin.users.ban": "Ban User",
"admin.users.banHint": "Block the user from signing in and invalidate any active sessions.",
"admin.users.banned": "Banned",
@ -724,10 +818,10 @@
"admin.users.changePassword": "Change Password",
"admin.users.changePasswordHint": "Change the user password. Note that the current password cannot be recovered.",
"admin.users.create": "Create User",
"admin.users.createdAt": "Created {date}",
"admin.users.createInvalidData": "Cannot create user as some fields are invalid or missing.",
"admin.users.createKeepOpened": "Keep dialog opened after create",
"admin.users.createSuccess": "User created successfully!",
"admin.users.createdAt": "Created {date}",
"admin.users.darkMode": "Dark Mode",
"admin.users.darkModeHint": "Display the user interface using dark mode.",
"admin.users.dateFormat": "Date Format",
@ -748,8 +842,8 @@
"admin.users.groupAlreadyAssigned": "User is already assigned to this group.",
"admin.users.groupAssign": "Assign",
"admin.users.groupAssignNotice": "Note that you cannot assign users to the Administrators or Guests groups from this panel.",
"admin.users.groupSelected": "Assign to {group}",
"admin.users.groups": "Groups",
"admin.users.groupSelected": "Assign to {group}",
"admin.users.groupsMissing": "You must assign the user to at least 1 group.",
"admin.users.groupsSelected": "Assign to {count} groups",
"admin.users.id": "ID",
@ -788,6 +882,8 @@
"admin.users.passwordTooShort": "Password is too short.",
"admin.users.preferences": "User Preferences",
"admin.users.profile": "User Profile",
"admin.users.pronouns": "Pronouns",
"admin.users.pronounsHint": "The pronouns used to address this user.",
"admin.users.pwdAuthActive": "Can Use Password Authentication",
"admin.users.pwdAuthActiveHint": "Whether the user can login using the password authentication.",
"admin.users.pwdAuthRestrict": "Restrict Password Authentication",
@ -844,8 +940,17 @@
"admin.utilities.cacheTitle": "Flush Cache",
"admin.utilities.contentSubtitle": "Various tools for pages",
"admin.utilities.contentTitle": "Content",
"admin.utilities.disconnectWS": "Disconnect WebSocket Sessions",
"admin.utilities.disconnectWSHint": "Force all active websocket connections to be closed.",
"admin.utilities.disconnectWSSuccess": "All active websocket connections have been terminated.",
"admin.utilities.export": "Export",
"admin.utilities.exportHint": "Export content to tarball for backup / migration.",
"admin.utilities.flushCache": "Flush Cache",
"admin.utilities.flushCacheHint": "Pages and Assets are cached to disk for better performance. You can flush the cache to force all content to be fetched from the DB again.",
"admin.utilities.graphEndpointSubtitle": "Change the GraphQL endpoint for Wiki.js",
"admin.utilities.graphEndpointTitle": "GraphQL Endpoint",
"admin.utilities.import": "Import",
"admin.utilities.importHint": "Import content from a tarball backup or a 2.X backup.",
"admin.utilities.importv1Subtitle": "Migrate data from a previous 1.x installation",
"admin.utilities.importv1Title": "Import from Wiki.js 1.x",
"admin.utilities.invalidAuthCertificates": "Invalidate Authentication Certificates",
@ -882,13 +987,13 @@
"admin.webhooks.eventNewComment": "Post a new comment",
"admin.webhooks.eventRenameAsset": "Rename / move an asset",
"admin.webhooks.eventRenamePage": "Rename / move a page",
"admin.webhooks.events": "Events",
"admin.webhooks.eventsMissing": "You must select at least 1 event.",
"admin.webhooks.eventsSelected": "No event selected | 1 event selected | {count} events selected",
"admin.webhooks.eventUploadAsset": "Upload a new asset",
"admin.webhooks.eventUserJoin": "Create / register a new user",
"admin.webhooks.eventUserLogin": "User logins",
"admin.webhooks.eventUserLogout": "User logouts",
"admin.webhooks.events": "Events",
"admin.webhooks.eventsMissing": "You must select at least 1 event.",
"admin.webhooks.eventsSelected": "No event selected | 1 event selected | {count} events selected",
"admin.webhooks.includeContent": "Include Content",
"admin.webhooks.includeContentHint": "Should the payload include content (e.g. the full page body). Make sure that your remote endpoint can accept large payloads!",
"admin.webhooks.includeMetadata": "Include Metadata",
@ -917,36 +1022,35 @@
"admin.webhooks.urlMissing": "The URL is missing or is not valid.",
"auth.actions.login": "Log In",
"auth.actions.register": "Register",
"auth.changePwd.currentPassword": "Current Password",
"auth.changePwd.instructions": "You must choose a new password:",
"auth.changePwd.loading": "Changing password...",
"auth.changePwd.currentPassword": "Current Password",
"auth.changePwd.newPassword": "New Password",
"auth.changePwd.newPasswordVerify": "Verify New Password",
"auth.changePwd.proceed": "Change Password",
"auth.changePwd.subtitle": "Choose a new password",
"auth.enterCredentials": "Enter your credentials",
"auth.errors.forgotPassword": "Missing or invalid email address.",
"auth.errors.invalidEmail": "Email is invalid.",
"auth.errors.invalidLogin": "Invalid Login",
"auth.errors.invalidLoginMsg": "The email or password is invalid.",
"auth.errors.invalidName": "Name is invalid.",
"auth.errors.invalidUserEmail": "Invalid User Email",
"auth.errors.login": "Missing or invalid login fields.",
"auth.errors.loginError": "Login error",
"auth.errors.notYetAuthorized": "You have not been authorized to login to this site yet.",
"auth.errors.tooManyAttempts": "Too many attempts!",
"auth.errors.tooManyAttemptsMsg": "You've made too many failed attempts in a short period of time, please try again {time}.",
"auth.errors.userNotFound": "User not found",
"auth.errors.missingName": "Name is missing.",
"auth.errors.invalidName": "Name is invalid.",
"auth.errors.missingEmail": "Email is missing.",
"auth.errors.invalidEmail": "Email is invalid.",
"auth.errors.missingName": "Name is missing.",
"auth.errors.missingPassword": "Password is missing.",
"auth.errors.passwordTooShort": "Password is too short.",
"auth.errors.missingUsername": "Username is missing.",
"auth.errors.missingVerifyPassword": "Password Verification is missing.",
"auth.errors.notYetAuthorized": "You have not been authorized to login to this site yet.",
"auth.errors.passwordsNotMatch": "Passwords do not match.",
"auth.errors.missingUsername": "Username is missing.",
"auth.errors.passwordTooShort": "Password is too short.",
"auth.errors.register": "One or more fields are invalid.",
"auth.errors.login": "Missing or invalid login fields.",
"auth.errors.forgotPassword": "Missing or invalid email address.",
"auth.errors.tfaMissing": "Missing or incomplete security code.",
"auth.login.title": "Login",
"auth.errors.tooManyAttempts": "Too many attempts!",
"auth.errors.tooManyAttemptsMsg": "You've made too many failed attempts in a short period of time, please try again {time}.",
"auth.errors.userNotFound": "User not found",
"auth.fields.email": "Email Address",
"auth.fields.emailUser": "Email / Username",
"auth.fields.name": "Name",
@ -963,6 +1067,7 @@
"auth.invalidEmail": "Email address is invalid.",
"auth.invalidEmailUsername": "Enter a valid email / username.",
"auth.invalidPassword": "Enter a valid password.",
"auth.login.title": "Login",
"auth.loginRequired": "Login required",
"auth.loginSuccess": "Login Successful! Redirecting...",
"auth.loginUsingStrategy": "Login using {strategy}",
@ -976,10 +1081,10 @@
"auth.passwordTooShort": "Password is too short.",
"auth.pleaseWait": "Please wait",
"auth.registerCheckEmail": "Check your emails to activate your account.",
"auth.registering": "Creating account...",
"auth.registerSubTitle": "Fill-in the form below to create an account.",
"auth.registerSuccess": "Account created successfully!",
"auth.registerTitle": "Create an account",
"auth.registering": "Creating account...",
"auth.selectAuthProvider": "Sign in with",
"auth.sendResetPassword": "Reset Password",
"auth.signingIn": "Signing In...",
@ -1052,8 +1157,8 @@
"common.comments.newPlaceholder": "Write a new comment...",
"common.comments.none": "No comments yet.",
"common.comments.postComment": "Post Comment",
"common.comments.postSuccess": "New comment posted successfully.",
"common.comments.postingAs": "Posting as {name}",
"common.comments.postSuccess": "New comment posted successfully.",
"common.comments.sdTitle": "Talk",
"common.comments.title": "Comments",
"common.comments.updateComment": "Update Comment",
@ -1065,11 +1170,21 @@
"common.duration.minutes": "Minute(s)",
"common.duration.months": "Month(s)",
"common.duration.years": "Year(s)",
"common.error.generic.hint": "Oops, something went wrong...",
"common.error.generic.title": "Unexpected Error",
"common.error.notfound.hint": "That page doesn't exist or is not available.",
"common.error.notfound.title": "Not Found",
"common.error.title": "Error",
"common.error.unauthorized.hint": "You don't have the required permissions to access this page.",
"common.error.unauthorized.title": "Unauthorized",
"common.error.unknownsite.hint": "There's no wiki site at this host.",
"common.error.unknownsite.title": "Unknown Site",
"common.error.unexpected": "An unexpected error occurred.",
"common.field.createdOn": "Created On",
"common.field.id": "ID",
"common.field.lastUpdated": "Last Updated",
"common.field.name": "Name",
"common.field.task": "Task",
"common.footer.copyright": "© {year} {company}. All rights reserved.",
"common.footer.license": "Content is available under the {license}, by {company}.",
"common.footer.poweredBy": "Powered by",
@ -1150,11 +1265,11 @@
"common.pageSelector.pages": "Pages",
"common.pageSelector.selectTitle": "Select a Page",
"common.pageSelector.virtualFolders": "Virtual Folders",
"common.password.good": "Good",
"common.password.average": "Average",
"common.password.good": "Good",
"common.password.poor": "Poor",
"common.password.strong": "Strong",
"common.password.weak": "Weak",
"common.password.poor": "Poor",
"common.sidebar.browse": "Browse",
"common.sidebar.currentDirectory": "Current Directory",
"common.sidebar.mainMenu": "Main Menu",
@ -1188,8 +1303,8 @@
"editor.assets.folderNameNamingRulesLink": "naming rules",
"editor.assets.headerActions": "Actions",
"editor.assets.headerAdded": "Added",
"editor.assets.headerFileSize": "File Size",
"editor.assets.headerFilename": "Filename",
"editor.assets.headerFileSize": "File Size",
"editor.assets.headerId": "ID",
"editor.assets.headerType": "Type",
"editor.assets.imageAlign": "Image Alignment",
@ -1315,9 +1430,9 @@
"editor.props.pageProperties": "Page Properties",
"editor.props.password": "Password",
"editor.props.passwordHint": "The page must be published and the user must have read access rights.",
"editor.props.publishState": "Publishing State",
"editor.props.published": "Published",
"editor.props.publishedHint": "Visible to all users with read access.",
"editor.props.publishState": "Publishing State",
"editor.props.relationAdd": "Add Relation...",
"editor.props.relationAddHint": "Add links to other pages in the footer (e.g. as part of a series of articles)",
"editor.props.relations": "Relations",
@ -1353,24 +1468,13 @@
"history.restore.confirmText": "Are you sure you want to restore this page content as it was on {date}? This version will be copied on top of the current history. As such, newer versions will still be preserved.",
"history.restore.confirmTitle": "Restore page version?",
"history.restore.success": "Page version restored succesfully!",
"profile.activity.commentsPosted": "Comments posted",
"profile.activity.joinedOn": "Joined on",
"profile.activity.lastLoginOn": "Last login on",
"profile.activity.lastUpdatedOn": "Profile last updated on",
"profile.activity.pagesCreated": "Pages created",
"profile.activity.title": "Activity",
"profile.activity": "Activity",
"profile.appearance": "Site Appearance",
"profile.appearanceDark": "Dark",
"profile.appearanceDefault": "Site Default",
"profile.appearanceHint": "Use the light or dark theme.",
"profile.appearanceLight": "Light",
"profile.auth.changePassSuccess": "Password changed successfully.",
"profile.auth.changePassword": "Change Password",
"profile.auth.currentPassword": "Current Password",
"profile.auth.newPassword": "New Password",
"profile.auth.provider": "Provider",
"profile.auth.title": "Authentication",
"profile.auth.verifyPassword": "Confirm New Password",
"profile.comments.title": "Comments",
"profile.auth": "Authentication",
"profile.darkMode": "Dark Mode",
"profile.darkModeHint": "Change the appareance of the site to a dark theme.",
"profile.dateFormat": "Date Format",
@ -1379,7 +1483,8 @@
"profile.displayNameHint": "Your full name; shown when authoring content (e.g. pages, comments, etc.).",
"profile.email": "Email Address",
"profile.emailHint": "The email address used for login.",
"profile.groups.title": "Groups",
"profile.groups": "Groups",
"profile.groupsLoadingFailed": "Failed to load groups.",
"profile.jobTitle": "Job Title",
"profile.jobTitleHint": "Your position in your organization; shown on your profile page.",
"profile.localeDefault": "Locale Default",
@ -1398,6 +1503,9 @@
"profile.pronouns": "Pronouns",
"profile.pronounsHint": "Let people know which pronouns should they use when referring to you.",
"profile.save.success": "Profile saved successfully.",
"profile.saveFailed": "Failed to save profile changes.",
"profile.saveSuccess": "Profile saved successfully.",
"profile.saving": "Saving profile...",
"profile.subtitle": "My personal info",
"profile.timeFormat": "Time Format",
"profile.timeFormat12h": "12 hour",
@ -1414,8 +1522,8 @@
"tags.noResults": "Couldn't find any page with the selected tags.",
"tags.noResultsWithFilter": "Couldn't find any page matching the current filtering options.",
"tags.orderBy": "Order By",
"tags.orderByField.ID": "ID",
"tags.orderByField.creationDate": "Creation Date",
"tags.orderByField.ID": "ID",
"tags.orderByField.lastModified": "Last Modified",
"tags.orderByField.path": "Path",
"tags.orderByField.title": "Title",
@ -1424,125 +1532,24 @@
"tags.searchWithinResultsPlaceholder": "Search within results...",
"tags.selectOneMoreTags": "Select one or more tags",
"tags.selectOneMoreTagsHint": "Select one or more tags on the left.",
"admin.general.sitemapHint": "Make a sitemap.xml available to search engines with all pages accessible to guests.",
"admin.groups.usersNone": "This group doesn't have any user yet.",
"admin.mail.smtpName": "Client Identifying Name",
"admin.mail.smtpNameHint": "An optional name to send to the SMTP server to identify your mailer. Leave empty to use server hostname. For Google Workspace customers, this should be your main domain name.",
"admin.general.footerExtra": "Additional Footer Text",
"admin.general.footerExtraHint": "Optionally add more content to the footer, such as additional copyright terms or mandatory regulatory info.",
"admin.general.urlHandling": "URL Handling",
"admin.general.pageExtensions": "Page Extensions",
"admin.general.pageExtensionsHint": "A comma-separated list of URL extensions that will be treated as pages. For example, adding md will treat /foobar.md the same as /foobar.",
"admin.theme.appearance": "Appearance",
"admin.theme.fonts": "Fonts",
"admin.theme.baseFont": "Base Font",
"admin.theme.baseFontHint": "The font used across the site for the interface.",
"admin.theme.contentFont": "Content Font",
"admin.theme.contentFontHint": "The font used specifically for page content.",
"admin.theme.contentWidth": "Content Width",
"admin.theme.contentWidthHint": "Should the content use all available viewport space or stay centered.",
"admin.utilities.export": "Export",
"admin.utilities.import": "Import",
"admin.utilities.exportHint": "Export content to tarball for backup / migration.",
"admin.utilities.importHint": "Import content from a tarball backup or a 2.X backup.",
"admin.utilities.flushCache": "Flush Cache",
"admin.utilities.flushCacheHint": "Pages and Assets are cached to disk for better performance. You can flush the cache to force all content to be fetched from the DB again.",
"admin.auth.enabledForced": "This strategy cannot be disabled.",
"admin.auth.enabled": "Enabled",
"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.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.",
"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.",
"welcome.title": "Welcome to Wiki.js!",
"welcome.subtitle": "Let's get started...",
"welcome.createHome": "Create the homepage",
"welcome.admin": "Administration Area",
"admin.terminal.title": "Terminal",
"admin.terminal.subtitle": "View process logs in real-time",
"admin.terminal.command": "Command",
"admin.terminal.logs": "Logs",
"admin.terminal.connect": "Connect",
"admin.terminal.disconnect": "Disconnect",
"admin.terminal.clear": "Clear",
"admin.terminal.connecting": "Connecting to server...",
"admin.terminal.connected": "Connected.",
"admin.terminal.disconnected": "Disconnected.",
"admin.terminal.connectError": "Connection Error:",
"admin.utilities.disconnectWS": "Disconnect WebSocket Sessions",
"admin.utilities.disconnectWSHint": "Force all active websocket connections to be closed.",
"admin.utilities.disconnectWSSuccess": "All active websocket connections have been terminated.",
"admin.login.bgUploadSuccess": "Login background image uploaded successfully.",
"admin.login.saveSuccess": "Login configuration saved successfully.",
"profile.appearanceHint": "Use the light or dark theme.",
"profile.saving": "Saving profile...",
"profile.saveSuccess": "Profile saved successfully.",
"profile.saveFailed": "Failed to save profile changes.",
"admin.users.pronouns": "Pronouns",
"admin.users.pronounsHint": "The pronouns used to address this user.",
"admin.users.appearance": "Site Appearance",
"admin.scheduler.title": "Scheduler",
"admin.scheduler.subtitle": "View scheduled and completed jobs",
"admin.scheduler.active": "Active",
"admin.scheduler.completed": "Completed",
"admin.scheduler.scheduled": "Scheduled",
"admin.scheduler.activeNone": "There are no active jobs at the moment.",
"admin.scheduler.completedNone": "There are no recently completed job to display.",
"admin.scheduler.scheduledNone": "There are no scheduled jobs at the moment.",
"admin.scheduler.cron": "Cron",
"admin.scheduler.createdBy": "by instance {instance}",
"admin.scheduler.upcoming": "Upcoming",
"admin.scheduler.failed": "Failed",
"admin.scheduler.type": "Type",
"admin.scheduler.createdAt": "Created",
"admin.scheduler.updatedAt": "Last Updated",
"common.field.task": "Task",
"admin.scheduler.upcomingNone": "There are no upcoming job for the moment.",
"admin.scheduler.failedNone": "There are no recently failed job to display.",
"admin.scheduler.waitUntil": "Start",
"admin.scheduler.attempt": "Attempt",
"admin.scheduler.useWorker": "Execution Mode",
"admin.scheduler.schedule": "Schedule",
"admin.scheduler.state": "State",
"admin.scheduler.startedAt": "Started",
"admin.scheduler.result": "Result",
"admin.scheduler.completedIn": "Completed in {duration}",
"admin.scheduler.pending": "Pending",
"admin.scheduler.error": "Error",
"admin.scheduler.interrupted": "Interrupted",
"admin.instances.title": "Instances",
"admin.instances.subtitle": "View a list of active instances",
"admin.instances.lastSeen": "Last Seen",
"admin.instances.firstSeen": "First Seen",
"admin.instances.activeListeners": "Active Listeners",
"admin.instances.activeConnections": "Active Connections",
"admin.scheduler.cancelJob": "Cancel Job",
"admin.scheduler.cancelJobSuccess": "Job cancelled successfully.",
"admin.scheduler.retryJob": "Retry Job",
"admin.scheduler.retryJobSuccess": "Job has been rescheduled and will execute shortly."
"welcome.createHome": "Create the homepage",
"welcome.subtitle": "Let's get started...",
"welcome.title": "Welcome to Wiki.js!",
"profile.avatar": "Avatar",
"profile.uploadNewAvatar": "Upload New Image",
"profile.avatarUploadTitle": "Upload your user profile picture.",
"profile.avatarUploadHint": "For best results, use a 180x180 image of type JPG or PNG.",
"profile.groupsInfo": "You're currently part of the following groups:",
"profile.groupsNone": "You're not part of any group.",
"profile.authInfo": "Your account is associated with the following authentication methods:",
"profile.authSetTfa": "Set 2FA",
"profile.authModifyTfa": "Modify 2FA",
"profile.authChangePassword": "Change Password",
"profile.authLoadingFailed": "Failed to load authentication methods.",
"profile.notifications": "Notifications",
"profile.avatarUploadSuccess": "Profile picture uploaded successfully.",
"profile.avatarUploadFailed": "Failed to upload user profile picture.",
"profile.avatarClearSuccess": "Profile picture cleared successfully.",
"profile.avatarClearFailed": "Failed to clear profile picture."
}

@ -11,6 +11,7 @@ q-layout(view='hHh Lpr lff')
clickable
:to='`/_profile/` + navItem.key'
active-class='is-active'
:disabled='navItem.disabled'
v-ripple
)
q-item-section(side)
@ -21,12 +22,12 @@ q-layout(view='hHh Lpr lff')
q-item(
clickable
v-ripple
to='/_profile/me'
:to='`/_user/` + userStore.id'
)
q-item-section(side)
q-icon(name='las la-id-card')
q-item-section
q-item-label View Public Profile
q-item-label {{ t('profile.viewPublicProfile') }}
q-separator.q-my-sm(inset)
q-item(
clickable
@ -36,7 +37,7 @@ q-layout(view='hHh Lpr lff')
q-item-section(side)
q-icon(name='las la-sign-out-alt', color='negative')
q-item-section
q-item-label.text-negative Logout
q-item-label.text-negative {{ t('common.header.logout') }}
router-view
q-footer
q-bar.justify-center(dense)
@ -44,13 +45,12 @@ q-layout(view='hHh Lpr lff')
</template>
<script setup>
import gql from 'graphql-tag'
import { useI18n } from 'vue-i18n'
import { useMeta, useQuasar } from 'quasar'
import { onMounted, reactive, watch } from 'vue'
import { useSiteStore } from 'src/stores/site'
import { useUserStore } from 'src/stores/user'
import HeaderNav from '../components/HeaderNav.vue'
@ -61,62 +61,60 @@ const $q = useQuasar()
// STORES
const siteStore = useSiteStore()
const userStore = useUserStore()
// I18N
const { t } = useI18n()
// META
useMeta({
titleTemplate: title => `${title} - ${t('profile.title')} - Wiki.js`
})
// DATA
const sidenav = [
{
key: 'info',
label: 'Profile',
label: t('profile.title'),
icon: 'las la-user-circle'
},
{
key: 'avatar',
label: 'Avatar',
label: t('profile.avatar'),
icon: 'las la-otter'
},
{
key: 'password',
label: 'Authentication',
key: 'auth',
label: t('profile.auth'),
icon: 'las la-key'
},
{
key: 'groups',
label: 'Groups',
label: t('profile.groups'),
icon: 'las la-users'
},
{
key: 'notifications',
label: 'Notifications',
icon: 'las la-bell'
},
{
key: 'pages',
label: 'My Pages',
icon: 'las la-file-alt'
label: t('profile.notifications'),
icon: 'las la-bell',
disabled: true
},
// {
// key: 'pages',
// label: 'My Pages',
// icon: 'las la-file-alt',
// disabled: true
// },
{
key: 'activity',
label: 'Activity',
icon: 'las la-history'
label: t('profile.activity'),
icon: 'las la-history',
disabled: true
}
]
const thumbStyle = {
right: '2px',
borderRadius: '5px',
backgroundColor: '#FFF',
width: '5px',
opacity: 0.5
}
const barStyle = {
backgroundColor: '#000',
width: '9px',
opacity: 0.1
}
</script>
<style lang="scss">

@ -1,29 +0,0 @@
<template>
<div class="fullscreen bg-blue text-white text-center q-pa-md flex flex-center">
<div>
<div style="font-size: 30vh">
404
</div>
<div class="text-h2" style="opacity:.4">
Oops. Nothing here...
</div>
<q-btn
class="q-mt-xl"
color="white"
text-color="blue"
unelevated
to="/"
label="Go Home"
no-caps
/>
</div>
</div>
</template>
<script>
export default {
name: 'Error404'
}
</script>

@ -7,6 +7,7 @@
.errorpage-hint {{error.hint}}
.errorpage-actions
q-btn(
v-if='error.showHomeBtn'
push
color='primary'
label='Go to home'
@ -38,6 +39,10 @@ const actions = {
notfound: {
code: 404
},
unknownsite: {
code: 'X!?',
showHomeBtn: false
},
generic: {
code: '!?0'
}
@ -62,12 +67,14 @@ useMeta({
const error = computed(() => {
if (route.params.action && actions[route.params.action]) {
return {
showHomeBtn: true,
...actions[route.params.action],
title: t(`common.error.${route.params.action}.title`),
hint: t(`common.error.${route.params.action}.hint`)
}
} else {
return {
showHomeBtn: true,
...actions.generic,
title: t('common.error.generic.title'),
hint: t('common.error.generic.hint')

@ -1,31 +0,0 @@
<template>
<div class="fullscreen bg-blue text-white text-center q-pa-md flex flex-center">
<div>
<div style="font-size: 30vh">
404
</div>
<div class="text-h2" style="opacity:.4">
Oops. Nothing here...
</div>
<q-btn
class="q-mt-xl"
color="white"
text-color="blue"
unelevated
to="/"
label="Go Home"
no-caps
/>
</div>
</div>
</template>
<script>
import { defineComponent } from 'vue'
export default defineComponent({
name: 'ErrorNotFound'
})
</script>

@ -0,0 +1,137 @@
<template lang="pug">
q-page.q-py-md(:style-fn='pageStyle')
.text-header {{t('profile.auth')}}
.q-pa-md
.text-body2 {{ t('profile.authInfo') }}
q-list.q-mt-lg(
bordered
separator
)
q-item(
v-for='auth of state.authMethods'
:key='auth.id'
)
q-item-section(avatar)
q-avatar(
color='dark-5'
text-color='white'
rounded
)
q-icon(:name='`img:` + auth.strategyIcon')
q-item-section
strong {{auth.authName}}
template(v-if='auth.strategyKey === `local`')
q-item-section(v-if='auth.config.isTfaSetup', side)
q-btn(
icon='las la-fingerprint'
unelevated
:label='t(`profile.authModifyTfa`)'
color='primary'
@click=''
)
q-item-section(v-else, side)
q-btn(
icon='las la-fingerprint'
unelevated
:label='t(`profile.authSetTfa`)'
color='primary'
@click=''
)
q-item-section(side)
q-btn(
icon='las la-key'
unelevated
:label='t(`profile.authChangePassword`)'
color='primary'
@click=''
)
q-inner-loading(:showing='state.loading > 0')
</template>
<script setup>
import gql from 'graphql-tag'
import { useI18n } from 'vue-i18n'
import { useMeta, useQuasar } from 'quasar'
import { onMounted, reactive } from 'vue'
import { useUserStore } from 'src/stores/user'
// QUASAR
const $q = useQuasar()
// STORES
const userStore = useUserStore()
// I18N
const { t } = useI18n()
// META
useMeta({
title: t('profile.auth')
})
// DATA
const state = reactive({
authMethods: [],
loading: 0
})
// METHODS
function pageStyle (offset, height) {
return {
'min-height': `${height - 100 - offset}px`
}
}
async function fetchAuthMethods () {
state.loading++
try {
const respRaw = await APOLLO_CLIENT.query({
query: gql`
query getUserProfileAuthMethods (
$id: UUID!
) {
userById (
id: $id
) {
id
auth {
authId
authName
strategyKey
strategyIcon
config
}
}
}
`,
variables: {
id: userStore.id
},
fetchPolicy: 'network-only'
})
state.authMethods = respRaw.data?.userById?.auth ?? []
} catch (err) {
$q.notify({
type: 'negative',
message: t('profile.authLoadingFailed'),
caption: err.message
})
}
state.loading--
}
// MOUNTED
onMounted(() => {
fetchAuthMethods()
})
</script>

@ -0,0 +1,194 @@
<template lang="pug">
q-page.q-py-md(:style-fn='pageStyle')
.text-header {{t('profile.avatar')}}
.row.q-gutter-lg.q-mt-xl.align-center
.col.text-center
q-avatar.profile-avatar-circ(
size='180px'
:color='userStore.hasAvatar ? `dark-1` : `primary`'
text-color='white'
:class='userStore.hasAvatar ? `is-image` : ``'
)
img(
v-if='userStore.hasAvatar',
:src='`/_user/` + userStore.id + `/avatar?` + state.assetTimestamp'
)
q-icon(
v-else,
name='las la-user'
)
.col
.text-body1 {{ t('profile.avatarUploadTitle') }}
.text-caption {{ t('profile.avatarUploadHint') }}
.q-mt-md
q-btn(
icon='las la-upload'
unelevated
:label='t(`profile.uploadNewAvatar`)'
color='primary'
@click='uploadImage'
)
.q-mt-md
q-btn.q-mr-sm(
icon='las la-times'
outline
:label='t(`common.actions.clear`)'
color='primary'
@click='clearImage'
:disable='!userStore.hasAvatar'
)
q-inner-loading(:showing='state.loading > 0')
</template>
<script setup>
import gql from 'graphql-tag'
import { useI18n } from 'vue-i18n'
import { useMeta, useQuasar } from 'quasar'
import { reactive } from 'vue'
import { useUserStore } from 'src/stores/user'
// QUASAR
const $q = useQuasar()
// STORES
const userStore = useUserStore()
// I18N
const { t } = useI18n()
// META
useMeta({
title: t('profile.avatar')
})
// DATA
const state = reactive({
loading: 0,
assetTimestamp: (new Date()).toISOString()
})
// METHODS
function pageStyle (offset, height) {
return {
'min-height': `${height - 100 - offset}px`
}
}
async function uploadImage () {
const input = document.createElement('input')
input.type = 'file'
input.onchange = async e => {
state.loading++
try {
const resp = await APOLLO_CLIENT.mutate({
mutation: gql`
mutation uploadUserAvatar (
$id: UUID!
$image: Upload!
) {
uploadUserAvatar (
id: $id
image: $image
) {
operation {
succeeded
message
}
}
}
`,
variables: {
id: userStore.id,
image: e.target.files[0]
}
})
if (resp?.data?.uploadUserAvatar?.operation?.succeeded) {
$q.notify({
type: 'positive',
message: t('profile.avatarUploadSuccess')
})
state.assetTimestamp = (new Date()).toISOString()
userStore.$patch({
hasAvatar: true
})
} else {
throw new Error(resp?.data?.uploadUserAvatar?.operation?.message || 'An unexpected error occured.')
}
} catch (err) {
$q.notify({
type: 'negative',
message: t('profile.avatarUploadFailed'),
caption: err.message
})
}
state.loading--
}
input.click()
}
async function clearImage () {
state.loading++
try {
const resp = await APOLLO_CLIENT.mutate({
mutation: gql`
mutation clearUserAvatar (
$id: UUID!
) {
clearUserAvatar (
id: $id
) {
operation {
succeeded
message
}
}
}
`,
variables: {
id: userStore.id
}
})
if (resp?.data?.clearUserAvatar?.operation?.succeeded) {
$q.notify({
type: 'positive',
message: t('profile.avatarClearSuccess')
})
state.assetTimestamp = (new Date()).toISOString()
userStore.$patch({
hasAvatar: false
})
} else {
throw new Error(resp?.data?.uploadUserAvatar?.operation?.message || 'An unexpected error occured.')
}
} catch (err) {
$q.notify({
type: 'negative',
message: t('profile.avatarClearFailed'),
caption: err.message
})
}
state.loading--
}
</script>
<style lang="scss">
.profile-avatar-circ {
box-shadow: 2px 2px 15px -5px var(--q-primary), -2px -2px 15px -5px var(--q-primary), inset 0 0 2px 8px rgba(255,255,255,.15);
&.is-image {
box-shadow: 0 0 0 5px rgba(0,0,0,.1);
}
}
</style>

@ -0,0 +1,114 @@
<template lang="pug">
q-page.q-py-md(:style-fn='pageStyle')
.text-header {{t('profile.groups')}}
.q-pa-md
.text-body2 {{ t('profile.groupsInfo') }}
q-list.q-mt-lg(
bordered
separator
)
q-item(
v-if='state.groups.length === 0 && state.loading < 1'
)
q-item-section
span.text-negative {{ t('profile.groupsNone') }}
q-item(
v-for='grp of state.groups'
:key='grp.id'
)
q-item-section(avatar)
q-avatar(
color='secondary'
text-color='white'
icon='las la-users'
rounded
)
q-item-section
strong {{grp.name}}
q-inner-loading(:showing='state.loading > 0')
</template>
<script setup>
import gql from 'graphql-tag'
import { useI18n } from 'vue-i18n'
import { useMeta, useQuasar } from 'quasar'
import { onMounted, reactive } from 'vue'
import { useUserStore } from 'src/stores/user'
// QUASAR
const $q = useQuasar()
// STORES
const userStore = useUserStore()
// I18N
const { t } = useI18n()
// META
useMeta({
title: t('profile.avatar')
})
// DATA
const state = reactive({
groups: [],
loading: 0
})
// METHODS
function pageStyle (offset, height) {
return {
'min-height': `${height - 100 - offset}px`
}
}
async function fetchGroups () {
state.loading++
try {
const respRaw = await APOLLO_CLIENT.query({
query: gql`
query getUserProfileGroups (
$id: UUID!
) {
userById (
id: $id
) {
id
groups {
id
name
}
}
}
`,
variables: {
id: userStore.id
},
fetchPolicy: 'network-only'
})
state.groups = respRaw.data?.userById?.groups ?? []
} catch (err) {
$q.notify({
type: 'negative',
message: t('profile.groupsLoadingFailed'),
caption: err.message
})
}
state.loading--
}
// MOUNTED
onMounted(() => {
fetchGroups()
})
</script>

@ -80,11 +80,10 @@ q-page.q-py-md(:style-fn='pageStyle')
q-select(
outlined
v-model='state.config.timezone'
:options='dataStore.timezones'
option-value='value'
option-label='text'
emit-value
map-options
:options='timezones'
:virtual-scroll-slice-size='100'
:virtual-scroll-slice-ratio-before='2'
:virtual-scroll-slice-ratio-after='2'
dense
options-dense
:aria-label='t(`admin.general.defaultTimezone`)'
@ -153,7 +152,6 @@ import { useMeta, useQuasar } from 'quasar'
import { onMounted, reactive, watch } from 'vue'
import { useSiteStore } from 'src/stores/site'
import { useDataStore } from 'src/stores/data'
import { useUserStore } from 'src/stores/user'
// QUASAR
@ -163,7 +161,6 @@ const $q = useQuasar()
// STORES
const siteStore = useSiteStore()
const dataStore = useDataStore()
const userStore = useUserStore()
// I18N
@ -173,7 +170,7 @@ const { t } = useI18n()
// META
useMeta({
title: t('profile.title')
title: t('profile.myInfo')
})
// DATA
@ -209,6 +206,7 @@ const appearances = [
{ value: 'light', label: t('profile.appearanceLight') },
{ value: 'dark', label: t('profile.appearanceDark') }
]
const timezones = Intl.supportedValuesOf('timeZone')
// METHODS

@ -1,21 +0,0 @@
<template lang='pug'>
.fullscreen.bg-blue.text-white.text-center.q-pa-md.flex.flex-center
div
.text-h1 Unknown Site
.text-h2(style="opacity:.4") Oops. Nothing here...
q-btn(
class="q-mt-xl"
color="white"
text-color="blue"
unelevated
to="/"
label="Go Home"
no-caps
)
</template>
<script>
export default {
name: 'UnknownSite'
}
</script>

@ -20,7 +20,10 @@ const routes = [
component: () => import('layouts/ProfileLayout.vue'),
children: [
{ path: '', redirect: '/_profile/info' },
{ path: 'info', component: () => import('pages/Profile.vue') }
{ path: 'info', component: () => import('src/pages/ProfileInfo.vue') },
{ path: 'avatar', component: () => import('src/pages/ProfileAvatar.vue') },
{ path: 'auth', component: () => import('src/pages/ProfileAuth.vue') },
{ path: 'groups', component: () => import('src/pages/ProfileGroups.vue') }
]
},
{
@ -70,8 +73,16 @@ const routes = [
// component: () => import('../pages/UnknownSite.vue')
// },
// Always leave this as last one,
// but you can also remove it
// --------------------------------
// SYSTEM ROUTES CATCH-ALL FALLBACK
// --------------------------------
{
path: '/_:catchAll(.*)*',
redirect: '/_error/notfound'
},
// -----------------------
// STANDARD PAGE CATCH-ALL
// -----------------------
{
path: '/:catchAll(.*)*',
component: () => import('../layouts/MainLayout.vue'),

@ -1,260 +0,0 @@
import { defineStore } from 'pinia'
export const useDataStore = defineStore('data', {
state: () => ({
timezones: [
{ text: '(GMT-11:00) Niue', value: 'Pacific/Niue' },
{ text: '(GMT-11:00) Pago Pago', value: 'Pacific/Pago_Pago' },
{ text: '(GMT-10:00) Hawaii Time', value: 'Pacific/Honolulu' },
{ text: '(GMT-10:00) Rarotonga', value: 'Pacific/Rarotonga' },
{ text: '(GMT-10:00) Tahiti', value: 'Pacific/Tahiti' },
{ text: '(GMT-09:30) Marquesas', value: 'Pacific/Marquesas' },
{ text: '(GMT-09:00) Alaska Time', value: 'America/Anchorage' },
{ text: '(GMT-09:00) Gambier', value: 'Pacific/Gambier' },
{ text: '(GMT-08:00) Pacific Time', value: 'America/Los_Angeles' },
{ text: '(GMT-08:00) Pacific Time - Tijuana', value: 'America/Tijuana' },
{ text: '(GMT-08:00) Pacific Time - Vancouver', value: 'America/Vancouver' },
{ text: '(GMT-08:00) Pacific Time - Whitehorse', value: 'America/Whitehorse' },
{ text: '(GMT-08:00) Pitcairn', value: 'Pacific/Pitcairn' },
{ text: '(GMT-07:00) Mountain Time', value: 'America/Denver' },
{ text: '(GMT-07:00) Mountain Time - Arizona', value: 'America/Phoenix' },
{ text: '(GMT-07:00) Mountain Time - Chihuahua, Mazatlan', value: 'America/Mazatlan' },
{ text: '(GMT-07:00) Mountain Time - Dawson Creek', value: 'America/Dawson_Creek' },
{ text: '(GMT-07:00) Mountain Time - Edmonton', value: 'America/Edmonton' },
{ text: '(GMT-07:00) Mountain Time - Hermosillo', value: 'America/Hermosillo' },
{ text: '(GMT-07:00) Mountain Time - Yellowknife', value: 'America/Yellowknife' },
{ text: '(GMT-06:00) Belize', value: 'America/Belize' },
{ text: '(GMT-06:00) Central Time', value: 'America/Chicago' },
{ text: '(GMT-06:00) Central Time - Mexico City', value: 'America/Mexico_City' },
{ text: '(GMT-06:00) Central Time - Regina', value: 'America/Regina' },
{ text: '(GMT-06:00) Central Time - Tegucigalpa', value: 'America/Tegucigalpa' },
{ text: '(GMT-06:00) Central Time - Winnipeg', value: 'America/Winnipeg' },
{ text: '(GMT-06:00) Costa Rica', value: 'America/Costa_Rica' },
{ text: '(GMT-06:00) El Salvador', value: 'America/El_Salvador' },
{ text: '(GMT-06:00) Galapagos', value: 'Pacific/Galapagos' },
{ text: '(GMT-06:00) Guatemala', value: 'America/Guatemala' },
{ text: '(GMT-06:00) Managua', value: 'America/Managua' },
{ text: '(GMT-05:00) America Cancun', value: 'America/Cancun' },
{ text: '(GMT-05:00) Bogota', value: 'America/Bogota' },
{ text: '(GMT-05:00) Easter Island', value: 'Pacific/Easter' },
{ text: '(GMT-05:00) Eastern Time', value: 'America/New_York' },
{ text: '(GMT-05:00) Eastern Time - Iqaluit', value: 'America/Iqaluit' },
{ text: '(GMT-05:00) Eastern Time - Toronto', value: 'America/Toronto' },
{ text: '(GMT-05:00) Guayaquil', value: 'America/Guayaquil' },
{ text: '(GMT-05:00) Havana', value: 'America/Havana' },
{ text: '(GMT-05:00) Jamaica', value: 'America/Jamaica' },
{ text: '(GMT-05:00) Lima', value: 'America/Lima' },
{ text: '(GMT-05:00) Nassau', value: 'America/Nassau' },
{ text: '(GMT-05:00) Panama', value: 'America/Panama' },
{ text: '(GMT-05:00) Port-au-Prince', value: 'America/Port-au-Prince' },
{ text: '(GMT-05:00) Rio Branco', value: 'America/Rio_Branco' },
{ text: '(GMT-04:00) Atlantic Time - Halifax', value: 'America/Halifax' },
{ text: '(GMT-04:00) Barbados', value: 'America/Barbados' },
{ text: '(GMT-04:00) Bermuda', value: 'Atlantic/Bermuda' },
{ text: '(GMT-04:00) Boa Vista', value: 'America/Boa_Vista' },
{ text: '(GMT-04:00) Caracas', value: 'America/Caracas' },
{ text: '(GMT-04:00) Curacao', value: 'America/Curacao' },
{ text: '(GMT-04:00) Grand Turk', value: 'America/Grand_Turk' },
{ text: '(GMT-04:00) Guyana', value: 'America/Guyana' },
{ text: '(GMT-04:00) La Paz', value: 'America/La_Paz' },
{ text: '(GMT-04:00) Manaus', value: 'America/Manaus' },
{ text: '(GMT-04:00) Martinique', value: 'America/Martinique' },
{ text: '(GMT-04:00) Port of Spain', value: 'America/Port_of_Spain' },
{ text: '(GMT-04:00) Porto Velho', value: 'America/Porto_Velho' },
{ text: '(GMT-04:00) Puerto Rico', value: 'America/Puerto_Rico' },
{ text: '(GMT-04:00) Santo Domingo', value: 'America/Santo_Domingo' },
{ text: '(GMT-04:00) Thule', value: 'America/Thule' },
{ text: '(GMT-03:30) Newfoundland Time - St. Johns', value: 'America/St_Johns' },
{ text: '(GMT-03:00) Araguaina', value: 'America/Araguaina' },
{ text: '(GMT-03:00) Asuncion', value: 'America/Asuncion' },
{ text: '(GMT-03:00) Belem', value: 'America/Belem' },
{ text: '(GMT-03:00) Buenos Aires', value: 'America/Argentina/Buenos_Aires' },
{ text: '(GMT-03:00) Campo Grande', value: 'America/Campo_Grande' },
{ text: '(GMT-03:00) Cayenne', value: 'America/Cayenne' },
{ text: '(GMT-03:00) Cuiaba', value: 'America/Cuiaba' },
{ text: '(GMT-03:00) Fortaleza', value: 'America/Fortaleza' },
{ text: '(GMT-03:00) Godthab', value: 'America/Godthab' },
{ text: '(GMT-03:00) Maceio', value: 'America/Maceio' },
{ text: '(GMT-03:00) Miquelon', value: 'America/Miquelon' },
{ text: '(GMT-03:00) Montevideo', value: 'America/Montevideo' },
{ text: '(GMT-03:00) Palmer', value: 'Antarctica/Palmer' },
{ text: '(GMT-03:00) Paramaribo', value: 'America/Paramaribo' },
{ text: '(GMT-03:00) Punta Arenas', value: 'America/Punta_Arenas' },
{ text: '(GMT-03:00) Recife', value: 'America/Recife' },
{ text: '(GMT-03:00) Rothera', value: 'Antarctica/Rothera' },
{ text: '(GMT-03:00) Salvador', value: 'America/Bahia' },
{ text: '(GMT-03:00) Santiago', value: 'America/Santiago' },
{ text: '(GMT-03:00) Stanley', value: 'Atlantic/Stanley' },
{ text: '(GMT-02:00) Noronha', value: 'America/Noronha' },
{ text: '(GMT-02:00) Sao Paulo', value: 'America/Sao_Paulo' },
{ text: '(GMT-02:00) South Georgia', value: 'Atlantic/South_Georgia' },
{ text: '(GMT-01:00) Azores', value: 'Atlantic/Azores' },
{ text: '(GMT-01:00) Cape Verde', value: 'Atlantic/Cape_Verde' },
{ text: '(GMT-01:00) Scoresbysund', value: 'America/Scoresbysund' },
{ text: '(GMT+00:00) Abidjan', value: 'Africa/Abidjan' },
{ text: '(GMT+00:00) Accra', value: 'Africa/Accra' },
{ text: '(GMT+00:00) Bissau', value: 'Africa/Bissau' },
{ text: '(GMT+00:00) Canary Islands', value: 'Atlantic/Canary' },
{ text: '(GMT+00:00) Casablanca', value: 'Africa/Casablanca' },
{ text: '(GMT+00:00) Danmarkshavn', value: 'America/Danmarkshavn' },
{ text: '(GMT+00:00) Dublin', value: 'Europe/Dublin' },
{ text: '(GMT+00:00) El Aaiun', value: 'Africa/El_Aaiun' },
{ text: '(GMT+00:00) Faeroe', value: 'Atlantic/Faroe' },
{ text: '(GMT+00:00) GMT (no daylight saving)', value: 'Etc/GMT' },
{ text: '(GMT+00:00) Lisbon', value: 'Europe/Lisbon' },
{ text: '(GMT+00:00) London', value: 'Europe/London' },
{ text: '(GMT+00:00) Monrovia', value: 'Africa/Monrovia' },
{ text: '(GMT+00:00) Reykjavik', value: 'Atlantic/Reykjavik' },
{ text: '(GMT+01:00) Algiers', value: 'Africa/Algiers' },
{ text: '(GMT+01:00) Amsterdam', value: 'Europe/Amsterdam' },
{ text: '(GMT+01:00) Andorra', value: 'Europe/Andorra' },
{ text: '(GMT+01:00) Berlin', value: 'Europe/Berlin' },
{ text: '(GMT+01:00) Brussels', value: 'Europe/Brussels' },
{ text: '(GMT+01:00) Budapest', value: 'Europe/Budapest' },
{ text: '(GMT+01:00) Central European Time - Belgrade', value: 'Europe/Belgrade' },
{ text: '(GMT+01:00) Central European Time - Prague', value: 'Europe/Prague' },
{ text: '(GMT+01:00) Ceuta', value: 'Africa/Ceuta' },
{ text: '(GMT+01:00) Copenhagen', value: 'Europe/Copenhagen' },
{ text: '(GMT+01:00) Gibraltar', value: 'Europe/Gibraltar' },
{ text: '(GMT+01:00) Lagos', value: 'Africa/Lagos' },
{ text: '(GMT+01:00) Luxembourg', value: 'Europe/Luxembourg' },
{ text: '(GMT+01:00) Madrid', value: 'Europe/Madrid' },
{ text: '(GMT+01:00) Malta', value: 'Europe/Malta' },
{ text: '(GMT+01:00) Monaco', value: 'Europe/Monaco' },
{ text: '(GMT+01:00) Ndjamena', value: 'Africa/Ndjamena' },
{ text: '(GMT+01:00) Oslo', value: 'Europe/Oslo' },
{ text: '(GMT+01:00) Paris', value: 'Europe/Paris' },
{ text: '(GMT+01:00) Rome', value: 'Europe/Rome' },
{ text: '(GMT+01:00) Stockholm', value: 'Europe/Stockholm' },
{ text: '(GMT+01:00) Tirane', value: 'Europe/Tirane' },
{ text: '(GMT+01:00) Tunis', value: 'Africa/Tunis' },
{ text: '(GMT+01:00) Vienna', value: 'Europe/Vienna' },
{ text: '(GMT+01:00) Warsaw', value: 'Europe/Warsaw' },
{ text: '(GMT+01:00) Zurich', value: 'Europe/Zurich' },
{ text: '(GMT+02:00) Amman', value: 'Asia/Amman' },
{ text: '(GMT+02:00) Athens', value: 'Europe/Athens' },
{ text: '(GMT+02:00) Beirut', value: 'Asia/Beirut' },
{ text: '(GMT+02:00) Bucharest', value: 'Europe/Bucharest' },
{ text: '(GMT+02:00) Cairo', value: 'Africa/Cairo' },
{ text: '(GMT+02:00) Chisinau', value: 'Europe/Chisinau' },
{ text: '(GMT+02:00) Damascus', value: 'Asia/Damascus' },
{ text: '(GMT+02:00) Gaza', value: 'Asia/Gaza' },
{ text: '(GMT+02:00) Helsinki', value: 'Europe/Helsinki' },
{ text: '(GMT+02:00) Jerusalem', value: 'Asia/Jerusalem' },
{ text: '(GMT+02:00) Johannesburg', value: 'Africa/Johannesburg' },
{ text: '(GMT+02:00) Khartoum', value: 'Africa/Khartoum' },
{ text: '(GMT+02:00) Kiev', value: 'Europe/Kiev' },
{ text: '(GMT+02:00) Maputo', value: 'Africa/Maputo' },
{ text: '(GMT+02:00) Moscow-01 - Kaliningrad', value: 'Europe/Kaliningrad' },
{ text: '(GMT+02:00) Nicosia', value: 'Asia/Nicosia' },
{ text: '(GMT+02:00) Riga', value: 'Europe/Riga' },
{ text: '(GMT+02:00) Sofia', value: 'Europe/Sofia' },
{ text: '(GMT+02:00) Tallinn', value: 'Europe/Tallinn' },
{ text: '(GMT+02:00) Tripoli', value: 'Africa/Tripoli' },
{ text: '(GMT+02:00) Vilnius', value: 'Europe/Vilnius' },
{ text: '(GMT+02:00) Windhoek', value: 'Africa/Windhoek' },
{ text: '(GMT+03:00) Baghdad', value: 'Asia/Baghdad' },
{ text: '(GMT+03:00) Istanbul', value: 'Europe/Istanbul' },
{ text: '(GMT+03:00) Minsk', value: 'Europe/Minsk' },
{ text: '(GMT+03:00) Moscow+00 - Moscow', value: 'Europe/Moscow' },
{ text: '(GMT+03:00) Nairobi', value: 'Africa/Nairobi' },
{ text: '(GMT+03:00) Qatar', value: 'Asia/Qatar' },
{ text: '(GMT+03:00) Riyadh', value: 'Asia/Riyadh' },
{ text: '(GMT+03:00) Syowa', value: 'Antarctica/Syowa' },
{ text: '(GMT+03:30) Tehran', value: 'Asia/Tehran' },
{ text: '(GMT+04:00) Baku', value: 'Asia/Baku' },
{ text: '(GMT+04:00) Dubai', value: 'Asia/Dubai' },
{ text: '(GMT+04:00) Mahe', value: 'Indian/Mahe' },
{ text: '(GMT+04:00) Mauritius', value: 'Indian/Mauritius' },
{ text: '(GMT+04:00) Moscow+01 - Samara', value: 'Europe/Samara' },
{ text: '(GMT+04:00) Reunion', value: 'Indian/Reunion' },
{ text: '(GMT+04:00) Tbilisi', value: 'Asia/Tbilisi' },
{ text: '(GMT+04:00) Yerevan', value: 'Asia/Yerevan' },
{ text: '(GMT+04:30) Kabul', value: 'Asia/Kabul' },
{ text: '(GMT+05:00) Aqtau', value: 'Asia/Aqtau' },
{ text: '(GMT+05:00) Aqtobe', value: 'Asia/Aqtobe' },
{ text: '(GMT+05:00) Ashgabat', value: 'Asia/Ashgabat' },
{ text: '(GMT+05:00) Dushanbe', value: 'Asia/Dushanbe' },
{ text: '(GMT+05:00) Karachi', value: 'Asia/Karachi' },
{ text: '(GMT+05:00) Kerguelen', value: 'Indian/Kerguelen' },
{ text: '(GMT+05:00) Maldives', value: 'Indian/Maldives' },
{ text: '(GMT+05:00) Mawson', value: 'Antarctica/Mawson' },
{ text: '(GMT+05:00) Moscow+02 - Yekaterinburg', value: 'Asia/Yekaterinburg' },
{ text: '(GMT+05:00) Tashkent', value: 'Asia/Tashkent' },
{ text: '(GMT+05:30) Colombo', value: 'Asia/Colombo' },
{ text: '(GMT+05:30) India Standard Time', value: 'Asia/Kolkata' },
{ text: '(GMT+05:45) Kathmandu', value: 'Asia/Kathmandu' },
{ text: '(GMT+06:00) Almaty', value: 'Asia/Almaty' },
{ text: '(GMT+06:00) Bishkek', value: 'Asia/Bishkek' },
{ text: '(GMT+06:00) Chagos', value: 'Indian/Chagos' },
{ text: '(GMT+06:00) Dhaka', value: 'Asia/Dhaka' },
{ text: '(GMT+06:00) Moscow+03 - Omsk', value: 'Asia/Omsk' },
{ text: '(GMT+06:00) Thimphu', value: 'Asia/Thimphu' },
{ text: '(GMT+06:00) Vostok', value: 'Antarctica/Vostok' },
{ text: '(GMT+06:30) Cocos', value: 'Indian/Cocos' },
{ text: '(GMT+06:30) Rangoon', value: 'Asia/Yangon' },
{ text: '(GMT+07:00) Bangkok', value: 'Asia/Bangkok' },
{ text: '(GMT+07:00) Christmas', value: 'Indian/Christmas' },
{ text: '(GMT+07:00) Davis', value: 'Antarctica/Davis' },
{ text: '(GMT+07:00) Hanoi', value: 'Asia/Saigon' },
{ text: '(GMT+07:00) Hovd', value: 'Asia/Hovd' },
{ text: '(GMT+07:00) Jakarta', value: 'Asia/Jakarta' },
{ text: '(GMT+07:00) Moscow+04 - Krasnoyarsk', value: 'Asia/Krasnoyarsk' },
{ text: '(GMT+08:00) Brunei', value: 'Asia/Brunei' },
{ text: '(GMT+08:00) China Time - Beijing', value: 'Asia/Shanghai' },
{ text: '(GMT+08:00) Choibalsan', value: 'Asia/Choibalsan' },
{ text: '(GMT+08:00) Hong Kong', value: 'Asia/Hong_Kong' },
{ text: '(GMT+08:00) Kuala Lumpur', value: 'Asia/Kuala_Lumpur' },
{ text: '(GMT+08:00) Macau', value: 'Asia/Macau' },
{ text: '(GMT+08:00) Makassar', value: 'Asia/Makassar' },
{ text: '(GMT+08:00) Manila', value: 'Asia/Manila' },
{ text: '(GMT+08:00) Moscow+05 - Irkutsk', value: 'Asia/Irkutsk' },
{ text: '(GMT+08:00) Singapore', value: 'Asia/Singapore' },
{ text: '(GMT+08:00) Taipei', value: 'Asia/Taipei' },
{ text: '(GMT+08:00) Ulaanbaatar', value: 'Asia/Ulaanbaatar' },
{ text: '(GMT+08:00) Western Time - Perth', value: 'Australia/Perth' },
{ text: '(GMT+08:30) Pyongyang', value: 'Asia/Pyongyang' },
{ text: '(GMT+09:00) Dili', value: 'Asia/Dili' },
{ text: '(GMT+09:00) Jayapura', value: 'Asia/Jayapura' },
{ text: '(GMT+09:00) Moscow+06 - Yakutsk', value: 'Asia/Yakutsk' },
{ text: '(GMT+09:00) Palau', value: 'Pacific/Palau' },
{ text: '(GMT+09:00) Seoul', value: 'Asia/Seoul' },
{ text: '(GMT+09:00) Tokyo', value: 'Asia/Tokyo' },
{ text: '(GMT+09:30) Central Time - Darwin', value: 'Australia/Darwin' },
{ text: '(GMT+10:00) Dumont D\'Urville', value: 'Antarctica/DumontDUrville' },
{ text: '(GMT+10:00) Eastern Time - Brisbane', value: 'Australia/Brisbane' },
{ text: '(GMT+10:00) Guam', value: 'Pacific/Guam' },
{ text: '(GMT+10:00) Moscow+07 - Vladivostok', value: 'Asia/Vladivostok' },
{ text: '(GMT+10:00) Port Moresby', value: 'Pacific/Port_Moresby' },
{ text: '(GMT+10:00) Truk', value: 'Pacific/Chuuk' },
{ text: '(GMT+10:30) Central Time - Adelaide', value: 'Australia/Adelaide' },
{ text: '(GMT+11:00) Casey', value: 'Antarctica/Casey' },
{ text: '(GMT+11:00) Eastern Time - Hobart', value: 'Australia/Hobart' },
{ text: '(GMT+11:00) Eastern Time - Melbourne, Sydney', value: 'Australia/Sydney' },
{ text: '(GMT+11:00) Efate', value: 'Pacific/Efate' },
{ text: '(GMT+11:00) Guadalcanal', value: 'Pacific/Guadalcanal' },
{ text: '(GMT+11:00) Kosrae', value: 'Pacific/Kosrae' },
{ text: '(GMT+11:00) Moscow+08 - Magadan', value: 'Asia/Magadan' },
{ text: '(GMT+11:00) Norfolk', value: 'Pacific/Norfolk' },
{ text: '(GMT+11:00) Noumea', value: 'Pacific/Noumea' },
{ text: '(GMT+11:00) Ponape', value: 'Pacific/Pohnpei' },
{ text: '(GMT+12:00) Funafuti', value: 'Pacific/Funafuti' },
{ text: '(GMT+12:00) Kwajalein', value: 'Pacific/Kwajalein' },
{ text: '(GMT+12:00) Majuro', value: 'Pacific/Majuro' },
{ text: '(GMT+12:00) Moscow+09 - Petropavlovsk-Kamchatskiy', value: 'Asia/Kamchatka' },
{ text: '(GMT+12:00) Nauru', value: 'Pacific/Nauru' },
{ text: '(GMT+12:00) Tarawa', value: 'Pacific/Tarawa' },
{ text: '(GMT+12:00) Wake', value: 'Pacific/Wake' },
{ text: '(GMT+12:00) Wallis', value: 'Pacific/Wallis' },
{ text: '(GMT+13:00) Auckland', value: 'Pacific/Auckland' },
{ text: '(GMT+13:00) Enderbury', value: 'Pacific/Enderbury' },
{ text: '(GMT+13:00) Fakaofo', value: 'Pacific/Fakaofo' },
{ text: '(GMT+13:00) Fiji', value: 'Pacific/Fiji' },
{ text: '(GMT+13:00) Tongatapu', value: 'Pacific/Tongatapu' },
{ text: '(GMT+14:00) Apia', value: 'Pacific/Apia' },
{ text: '(GMT+14:00) Kiritimati', value: 'Pacific/Kiritimati' }
]
}),
getters: {},
actions: {}
})

@ -9,7 +9,7 @@ export const useUserStore = defineStore('user', {
id: '10000000-0000-4000-8000-000000000001',
email: '',
name: '',
pictureUrl: '',
hasAvatar: false,
localeCode: '',
timezone: '',
dateFormat: 'YYYY-MM-DD',
@ -58,6 +58,7 @@ export const useUserStore = defineStore('user', {
id
name
email
hasAvatar
meta
prefs
lastLoginAt
@ -78,7 +79,7 @@ export const useUserStore = defineStore('user', {
}
this.name = resp.name || 'Unknown User'
this.email = resp.email
this.pictureUrl = (resp.pictureUrl === 'local') ? `/_user/${this.id}/avatar` : resp.pictureUrl
this.hasAvatar = resp.hasAvatar ?? false
this.location = resp.meta.location || ''
this.jobTitle = resp.meta.jobTitle || ''
this.pronouns = resp.meta.pronouns || ''

Loading…
Cancel
Save