Co-authored-by: cetinmustafa83 <30570791+cetinmustafa83@users.noreply.github.com>
180 lines
4.7 KiB
TypeScript
180 lines
4.7 KiB
TypeScript
import bcrypt from 'bcryptjs';
|
|
import { cookies } from 'next/headers';
|
|
import { Prisma } from '@prisma/client';
|
|
import { db } from '@/lib/db';
|
|
|
|
export const SESSION_COOKIE_NAME = 'ct_session';
|
|
export const SESSION_COOKIE_MAX_AGE = 60 * 60 * 24 * 7; // 7 days
|
|
|
|
/**
|
|
* Determine whether the Secure flag should be set on cookies.
|
|
* Returns true when:
|
|
* - NODE_ENV is production, OR
|
|
* - the request came through an HTTPS proxy (x-forwarded-proto: https)
|
|
*/
|
|
export function isSecureRequest(request: Request): boolean {
|
|
if (process.env.NODE_ENV === 'production') return true;
|
|
const forwarded = request.headers.get('x-forwarded-proto');
|
|
return forwarded === 'https';
|
|
}
|
|
|
|
/**
|
|
* Returns the cookie options object for the session cookie.
|
|
*/
|
|
export function getSessionCookieOptions(secure: boolean) {
|
|
return {
|
|
httpOnly: true,
|
|
secure,
|
|
sameSite: 'lax' as const,
|
|
maxAge: SESSION_COOKIE_MAX_AGE,
|
|
path: '/',
|
|
};
|
|
}
|
|
|
|
export async function hashPassword(password: string): Promise<string> {
|
|
const salt = await bcrypt.genSalt(10);
|
|
return bcrypt.hash(password, salt);
|
|
}
|
|
|
|
export async function verifyPassword(
|
|
password: string,
|
|
hash: string
|
|
): Promise<boolean> {
|
|
return bcrypt.compare(password, hash);
|
|
}
|
|
|
|
export async function createSession(userId: string, secure?: boolean): Promise<void> {
|
|
const cookieStore = await cookies();
|
|
const cookieSecure = secure ?? (process.env.NODE_ENV === 'production');
|
|
cookieStore.set(SESSION_COOKIE_NAME, userId, getSessionCookieOptions(cookieSecure));
|
|
}
|
|
|
|
const sessionUserSelect = Prisma.validator<Prisma.UserSelect>()({
|
|
id: true,
|
|
email: true,
|
|
firstName: true,
|
|
lastName: true,
|
|
role: true,
|
|
schoolId: true,
|
|
locale: true,
|
|
isDemo: true,
|
|
createdAt: true,
|
|
updatedAt: true,
|
|
});
|
|
|
|
export type SessionUser = Prisma.UserGetPayload<{ select: typeof sessionUserSelect }>;
|
|
|
|
export async function getSession(): Promise<{
|
|
userId: string;
|
|
user: SessionUser | null;
|
|
} | null> {
|
|
const cookieStore = await cookies();
|
|
const userId = cookieStore.get(SESSION_COOKIE_NAME)?.value;
|
|
|
|
if (!userId) return null;
|
|
|
|
const user = await db.user.findFirst({
|
|
where: { id: userId, deletedAt: null },
|
|
select: sessionUserSelect,
|
|
});
|
|
|
|
if (!user) return null;
|
|
|
|
return { userId, user };
|
|
}
|
|
|
|
export async function clearSession(): Promise<void> {
|
|
const cookieStore = await cookies();
|
|
cookieStore.delete(SESSION_COOKIE_NAME);
|
|
}
|
|
|
|
// ─── Module L: Access Control & DPO Role ───────────────────────────────
|
|
|
|
/**
|
|
* Check if user has DPO (Data Protection Officer) role
|
|
*/
|
|
export async function isDPORole(userId: string): Promise<boolean> {
|
|
const cookieStore = await cookies();
|
|
const sessionUserId = cookieStore.get(SESSION_COOKIE_NAME)?.value;
|
|
|
|
if (!sessionUserId || sessionUserId !== userId) {
|
|
return false;
|
|
}
|
|
|
|
const user = await db.user.findUnique({
|
|
where: { id: userId },
|
|
select: { role: true },
|
|
});
|
|
|
|
return user?.role === 'DPO';
|
|
}
|
|
|
|
/**
|
|
* Check if user has admin role (SCHOOL_ADMIN or higher)
|
|
*/
|
|
export async function isAdminRole(userId: string): Promise<boolean> {
|
|
const user = await db.user.findUnique({
|
|
where: { id: userId },
|
|
select: { role: true },
|
|
});
|
|
|
|
return user?.role === 'SCHOOL_ADMIN' || user?.role === 'SUPER_ADMIN';
|
|
}
|
|
|
|
/**
|
|
* Check if user can access compliance features (DPO or Admin)
|
|
*/
|
|
export async function canAccessCompliance(userId: string): Promise<boolean> {
|
|
const isDPO = await isDPORole(userId);
|
|
const isAdmin = await isAdminRole(userId);
|
|
return isDPO || isAdmin;
|
|
}
|
|
|
|
/**
|
|
* Verify user can perform compliance actions
|
|
* Returns { allowed: boolean, reason?: string }
|
|
*/
|
|
export async function verifyComplianceAccess(
|
|
userId: string
|
|
): Promise<{ allowed: boolean; reason?: string }> {
|
|
const user = await db.user.findUnique({
|
|
where: { id: userId, deletedAt: null },
|
|
select: { role: true, schoolId: true },
|
|
});
|
|
|
|
if (!user) {
|
|
return { allowed: false, reason: 'User not found' };
|
|
}
|
|
|
|
if (user.role !== 'DPO' && user.role !== 'SCHOOL_ADMIN' && user.role !== 'SUPER_ADMIN') {
|
|
return {
|
|
allowed: false,
|
|
reason: 'Only DPO and administrators can access compliance features',
|
|
};
|
|
}
|
|
|
|
if (!user.schoolId) {
|
|
return { allowed: false, reason: 'User not associated with a school' };
|
|
}
|
|
|
|
return { allowed: true };
|
|
}
|
|
|
|
/**
|
|
* Check if user can view audit logs
|
|
* Only DPO and admins can view full audit logs
|
|
*/
|
|
export async function canViewAuditLogs(userId: string): Promise<boolean> {
|
|
const canAccess = await canAccessCompliance(userId);
|
|
return canAccess;
|
|
}
|
|
|
|
/**
|
|
* Check if user can approve deletion flags
|
|
* Only DPO can approve data deletion
|
|
*/
|
|
export async function canApproveDeletion(userId: string): Promise<boolean> {
|
|
const isDPO = await isDPORole(userId);
|
|
return isDPO;
|
|
}
|