Instruction file imported from laureenwr/BuildingBridges (
.cursor/rules/authentication-patterns.mdc). Copyright stays with the author.
Authentication & Session Management Patterns
Session-Based Authentication
✅ DO: Secure Session Configuration
// lib/auth/session.ts
import { cookies } from 'next/headers';
const sessionConfig = {
name: 'session',
secure: process.env.NODE_ENV === 'production',
httpOnly: true,
sameSite: 'lax' as const,
path: '/',
maxAge: 60 * 60 * 24 * 7, // 7 days
};
export async function createSession(userId: string, role: string) {
const sessionData = {
userId,
role,
createdAt: new Date().toISOString(),
};
const encrypted = await encrypt(sessionData);
cookies().set(sessionConfig.name, encrypted, sessionConfig);
}
Password Security
// Always hash passwords with bcryptjs
import bcrypt from 'bcryptjs';
export async function hashPassword(password: string): Promise<string> {
const saltRounds = 12; // High security for academic/research data
return bcrypt.hash(password, saltRounds);
}
export async function verifyPassword(password: string, hash: string): Promise<boolean> {
return bcrypt.compare(password, hash);
}
Role-Based Access Control
User Roles
// Define clear role hierarchy
export enum UserRole {
STUDENT = 'STUDENT',
MENTOR = 'MENTOR',
ADMIN = 'ADMIN'
}
// Role permissions matrix
export const rolePermissions = {
STUDENT: ['read:profile', 'update:profile', 'read:mentors'],
MENTOR: ['read:profile', 'update:profile', 'read:students', 'create:sessions'],
ADMIN: ['*'] // Full access
} as const;
Middleware Protection
// middleware.ts
import { NextRequest, NextResponse } from 'next/server';
import { getSession } from '@/lib/auth/session';
export async function middleware(request: NextRequest) {
const session = await getSession();
const { pathname } = request.nextUrl;
// Public routes
if (pathname.startsWith('/api/auth') ||
pathname === '/' ||
pathname === '/contact' ||
pathname === '/workshops') {
return NextResponse.next();
}
// Protected dashboard routes
if (pathname.startsWith('/dashboard')) {
if (!session) {
return NextResponse.redirect(new URL('/sign-in', request.url));
}
// Role-specific access
if (pathname.startsWith('/dashboard/admin') && session.role !== 'ADMIN') {
return NextResponse.redirect(new URL('/dashboard', request.url));
}
}
return NextResponse.next();
}
export const config = {
matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'],
};
Server Actions for Authentication
✅ DO: Simplified Server Actions
// app/(login)/actions.ts
'use server';
import { redirect } from 'next/navigation';
import { createSession } from '@/lib/auth/session';
export async function signInAction(formData: FormData) {
try {
const email = formData.get('email') as string;
const password = formData.get('password') as string;
// Validate credentials
const user = await validateCredentials(email, password);
if (!user) {
return { error: 'Ungültige Anmeldedaten' };
}
// Create session
await createSession(user.id, user.role);
// Redirect based on role
const dashboardPath = getDashboardPath(user.role);
redirect(dashboardPath);
} catch (error) {
console.error('Sign in error:', error);
return { error: 'Ein Fehler ist aufgetreten' };
}
}
function getDashboardPath(role: string): string {
switch (role) {
case 'ADMIN': return '/dashboard';
case 'MENTOR': return '/dashboard/mentors-redirect';
case 'STUDENT': return '/dashboard';
default: return '/dashboard';
}
}
Form State Management
// Use React 18 compatible hooks
'use client';
import { useFormState } from 'react-dom';
import { signInAction } from './actions';
export default function SignInForm() {
const [state, formAction, isPending] = useFormState(signInAction, {
error: '',
success: ''
});
return (
<form action={formAction} className="space-y-4">
<input
type="email"
name="email"
placeholder="E-Mail-Adresse"
required
className="w-full px-4 py-2 border rounded-lg"
/>
<input
type="password"
name="password"
placeholder="Passwort"
required
className="w-full px-4 py-2 border rounded-lg"
/>
{state.error && (
<div className="text-red-600 text-sm">{state.error}</div>
)}
<button
type="submit"
disabled={isPending}
className="w-full bg-blue-600 text-white py-2 rounded-lg disabled:opacity-50"
>
{isPending ? 'Anmelden...' : 'Anmelden'}
</button>
</form>
);
}
Database Integration
User Schema
// lib/db/schema.ts
import { pgTable, text, timestamp, varchar } from 'drizzle-orm/pg-core';
export const users = pgTable('users', {
id: text('id').primaryKey(),
email: varchar('email', { length: 255 }).unique().notNull(),
password: text('password').notNull(),
role: varchar('role', { length: 20 }).notNull().default('STUDENT'),
firstName: varchar('first_name', { length: 100 }),
lastName: varchar('last_name', { length: 100 }),
emailVerified: timestamp('email_verified'),
createdAt: timestamp('created_at').defaultNow(),
updatedAt: timestamp('updated_at').defaultNow(),
});
export const onboardingData = pgTable('onboarding_data', {
id: text('id').primaryKey(),
userId: text('user_id').references(() => users.id).notNull(),
challenges: text('challenges'),
goals: text('goals'),
interests: text('interests'),
background: text('background'),
createdAt: timestamp('created_at').defaultNow(),
});
Database Queries
// lib/db/queries.ts
import { db } from './drizzle';
import { users } from './schema';
import { eq } from 'drizzle-orm';
export async function getUserByEmail(email: string) {
try {
const user = await db.query.users.findFirst({
where: eq(users.email, email),
});
return user;
} catch (error) {
console.error('Database query error:', error);
return null;
}
}
export async function createUser(userData: {
email: string;
password: string;
firstName: string;
lastName: string;
role: string;
}) {
try {
const [user] = await db.insert(users).values({
id: crypto.randomUUID(),
...userData,
}).returning();
return user;
} catch (error) {
console.error('User creation error:', error);
throw new Error('Benutzer konnte nicht erstellt werden');
}
}
Error Handling
Authentication Errors
// Centralized error handling
export class AuthError extends Error {
constructor(
message: string,
public code: 'INVALID_CREDENTIALS' | 'USER_NOT_FOUND' | 'SESSION_EXPIRED'
) {
super(message);
this.name = 'AuthError';
}
}
export function handleAuthError(error: unknown): string {
if (error instanceof AuthError) {
switch (error.code) {
case 'INVALID_CREDENTIALS':
return 'Ungültige Anmeldedaten';
case 'USER_NOT_FOUND':
return 'Benutzer nicht gefunden';
case 'SESSION_EXPIRED':
return 'Sitzung abgelaufen. Bitte melden Sie sich erneut an.';
default:
return 'Ein Authentifizierungsfehler ist aufgetreten';
}
}
console.error('Unexpected auth error:', error);
return 'Ein unerwarteter Fehler ist aufgetreten';
}
Session Management
Session Validation
// lib/auth/session.ts
export async function getSession(): Promise<Session | null> {
try {
const sessionCookie = cookies().get('session');
if (!sessionCookie?.value) return null;
const sessionData = await decrypt(sessionCookie.value);
if (!sessionData) return null;
// Validate session hasn't expired
const createdAt = new Date(sessionData.createdAt);
const now = new Date();
const maxAge = 7 * 24 * 60 * 60 * 1000; // 7 days
if (now.getTime() - createdAt.getTime() > maxAge) {
await deleteSession();
return null;
}
return sessionData;
} catch (error) {
console.error('Session validation error:', error);
return null;
}
}
export async function deleteSession() {
cookies().delete('session');
}
User Context Provider
// lib/auth/index.tsx
'use client';
import { createContext, useContext, useEffect, useState } from 'react';
interface User {
id: string;
email: string;
role: string;
firstName?: string;
lastName?: string;
}
interface UserContextType {
user: User | null;
loading: boolean;
}
const UserContext = createContext<UserContextType>({
user: null,
loading: true,
});
export function UserProvider({ children }: { children: React.ReactNode }) {
const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
// Fetch user session on mount
fetchUser();
}, []);
const fetchUser = async () => {
try {
const response = await fetch('/api/auth/me');
if (response.ok) {
const userData = await response.json();
setUser(userData);
}
} catch (error) {
console.error('Failed to fetch user:', error);
} finally {
setLoading(false);
}
};
return (
<UserContext.Provider value={{ user, loading }}>
{children}
</UserContext.Provider>
);
}
export const useUser = () => useContext(UserContext);