| name | nextauth-rbac |
| description | ForemanOS NextAuth.js authentication and RBAC patterns. Use when writing API routes, auth checks, or role-based access control. |
NextAuth RBAC (ForemanOS)
NextAuth.js with JWT strategy (no session adapter), CredentialsProvider, and multi-tier RBAC.
When to Use This Skill
- Writing new API route handlers
- Adding authentication checks
- Implementing role-based access control
- Working with guest/PIN-based auth
- Modifying the auth flow or session data
Core Patterns
Auth Configuration (lib/auth-options.ts)
Key architectural decisions:
- JWT-only sessions — no database adapter (reduces DB load by 90%)
- CredentialsProvider — username/email + password (case-insensitive lookup)
- Guest PIN system — password-less login for
role === 'guest' users
- Session lifetime: 30 days, updated every 24 hours
- Login page:
/login
API Route Auth Check
Every API route MUST start with getServerSession:
import { getServerSession } from 'next-auth';
import { authOptions } from '@/lib/auth-options';
export async function GET(req: NextRequest) {
try {
const session = await getServerSession(authOptions);
if (!session?.user?.email) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const userId = session.user.id;
const role = session.user.role;
const username = session.user.username;
} catch (error) {
return NextResponse.json({ error: 'Internal error' }, { status: 500 });
}
}
Or with apiError helper (preferred for new routes):
import { getServerSession } from 'next-auth';
import { authOptions } from '@/lib/auth-options';
import { apiError, apiSuccess } from '@/lib/api-error';
export async function GET(req: NextRequest) {
try {
const session = await getServerSession(authOptions);
if (!session?.user?.email) {
return apiError('Unauthorized', 401, 'UNAUTHORIZED');
}
if (session.user.role !== 'admin') {
return apiError('Admin access required', 403, 'FORBIDDEN');
}
const data = await fetchData();
return apiSuccess({ data });
} catch (error) {
return apiError('Failed to fetch data', 500, 'INTERNAL_ERROR');
}
}
JWT Token Shape
The JWT token carries these custom fields (set in lib/auth-options.ts):
token.id
token.username
token.role
token.Project_User_assignedProjectIdToProjectId
token.subscriptionTier
token.loginLogged
Session shape accessible in API routes:
session.user.id
session.user.username
session.user.role
session.user.assignedProjectId
session.user.subscriptionTier
Middleware (middleware.ts)
Uses withAuth from next-auth/middleware:
- Protected routes:
/dashboard/*, /projects/*, /admin/*, /profile/*, /settings/*, /chat/*, /api/*
- Public API patterns bypass auth:
/api/auth/*, /api/webhooks/*, /api/cron/*, /api/documents/presigned-url
- Unauthenticated users redirected to
/login
Role System
Three-tier access control (lib/access-control.ts):
| Role | Access Level |
|---|
admin | Full access to all documents and features |
client | Client + guest documents (no financial restrictions) |
guest | Guest documents only (plans, specs, schedule — no budget/cost data) |
import { hasDocumentAccess, isRestrictedQuery } from '@/lib/access-control';
if (!hasDocumentAccess(userLevel, documentName)) {
return apiError('Access denied', 403, 'FORBIDDEN');
}
if (isRestrictedQuery(query, userLevel)) {
return apiError(getAccessDenialMessage(), 403, 'FORBIDDEN');
}
Daily Report RBAC (lib/daily-report-permissions.ts)
Four-tier role hierarchy for field operations:
| Role | Create | Edit Own | Edit All | Submit | Approve | Delete |
|---|
| VIEWER | No | No | No | No | No | No |
| REPORTER | Yes | Draft/Rejected | No | Own | No | No |
| SUPERVISOR | Yes | Yes | Yes | Yes | Yes | No |
| ADMIN | Yes | Yes | Yes | Yes | Yes | Yes |
import {
getDailyReportRole,
canCreateReport,
canEditReport,
canApproveReport,
type DailyReportRole,
} from '@/lib/daily-report-permissions';
const role = await getDailyReportRole(userId, projectId);
if (!role || !canCreateReport(role)) {
return apiError('Insufficient permissions', 403, 'FORBIDDEN');
}
Status state machine: DRAFT -> SUBMITTED -> APPROVED | REJECTED -> DRAFT
Guest PIN System (lib/guest-pin-utils.ts)
PINs are namespaced per owner to prevent collisions:
import { namespacePIN, stripPINPrefix } from '@/lib/guest-pin-utils';
const storedPin = namespacePIN(ownerId, 'Job456');
const displayPin = stripPINPrefix(storedPin);
Login flow: user types their PIN as username, auth-options.ts finds the user via endsWith match on namespaced username.
XSS Sanitization
Always sanitize user text input in daily reports:
import { sanitizeText } from '@/lib/daily-report-permissions';
const cleanInput = sanitizeText(userInput);
Anti-Patterns
- Never skip
getServerSession(authOptions) in API routes — middleware is a first pass, routes must re-check
- Never trust
session.user.role without importing authOptions — passing wrong options gives stale data
- Never use
getSession() (client-side) in API routes — use getServerSession() (server-side)
- Never store passwords for guest accounts — guests authenticate without passwords
- Never allow password-less login for non-guest roles — security check exists in auth-options.ts
Quick Reference
| Pattern | Import |
|---|
| Server auth check | import { getServerSession } from 'next-auth' |
| Auth config | import { authOptions } from '@/lib/auth-options' |
| API error helper | import { apiError, apiSuccess } from '@/lib/api-error' |
| Access control | import { hasDocumentAccess } from '@/lib/access-control' |
| DR permissions | import { getDailyReportRole, canCreateReport } from '@/lib/daily-report-permissions' |
| Guest PINs | import { namespacePIN, stripPINPrefix } from '@/lib/guest-pin-utils' |
| Input sanitization | import { sanitizeText } from '@/lib/daily-report-permissions' |