mirror of https://github.com/requarks/wiki
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
205 lines
6.6 KiB
205 lines
6.6 KiB
import { defineStore } from 'pinia'
|
|
|
|
import { getAccessibleColor } from '@/helpers/accessibility'
|
|
|
|
import { useSiteStore } from './site'
|
|
|
|
const pad = (value) => String(value).padStart(2, '0')
|
|
|
|
/**
|
|
* Render the date part of a moment the way the user asked for it.
|
|
*
|
|
* The stored preference is one of a handful of explicit patterns, or an empty string meaning "whatever
|
|
* this locale does" — which is the only case a formatter can be left to decide on its own.
|
|
*/
|
|
function formatDatePart (zoned, dateFormat) {
|
|
switch (dateFormat) {
|
|
case 'DD/MM/YYYY':
|
|
return `${pad(zoned.day)}/${pad(zoned.month)}/${zoned.year}`
|
|
case 'DD.MM.YYYY':
|
|
return `${pad(zoned.day)}.${pad(zoned.month)}.${zoned.year}`
|
|
case 'MM/DD/YYYY':
|
|
return `${pad(zoned.month)}/${pad(zoned.day)}/${zoned.year}`
|
|
case 'YYYY-MM-DD':
|
|
return `${zoned.year}-${pad(zoned.month)}-${pad(zoned.day)}`
|
|
case 'YYYY/MM/DD':
|
|
return `${zoned.year}/${pad(zoned.month)}/${pad(zoned.day)}`
|
|
default:
|
|
// -> Numeric parts rather than `dateStyle: 'short'`, which abbreviates the year to two digits
|
|
return zoned.toLocaleString(undefined, { year: 'numeric', month: 'numeric', day: 'numeric' })
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Render the time part. `hourCycle` rather than `hour12: false`, which some locales render as 24:00
|
|
* where they mean 00:00.
|
|
*/
|
|
function formatTimePart (zoned, timeFormat) {
|
|
return zoned.toLocaleString(
|
|
undefined,
|
|
timeFormat === '24h'
|
|
? { hour: '2-digit', minute: '2-digit', hourCycle: 'h23' }
|
|
: { hour: 'numeric', minute: '2-digit', hour12: true }
|
|
)
|
|
}
|
|
|
|
export const useUserStore = defineStore('user', {
|
|
state: () => ({
|
|
id: '10000000-0000-4000-8000-000000000001',
|
|
email: '',
|
|
name: '',
|
|
hasAvatar: false,
|
|
localeCode: '',
|
|
timezone: '',
|
|
dateFormat: 'YYYY-MM-DD',
|
|
timeFormat: '12h',
|
|
appearance: 'site',
|
|
cvd: 'none',
|
|
permissions: [],
|
|
pagePermissions: [],
|
|
authenticated: false,
|
|
profileLoaded: false
|
|
}),
|
|
getters: {
|
|
// -> Luxon format tokens, for the call sites that still format dates with luxon themselves. They
|
|
// retire with the last of those; `formatDateTime()` no longer goes through them.
|
|
preferredDateFormat: (state) => {
|
|
if (!state.dateFormat) {
|
|
return 'D'
|
|
} else {
|
|
return state.dateFormat.replaceAll('Y', 'y').replaceAll('D', 'd')
|
|
}
|
|
},
|
|
preferredTimeFormat: (state) => {
|
|
return state.timeFormat === '24h' ? 'T' : 't'
|
|
}
|
|
},
|
|
actions: {
|
|
async refreshProfile() {
|
|
try {
|
|
const resp = await API_CLIENT.get('users/whoami', {
|
|
cache: 'no-store'
|
|
}).json()
|
|
if (!resp || !resp.authenticated) {
|
|
this.setToGuest()
|
|
} else {
|
|
this.$patch({
|
|
name: resp.name || 'Unknown User',
|
|
email: resp.email,
|
|
hasAvatar: resp.hasAvatar ?? false,
|
|
location: resp.location || '',
|
|
jobTitle: resp.jobTitle || '',
|
|
pronouns: resp.pronouns || '',
|
|
timezone: resp.timezone || Intl.DateTimeFormat().resolvedOptions().timeZone || '',
|
|
dateFormat: resp.dateFormat || '',
|
|
timeFormat: resp.timeFormat || '12h',
|
|
appearance: resp.appearance || 'site',
|
|
cvd: resp.cvd || 'none',
|
|
permissions: resp.permissions || [],
|
|
authenticated: true,
|
|
profileLoaded: true
|
|
})
|
|
}
|
|
} catch (err) {
|
|
console.warn(err)
|
|
}
|
|
},
|
|
async logout() {
|
|
const siteStore = useSiteStore()
|
|
let redirect = '/'
|
|
try {
|
|
const resp = await API_CLIENT.post(`sites/${siteStore.id}/auth/logout`).json()
|
|
redirect = resp?.redirect || '/'
|
|
} catch (err) {
|
|
// -> Clear the client either way. Whatever went wrong, someone who clicked Logout must not be
|
|
// left looking at a page that still says they are signed in.
|
|
console.warn(err)
|
|
}
|
|
this.setToGuest()
|
|
EVENT_BUS.emit('logout', { redirect })
|
|
},
|
|
setToGuest() {
|
|
this.$patch({
|
|
id: '10000000-0000-4000-8000-000000000001',
|
|
email: '',
|
|
name: '',
|
|
hasAvatar: false,
|
|
timezone: '',
|
|
dateFormat: 'YYYY-MM-DD',
|
|
timeFormat: '12h',
|
|
appearance: 'site',
|
|
cvd: 'none',
|
|
permissions: [],
|
|
// -> Page permissions are only refetched on the next navigation, so leaving them would keep
|
|
// edit buttons on screen for a user who is no longer logged in
|
|
pagePermissions: [],
|
|
authenticated: false,
|
|
profileLoaded: false
|
|
})
|
|
},
|
|
getAccessibleColor(base, hexBase) {
|
|
return getAccessibleColor(base, hexBase, this.cvd)
|
|
},
|
|
can(permission) {
|
|
if (
|
|
this.permissions.includes('manage:system') ||
|
|
this.permissions.includes(permission) ||
|
|
this.pagePermissions.includes(permission)
|
|
) {
|
|
return true
|
|
}
|
|
return false
|
|
},
|
|
async fetchPagePermissions(path) {
|
|
if (path.startsWith('/_')) {
|
|
this.pagePermissions = []
|
|
return
|
|
}
|
|
const siteStore = useSiteStore()
|
|
try {
|
|
this.pagePermissions = await API_CLIENT.post(
|
|
`sites/${siteStore.id}/pages/userPermissions`,
|
|
{
|
|
json: {
|
|
path
|
|
}
|
|
}
|
|
).json()
|
|
} catch (err) {
|
|
console.warn(`Failed to fetch page permissions at path ${path}!`)
|
|
}
|
|
},
|
|
/**
|
|
* Format a moment as this user asked to see it: their date pattern, their 12h/24h choice, and their
|
|
* time zone. Word order comes from the locale, which is why `t` is passed in.
|
|
*
|
|
* @param date A `Temporal.Instant`, a `Date`, or a string one can be parsed from — what the API
|
|
* returns. Nullable columns like `lastLoginAt` are common, so nothing at all formats as
|
|
* an empty string rather than blowing up mid-render.
|
|
*/
|
|
formatDateTime(t, date) {
|
|
if (!date) {
|
|
return ''
|
|
}
|
|
let instant = date
|
|
if (typeof date === 'string') {
|
|
instant = Temporal.Instant.from(date)
|
|
} else if (date instanceof Date) {
|
|
instant = date.toTemporalInstant()
|
|
}
|
|
// -> A preference set before the zone list changed, or none at all, falls back to this browser's
|
|
// zone rather than throwing in the middle of a table
|
|
let zoned
|
|
try {
|
|
zoned = instant.toZonedDateTimeISO(this.timezone || Temporal.Now.timeZoneId())
|
|
} catch {
|
|
zoned = instant.toZonedDateTimeISO(Temporal.Now.timeZoneId())
|
|
}
|
|
return t('common.datetime', {
|
|
date: formatDatePart(zoned, this.dateFormat),
|
|
time: formatTimePart(zoned, this.timeFormat)
|
|
})
|
|
}
|
|
}
|
|
})
|