Add paginated users/roles, user status, frontend permissions, profile pictures, identifier file storage
- Users page: paginated, searchable, sortable with inline roles (no N+1) - Roles page: paginated, searchable, sortable + /roles/all for dropdowns - User is_active field with migration, PATCH toggle, auth check (disabled=401) - Frontend permission checks: auth store loads permissions, sidebar/buttons conditional - Profile pictures via file storage for users and members, avatar component - Identifier images use file storage API instead of base64 - Fix TypeScript errors across admin UI - 64 API tests passing (10 new)
This commit is contained in:
@@ -12,16 +12,49 @@ interface AuthState {
|
||||
token: string | null
|
||||
user: User | null
|
||||
companyId: string | null
|
||||
permissions: Set<string>
|
||||
permissionsLoaded: boolean
|
||||
setAuth: (token: string, user: User) => void
|
||||
setPermissions: (slugs: string[]) => void
|
||||
hasPermission: (slug: string) => boolean
|
||||
logout: () => void
|
||||
}
|
||||
|
||||
/**
|
||||
* Permission inheritance: admin implies edit implies view for the same domain.
|
||||
* Must match the backend logic in plugins/auth.ts.
|
||||
*/
|
||||
const ACTION_HIERARCHY: Record<string, string[]> = {
|
||||
admin: ['admin', 'edit', 'view'],
|
||||
edit: ['edit', 'view'],
|
||||
view: ['view'],
|
||||
upload: ['upload'],
|
||||
delete: ['delete'],
|
||||
send: ['send'],
|
||||
export: ['export'],
|
||||
}
|
||||
|
||||
function expandPermissions(slugs: string[]): Set<string> {
|
||||
const expanded = new Set<string>()
|
||||
for (const slug of slugs) {
|
||||
expanded.add(slug)
|
||||
const [domain, action] = slug.split('.')
|
||||
const implied = ACTION_HIERARCHY[action]
|
||||
if (implied && domain) {
|
||||
for (const a of implied) {
|
||||
expanded.add(`${domain}.${a}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
return expanded
|
||||
}
|
||||
|
||||
function decodeJwtPayload(token: string): { id: string; companyId: string; role: string } {
|
||||
const payload = token.split('.')[1]
|
||||
return JSON.parse(atob(payload))
|
||||
}
|
||||
|
||||
function loadSession(): { token: string; user: User; companyId: string } | null {
|
||||
function loadSession(): { token: string; user: User; companyId: string; permissions?: string[] } | null {
|
||||
try {
|
||||
const raw = sessionStorage.getItem('forte-auth')
|
||||
if (!raw) return null
|
||||
@@ -31,20 +64,23 @@ function loadSession(): { token: string; user: User; companyId: string } | null
|
||||
}
|
||||
}
|
||||
|
||||
function saveSession(token: string, user: User, companyId: string) {
|
||||
sessionStorage.setItem('forte-auth', JSON.stringify({ token, user, companyId }))
|
||||
function saveSession(token: string, user: User, companyId: string, permissions?: string[]) {
|
||||
sessionStorage.setItem('forte-auth', JSON.stringify({ token, user, companyId, permissions }))
|
||||
}
|
||||
|
||||
function clearSession() {
|
||||
sessionStorage.removeItem('forte-auth')
|
||||
}
|
||||
|
||||
export const useAuthStore = create<AuthState>((set) => {
|
||||
export const useAuthStore = create<AuthState>((set, get) => {
|
||||
const initial = typeof window !== 'undefined' ? loadSession() : null
|
||||
const initialPerms = initial?.permissions ? expandPermissions(initial.permissions) : new Set<string>()
|
||||
return {
|
||||
token: initial?.token ?? null,
|
||||
user: initial?.user ?? null,
|
||||
companyId: initial?.companyId ?? null,
|
||||
permissions: initialPerms,
|
||||
permissionsLoaded: initialPerms.size > 0,
|
||||
|
||||
setAuth: (token, user) => {
|
||||
const payload = decodeJwtPayload(token)
|
||||
@@ -52,8 +88,22 @@ export const useAuthStore = create<AuthState>((set) => {
|
||||
set({ token, user, companyId: payload.companyId })
|
||||
},
|
||||
|
||||
setPermissions: (slugs: string[]) => {
|
||||
const expanded = expandPermissions(slugs)
|
||||
// Update session storage to include permissions
|
||||
const { token, user, companyId } = get()
|
||||
if (token && user && companyId) {
|
||||
saveSession(token, user, companyId, slugs)
|
||||
}
|
||||
set({ permissions: expanded, permissionsLoaded: true })
|
||||
},
|
||||
|
||||
hasPermission: (slug: string) => {
|
||||
return get().permissions.has(slug)
|
||||
},
|
||||
|
||||
logout: () => {
|
||||
clearSession()
|
||||
set({ token: null, user: null, companyId: null })
|
||||
set({ token: null, user: null, companyId: null, permissions: new Set(), permissionsLoaded: false })
|
||||
},
|
||||
}})
|
||||
|
||||
Reference in New Issue
Block a user