import { sha256 } from '@oslojs/crypto/sha2'; import { encodeBase32LowerCaseNoPadding, encodeHexLowerCase } from '@oslojs/encoding'; import { type Session, type User, UserSecurityAuditLogType } from '@prisma/client'; import type { RequestMetadata } from '@documenso/lib/universal/extract-request-metadata'; import { prisma } from '@documenso/prisma'; /** * The user object to pass around the app. * * Do not put anything sensitive in here since it will be public. */ export type SessionUser = Pick< User, | 'id' | 'name' | 'email' | 'emailVerified' | 'avatarImageId' | 'twoFactorEnabled' | 'roles' | 'signature' | 'url' >; export type SessionValidationResult = | { session: Session; user: SessionUser; isAuthenticated: true; } | { session: null; user: null; isAuthenticated: false }; export const generateSessionToken = (): string => { const bytes = new Uint8Array(20); crypto.getRandomValues(bytes); const token = encodeBase32LowerCaseNoPadding(bytes); return token; }; export const createSession = async ( token: string, userId: number, metadata: RequestMetadata, ): Promise => { const hashedSessionId = encodeHexLowerCase(sha256(new TextEncoder().encode(token))); const session: Session = { id: hashedSessionId, sessionToken: hashedSessionId, userId, updatedAt: new Date(), createdAt: new Date(), expiresAt: new Date(Date.now() + 1000 * 60 * 60 * 24 * 30), ipAddress: metadata.ipAddress ?? null, userAgent: metadata.userAgent ?? null, }; await prisma.session.create({ data: session, }); await prisma.userSecurityAuditLog.create({ data: { userId, ipAddress: metadata.ipAddress, userAgent: metadata.userAgent, type: UserSecurityAuditLogType.SIGN_IN, }, }); return session; }; export const validateSessionToken = async (token: string): Promise => { const sessionId = encodeHexLowerCase(sha256(new TextEncoder().encode(token))); const result = await prisma.session.findUnique({ where: { id: sessionId, }, include: { user: { /** * Do not expose anything sensitive here. */ select: { id: true, name: true, email: true, emailVerified: true, avatarImageId: true, twoFactorEnabled: true, roles: true, signature: true, url: true, }, }, }, }); if (!result?.user) { return { session: null, user: null, isAuthenticated: false }; } const { user, ...session } = result; if (Date.now() >= session.expiresAt.getTime()) { await prisma.session.delete({ where: { id: sessionId } }); return { session: null, user: null, isAuthenticated: false }; } if (Date.now() >= session.expiresAt.getTime() - 1000 * 60 * 60 * 24 * 15) { session.expiresAt = new Date(Date.now() + 1000 * 60 * 60 * 24 * 30); await prisma.session.update({ where: { id: session.id, }, data: { expiresAt: session.expiresAt, }, }); } return { session, user, isAuthenticated: true }; }; export const invalidateSession = async ( sessionId: string, metadata: RequestMetadata, ): Promise => { const session = await prisma.session.delete({ where: { id: sessionId } }); await prisma.userSecurityAuditLog.create({ data: { userId: session.userId, ipAddress: metadata.ipAddress, userAgent: metadata.userAgent, type: UserSecurityAuditLogType.SIGN_OUT, }, }); };