mirror of
https://github.com/fosrl/pangolin.git
synced 2026-02-14 08:56:39 +00:00
Added the feature for admins to force 2FA on accounts. The next time the user logs in they will have to setup 2FA on their account.
127 lines
3.6 KiB
TypeScript
127 lines
3.6 KiB
TypeScript
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<typeof setupTotpSecretBody>;
|
|
|
|
export type SetupTotpSecretResponse = {
|
|
secret: string;
|
|
uri: string;
|
|
};
|
|
|
|
export async function setupTotpSecret(
|
|
req: Request,
|
|
res: Response,
|
|
next: NextFunction
|
|
): Promise<any> {
|
|
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<SetupTotpSecretResponse>(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"
|
|
)
|
|
);
|
|
}
|
|
} |