import { Request, Response, NextFunction } from "express"; import createHttpError from "http-errors"; import { z } from "zod"; import { fromError } from "zod-validation-error"; import { encodeHex } from "oslo/encoding"; import HttpCode from "@server/types/HttpCode"; import { response } from "@server/lib"; import { db } from "@server/db"; import { User, users } from "@server/db"; import { eq, and } from "drizzle-orm"; import { createTOTPKeyURI } from "oslo/otp"; import logger from "@server/logger"; import { verifyPassword } from "@server/auth/password"; import { UserType } from "@server/types/UserTypes"; export const setupTotpSecretBody = z .object({ email: z.string().email(), password: z.string() }) .strict(); export type SetupTotpSecretBody = z.infer; export type SetupTotpSecretResponse = { secret: string; uri: string; }; export async function setupTotpSecret( req: Request, res: Response, next: NextFunction ): Promise { const parsedBody = setupTotpSecretBody.safeParse(req.body); if (!parsedBody.success) { return next( createHttpError( HttpCode.BAD_REQUEST, fromError(parsedBody.error).toString() ) ); } const { email, password } = parsedBody.data; try { // Find the user by email const [user] = await db .select() .from(users) .where(and(eq(users.email, email), eq(users.type, UserType.Internal))) .limit(1); if (!user) { return next( createHttpError( HttpCode.UNAUTHORIZED, "Invalid credentials" ) ); } // Verify password const validPassword = await verifyPassword(password, user.passwordHash!); if (!validPassword) { return next( createHttpError( HttpCode.UNAUTHORIZED, "Invalid credentials" ) ); } // Check if 2FA is enabled but no secret exists (forced setup scenario) if (!user.twoFactorEnabled) { return next( createHttpError( HttpCode.BAD_REQUEST, "Two-factor authentication is not required for this user" ) ); } if (user.twoFactorSecret) { return next( createHttpError( HttpCode.BAD_REQUEST, "User has already completed two-factor authentication setup" ) ); } // Generate new TOTP secret const hex = crypto.getRandomValues(new Uint8Array(20)); const secret = encodeHex(hex); const uri = createTOTPKeyURI("Pangolin", user.email!, hex); // Save the secret to the database await db .update(users) .set({ twoFactorSecret: secret }) .where(eq(users.userId, user.userId)); return response(res, { data: { secret, uri }, success: true, error: false, message: "TOTP secret generated successfully", status: HttpCode.OK }); } catch (error) { logger.error(error); return next( createHttpError( HttpCode.INTERNAL_SERVER_ERROR, "Failed to generate TOTP secret" ) ); } }