mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-06-21 06:11:45 +00:00
feat: add configurable permissions system with admin panel
Adds a full permissions management feature allowing admins to control who can perform actions across the app (trip CRUD, files, places, budget, packing, reservations, collab, members, share links). - New server/src/services/permissions.ts: 16 configurable actions, in-memory cache, checkPermission() helper, backwards-compatible defaults matching upstream behaviour - GET/PUT /admin/permissions endpoints; permissions loaded into app-config response so clients have them on startup - checkPermission() applied to all mutating route handlers across 10 server route files; getTripOwnerId() helper eliminates repeated inline DB queries; trips.ts and files.ts now reuse canAccessTrip() result to avoid redundant DB round-trips - New client/src/store/permissionsStore.ts: Zustand store + useCanDo() hook; TripOwnerContext type accepts both Trip and DashboardTrip shapes without casting at call sites - New client/src/components/Admin/PermissionsPanel.tsx: categorised UI with per-action dropdowns, customised badge, save/reset - AdminPage, DashboardPage, FileManager, PlacesSidebar, TripMembersModal gated via useCanDo(); no prop drilling - 46 perm.* translation keys added to all 12 language files
This commit is contained in:
@@ -0,0 +1,52 @@
|
||||
import { create } from 'zustand'
|
||||
import { useAuthStore } from './authStore'
|
||||
|
||||
export type PermissionLevel = 'admin' | 'trip_owner' | 'trip_member' | 'everybody'
|
||||
|
||||
/** Minimal trip shape used by permission checks — accepts both Trip and DashboardTrip */
|
||||
type TripOwnerContext = { user_id?: unknown; owner_id?: unknown; is_owner?: unknown }
|
||||
|
||||
interface PermissionsState {
|
||||
permissions: Record<string, PermissionLevel>
|
||||
setPermissions: (perms: Record<string, PermissionLevel>) => void
|
||||
}
|
||||
|
||||
export const usePermissionsStore = create<PermissionsState>((set) => ({
|
||||
permissions: {},
|
||||
setPermissions: (perms) => set({ permissions: perms }),
|
||||
}))
|
||||
|
||||
/**
|
||||
* Hook that returns a permission checker bound to the current user.
|
||||
* Usage: const can = useCanDo(); can('trip_create') or can('file_upload', trip)
|
||||
*/
|
||||
export function useCanDo() {
|
||||
const perms = usePermissionsStore((s: PermissionsState) => s.permissions)
|
||||
const user = useAuthStore((s) => s.user)
|
||||
|
||||
return function can(
|
||||
actionKey: string,
|
||||
trip?: TripOwnerContext | null,
|
||||
): boolean {
|
||||
if (!user) return false
|
||||
if (user.role === 'admin') return true
|
||||
|
||||
const level = perms[actionKey]
|
||||
if (!level) return true // not configured = allow
|
||||
|
||||
// Support both Trip (owner_id) and DashboardTrip/server response (user_id)
|
||||
const tripOwnerId = (trip?.user_id as number | undefined) ?? (trip?.owner_id as number | undefined) ?? null
|
||||
const isOwnerFlag = trip?.is_owner === true || trip?.is_owner === 1
|
||||
const isOwner = isOwnerFlag || (tripOwnerId !== null && tripOwnerId === user.id)
|
||||
const isMember = !isOwner && trip != null
|
||||
|
||||
switch (level) {
|
||||
case 'admin': return false
|
||||
case 'trip_owner': return isOwner
|
||||
case 'trip_member': return isOwner || isMember
|
||||
case 'everybody': return true
|
||||
default: return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user