The lobby-display and screensaver slide manager was admin-only. Add a shared slides.manage permission so a curator can manage both surfaces without full admin. Admins keep access via the require_permission admin bypass. Backend: - plugins/slides/api/routes.py: all 5 management routes require slides.manage - plugins/slides/plugin.py: declare it via get_permissions(); nav item carries the permission so the frontend can gate visibility - shopdb/core/api/auth.py: login response now returns the user's permissions (matches /me) so the frontend authStore has them on fresh login Frontend: - stores/auth.js: hasPermission(name) getter (admin true, else granted list) - router/index.js: guard supports requiresPermission - views/AppLayout.vue: hide nav items whose permission the user lacks - plugins/slides/frontend/routes.js: slide manager gated requiresPermission Tests: no-perm user 403, curator role with the perm 200 (+ login advertises it), admin 200 via bypass. Deploy: run `flask seed permissions` to create the row, then grant it to a role in Settings > Users & Roles.
107 lines
3.8 KiB
JavaScript
107 lines
3.8 KiB
JavaScript
import { defineStore } from 'pinia'
|
|
import { authApi, employeesApi } from '../api'
|
|
|
|
export const useAuthStore = defineStore('auth', {
|
|
state: () => ({
|
|
user: JSON.parse(localStorage.getItem('user') || 'null'),
|
|
token: localStorage.getItem('token') || null
|
|
}),
|
|
|
|
getters: {
|
|
isAuthenticated: (state) => !!state.token,
|
|
username: (state) => state.user?.username || '',
|
|
roles: (state) => state.user?.roles || [],
|
|
hasRole: (state) => (role) => state.user?.roles?.includes(role) || false,
|
|
isAdmin: (state) => state.user?.roles?.includes('admin') || false,
|
|
// True if the user holds a named permission. Admins hold every permission
|
|
// (mirrors the backend require_permission admin bypass), so they pass
|
|
// regardless of the permissions list. Non-admins check their granted list.
|
|
hasPermission: (state) => (name) =>
|
|
state.user?.roles?.includes('admin')
|
|
|| state.user?.permissions?.includes(name)
|
|
|| false,
|
|
// True when an admin-set temporary password must be changed before use.
|
|
mustChangePassword: (state) => !!state.user?.mustchangepassword,
|
|
// Full name from the employee directory (falls back to username/SSO).
|
|
displayName: (state) => state.user?.directoryname || state.user?.username || '',
|
|
// Employee photo URL if the directory has one for this SSO. The directory
|
|
// resolver already returns a usable URL (self-hosted upload or external HR
|
|
// path), so it is used as-is.
|
|
avatarUrl: (state) => state.user?.directoryphotourl || null
|
|
},
|
|
|
|
actions: {
|
|
async login(username, password) {
|
|
try {
|
|
const response = await authApi.login(username, password)
|
|
const { access_token, refresh_token, user } = response.data.data
|
|
|
|
this.token = access_token
|
|
this.user = user
|
|
|
|
this.token = access_token
|
|
localStorage.setItem('token', access_token)
|
|
localStorage.setItem('refreshToken', refresh_token)
|
|
await this.enrichFromDirectory()
|
|
|
|
return { success: true }
|
|
} catch (error) {
|
|
const message = error.response?.data?.message || 'Login failed'
|
|
return { success: false, message }
|
|
}
|
|
},
|
|
|
|
async logout() {
|
|
try {
|
|
await authApi.logout()
|
|
} catch (e) {
|
|
// Ignore logout errors
|
|
}
|
|
|
|
this.token = null
|
|
this.user = null
|
|
|
|
localStorage.removeItem('token')
|
|
localStorage.removeItem('refreshToken')
|
|
localStorage.removeItem('user')
|
|
},
|
|
|
|
// Clear the forced-password-change flag after a successful change so the
|
|
// router guard stops steering the user to the change-password view.
|
|
clearMustChangePassword() {
|
|
if (this.user) {
|
|
this.user.mustchangepassword = false
|
|
localStorage.setItem('user', JSON.stringify(this.user))
|
|
}
|
|
},
|
|
|
|
async fetchUser() {
|
|
try {
|
|
const response = await authApi.me()
|
|
this.user = response.data.data
|
|
await this.enrichFromDirectory()
|
|
} catch (error) {
|
|
this.logout()
|
|
}
|
|
},
|
|
|
|
// Pull the logged-in user's full name + photo from the employee directory
|
|
// by their SSO (= username). Best-effort: skipped/ignored when the employees
|
|
// plugin is off, the SSO is not in the directory, or the lookup fails.
|
|
async enrichFromDirectory() {
|
|
const sso = this.user?.username
|
|
if (sso && /^\d+$/.test(sso)) {
|
|
try {
|
|
const response = await employeesApi.lookup(sso)
|
|
const emp = response.data?.data
|
|
if (emp) {
|
|
this.user.directoryname = `${emp.First_Name || ''} ${emp.Last_Name || ''}`.trim() || null
|
|
this.user.directoryphotourl = emp.photourl || null
|
|
}
|
|
} catch (err) { /* directory unavailable - fall back to username */ }
|
|
}
|
|
localStorage.setItem('user', JSON.stringify(this.user))
|
|
}
|
|
}
|
|
})
|