feat: update profile + user theme

parent 8e87a5d4
...@@ -580,7 +580,7 @@ exports.up = async knex => { ...@@ -580,7 +580,7 @@ exports.up = async knex => {
timezone: 'America/New_York', timezone: 'America/New_York',
dateFormat: 'YYYY-MM-DD', dateFormat: 'YYYY-MM-DD',
timeFormat: '12h', timeFormat: '12h',
darkMode: false appearance: 'site'
}, },
localeCode: 'en' localeCode: 'en'
}, },
...@@ -597,7 +597,7 @@ exports.up = async knex => { ...@@ -597,7 +597,7 @@ exports.up = async knex => {
timezone: 'America/New_York', timezone: 'America/New_York',
dateFormat: 'YYYY-MM-DD', dateFormat: 'YYYY-MM-DD',
timeFormat: '12h', timeFormat: '12h',
darkMode: false appearance: 'site'
}, },
localeCode: 'en' localeCode: 'en'
} }
......
...@@ -40,6 +40,10 @@ module.exports = { ...@@ -40,6 +40,10 @@ module.exports = {
async userById (obj, args, context, info) { async userById (obj, args, context, info) {
const usr = await WIKI.models.users.query().findById(args.id) const usr = await WIKI.models.users.query().findById(args.id)
if (!usr) {
throw new Error('Invalid User')
}
// const str = _.get(WIKI.auth.strategies, usr.providerKey) // const str = _.get(WIKI.auth.strategies, usr.providerKey)
// str.strategy = _.find(WIKI.data.authentication, ['key', str.strategyKey]) // str.strategy = _.find(WIKI.data.authentication, ['key', str.strategyKey])
// usr.providerName = str.displayName // usr.providerName = str.displayName
...@@ -56,25 +60,25 @@ module.exports = { ...@@ -56,25 +60,25 @@ module.exports = {
return usr return usr
}, },
async profile (obj, args, context, info) { // async profile (obj, args, context, info) {
if (!context.req.user || context.req.user.id < 1 || context.req.user.id === 2) { // if (!context.req.user || context.req.user.id < 1 || context.req.user.id === 2) {
throw new WIKI.Error.AuthRequired() // throw new WIKI.Error.AuthRequired()
} // }
const usr = await WIKI.models.users.query().findById(context.req.user.id) // const usr = await WIKI.models.users.query().findById(context.req.user.id)
if (!usr.isActive) { // if (!usr.isActive) {
throw new WIKI.Error.AuthAccountBanned() // throw new WIKI.Error.AuthAccountBanned()
} // }
const providerInfo = _.get(WIKI.auth.strategies, usr.providerKey, {}) // const providerInfo = _.get(WIKI.auth.strategies, usr.providerKey, {})
usr.providerName = providerInfo.displayName || 'Unknown' // usr.providerName = providerInfo.displayName || 'Unknown'
usr.lastLoginAt = usr.lastLoginAt || usr.updatedAt // usr.lastLoginAt = usr.lastLoginAt || usr.updatedAt
usr.password = '' // usr.password = ''
usr.providerId = '' // usr.providerId = ''
usr.tfaSecret = '' // usr.tfaSecret = ''
return usr // return usr
}, // },
async lastLogins (obj, args, context, info) { async lastLogins (obj, args, context, info) {
return WIKI.models.users.query() return WIKI.models.users.query()
.select('id', 'name', 'lastLoginAt') .select('id', 'name', 'lastLoginAt')
...@@ -193,7 +197,7 @@ module.exports = { ...@@ -193,7 +197,7 @@ module.exports = {
}, },
async updateProfile (obj, args, context) { async updateProfile (obj, args, context) {
try { try {
if (!context.req.user || context.req.user.id < 1 || context.req.user.id === 2) { if (!context.req.user || context.req.user.id === WIKI.auth.guest.id) {
throw new WIKI.Error.AuthRequired() throw new WIKI.Error.AuthRequired()
} }
const usr = await WIKI.models.users.query().findById(context.req.user.id) const usr = await WIKI.models.users.query().findById(context.req.user.id)
...@@ -204,29 +208,33 @@ module.exports = { ...@@ -204,29 +208,33 @@ module.exports = {
throw new WIKI.Error.AuthAccountNotVerified() throw new WIKI.Error.AuthAccountNotVerified()
} }
if (!['', 'DD/MM/YYYY', 'DD.MM.YYYY', 'MM/DD/YYYY', 'YYYY-MM-DD', 'YYYY/MM/DD'].includes(args.dateFormat)) { if (args.dateFormat && !['', 'DD/MM/YYYY', 'DD.MM.YYYY', 'MM/DD/YYYY', 'YYYY-MM-DD', 'YYYY/MM/DD'].includes(args.dateFormat)) {
throw new WIKI.Error.InputInvalid() throw new WIKI.Error.InputInvalid()
} }
if (!['', 'light', 'dark'].includes(args.appearance)) { if (args.appearance && !['site', 'light', 'dark'].includes(args.appearance)) {
throw new WIKI.Error.InputInvalid() throw new WIKI.Error.InputInvalid()
} }
await WIKI.models.users.updateUser({ await WIKI.models.users.query().findById(usr.id).patch({
id: usr.id, name: args.name?.trim() ?? usr.name,
name: _.trim(args.name), meta: {
jobTitle: _.trim(args.jobTitle), ...usr.meta,
location: _.trim(args.location), location: args.location?.trim() ?? usr.meta.location,
timezone: args.timezone, jobTitle: args.jobTitle?.trim() ?? usr.meta.jobTitle,
dateFormat: args.dateFormat, pronouns: args.pronouns?.trim() ?? usr.meta.pronouns
appearance: args.appearance },
prefs: {
...usr.prefs,
timezone: args.timezone || usr.prefs.timezone,
dateFormat: args.dateFormat ?? usr.prefs.dateFormat,
timeFormat: args.timeFormat ?? usr.prefs.timeFormat,
appearance: args.appearance || usr.prefs.appearance
}
}) })
const newToken = await WIKI.models.users.refreshToken(usr.id)
return { return {
operation: graphHelper.generateSuccess('User profile updated successfully'), operation: graphHelper.generateSuccess('User profile updated successfully')
jwt: newToken.token
} }
} catch (err) { } catch (err) {
return graphHelper.generateError(err) return graphHelper.generateError(err)
...@@ -273,15 +281,15 @@ module.exports = { ...@@ -273,15 +281,15 @@ module.exports = {
groups (usr) { groups (usr) {
return usr.$relatedQuery('groups') return usr.$relatedQuery('groups')
} }
},
UserProfile: {
async groups (usr) {
const usrGroups = await usr.$relatedQuery('groups')
return usrGroups.map(g => g.name)
},
async pagesTotal (usr) {
const result = await WIKI.models.pages.query().count('* as total').where('creatorId', usr.id).first()
return _.toSafeInteger(result.total)
}
} }
// UserProfile: {
// async groups (usr) {
// const usrGroups = await usr.$relatedQuery('groups')
// return usrGroups.map(g => g.name)
// },
// async pagesTotal (usr) {
// const result = await WIKI.models.pages.query().count('* as total').where('creatorId', usr.id).first()
// return _.toSafeInteger(result.total)
// }
// }
} }
...@@ -16,8 +16,6 @@ extend type Query { ...@@ -16,8 +16,6 @@ extend type Query {
id: UUID! id: UUID!
): User ): User
profile: UserProfile
lastLogins: [UserLastLogin] lastLogins: [UserLastLogin]
} }
...@@ -66,13 +64,15 @@ extend type Mutation { ...@@ -66,13 +64,15 @@ extend type Mutation {
): DefaultResponse ): DefaultResponse
updateProfile( updateProfile(
name: String! name: String
location: String! location: String
jobTitle: String! jobTitle: String
timezone: String! pronouns: String
dateFormat: String! timezone: String
appearance: String! dateFormat: String
): UserTokenResponse timeFormat: String
appearance: UserSiteAppearance
): DefaultResponse
} }
# ----------------------------------------------- # -----------------------------------------------
...@@ -110,32 +110,13 @@ type User { ...@@ -110,32 +110,13 @@ type User {
isVerified: Boolean isVerified: Boolean
meta: JSON meta: JSON
prefs: JSON prefs: JSON
pictureUrl: String
createdAt: Date createdAt: Date
updatedAt: Date updatedAt: Date
lastLoginAt: Date lastLoginAt: Date
groups: [Group] groups: [Group]
} }
type UserProfile {
id: Int
name: String
email: String
providerKey: String
providerName: String
isSystem: Boolean
isVerified: Boolean
location: String
jobTitle: String
timezone: String
dateFormat: String
appearance: String
createdAt: Date
updatedAt: Date
lastLoginAt: Date
groups: [String]
pagesTotal: Int
}
type UserTokenResponse { type UserTokenResponse {
operation: Operation operation: Operation
jwt: String jwt: String
...@@ -150,6 +131,12 @@ enum UserOrderBy { ...@@ -150,6 +131,12 @@ enum UserOrderBy {
lastLoginAt lastLoginAt
} }
enum UserSiteAppearance {
site
light
dark
}
input UserUpdateInput { input UserUpdateInput {
email: String email: String
name: String name: String
......
...@@ -22,7 +22,7 @@ module.exports = class User extends Model { ...@@ -22,7 +22,7 @@ module.exports = class User extends Model {
properties: { properties: {
id: {type: 'string'}, id: {type: 'string'},
email: {type: 'string', format: 'email'}, email: {type: 'string'},
name: {type: 'string', minLength: 1, maxLength: 255}, name: {type: 'string', minLength: 1, maxLength: 255},
pictureUrl: {type: 'string'}, pictureUrl: {type: 'string'},
isSystem: {type: 'boolean'}, isSystem: {type: 'boolean'},
......
...@@ -77,12 +77,12 @@ module.exports = configure(function (/* ctx */) { ...@@ -77,12 +77,12 @@ module.exports = configure(function (/* ctx */) {
extendViteConf (viteConf) { extendViteConf (viteConf) {
viteConf.build.assetsDir = '_assets' viteConf.build.assetsDir = '_assets'
viteConf.build.rollupOptions = { // viteConf.build.rollupOptions = {
...viteConf.build.rollupOptions ?? {}, // ...viteConf.build.rollupOptions ?? {},
external: [ // external: [
/^\/_site\// // /^\/_site\//
] // ]
} // }
}, },
// viteVuePluginOptions: {}, // viteVuePluginOptions: {},
......
...@@ -3,9 +3,10 @@ router-view ...@@ -3,9 +3,10 @@ router-view
</template> </template>
<script setup> <script setup>
import { nextTick, onMounted, reactive } from 'vue' import { nextTick, onMounted, reactive, watch } from 'vue'
import { useRouter, useRoute } from 'vue-router' import { useRouter, useRoute } from 'vue-router'
import { useSiteStore } from 'src/stores/site' import { useSiteStore } from 'src/stores/site'
import { useUserStore } from 'src/stores/user'
import { setCssVar, useQuasar } from 'quasar' import { setCssVar, useQuasar } from 'quasar'
/* global siteConfig */ /* global siteConfig */
...@@ -17,6 +18,7 @@ const $q = useQuasar() ...@@ -17,6 +18,7 @@ const $q = useQuasar()
// STORES // STORES
const siteStore = useSiteStore() const siteStore = useSiteStore()
const userStore = useUserStore()
// ROUTER // ROUTER
...@@ -28,10 +30,24 @@ const state = reactive({ ...@@ -28,10 +30,24 @@ const state = reactive({
isInitialized: false isInitialized: false
}) })
// WATCHERS
watch(() => userStore.appearance, (newValue) => {
if (newValue === 'site') {
$q.dark.set(siteStore.theme.dark)
} else {
$q.dark.set(newValue === 'dark')
}
})
// THEME // THEME
function applyTheme () { function applyTheme () {
$q.dark.set(siteStore.theme.dark) if (userStore.appearance === 'site') {
$q.dark.set(siteStore.theme.dark)
} else {
$q.dark.set(userStore.appearance === 'dark')
}
setCssVar('primary', siteStore.theme.colorPrimary) setCssVar('primary', siteStore.theme.colorPrimary)
setCssVar('secondary', siteStore.theme.colorSecondary) setCssVar('secondary', siteStore.theme.colorSecondary)
setCssVar('accent', siteStore.theme.colorAccent) setCssVar('accent', siteStore.theme.colorAccent)
...@@ -51,12 +67,21 @@ if (typeof siteConfig !== 'undefined') { ...@@ -51,12 +67,21 @@ if (typeof siteConfig !== 'undefined') {
router.beforeEach(async (to, from) => { router.beforeEach(async (to, from) => {
siteStore.routerLoading = true siteStore.routerLoading = true
// Site Info
if (!siteStore.id) { if (!siteStore.id) {
console.info('No pre-cached site config. Loading site info...') console.info('No pre-cached site config. Loading site info...')
await siteStore.loadSite(window.location.hostname) await siteStore.loadSite(window.location.hostname)
console.info(`Using Site ID ${siteStore.id}`) console.info(`Using Site ID ${siteStore.id}`)
applyTheme()
} }
// User Auth
await userStore.refreshAuth()
// User Profile
if (userStore.authenticated && !userStore.profileLoaded) {
console.info(`Refreshing user ${userStore.id} profile...`)
await userStore.refreshProfile()
}
// Apply Theme
applyTheme()
}) })
router.afterEach(() => { router.afterEach(() => {
if (!state.isInitialized) { if (!state.isInitialized) {
......
...@@ -3,10 +3,10 @@ import { ApolloClient, InMemoryCache } from '@apollo/client/core' ...@@ -3,10 +3,10 @@ import { ApolloClient, InMemoryCache } from '@apollo/client/core'
import { setContext } from '@apollo/client/link/context' import { setContext } from '@apollo/client/link/context'
import { createUploadLink } from 'apollo-upload-client' import { createUploadLink } from 'apollo-upload-client'
export default boot(({ app }) => { export default boot(({ app, store }) => {
// Authentication Link // Authentication Link
const authLink = setContext(async (req, { headers }) => { const authLink = setContext(async (req, { headers }) => {
const token = 'test' // await window.auth0Client.getTokenSilently() const token = store.state.value.user.token
return { return {
headers: { headers: {
...headers, ...headers,
......
<template lang='pug'> <template lang='pug'>
q-btn.q-ml-md(flat, round, dense, color='grey') q-btn.q-ml-md(flat, round, dense, color='grey')
q-icon(v-if='!state.user.picture', name='las la-user-circle') q-icon(v-if='!userStore.authenticated || !userStore.pictureUrl', name='las la-user-circle')
q-avatar(v-else) q-avatar(v-else)
img(:src='state.user.picture') img(:src='userStore.pictureUrl')
q-menu(auto-close) q-menu(auto-close)
q-card(flat, style='width: 300px;', :dark='false') q-card(flat, style='width: 300px;', :dark='false')
q-card-section(align='center') q-card-section(align='center')
.text-subtitle1.text-grey-7 {{state.user.name}} .text-subtitle1.text-grey-7 {{userStore.name}}
.text-caption.text-grey-8 {{state.user.email}} .text-caption.text-grey-8 {{userStore.email}}
q-separator(:dark='false') q-separator(:dark='false')
q-card-actions(align='center') q-card-actions(align='center')
q-btn( q-btn(
...@@ -15,7 +15,7 @@ q-btn.q-ml-md(flat, round, dense, color='grey') ...@@ -15,7 +15,7 @@ q-btn.q-ml-md(flat, round, dense, color='grey')
label='Profile' label='Profile'
icon='las la-user-alt' icon='las la-user-alt'
color='primary' color='primary'
href='/_profile' to='/_profile'
no-caps no-caps
) )
q-btn(flat q-btn(flat
...@@ -29,13 +29,7 @@ q-btn.q-ml-md(flat, round, dense, color='grey') ...@@ -29,13 +29,7 @@ q-btn.q-ml-md(flat, round, dense, color='grey')
</template> </template>
<script setup> <script setup>
import { reactive } from 'vue' import { useUserStore } from 'src/stores/user'
const state = reactive({ const userStore = useUserStore()
user: {
name: 'John Doe',
email: 'test@example.com',
picture: null
}
})
</script> </script>
...@@ -17,10 +17,10 @@ q-header.bg-header.text-white.site-header( ...@@ -17,10 +17,10 @@ q-header.bg-header.text-white.site-header(
size='34px' size='34px'
square square
) )
img(src='/_site/logo') img(:src='`/_site/logo`')
img( img(
v-else v-else
src='/_site/logo' :src='`/_site/logo`'
style='height: 34px' style='height: 34px'
) )
q-toolbar-title.text-h6(v-if='siteStore.logoText') {{siteStore.title}} q-toolbar-title.text-h6(v-if='siteStore.logoText') {{siteStore.title}}
......
...@@ -117,6 +117,19 @@ q-layout(view='hHh lpR fFf', container) ...@@ -117,6 +117,19 @@ q-layout(view='hHh lpR fFf', container)
dense dense
:aria-label='t(`admin.users.jobTitle`)' :aria-label='t(`admin.users.jobTitle`)'
) )
q-separator.q-my-sm(inset)
q-item
blueprint-icon(icon='gender')
q-item-section
q-item-label {{t(`admin.users.pronouns`)}}
q-item-label(caption) {{t(`admin.users.pronounsHint`)}}
q-item-section
q-input(
outlined
v-model='state.user.meta.pronouns'
dense
:aria-label='t(`admin.users.pronouns`)'
)
q-card.shadow-1.q-pb-sm.q-mt-md(v-if='state.user.meta') q-card.shadow-1.q-pb-sm.q-mt-md(v-if='state.user.meta')
q-card-section q-card-section
...@@ -181,18 +194,23 @@ q-layout(view='hHh lpR fFf', container) ...@@ -181,18 +194,23 @@ q-layout(view='hHh lpR fFf', container)
]` ]`
) )
q-separator.q-my-sm(inset) q-separator.q-my-sm(inset)
q-item(tag='label', v-ripple) q-item
blueprint-icon(icon='light-on') blueprint-icon(icon='light-on')
q-item-section q-item-section
q-item-label {{t(`admin.users.darkMode`)}} q-item-label {{t(`admin.users.appearance`)}}
q-item-label(caption) {{t(`admin.users.darkModeHint`)}} q-item-label(caption) {{t(`admin.users.darkModeHint`)}}
q-item-section(avatar) q-item-section.col-auto
q-toggle( q-btn-toggle(
v-model='state.user.prefs.darkMode' v-model='state.user.prefs.appearance'
color='primary' push
checked-icon='las la-check' glossy
unchecked-icon='las la-times' no-caps
:aria-label='t(`admin.users.darkMode`)' toggle-color='primary'
:options=`[
{ label: t('profile.appearanceDefault'), value: 'site' },
{ label: t('profile.appearanceLight'), value: 'light' },
{ label: t('profile.appearanceDark'), value: 'dark' }
]`
) )
.col-12.col-lg-4 .col-12.col-lg-4
......
...@@ -1359,7 +1359,7 @@ ...@@ -1359,7 +1359,7 @@
"profile.activity.lastUpdatedOn": "Profile last updated on", "profile.activity.lastUpdatedOn": "Profile last updated on",
"profile.activity.pagesCreated": "Pages created", "profile.activity.pagesCreated": "Pages created",
"profile.activity.title": "Activity", "profile.activity.title": "Activity",
"profile.appearance": "Appearance", "profile.appearance": "Site Appearance",
"profile.appearanceDark": "Dark", "profile.appearanceDark": "Dark",
"profile.appearanceDefault": "Site Default", "profile.appearanceDefault": "Site Default",
"profile.appearanceLight": "Light", "profile.appearanceLight": "Light",
...@@ -1498,5 +1498,12 @@ ...@@ -1498,5 +1498,12 @@
"admin.utilities.disconnectWSHint": "Force all active websocket connections to be closed.", "admin.utilities.disconnectWSHint": "Force all active websocket connections to be closed.",
"admin.utilities.disconnectWSSuccess": "All active websocket connections have been terminated.", "admin.utilities.disconnectWSSuccess": "All active websocket connections have been terminated.",
"admin.login.bgUploadSuccess": "Login background image uploaded successfully.", "admin.login.bgUploadSuccess": "Login background image uploaded successfully.",
"admin.login.saveSuccess": "Login configuration saved 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"
} }
...@@ -31,6 +31,7 @@ q-layout(view='hHh Lpr lff') ...@@ -31,6 +31,7 @@ q-layout(view='hHh Lpr lff')
q-item( q-item(
clickable clickable
v-ripple v-ripple
href='/logout'
) )
q-item-section(side) q-item-section(side)
q-icon(name='las la-sign-out-alt', color='negative') q-icon(name='las la-sign-out-alt', color='negative')
...@@ -80,7 +81,7 @@ const sidenav = [ ...@@ -80,7 +81,7 @@ const sidenav = [
}, },
{ {
key: 'password', key: 'password',
label: 'Password', label: 'Authentication',
icon: 'las la-key' icon: 'las la-key'
}, },
{ {
......
...@@ -2,12 +2,12 @@ ...@@ -2,12 +2,12 @@
.auth .auth
.auth-content .auth-content
.auth-logo .auth-logo
img(src='/_site/logo' :alt='siteStore.title') img(:src='`/_site/logo`' :alt='siteStore.title')
h2.auth-site-title(v-if='siteStore.logoText') {{ siteStore.title }} h2.auth-site-title(v-if='siteStore.logoText') {{ siteStore.title }}
p.text-grey-7 Login to continue p.text-grey-7 Login to continue
auth-login-panel auth-login-panel
.auth-bg(aria-hidden="true") .auth-bg(aria-hidden="true")
img(src='/_site/loginbg' alt='') img(:src='`/_site/loginbg`' alt='')
</template> </template>
<script setup> <script setup>
......
...@@ -121,25 +121,27 @@ q-page.q-py-md(:style-fn='pageStyle') ...@@ -121,25 +121,27 @@ q-page.q-py-md(:style-fn='pageStyle')
:options='timeFormats' :options='timeFormats'
) )
q-separator.q-my-sm(inset) q-separator.q-my-sm(inset)
q-item(tag='label', v-ripple) q-item
blueprint-icon(icon='light-on') blueprint-icon(icon='light-on')
q-item-section q-item-section
q-item-label {{t(`profile.darkMode`)}} q-item-label {{t(`profile.appearance`)}}
q-item-label(caption) {{t(`profile.darkModeHint`)}} q-item-label(caption) {{t(`profile.appearanceHint`)}}
q-item-section(avatar) q-item-section.col-auto
q-toggle( q-btn-toggle(
v-model='state.config.darkMode' v-model='state.config.appearance'
color='primary' push
checked-icon='las la-check' glossy
unchecked-icon='las la-times' no-caps
:aria-label='t(`profile.darkMode`)' toggle-color='primary'
:options='appearances'
) )
.actions-bar.q-mt-lg .actions-bar.q-mt-lg
q-btn( q-btn(
icon='las la-check' icon='las la-check'
unelevated unelevated
label='Save Changes' :label='t(`common.actions.saveChanges`)'
color='secondary' color='secondary'
@click='save'
) )
</template> </template>
...@@ -152,6 +154,7 @@ import { onMounted, reactive, watch } from 'vue' ...@@ -152,6 +154,7 @@ import { onMounted, reactive, watch } from 'vue'
import { useSiteStore } from 'src/stores/site' import { useSiteStore } from 'src/stores/site'
import { useDataStore } from 'src/stores/data' import { useDataStore } from 'src/stores/data'
import { useUserStore } from 'src/stores/user'
// QUASAR // QUASAR
...@@ -161,6 +164,7 @@ const $q = useQuasar() ...@@ -161,6 +164,7 @@ const $q = useQuasar()
const siteStore = useSiteStore() const siteStore = useSiteStore()
const dataStore = useDataStore() const dataStore = useDataStore()
const userStore = useUserStore()
// I18N // I18N
...@@ -176,14 +180,15 @@ useMeta({ ...@@ -176,14 +180,15 @@ useMeta({
const state = reactive({ const state = reactive({
config: { config: {
name: 'John Doe', name: '',
email: 'john.doe@company.com', email: '',
location: '', location: '',
jobTitle: '', jobTitle: '',
pronouns: '', pronouns: '',
timezone: '',
dateFormat: '', dateFormat: '',
timeFormat: '12h', timeFormat: '12h',
darkMode: false appearance: 'site'
} }
}) })
...@@ -199,6 +204,11 @@ const timeFormats = [ ...@@ -199,6 +204,11 @@ const timeFormats = [
{ value: '12h', label: t('admin.general.defaultTimeFormat12h') }, { value: '12h', label: t('admin.general.defaultTimeFormat12h') },
{ value: '24h', label: t('admin.general.defaultTimeFormat24h') } { value: '24h', label: t('admin.general.defaultTimeFormat24h') }
] ]
const appearances = [
{ value: 'site', label: t('profile.appearanceDefault') },
{ value: 'light', label: t('profile.appearanceLight') },
{ value: 'dark', label: t('profile.appearanceDark') }
]
// METHODS // METHODS
...@@ -207,4 +217,73 @@ function pageStyle (offset, height) { ...@@ -207,4 +217,73 @@ function pageStyle (offset, height) {
'min-height': `${height - 100 - offset}px` 'min-height': `${height - 100 - offset}px`
} }
} }
async function save () {
$q.loading.show({
message: t('profile.saving')
})
try {
const respRaw = await APOLLO_CLIENT.mutate({
mutation: gql`
mutation saveProfile (
$name: String
$location: String
$jobTitle: String
$pronouns: String
$timezone: String
$dateFormat: String
$timeFormat: String
$appearance: UserSiteAppearance
) {
updateProfile (
name: $name
location: $location
jobTitle: $jobTitle
pronouns: $pronouns
timezone: $timezone
dateFormat: $dateFormat
timeFormat: $timeFormat
appearance: $appearance
) {
operation {
succeeded
message
}
}
}
`,
variables: state.config
})
if (respRaw.data?.updateProfile?.operation?.succeeded) {
$q.notify({
type: 'positive',
message: t('profile.saveSuccess')
})
userStore.$patch(state.config)
} else {
throw new Error(respRaw.data?.updateProfile?.operation?.message || 'An unexpected error occured')
}
} catch (err) {
$q.notify({
type: 'negative',
message: t('profile.saveFailed'),
caption: err.message
})
}
$q.loading.hide()
}
// MOUNTED
onMounted(() => {
state.config.name = userStore.name || ''
state.config.email = userStore.email
state.config.location = userStore.location || ''
state.config.jobTitle = userStore.jobTitle || ''
state.config.pronouns = userStore.pronouns || ''
state.config.timezone = userStore.timezone || Intl.DateTimeFormat().resolvedOptions().timeZone || ''
state.config.dateFormat = userStore.dateFormat || ''
state.config.timeFormat = userStore.timeFormat || '12h'
state.config.appearance = userStore.appearance || 'site'
})
</script> </script>
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import jwtDecode from 'jwt-decode' import jwtDecode from 'jwt-decode'
import Cookies from 'js-cookie' import Cookies from 'js-cookie'
import gql from 'graphql-tag'
import { DateTime } from 'luxon'
export const useUserStore = defineStore('user', { export const useUserStore = defineStore('user', {
state: () => ({ state: () => ({
id: 0, id: '10000000-0000-4000-8000-000000000001',
email: '', email: '',
name: '', name: '',
pictureUrl: '', pictureUrl: '',
localeCode: '', localeCode: '',
defaultEditor: '',
timezone: '', timezone: '',
dateFormat: '', dateFormat: 'YYYY-MM-DD',
appearance: '', timeFormat: '12h',
appearance: 'site',
permissions: [], permissions: [],
iat: 0, iat: 0,
exp: 0, exp: null,
authenticated: false authenticated: false,
token: '',
profileLoaded: false
}), }),
getters: {}, getters: {},
actions: { actions: {
refreshAuth () { async refreshAuth () {
const jwtCookie = Cookies.get('jwt') const jwtCookie = Cookies.get('jwt')
if (jwtCookie) { if (jwtCookie) {
try { try {
const jwtData = jwtDecode(jwtCookie) const jwtData = jwtDecode(jwtCookie)
this.id = jwtData.id this.id = jwtData.id
this.email = jwtData.email this.email = jwtData.email
this.name = jwtData.name
this.pictureUrl = jwtData.av
this.localeCode = jwtData.lc
this.timezone = jwtData.tz || Intl.DateTimeFormat().resolvedOptions().timeZone || ''
this.dateFormat = jwtData.df || ''
this.appearance = jwtData.ap || ''
// this.defaultEditor = jwtData.defaultEditor
this.permissions = jwtData.permissions
this.iat = jwtData.iat this.iat = jwtData.iat
this.exp = jwtData.exp this.exp = DateTime.fromSeconds(jwtData.exp, { zone: 'utc' })
this.authenticated = true this.token = jwtCookie
if (this.exp <= DateTime.utc()) {
console.info('Token has expired. Attempting renew...')
} else {
this.authenticated = true
}
} catch (err) { } catch (err) {
console.debug('Invalid JWT. Silent authentication skipped.') console.debug('Invalid JWT. Silent authentication skipped.')
} }
} }
},
async refreshProfile () {
if (!this.authenticated || !this.id) {
return
}
try {
const respRaw = await APOLLO_CLIENT.query({
query: gql`
query refreshProfile (
$id: UUID!
) {
userById(id: $id) {
id
name
email
meta
prefs
lastLoginAt
groups {
id
name
}
}
}
`,
variables: {
id: this.id
}
})
const resp = respRaw?.data?.userById
if (!resp || resp.id !== this.id) {
throw new Error('Failed to fetch user profile!')
}
this.name = resp.name || 'Unknown User'
this.email = resp.email
this.pictureUrl = (resp.pictureUrl === 'local') ? `/_user/${this.id}/avatar` : resp.pictureUrl
this.location = resp.meta.location || ''
this.jobTitle = resp.meta.jobTitle || ''
this.pronouns = resp.meta.pronouns || ''
this.timezone = resp.prefs.timezone || Intl.DateTimeFormat().resolvedOptions().timeZone || ''
this.dateFormat = resp.prefs.dateFormat || ''
this.timeFormat = resp.prefs.timeFormat || '12h'
this.appearance = resp.prefs.appearance || 'site'
this.profileLoaded = true
} catch (err) {
console.warn(err)
}
} }
} }
}) })
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment