| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| import { cookies } from "next/headers"; |
| import { nanoid } from "nanoid"; |
| import { randomBytes, scryptSync, timingSafeEqual } from "crypto"; |
| import { |
| createSession, |
| getSessionByToken, |
| deleteSession, |
| getUser, |
| createOrganization, |
| getOrganizationBySlug, |
| createUser, |
| ensureDefaultOrg, |
| DEFAULT_ORG_ID, |
| setUserPasswordHash, |
| getUserByEmail, |
| userExistsByEmail, |
| _getDb as getDb, |
| type User, |
| type Session, |
| } from "@/lib/db"; |
| import { auditLog } from "@/lib/db"; |
|
|
| export const SESSION_COOKIE = "foundry_session"; |
| const SESSION_DURATION_MS = 7 * 24 * 60 * 60 * 1000; |
|
|
| export interface AuthContext { |
| user: User; |
| orgId: string; |
| isAuthenticated: boolean; |
| token: string; |
| } |
|
|
| |
| |
| |
| |
| export async function getAuthContext(): Promise<AuthContext> { |
| const cookieStore = cookies(); |
| const token = cookieStore.get(SESSION_COOKIE)?.value; |
|
|
| if (token) { |
| const session = getSessionByToken(token); |
| if (session) { |
| const user = getUser(session.user_id); |
| if (user) { |
| return { user, orgId: session.org_id, isAuthenticated: true, token }; |
| } |
| } |
| } |
|
|
| |
| ensureDefaultOrg(); |
| const demoUser = getOrCreateDemoUser(); |
| return { |
| user: demoUser, |
| orgId: DEFAULT_ORG_ID, |
| isAuthenticated: false, |
| token: "", |
| }; |
| } |
|
|
| |
| |
| |
| |
| export async function requireAuthContext(): Promise<AuthContext> { |
| const cookieStore = cookies(); |
| const token = cookieStore.get(SESSION_COOKIE)?.value; |
|
|
| if (!token) { |
| throw new Error("Authentication required"); |
| } |
|
|
| const session = getSessionByToken(token); |
| if (!session) { |
| throw new Error("Session expired or invalid"); |
| } |
|
|
| const user = getUser(session.user_id); |
| if (!user) { |
| throw new Error("User not found"); |
| } |
|
|
| return { user, orgId: session.org_id, isAuthenticated: true, token }; |
| } |
|
|
| |
| |
| |
| export async function getCurrentUserId(): Promise<string> { |
| const ctx = await getAuthContext(); |
| return ctx.user.id; |
| } |
|
|
| |
| |
| |
| export async function getCurrentOrgId(): Promise<string> { |
| const ctx = await getAuthContext(); |
| return ctx.orgId; |
| } |
|
|
| |
| |
| |
| |
| export function hashPassword(password: string): string { |
| const salt = randomBytes(32).toString("base64url"); |
| const derived = scryptSync(password, salt, 64).toString("base64url"); |
| return `${salt}:${derived}`; |
| } |
|
|
| |
| |
| |
| export function verifyPassword(password: string, hash: string): boolean { |
| const parts = hash.split(":"); |
| if (parts.length !== 2) return false; |
| const [salt, stored] = parts; |
| try { |
| const derived = scryptSync(password, salt, 64); |
| const storedBuf = Buffer.from(stored, "base64url"); |
| if (derived.length !== storedBuf.length) return false; |
| return timingSafeEqual(derived, storedBuf); |
| } catch { |
| return false; |
| } |
| } |
|
|
| |
| |
| |
| |
| export function loginWithEmail( |
| orgSlug: string, |
| email: string, |
| password: string, |
| ): { success: boolean; token?: string; error?: string } { |
| const org = getOrganizationBySlug(orgSlug); |
| if (!org) { |
| return { success: false, error: "Organization not found" }; |
| } |
|
|
| const user = getUserByEmail(org.id, email); |
| if (!user || !user.password_hash) { |
| return { success: false, error: "Invalid credentials" }; |
| } |
|
|
| if (!verifyPassword(password, user.password_hash)) { |
| return { success: false, error: "Invalid credentials" }; |
| } |
|
|
| const token = nanoid(32); |
| const expiresAt = new Date(Date.now() + SESSION_DURATION_MS).toISOString(); |
| createSession(`sess_${nanoid(16)}`, user.id, org.id, token, expiresAt); |
|
|
| auditLog(org.id, user.id, "auth.login", "user", user.id); |
|
|
| return { success: true, token }; |
| } |
|
|
| |
| |
| |
| |
| |
| export function registerUser(input: { |
| orgSlug: string; |
| orgName?: string; |
| email: string; |
| password: string; |
| name: string; |
| role?: string; |
| therapeuticArea?: string | null; |
| setupToken?: string; |
| }): { success: boolean; user?: User; error?: string } { |
| const setupToken = process.env.FOUNDRY_SETUP_TOKEN; |
| const anyUser = getDb().prepare("SELECT 1 FROM users LIMIT 1").get() as |
| | { "1": number } |
| | undefined; |
|
|
| if (anyUser && setupToken && input.setupToken !== setupToken) { |
| return { |
| success: false, |
| error: "A setup token is required to register additional users", |
| }; |
| } |
|
|
| let org = getOrganizationBySlug(input.orgSlug); |
| if (!org) { |
| if (anyUser && !input.setupToken) { |
| return { success: false, error: "Organization not found" }; |
| } |
| org = createOrganization( |
| input.orgSlug, |
| input.orgName || input.orgSlug, |
| input.orgSlug, |
| { tier: "enterprise", industry: "pharma" }, |
| ); |
| } |
|
|
| if (userExistsByEmail(org.id, input.email)) { |
| return { success: false, error: "Email already registered" }; |
| } |
|
|
| const userId = `usr_${nanoid(12)}`; |
| const user = createUser( |
| userId, |
| org.id, |
| input.email, |
| input.name, |
| input.role || "field_rep", |
| input.therapeuticArea || null, |
| ); |
|
|
| setUserPasswordHash(user.id, hashPassword(input.password)); |
|
|
| auditLog(org.id, user.id, "auth.register", "user", user.id); |
|
|
| return { success: true, user: { ...user, password_hash: null } }; |
| } |
|
|
| |
| |
| |
| export async function logout(): Promise<void> { |
| const cookieStore = cookies(); |
| const token = cookieStore.get(SESSION_COOKIE)?.value; |
| if (token) { |
| deleteSession(token); |
| } |
| } |
|
|
| |
| |
| |
| export function setSessionCookie(token: string): void { |
| const secure = process.env.NODE_ENV === "production"; |
| cookies().set(SESSION_COOKIE, token, { |
| httpOnly: true, |
| secure, |
| sameSite: "lax", |
| path: "/", |
| maxAge: 7 * 24 * 60 * 60, |
| }); |
| } |
|
|
| |
| |
| |
| export function clearSessionCookie(): void { |
| cookies().delete(SESSION_COOKIE); |
| } |
|
|
| |
| export function setUserPassword(userId: string, password: string): void { |
| setUserPasswordHash(userId, hashPassword(password)); |
| } |
|
|
| |
|
|
| |
| const DEMO_CREDENTIALS: Record<string, { userId: string; password: string; name: string; role: string; therapeuticArea?: string }> = {}; |
|
|
| let _demoUser: User | null = null; |
|
|
| function getOrCreateDemoUser(): User { |
| if (_demoUser) return _demoUser; |
| ensureDefaultOrg(); |
| const demoId = "emp-001"; |
| let user = getUser(demoId); |
| if (!user) { |
| user = createUser( |
| demoId, |
| DEFAULT_ORG_ID, |
| "demo@advantagefoundry.com", |
| "Demo User", |
| "field_rep", |
| null, |
| ); |
| } |
| _demoUser = user; |
| return user; |
| } |
|
|
| |
| |
| |
|
|
| |
| |
| |
| |
| |
| export function ensureGileadDemoOrg(): void { |
| ensureDefaultOrg(); |
| const gileadUserId = "emp-gilead-001"; |
| let user = getUser(gileadUserId); |
| if (!user) { |
| user = createUser( |
| gileadUserId, |
| DEFAULT_ORG_ID, |
| "gilead-demo@advantagefoundry.com", |
| "Gilead Demo User", |
| "field_rep", |
| "Oncology", |
| ); |
| } |
| } |
|
|