fix: restore complete admin feature dependencies
This commit is contained in:
1 parent
5b4228261a
commit
0cd753c735
115 files changed
+18565
-4930
No files matched your search
@@ -0,0 +1,193 @@
|
||||
import { unstable_cache } from 'next/cache'
|
||||
import { redirect } from 'next/navigation'
|
||||
import { cache } from 'react'
|
||||
import { auth } from './auth'
|
||||
import { sessionUserId } from './auth/session-user'
|
||||
import { prisma } from './prisma'
|
||||
|
||||
// Re-export PERMS from the standalone file (safe for client components)
|
||||
export { PERMS } from './permission-slugs'
|
||||
|
||||
import { PERMS } from './permission-slugs'
|
||||
|
||||
// ── Permission Set ──────────────────────────────────────────────────
|
||||
|
||||
export type { PermissionSet } from '@/types/admin'
|
||||
|
||||
import type { PermissionSet } from '@/types/admin'
|
||||
|
||||
function createEmptySet(): PermissionSet {
|
||||
return {
|
||||
has: () => false,
|
||||
hasAny: () => false,
|
||||
hasAll: () => false,
|
||||
isSuperAdmin: false,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch all permission slugs for a user in a single SQL query.
|
||||
* Cached via unstable_cache with 60s TTL — invalidated via revalidateTag('permissions').
|
||||
*/
|
||||
const getCachedPermissionSlugs = unstable_cache(
|
||||
async (userId: number, rank: number): Promise<string[]> => {
|
||||
const rows = await prisma.$queryRaw<{ slug: string }[]>`
|
||||
SELECT DISTINCT p.slug
|
||||
FROM acl_model_permissions mp
|
||||
JOIN acl_permissions p ON p.id = mp.permission_id
|
||||
WHERE mp.model_type = 'Role'
|
||||
AND mp.model_id IN (
|
||||
SELECT amr.role_id
|
||||
FROM acl_model_roles amr
|
||||
WHERE amr.model_type = 'User' AND amr.model_id = ${userId}
|
||||
UNION
|
||||
SELECT ar.id
|
||||
FROM acl_roles ar
|
||||
WHERE ar.slug = ${`rank_${rank}`}
|
||||
)
|
||||
`
|
||||
return rows.map((r) => r.slug)
|
||||
},
|
||||
['user-permissions'],
|
||||
{ revalidate: 60, tags: ['permissions'] },
|
||||
)
|
||||
|
||||
/**
|
||||
* Load permission slugs for a user. Rank is taken from the JWT session
|
||||
* to avoid an extra DB query. Super admin (rank >= 7) bypasses all checks.
|
||||
* Wrapped with React cache() to de-duplicate within the same request.
|
||||
*/
|
||||
export const loadUserPermissions = cache(async function loadUserPermissions(
|
||||
userId: number,
|
||||
rank: number,
|
||||
): Promise<PermissionSet> {
|
||||
try {
|
||||
// Super admin bypasses all permission checks — zero DB queries
|
||||
if (rank >= 7) {
|
||||
return {
|
||||
has: () => true,
|
||||
hasAny: () => true,
|
||||
hasAll: () => true,
|
||||
isSuperAdmin: true,
|
||||
}
|
||||
}
|
||||
|
||||
const slugArray = await getCachedPermissionSlugs(userId, rank)
|
||||
if (slugArray.length === 0) return createEmptySet()
|
||||
|
||||
const slugs = new Set(slugArray)
|
||||
return {
|
||||
has: (perm: string) => slugs.has(perm),
|
||||
hasAny: (...perms: string[]) => perms.some((p) => slugs.has(p)),
|
||||
hasAll: (...perms: string[]) => perms.every((p) => slugs.has(p)),
|
||||
isSuperAdmin: false,
|
||||
}
|
||||
} catch {
|
||||
// Fail-closed: return empty set on any error
|
||||
return createEmptySet()
|
||||
}
|
||||
})
|
||||
|
||||
// ── Context Helpers ─────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* For server components: get session + load permissions.
|
||||
* Redirects to login if not authenticated.
|
||||
*/
|
||||
export async function getAdminContext() {
|
||||
const session = await auth()
|
||||
if (!session?.user) {
|
||||
redirect('/login')
|
||||
}
|
||||
|
||||
const userId = sessionUserId(session.user.id)
|
||||
if (!userId) redirect('/login')
|
||||
const permissions = await loadUserPermissions(userId, session.user.rank)
|
||||
return { session: { ...session, user: { ...session.user, id: userId } }, permissions }
|
||||
}
|
||||
|
||||
/**
|
||||
* For API routes: get session + load permissions.
|
||||
* Returns null if not authenticated (caller handles 401).
|
||||
*/
|
||||
export async function getApiAdminContext() {
|
||||
const session = await auth()
|
||||
if (!session?.user) return null
|
||||
|
||||
const userId = sessionUserId(session.user.id)
|
||||
if (!userId) return null
|
||||
const permissions = await loadUserPermissions(userId, session.user.rank)
|
||||
return { session: { ...session, user: { ...session.user, id: userId } }, permissions }
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user has a specific permission.
|
||||
* Super admins (rank >= 7) bypass all checks via PermissionSet.isSuperAdmin.
|
||||
* Rank >= 6 gets fallback access to admin dashboard and admin .view permissions.
|
||||
* Rank >= 3 gets fallback access to mod dashboard and mod .view permissions.
|
||||
*/
|
||||
export function canAccess(permissions: PermissionSet, slug: string, rank: number): boolean {
|
||||
if (permissions.has(slug)) return true
|
||||
// Rank 6+ fallback: admin dashboard and admin view-only permissions
|
||||
if (
|
||||
rank >= 6 &&
|
||||
(slug === PERMS.ADMIN_DASHBOARD || (slug.startsWith('admin.') && slug.endsWith('.view')))
|
||||
)
|
||||
return true
|
||||
// Rank 3+ fallback: mod dashboard and mod view-only permissions + mod actions
|
||||
if (
|
||||
rank >= 3 &&
|
||||
(slug === PERMS.MOD_DASHBOARD ||
|
||||
(slug.startsWith('mod.') && slug.endsWith('.view')) ||
|
||||
slug === PERMS.MOD_ACTIONS)
|
||||
)
|
||||
return true
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* For mod panel server components: get session + load permissions.
|
||||
* Redirects to login if not authenticated, to / if rank < 3.
|
||||
*/
|
||||
export async function getModContext() {
|
||||
const session = await auth()
|
||||
if (!session?.user) {
|
||||
redirect('/login')
|
||||
}
|
||||
|
||||
if (session.user.rank < 3) {
|
||||
redirect('/')
|
||||
}
|
||||
|
||||
const userId = sessionUserId(session.user.id)
|
||||
if (!userId) redirect('/login')
|
||||
const permissions = await loadUserPermissions(userId, session.user.rank)
|
||||
return { session: { ...session, user: { ...session.user, id: userId } }, permissions }
|
||||
}
|
||||
|
||||
// ── Legacy single-check functions (kept for backward compatibility) ──
|
||||
|
||||
/** Check if a user has a CMS permission. Rank >= 7 bypasses all checks. */
|
||||
export async function checkPermission(
|
||||
userId: number,
|
||||
rank: number,
|
||||
permission: string,
|
||||
): Promise<boolean> {
|
||||
const perms = await loadUserPermissions(userId, rank)
|
||||
return perms.has(permission)
|
||||
}
|
||||
|
||||
/** Check multiple permissions (user needs ALL of them) */
|
||||
export async function checkAllPermissions(
|
||||
userId: number,
|
||||
rank: number,
|
||||
permissions: string[],
|
||||
): Promise<boolean> {
|
||||
const perms = await loadUserPermissions(userId, rank)
|
||||
return perms.hasAll(...permissions)
|
||||
}
|
||||
|
||||
/** Check if user has admin access */
|
||||
export async function hasAdminAccess(userId: number, rank: number): Promise<boolean> {
|
||||
return checkPermission(userId, rank, PERMS.ADMIN_DASHBOARD)
|
||||
}
|
||||
Reference in new issue
Block a user