Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
W
wiki-js
Project
Project
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
1
Issues
1
List
Board
Labels
Milestones
Merge Requests
1
Merge Requests
1
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Registry
Registry
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
Jacklull
wiki-js
Commits
55b0b00c
You need to sign in or sign up before continuing.
Unverified
Commit
55b0b00c
authored
Sep 12, 2022
by
Nicolas Giard
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat: update profile + user theme
parent
8e87a5d4
Show whitespace changes
Inline
Side-by-side
Showing
15 changed files
with
309 additions
and
142 deletions
+309
-142
3.0.0.js
server/db/migrations/3.0.0.js
+2
-2
user.js
server/graph/resolvers/user.js
+49
-41
user.graphql
server/graph/schemas/user.graphql
+16
-29
users.js
server/models/users.js
+1
-1
quasar.config.js
ux/quasar.config.js
+6
-6
App.vue
ux/src/App.vue
+27
-2
apollo.js
ux/src/boot/apollo.js
+2
-2
AccountMenu.vue
ux/src/components/AccountMenu.vue
+7
-13
HeaderNav.vue
ux/src/components/HeaderNav.vue
+2
-2
UserEditOverlay.vue
ux/src/components/UserEditOverlay.vue
+27
-9
en.json
ux/src/i18n/locales/en.json
+9
-2
ProfileLayout.vue
ux/src/layouts/ProfileLayout.vue
+2
-1
Login.vue
ux/src/pages/Login.vue
+2
-2
Profile.vue
ux/src/pages/Profile.vue
+93
-14
user.js
ux/src/stores/user.js
+64
-16
No files found.
server/db/migrations/3.0.0.js
View file @
55b0b00c
...
@@ -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'
}
}
...
...
server/graph/resolvers/user.js
View file @
55b0b00c
...
@@ -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)
// }
// }
}
}
server/graph/schemas/user.graphql
View file @
55b0b00c
...
@@ -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
...
...
server/models/users.js
View file @
55b0b00c
...
@@ -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'
},
...
...
ux/quasar.config.js
View file @
55b0b00c
...
@@ -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: {},
...
...
ux/src/App.vue
View file @
55b0b00c
...
@@ -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
()
{
if
(
userStore
.
appearance
===
'site'
)
{
$q
.
dark
.
set
(
siteStore
.
theme
.
dark
)
$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
)
{
...
...
ux/src/boot/apollo.js
View file @
55b0b00c
...
@@ -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
,
...
...
ux/src/components/AccountMenu.vue
View file @
55b0b00c
<
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
>
ux/src/components/HeaderNav.vue
View file @
55b0b00c
...
@@ -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
}}
...
...
ux/src/components/UserEditOverlay.vue
View file @
55b0b00c
...
@@ -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.
darkMod
e`
)
}}
q-item-label
{{
t
(
`admin.users.
appearanc
e`
)
}}
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
...
...
ux/src/i18n/locales/en.json
View file @
55b0b00c
...
@@ -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"
}
}
ux/src/layouts/ProfileLayout.vue
View file @
55b0b00c
...
@@ -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'
},
},
{
{
...
...
ux/src/pages/Login.vue
View file @
55b0b00c
...
@@ -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
>
...
...
ux/src/pages/Profile.vue
View file @
55b0b00c
...
@@ -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
>
ux/src/stores/user.js
View file @
55b0b00c
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
.
token
=
jwtCookie
if
(
this
.
exp
<=
DateTime
.
utc
())
{
console
.
info
(
'Token has expired. Attempting renew...'
)
}
else
{
this
.
authenticated
=
true
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
)
}
}
}
}
}
})
})
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment