Files
hackerspace.sh/lib/certifications-guard.ts
T
Moheeb Zara f23abb0050 feat(certifications): server actions + page guard
certifications.ts: create/update/delete/list cert types (gated by
certifications.manage; delete refused once granted, archive instead),
and grant/revoke/renew/listMemberCertifications/getMyCertifications
(gated by certifications.grant, the Instructor capability). Grant
verifies cert and target member are in-space and the cert is active,
computes expiry from validity_months unless overridden, and relies on
the partial unique index for one active grant per member+cert. Revoke
is soft; nothing is ever deleted. getMyCertifications takes no params
(member resolved from session; RLS restricts to own rows).

certifications-guard.ts mirrors the forms guard for the admin pages.
Barrel re-export and API_REFERENCE updated.
2026-05-16 18:55:17 -07:00

32 lines
1.1 KiB
TypeScript

import { redirect } from 'next/navigation'
import { createClient } from '@/lib/supabase/server'
// Server-component guard for the /certifications management pages. Mirrors
// the forms guard: resolve the member, check certifications.manage via the
// same RPC the RLS uses, redirect non-managers before any data is fetched.
// The RLS independently enforces this; the redirect is UX, not the boundary.
export async function requireCertificationsManagerPage() {
const supabase = await createClient()
const {
data: { user },
} = await supabase.auth.getUser()
if (!user) redirect('/login')
const { data: member } = await supabase
.from('space_members')
.select('id, space_id, role, user_id, display_name')
.eq('user_id', user.id)
.in('status', ['current', 'unverified', 'late'])
.single()
if (!member) redirect('/login')
const { data: allowed } = await supabase.rpc('user_has_permission', {
uid: user.id,
sid: member.space_id,
perm: 'certifications.manage',
})
if (!allowed) redirect('/dashboard')
return { supabase, member }
}