Spaces:
Sleeping
Sleeping
File size: 5,583 Bytes
57a889c | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 | import { db } from '../db/database';
/**
* Permission levels (hierarchical, higher includes lower):
* admin > trip_owner > trip_member > everybody
*
* "everybody" means any authenticated user with trip access.
* For trip_create, "everybody" means any authenticated user (no trip context).
*/
export type PermissionLevel = 'admin' | 'trip_owner' | 'trip_member' | 'everybody';
export interface PermissionAction {
key: string;
defaultLevel: PermissionLevel;
allowedLevels: PermissionLevel[];
}
// All configurable actions with their defaults matching upstream behavior
export const PERMISSION_ACTIONS: PermissionAction[] = [
// Trip management
{ key: 'trip_create', defaultLevel: 'everybody', allowedLevels: ['admin', 'everybody'] },
{ key: 'trip_edit', defaultLevel: 'trip_owner', allowedLevels: ['trip_owner', 'trip_member'] },
{ key: 'trip_delete', defaultLevel: 'trip_owner', allowedLevels: ['admin', 'trip_owner'] },
{ key: 'trip_archive', defaultLevel: 'trip_owner', allowedLevels: ['trip_owner', 'trip_member'] },
{ key: 'trip_cover_upload', defaultLevel: 'trip_owner', allowedLevels: ['trip_owner', 'trip_member'] },
// Member management
{ key: 'member_manage', defaultLevel: 'trip_owner', allowedLevels: ['admin', 'trip_owner', 'trip_member'] },
// Files
{ key: 'file_upload', defaultLevel: 'trip_member', allowedLevels: ['admin', 'trip_owner', 'trip_member'] },
{ key: 'file_edit', defaultLevel: 'trip_member', allowedLevels: ['trip_owner', 'trip_member'] },
{ key: 'file_delete', defaultLevel: 'trip_member', allowedLevels: ['trip_owner', 'trip_member'] },
// Places
{ key: 'place_edit', defaultLevel: 'trip_member', allowedLevels: ['trip_owner', 'trip_member'] },
// Budget
{ key: 'budget_edit', defaultLevel: 'trip_member', allowedLevels: ['trip_owner', 'trip_member'] },
// Packing
{ key: 'packing_edit', defaultLevel: 'trip_member', allowedLevels: ['trip_owner', 'trip_member'] },
// Reservations
{ key: 'reservation_edit', defaultLevel: 'trip_member', allowedLevels: ['trip_owner', 'trip_member'] },
// Day notes & schedule
{ key: 'day_edit', defaultLevel: 'trip_member', allowedLevels: ['trip_owner', 'trip_member'] },
// Collaboration (notes, polls, messages)
{ key: 'collab_edit', defaultLevel: 'trip_member', allowedLevels: ['trip_owner', 'trip_member'] },
// Share link management
{ key: 'share_manage', defaultLevel: 'trip_owner', allowedLevels: ['trip_owner', 'trip_member'] },
];
const ACTIONS_MAP = new Map(PERMISSION_ACTIONS.map(a => [a.key, a]));
// In-memory cache, invalidated on save
let cache: Map<string, PermissionLevel> | null = null;
function loadPermissions(): Map<string, PermissionLevel> {
if (cache) return cache;
cache = new Map<string, PermissionLevel>();
try {
const rows = db.prepare("SELECT key, value FROM app_settings WHERE key LIKE 'perm_%'").all() as { key: string; value: string }[];
for (const row of rows) {
const actionKey = row.key.replace('perm_', '');
if (ACTIONS_MAP.has(actionKey)) {
cache.set(actionKey, row.value as PermissionLevel);
}
}
} catch { /* table might not exist yet during init */ }
return cache;
}
export function invalidatePermissionsCache(): void {
cache = null;
}
export function getPermissionLevel(actionKey: string): PermissionLevel {
const perms = loadPermissions();
const stored = perms.get(actionKey);
if (stored) return stored;
const action = ACTIONS_MAP.get(actionKey);
return action?.defaultLevel ?? 'trip_owner';
}
export function getAllPermissions(): Record<string, PermissionLevel> {
const perms = loadPermissions();
const result: Record<string, PermissionLevel> = {};
for (const action of PERMISSION_ACTIONS) {
result[action.key] = perms.get(action.key) ?? action.defaultLevel;
}
return result;
}
export function savePermissions(settings: Record<string, string>): { skipped: string[] } {
const skipped: string[] = [];
const upsert = db.prepare("INSERT OR REPLACE INTO app_settings (key, value) VALUES (?, ?)");
const txn = db.transaction(() => {
for (const [actionKey, level] of Object.entries(settings)) {
const action = ACTIONS_MAP.get(actionKey);
if (!action || !action.allowedLevels.includes(level as PermissionLevel)) {
skipped.push(actionKey);
continue;
}
upsert.run(`perm_${actionKey}`, level);
}
});
txn();
invalidatePermissionsCache();
return { skipped };
}
/**
* Check if a user passes the permission check for a given action.
*
* @param actionKey - The permission action key
* @param userRole - 'admin' | 'user'
* @param tripUserId - The trip owner's user ID (null for non-trip actions like trip_create)
* @param userId - The requesting user's ID
* @param isMember - Whether the user is a trip member (not owner)
*/
export function checkPermission(
actionKey: string,
userRole: string,
tripUserId: number | null,
userId: number,
isMember: boolean
): boolean {
// Admins always pass
if (userRole === 'admin') return true;
const required = getPermissionLevel(actionKey);
switch (required) {
case 'admin':
return false; // already checked above
case 'trip_owner':
return tripUserId !== null && tripUserId === userId;
case 'trip_member':
return (tripUserId !== null && tripUserId === userId) || isMember;
case 'everybody':
return true;
default:
return false;
}
}
|