diff --git a/__tests__/lib/auth/roles.test.ts b/__tests__/lib/auth/roles.test.ts new file mode 100644 index 0000000..ca91b17 --- /dev/null +++ b/__tests__/lib/auth/roles.test.ts @@ -0,0 +1,37 @@ +// __tests__/lib/auth/roles.test.ts +import { describe, it, expect } from 'vitest' +import { getRoleHome, ROLE_HOME, ALL_ROLES } from '@/lib/auth/roles' + +describe('getRoleHome', () => { + it('returns /reporter for reporter role', () => { + expect(getRoleHome('reporter')).toBe('/reporter') + }) + + it('returns /supervisor for supervisor role', () => { + expect(getRoleHome('supervisor')).toBe('/supervisor') + }) + + it('returns /hse for hse role', () => { + expect(getRoleHome('hse')).toBe('/hse') + }) + + it('returns /capa-owner for capa_owner role', () => { + expect(getRoleHome('capa_owner')).toBe('/capa-owner') + }) + + it('returns /management for management role', () => { + expect(getRoleHome('management')).toBe('/management') + }) + + it('returns /admin for admin role', () => { + expect(getRoleHome('admin')).toBe('/admin') + }) + + it('ROLE_HOME covers exactly 6 roles', () => { + expect(Object.keys(ROLE_HOME)).toHaveLength(6) + }) + + it('ALL_ROLES lists exactly 6 roles', () => { + expect(ALL_ROLES).toHaveLength(6) + }) +}) diff --git a/lib/auth/roles.ts b/lib/auth/roles.ts new file mode 100644 index 0000000..4a44bf0 --- /dev/null +++ b/lib/auth/roles.ts @@ -0,0 +1,34 @@ +// lib/auth/roles.ts +export type UserRole = + | 'reporter' + | 'supervisor' + | 'hse' + | 'capa_owner' + | 'management' + | 'admin' + +export const ALL_ROLES: UserRole[] = [ + 'reporter', + 'supervisor', + 'hse', + 'capa_owner', + 'management', + 'admin', +] + +export const ROLE_HOME: Record = { + reporter: '/reporter', + supervisor: '/supervisor', + hse: '/hse', + capa_owner: '/capa-owner', + management: '/management', + admin: '/admin', +} + +export function getRoleHome(role: UserRole): string { + return ROLE_HOME[role] +} + +export function isValidRole(value: unknown): value is UserRole { + return ALL_ROLES.includes(value as UserRole) +} diff --git a/middleware.ts b/middleware.ts new file mode 100644 index 0000000..0dcd857 --- /dev/null +++ b/middleware.ts @@ -0,0 +1,61 @@ +// middleware.ts +import { createServerClient } from '@supabase/ssr' +import { NextResponse, type NextRequest } from 'next/server' +import { ROLE_HOME, isValidRole, type UserRole } from '@/lib/auth/roles' + +export async function middleware(request: NextRequest) { + let supabaseResponse = NextResponse.next({ request }) + + const supabase = createServerClient( + process.env.NEXT_PUBLIC_SUPABASE_URL!, + process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!, + { + cookies: { + getAll() { + return request.cookies.getAll() + }, + setAll(cookiesToSet) { + cookiesToSet.forEach(({ name, value }) => request.cookies.set(name, value)) + supabaseResponse = NextResponse.next({ request }) + cookiesToSet.forEach(({ name, value, options }) => + supabaseResponse.cookies.set(name, value, options), + ) + }, + }, + }, + ) + + const { + data: { user }, + } = await supabase.auth.getUser() + + const { pathname } = request.nextUrl + const isPublicRoute = pathname.startsWith('/login') || pathname.startsWith('/auth') + + // Unauthenticated → force login + if (!user && !isPublicRoute) { + return NextResponse.redirect(new URL('/login', request.url)) + } + + // Authenticated + hitting root or login → redirect to role home + if (user && (pathname === '/' || pathname === '/login')) { + const { data: profile } = await supabase + .from('users') + .select('role') + .eq('id', user.id) + .single() + + const role = profile?.role + if (isValidRole(role)) { + return NextResponse.redirect(new URL(ROLE_HOME[role as UserRole], request.url)) + } + } + + return supabaseResponse +} + +export const config = { + matcher: [ + '/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)', + ], +}