mirror of
https://github.com/fosrl/pangolin.git
synced 2025-07-14 16:04:56 +02:00
verify email workflow working
This commit is contained in:
parent
e176295593
commit
76eeb335a3
23 changed files with 16363 additions and 15802 deletions
122
package-lock.json
generated
122
package-lock.json
generated
|
@ -10,7 +10,6 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@lucia-auth/adapter-drizzle": "1.1.0",
|
"@lucia-auth/adapter-drizzle": "1.1.0",
|
||||||
"@node-rs/argon2": "1.8.3",
|
"@node-rs/argon2": "1.8.3",
|
||||||
"@node-rs/argon2-linux-x64-gnu": "1.8.3",
|
|
||||||
"@react-email/components": "0.0.25",
|
"@react-email/components": "0.0.25",
|
||||||
"@react-email/tailwind": "0.1.0",
|
"@react-email/tailwind": "0.1.0",
|
||||||
"axios": "1.7.7",
|
"axios": "1.7.7",
|
||||||
|
@ -4088,6 +4087,7 @@
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"linux"
|
"linux"
|
||||||
],
|
],
|
||||||
|
@ -15607,6 +15607,126 @@
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"zod": "^3.18.0"
|
"zod": "^3.18.0"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"node_modules/react-email/node_modules/@next/swc-darwin-x64": {
|
||||||
|
"version": "14.2.13",
|
||||||
|
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.13.tgz",
|
||||||
|
"integrity": "sha512-Dv1RBGs2TTjkwEnFMVL5XIfJEavnLqqwYSD6LXgTPdEy/u6FlSrLBSSfe1pcfqhFEXRAgVL3Wpjibe5wXJzWog==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/react-email/node_modules/@next/swc-linux-arm64-gnu": {
|
||||||
|
"version": "14.2.13",
|
||||||
|
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.13.tgz",
|
||||||
|
"integrity": "sha512-yB1tYEFFqo4ZNWkwrJultbsw7NPAAxlPXURXioRl9SdW6aIefOLS+0TEsKrWBtbJ9moTDgU3HRILL6QBQnMevg==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/react-email/node_modules/@next/swc-linux-arm64-musl": {
|
||||||
|
"version": "14.2.13",
|
||||||
|
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.13.tgz",
|
||||||
|
"integrity": "sha512-v5jZ/FV/eHGoWhMKYrsAweQ7CWb8xsWGM/8m1mwwZQ/sutJjoFaXchwK4pX8NqwImILEvQmZWyb8pPTcP7htWg==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/react-email/node_modules/@next/swc-linux-x64-gnu": {
|
||||||
|
"version": "14.2.13",
|
||||||
|
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.13.tgz",
|
||||||
|
"integrity": "sha512-aVc7m4YL7ViiRv7SOXK3RplXzOEe/qQzRA5R2vpXboHABs3w8vtFslGTz+5tKiQzWUmTmBNVW0UQdhkKRORmGA==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/react-email/node_modules/@next/swc-linux-x64-musl": {
|
||||||
|
"version": "14.2.13",
|
||||||
|
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.13.tgz",
|
||||||
|
"integrity": "sha512-4wWY7/OsSaJOOKvMsu1Teylku7vKyTuocvDLTZQq0TYv9OjiYYWt63PiE1nTuZnqQ4RPvME7Xai+9enoiN0Wrg==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/react-email/node_modules/@next/swc-win32-arm64-msvc": {
|
||||||
|
"version": "14.2.13",
|
||||||
|
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.13.tgz",
|
||||||
|
"integrity": "sha512-uP1XkqCqV2NVH9+g2sC7qIw+w2tRbcMiXFEbMihkQ8B1+V6m28sshBwAB0SDmOe0u44ne1vFU66+gx/28RsBVQ==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/react-email/node_modules/@next/swc-win32-ia32-msvc": {
|
||||||
|
"version": "14.2.13",
|
||||||
|
"resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.13.tgz",
|
||||||
|
"integrity": "sha512-V26ezyjPqQpDBV4lcWIh8B/QICQ4v+M5Bo9ykLN+sqeKKBxJVDpEc6biDVyluTXTC40f5IqCU0ttth7Es2ZuMw==",
|
||||||
|
"cpu": [
|
||||||
|
"ia32"
|
||||||
|
],
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/react-email/node_modules/@next/swc-win32-x64-msvc": {
|
||||||
|
"version": "14.2.13",
|
||||||
|
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.13.tgz",
|
||||||
|
"integrity": "sha512-WwzOEAFBGhlDHE5Z73mNU8CO8mqMNLqaG+AO9ETmzdCQlJhVtWZnOl2+rqgVQS+YHunjOWptdFmNfbpwcUuEsw==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,7 +15,6 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@lucia-auth/adapter-drizzle": "1.1.0",
|
"@lucia-auth/adapter-drizzle": "1.1.0",
|
||||||
"@node-rs/argon2": "1.8.3",
|
"@node-rs/argon2": "1.8.3",
|
||||||
"@node-rs/argon2-linux-x64-gnu": "1.8.3",
|
|
||||||
"@react-email/components": "0.0.25",
|
"@react-email/components": "0.0.25",
|
||||||
"@react-email/tailwind": "0.1.0",
|
"@react-email/tailwind": "0.1.0",
|
||||||
"axios": "1.7.7",
|
"axios": "1.7.7",
|
||||||
|
|
|
@ -15,6 +15,7 @@ export const lucia = new Lucia(adapter, {
|
||||||
email: attributes.email,
|
email: attributes.email,
|
||||||
twoFactorEnabled: attributes.twoFactorEnabled,
|
twoFactorEnabled: attributes.twoFactorEnabled,
|
||||||
twoFactorSecret: attributes.twoFactorSecret,
|
twoFactorSecret: attributes.twoFactorSecret,
|
||||||
|
emailVerified: attributes.emailVerified,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
// getSessionAttributes: (attributes) => {
|
// getSessionAttributes: (attributes) => {
|
||||||
|
@ -49,6 +50,7 @@ interface DatabaseUserAttributes {
|
||||||
passwordHash: string;
|
passwordHash: string;
|
||||||
twoFactorEnabled: boolean;
|
twoFactorEnabled: boolean;
|
||||||
twoFactorSecret: string | null;
|
twoFactorSecret: string | null;
|
||||||
|
emailVerified: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface DatabaseSessionAttributes {
|
interface DatabaseSessionAttributes {
|
||||||
|
|
1
server/db/.gitignore
vendored
1
server/db/.gitignore
vendored
|
@ -1 +0,0 @@
|
||||||
names.json
|
|
|
@ -78,6 +78,9 @@ export const users = sqliteTable("user", {
|
||||||
.notNull()
|
.notNull()
|
||||||
.default(false),
|
.default(false),
|
||||||
twoFactorSecret: text("twoFactorSecret"),
|
twoFactorSecret: text("twoFactorSecret"),
|
||||||
|
emailVerified: integer("emailVerified", { mode: "boolean" })
|
||||||
|
.notNull()
|
||||||
|
.default(false),
|
||||||
});
|
});
|
||||||
|
|
||||||
// Sessions table
|
// Sessions table
|
||||||
|
@ -85,7 +88,7 @@ export const sessions = sqliteTable("session", {
|
||||||
id: text("id").primaryKey(), // has to be id not sessionId for lucia
|
id: text("id").primaryKey(), // has to be id not sessionId for lucia
|
||||||
userId: text("userId")
|
userId: text("userId")
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => users.id),
|
.references(() => users.id, { onDelete: "cascade" }),
|
||||||
expiresAt: integer("expiresAt").notNull(),
|
expiresAt: integer("expiresAt").notNull(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -99,6 +102,16 @@ export const userOrgs = sqliteTable("userOrgs", {
|
||||||
role: text("role").notNull(), // e.g., 'admin', 'member', etc.
|
role: text("role").notNull(), // e.g., 'admin', 'member', etc.
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const emailVerificationCodes = sqliteTable("emailVerificationCodes", {
|
||||||
|
id: integer("id").primaryKey({ autoIncrement: true }),
|
||||||
|
userId: text("userId")
|
||||||
|
.notNull()
|
||||||
|
.references(() => users.id, { onDelete: "cascade" }),
|
||||||
|
email: text("email").notNull(),
|
||||||
|
code: text("code").notNull(),
|
||||||
|
expiresAt: integer("expiresAt").notNull(),
|
||||||
|
});
|
||||||
|
|
||||||
// Define the model types for type inference
|
// Define the model types for type inference
|
||||||
export type Org = InferSelectModel<typeof orgs>;
|
export type Org = InferSelectModel<typeof orgs>;
|
||||||
export type User = InferSelectModel<typeof users>;
|
export type User = InferSelectModel<typeof users>;
|
||||||
|
@ -108,3 +121,6 @@ export type ExitNode = InferSelectModel<typeof exitNodes>;
|
||||||
export type Route = InferSelectModel<typeof routes>;
|
export type Route = InferSelectModel<typeof routes>;
|
||||||
export type Target = InferSelectModel<typeof targets>;
|
export type Target = InferSelectModel<typeof targets>;
|
||||||
export type Session = InferSelectModel<typeof sessions>;
|
export type Session = InferSelectModel<typeof sessions>;
|
||||||
|
export type EmailVerificationCode = InferSelectModel<
|
||||||
|
typeof emailVerificationCodes
|
||||||
|
>;
|
||||||
|
|
|
@ -62,6 +62,7 @@ const environment = {
|
||||||
EMAIL_SMTP_PORT: process.env.EMAIL_SMTP_PORT as string,
|
EMAIL_SMTP_PORT: process.env.EMAIL_SMTP_PORT as string,
|
||||||
EMAIL_SMTP_USER: process.env.EMAIL_SMTP_USER as string,
|
EMAIL_SMTP_USER: process.env.EMAIL_SMTP_USER as string,
|
||||||
EMAIL_SMTP_PASS: process.env.EMAIL_SMTP_PASS as string,
|
EMAIL_SMTP_PASS: process.env.EMAIL_SMTP_PASS as string,
|
||||||
|
EMAIL_NOREPLY: process.env.EMAIL_NOREPLY as string,
|
||||||
};
|
};
|
||||||
|
|
||||||
const parsedConfig = environmentSchema.safeParse(environment);
|
const parsedConfig = environmentSchema.safeParse(environment);
|
||||||
|
|
|
@ -30,7 +30,13 @@ app.prepare().then(() => {
|
||||||
externalServer.use(cors());
|
externalServer.use(cors());
|
||||||
externalServer.use(cookieParser());
|
externalServer.use(cookieParser());
|
||||||
externalServer.use(express.json());
|
externalServer.use(express.json());
|
||||||
externalServer.use(rateLimitMiddleware);
|
externalServer.use(
|
||||||
|
rateLimitMiddleware({
|
||||||
|
windowMin: 1,
|
||||||
|
max: 100,
|
||||||
|
type: "IP_ONLY",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
const prefix = `/api/v1`;
|
const prefix = `/api/v1`;
|
||||||
externalServer.use(prefix, unauthenticated);
|
externalServer.use(prefix, unauthenticated);
|
||||||
|
|
|
@ -11,6 +11,9 @@ export const errorHandlerMiddleware: ErrorRequestHandler = (
|
||||||
next: NextFunction,
|
next: NextFunction,
|
||||||
) => {
|
) => {
|
||||||
const statusCode = error.statusCode || HttpCode.INTERNAL_SERVER_ERROR;
|
const statusCode = error.statusCode || HttpCode.INTERNAL_SERVER_ERROR;
|
||||||
|
if (environment.ENVIRONMENT !== "prod") {
|
||||||
|
logger.error(error);
|
||||||
|
}
|
||||||
res?.status(statusCode).send({
|
res?.status(statusCode).send({
|
||||||
data: null,
|
data: null,
|
||||||
success: false,
|
success: false,
|
||||||
|
|
|
@ -2,3 +2,4 @@ export * from "./notFound";
|
||||||
export * from "./rateLimit";
|
export * from "./rateLimit";
|
||||||
export * from "./formatError";
|
export * from "./formatError";
|
||||||
export * from "./verifySession";
|
export * from "./verifySession";
|
||||||
|
export * from "./verifyUser";
|
||||||
|
|
|
@ -3,19 +3,47 @@ import createHttpError from "http-errors";
|
||||||
import { NextFunction, Request, Response } from "express";
|
import { NextFunction, Request, Response } from "express";
|
||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import HttpCode from "@server/types/HttpCode";
|
import HttpCode from "@server/types/HttpCode";
|
||||||
import environment from "@server/environment";
|
|
||||||
|
|
||||||
const limit = environment.RATE_LIMIT_MAX;
|
export function rateLimitMiddleware({
|
||||||
const minutes = environment.RATE_LIMIT_WINDOW_MIN;
|
windowMin,
|
||||||
|
max,
|
||||||
export const rateLimitMiddleware = rateLimit({
|
type,
|
||||||
windowMs: minutes * 60 * 1000,
|
skipCondition,
|
||||||
limit,
|
}: {
|
||||||
|
windowMin: number;
|
||||||
|
max: number;
|
||||||
|
type: "IP_ONLY" | "IP_AND_PATH";
|
||||||
|
skipCondition?: (req: Request, res: Response) => boolean;
|
||||||
|
}) {
|
||||||
|
if (type === "IP_AND_PATH") {
|
||||||
|
return rateLimit({
|
||||||
|
windowMs: windowMin * 60 * 1000,
|
||||||
|
max,
|
||||||
|
skip: skipCondition,
|
||||||
|
keyGenerator: (req: Request) => {
|
||||||
|
return `${req.ip}-${req.path}`;
|
||||||
|
},
|
||||||
handler: (req: Request, res: Response, next: NextFunction) => {
|
handler: (req: Request, res: Response, next: NextFunction) => {
|
||||||
const message = `Rate limit exceeded. You can make ${limit} requests every ${minutes} minute(s).`;
|
const message = `Rate limit exceeded. You can make ${max} requests every ${windowMin} minute(s).`;
|
||||||
|
logger.warn(
|
||||||
|
`Rate limit exceeded for IP ${req.ip} on path ${req.path}`,
|
||||||
|
);
|
||||||
|
return next(
|
||||||
|
createHttpError(HttpCode.TOO_MANY_REQUESTS, message),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return rateLimit({
|
||||||
|
windowMs: windowMin * 60 * 1000,
|
||||||
|
max,
|
||||||
|
skip: skipCondition,
|
||||||
|
handler: (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
const message = `Rate limit exceeded. You can make ${max} requests every ${windowMin} minute(s).`;
|
||||||
logger.warn(`Rate limit exceeded for IP ${req.ip}`);
|
logger.warn(`Rate limit exceeded for IP ${req.ip}`);
|
||||||
return next(createHttpError(HttpCode.TOO_MANY_REQUESTS, message));
|
return next(createHttpError(HttpCode.TOO_MANY_REQUESTS, message));
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export default rateLimitMiddleware;
|
export default rateLimitMiddleware;
|
||||||
|
|
|
@ -30,4 +30,6 @@ export const verifySessionMiddleware = async (
|
||||||
|
|
||||||
req.user = existingUser[0];
|
req.user = existingUser[0];
|
||||||
req.session = session;
|
req.session = session;
|
||||||
|
|
||||||
|
next();
|
||||||
};
|
};
|
||||||
|
|
41
server/middlewares/verifyUser.ts
Normal file
41
server/middlewares/verifyUser.ts
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
import { NextFunction, Response } from "express";
|
||||||
|
import ErrorResponse from "@server/types/ErrorResponse";
|
||||||
|
import { unauthorized, verifySession } from "@server/auth";
|
||||||
|
import { db } from "@server/db";
|
||||||
|
import { users } from "@server/db/schema";
|
||||||
|
import { eq } from "drizzle-orm";
|
||||||
|
import createHttpError from "http-errors";
|
||||||
|
import HttpCode from "@server/types/HttpCode";
|
||||||
|
|
||||||
|
export const verifySessionUserMiddleware = async (
|
||||||
|
req: any,
|
||||||
|
res: Response<ErrorResponse>,
|
||||||
|
next: NextFunction,
|
||||||
|
) => {
|
||||||
|
const { session, user } = await verifySession(req);
|
||||||
|
if (!session || !user) {
|
||||||
|
return next(unauthorized());
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingUser = await db
|
||||||
|
.select()
|
||||||
|
.from(users)
|
||||||
|
.where(eq(users.id, user.id));
|
||||||
|
|
||||||
|
if (!existingUser || !existingUser[0]) {
|
||||||
|
return next(
|
||||||
|
createHttpError(HttpCode.BAD_REQUEST, "User does not exist"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
req.user = existingUser[0];
|
||||||
|
req.session = session;
|
||||||
|
|
||||||
|
if (!existingUser[0].emailVerified) {
|
||||||
|
return next(
|
||||||
|
createHttpError(HttpCode.BAD_REQUEST, "Email is not verified"), // Might need to change the response type?
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
next();
|
||||||
|
};
|
|
@ -35,6 +35,7 @@ export async function disable2fa(
|
||||||
const { password } = parsedBody.data;
|
const { password } = parsedBody.data;
|
||||||
const user = req.user as User;
|
const user = req.user as User;
|
||||||
|
|
||||||
|
try {
|
||||||
const validPassword = await verify(user.passwordHash, password, {
|
const validPassword = await verify(user.passwordHash, password, {
|
||||||
memoryCost: 19456,
|
memoryCost: 19456,
|
||||||
timeCost: 2,
|
timeCost: 2,
|
||||||
|
@ -67,4 +68,12 @@ export async function disable2fa(
|
||||||
message: "Two-factor authentication disabled",
|
message: "Two-factor authentication disabled",
|
||||||
status: HttpCode.OK,
|
status: HttpCode.OK,
|
||||||
});
|
});
|
||||||
|
} catch (error) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.INTERNAL_SERVER_ERROR,
|
||||||
|
"Failed to disable two-factor authentication",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,3 +9,5 @@ export * from "./getUserOrgs";
|
||||||
export * from "./verifySiteAccess";
|
export * from "./verifySiteAccess";
|
||||||
export * from "./verifyResourceAccess";
|
export * from "./verifyResourceAccess";
|
||||||
export * from "./verifyTargetAccess";
|
export * from "./verifyTargetAccess";
|
||||||
|
export * from "./verifyEmail";
|
||||||
|
export * from "./requestEmailVerificationCode";
|
||||||
|
|
|
@ -4,6 +4,7 @@ import db from "@server/db";
|
||||||
import { users } from "@server/db/schema";
|
import { users } from "@server/db/schema";
|
||||||
import { sendEmail } from "@server/emails";
|
import { sendEmail } from "@server/emails";
|
||||||
import { VerifyEmail } from "@server/emails/templates/verifyEmailCode";
|
import { VerifyEmail } from "@server/emails/templates/verifyEmailCode";
|
||||||
|
import logger from "@server/logger";
|
||||||
import HttpCode from "@server/types/HttpCode";
|
import HttpCode from "@server/types/HttpCode";
|
||||||
import response from "@server/utils/response";
|
import response from "@server/utils/response";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
|
@ -24,6 +25,7 @@ export type LoginBody = z.infer<typeof loginBodySchema>;
|
||||||
|
|
||||||
export type LoginResponse = {
|
export type LoginResponse = {
|
||||||
codeRequested?: boolean;
|
codeRequested?: boolean;
|
||||||
|
emailVerificationRequired?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function login(
|
export async function login(
|
||||||
|
@ -44,6 +46,7 @@ export async function login(
|
||||||
|
|
||||||
const { email, password, code } = parsedBody.data;
|
const { email, password, code } = parsedBody.data;
|
||||||
|
|
||||||
|
try {
|
||||||
const { session: existingSession } = await verifySession(req);
|
const { session: existingSession } = await verifySession(req);
|
||||||
if (existingSession) {
|
if (existingSession) {
|
||||||
return response<null>(res, {
|
return response<null>(res, {
|
||||||
|
@ -70,12 +73,16 @@ export async function login(
|
||||||
|
|
||||||
const existingUser = existingUserRes[0];
|
const existingUser = existingUserRes[0];
|
||||||
|
|
||||||
const validPassword = await verify(existingUser.passwordHash, password, {
|
const validPassword = await verify(
|
||||||
|
existingUser.passwordHash,
|
||||||
|
password,
|
||||||
|
{
|
||||||
memoryCost: 19456,
|
memoryCost: 19456,
|
||||||
timeCost: 2,
|
timeCost: 2,
|
||||||
outputLen: 32,
|
outputLen: 32,
|
||||||
parallelism: 1,
|
parallelism: 1,
|
||||||
});
|
},
|
||||||
|
);
|
||||||
if (!validPassword) {
|
if (!validPassword) {
|
||||||
await new Promise((resolve) => setTimeout(resolve, 250)); // delay to prevent brute force attacks
|
await new Promise((resolve) => setTimeout(resolve, 250)); // delay to prevent brute force attacks
|
||||||
return next(
|
return next(
|
||||||
|
@ -128,6 +135,16 @@ export async function login(
|
||||||
lucia.createSessionCookie(session.id).serialize(),
|
lucia.createSessionCookie(session.id).serialize(),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (!existingUser.emailVerified) {
|
||||||
|
return response<LoginResponse>(res, {
|
||||||
|
data: { emailVerificationRequired: true },
|
||||||
|
success: true,
|
||||||
|
error: false,
|
||||||
|
message: "Email verification code sent",
|
||||||
|
status: HttpCode.ACCEPTED,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return response<null>(res, {
|
return response<null>(res, {
|
||||||
data: null,
|
data: null,
|
||||||
success: true,
|
success: true,
|
||||||
|
@ -135,4 +152,12 @@ export async function login(
|
||||||
message: "Logged in successfully",
|
message: "Logged in successfully",
|
||||||
status: HttpCode.OK,
|
status: HttpCode.OK,
|
||||||
});
|
});
|
||||||
|
} catch (e) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.INTERNAL_SERVER_ERROR,
|
||||||
|
"Failed to authenticate user",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@ import { lucia } from "@server/auth";
|
||||||
import createHttpError from "http-errors";
|
import createHttpError from "http-errors";
|
||||||
import HttpCode from "@server/types/HttpCode";
|
import HttpCode from "@server/types/HttpCode";
|
||||||
import response from "@server/utils/response";
|
import response from "@server/utils/response";
|
||||||
|
import logger from "@server/logger";
|
||||||
|
|
||||||
export async function logout(
|
export async function logout(
|
||||||
req: Request,
|
req: Request,
|
||||||
|
@ -20,8 +21,12 @@ export async function logout(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
await lucia.invalidateSession(sessionId);
|
await lucia.invalidateSession(sessionId);
|
||||||
res.setHeader("Set-Cookie", lucia.createBlankSessionCookie().serialize());
|
res.setHeader(
|
||||||
|
"Set-Cookie",
|
||||||
|
lucia.createBlankSessionCookie().serialize(),
|
||||||
|
);
|
||||||
|
|
||||||
return response<null>(res, {
|
return response<null>(res, {
|
||||||
data: null,
|
data: null,
|
||||||
|
@ -30,4 +35,12 @@ export async function logout(
|
||||||
message: "Logged out successfully",
|
message: "Logged out successfully",
|
||||||
status: HttpCode.OK,
|
status: HttpCode.OK,
|
||||||
});
|
});
|
||||||
|
} catch (error) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.INTERNAL_SERVER_ERROR,
|
||||||
|
"Failed to log out",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
50
server/routers/auth/requestEmailVerificationCode.ts
Normal file
50
server/routers/auth/requestEmailVerificationCode.ts
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
import { Request, Response, NextFunction } from "express";
|
||||||
|
import createHttpError from "http-errors";
|
||||||
|
import HttpCode from "@server/types/HttpCode";
|
||||||
|
import { response } from "@server/utils";
|
||||||
|
import { User } from "@server/db/schema";
|
||||||
|
import { sendEmailVerificationCode } from "./sendEmailVerificationCode";
|
||||||
|
|
||||||
|
export type RequestEmailVerificationCodeResponse = {
|
||||||
|
codeSent: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function requestEmailVerificationCode(
|
||||||
|
req: Request,
|
||||||
|
res: Response,
|
||||||
|
next: NextFunction,
|
||||||
|
): Promise<any> {
|
||||||
|
try {
|
||||||
|
const user = req.user as User;
|
||||||
|
|
||||||
|
if (user.emailVerified) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.BAD_REQUEST,
|
||||||
|
"Email is already verified",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await sendEmailVerificationCode(user.email, user.id);
|
||||||
|
|
||||||
|
return response<RequestEmailVerificationCodeResponse>(res, {
|
||||||
|
data: {
|
||||||
|
codeSent: true,
|
||||||
|
},
|
||||||
|
status: HttpCode.OK,
|
||||||
|
success: true,
|
||||||
|
error: false,
|
||||||
|
message: `Email verification code sent to ${user.email}`,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.INTERNAL_SERVER_ERROR,
|
||||||
|
"Failed to send email verification code",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default requestEmailVerificationCode;
|
|
@ -43,6 +43,7 @@ export async function requestTotpSecret(
|
||||||
|
|
||||||
const user = req.user as User;
|
const user = req.user as User;
|
||||||
|
|
||||||
|
try {
|
||||||
const validPassword = await verify(user.passwordHash, password, {
|
const validPassword = await verify(user.passwordHash, password, {
|
||||||
memoryCost: 19456,
|
memoryCost: 19456,
|
||||||
timeCost: 2,
|
timeCost: 2,
|
||||||
|
@ -83,4 +84,12 @@ export async function requestTotpSecret(
|
||||||
message: "TOTP secret generated successfully",
|
message: "TOTP secret generated successfully",
|
||||||
status: HttpCode.OK,
|
status: HttpCode.OK,
|
||||||
});
|
});
|
||||||
|
} catch (error) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.INTERNAL_SERVER_ERROR,
|
||||||
|
"Failed to generate TOTP secret",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
41
server/routers/auth/sendEmailVerificationCode.ts
Normal file
41
server/routers/auth/sendEmailVerificationCode.ts
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
import { TimeSpan, createDate } from "oslo";
|
||||||
|
import { generateRandomString, alphabet } from "oslo/crypto";
|
||||||
|
import db from "@server/db";
|
||||||
|
import { users, emailVerificationCodes } from "@server/db/schema";
|
||||||
|
import { eq } from "drizzle-orm";
|
||||||
|
import { sendEmail } from "@server/emails";
|
||||||
|
import VerifyEmail from "@server/emails/templates/verifyEmailCode";
|
||||||
|
import env from "@server/environment";
|
||||||
|
|
||||||
|
export async function sendEmailVerificationCode(
|
||||||
|
email: string,
|
||||||
|
userId: string,
|
||||||
|
): Promise<void> {
|
||||||
|
const code = await generateEmailVerificationCode(userId, email);
|
||||||
|
|
||||||
|
await sendEmail(VerifyEmail({ username: email, verificationCode: code }), {
|
||||||
|
to: email,
|
||||||
|
from: env.EMAIL_NOREPLY!,
|
||||||
|
subject: "Verify your email address",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function generateEmailVerificationCode(
|
||||||
|
userId: string,
|
||||||
|
email: string,
|
||||||
|
): Promise<string> {
|
||||||
|
await db
|
||||||
|
.delete(emailVerificationCodes)
|
||||||
|
.where(eq(emailVerificationCodes.userId, userId));
|
||||||
|
|
||||||
|
const code = generateRandomString(8, alphabet("0-9"));
|
||||||
|
|
||||||
|
await db.insert(emailVerificationCodes).values({
|
||||||
|
userId,
|
||||||
|
email,
|
||||||
|
code,
|
||||||
|
expiresAt: createDate(new TimeSpan(15, "m")).getTime(),
|
||||||
|
});
|
||||||
|
|
||||||
|
return code;
|
||||||
|
}
|
|
@ -10,6 +10,8 @@ import { fromError } from "zod-validation-error";
|
||||||
import createHttpError from "http-errors";
|
import createHttpError from "http-errors";
|
||||||
import response from "@server/utils/response";
|
import response from "@server/utils/response";
|
||||||
import { SqliteError } from "better-sqlite3";
|
import { SqliteError } from "better-sqlite3";
|
||||||
|
import { sendEmailVerificationCode } from "./sendEmailVerificationCode";
|
||||||
|
import logger from "@server/logger";
|
||||||
|
|
||||||
export const signupBodySchema = z.object({
|
export const signupBodySchema = z.object({
|
||||||
email: z.string().email(),
|
email: z.string().email(),
|
||||||
|
@ -28,6 +30,10 @@ export const signupBodySchema = z.object({
|
||||||
|
|
||||||
export type SignUpBody = z.infer<typeof signupBodySchema>;
|
export type SignUpBody = z.infer<typeof signupBodySchema>;
|
||||||
|
|
||||||
|
export type SignUpResponse = {
|
||||||
|
emailVerificationRequired: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
export async function signup(
|
export async function signup(
|
||||||
req: Request,
|
req: Request,
|
||||||
res: Response,
|
res: Response,
|
||||||
|
@ -68,11 +74,15 @@ export async function signup(
|
||||||
lucia.createSessionCookie(session.id).serialize(),
|
lucia.createSessionCookie(session.id).serialize(),
|
||||||
);
|
);
|
||||||
|
|
||||||
return response<null>(res, {
|
sendEmailVerificationCode(email, userId);
|
||||||
data: null,
|
|
||||||
|
return response<SignUpResponse>(res, {
|
||||||
|
data: {
|
||||||
|
emailVerificationRequired: true,
|
||||||
|
},
|
||||||
success: true,
|
success: true,
|
||||||
error: false,
|
error: false,
|
||||||
message: "User created successfully",
|
message: `User created successfully. We sent an email to ${email} with a verification code.`,
|
||||||
status: HttpCode.OK,
|
status: HttpCode.OK,
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
109
server/routers/auth/verifyEmail.ts
Normal file
109
server/routers/auth/verifyEmail.ts
Normal file
|
@ -0,0 +1,109 @@
|
||||||
|
import { Request, Response, NextFunction } from "express";
|
||||||
|
import createHttpError from "http-errors";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { fromError } from "zod-validation-error";
|
||||||
|
import HttpCode from "@server/types/HttpCode";
|
||||||
|
import { response } from "@server/utils";
|
||||||
|
import { db } from "@server/db";
|
||||||
|
import { User, emailVerificationCodes, users } from "@server/db/schema";
|
||||||
|
import { eq } from "drizzle-orm";
|
||||||
|
import { isWithinExpirationDate } from "oslo";
|
||||||
|
|
||||||
|
export const verifyEmailBody = z.object({
|
||||||
|
code: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type VerifyEmailBody = z.infer<typeof verifyEmailBody>;
|
||||||
|
|
||||||
|
export type VerifyEmailResponse = {
|
||||||
|
valid: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function verifyEmail(
|
||||||
|
req: Request,
|
||||||
|
res: Response,
|
||||||
|
next: NextFunction,
|
||||||
|
): Promise<any> {
|
||||||
|
const parsedBody = verifyEmailBody.safeParse(req.body);
|
||||||
|
|
||||||
|
if (!parsedBody.success) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.BAD_REQUEST,
|
||||||
|
fromError(parsedBody.error).toString(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { code } = parsedBody.data;
|
||||||
|
|
||||||
|
const user = req.user as User;
|
||||||
|
|
||||||
|
if (user.emailVerified) {
|
||||||
|
return next(
|
||||||
|
createHttpError(HttpCode.BAD_REQUEST, "Email is already verified"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const valid = await isValidCode(user, code);
|
||||||
|
|
||||||
|
if (valid) {
|
||||||
|
await db
|
||||||
|
.delete(emailVerificationCodes)
|
||||||
|
.where(eq(emailVerificationCodes.userId, user.id));
|
||||||
|
|
||||||
|
await db
|
||||||
|
.update(users)
|
||||||
|
.set({
|
||||||
|
emailVerified: true,
|
||||||
|
})
|
||||||
|
.where(eq(users.id, user.id));
|
||||||
|
}
|
||||||
|
|
||||||
|
return response<VerifyEmailResponse>(res, {
|
||||||
|
success: true,
|
||||||
|
error: false,
|
||||||
|
message: valid ? "Code is valid" : "Code is invalid",
|
||||||
|
status: HttpCode.OK,
|
||||||
|
data: {
|
||||||
|
valid,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.INTERNAL_SERVER_ERROR,
|
||||||
|
"Failed to verify email",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default verifyEmail;
|
||||||
|
|
||||||
|
async function isValidCode(user: User, code: string): Promise<boolean> {
|
||||||
|
const codeRecord = await db
|
||||||
|
.select()
|
||||||
|
.from(emailVerificationCodes)
|
||||||
|
.where(eq(emailVerificationCodes.userId, user.id))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (user.email !== codeRecord[0].email) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (codeRecord.length === 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (codeRecord[0].code !== code) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isWithinExpirationDate(new Date(codeRecord[0].expiresAt))) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
|
@ -58,6 +58,7 @@ export async function verifyTotp(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
const totpController = new TOTPController();
|
const totpController = new TOTPController();
|
||||||
const valid = await totpController.verify(
|
const valid = await totpController.verify(
|
||||||
code,
|
code,
|
||||||
|
@ -81,4 +82,12 @@ export async function verifyTotp(
|
||||||
: "Code is invalid",
|
: "Code is invalid",
|
||||||
status: HttpCode.OK,
|
status: HttpCode.OK,
|
||||||
});
|
});
|
||||||
|
} catch (error) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.INTERNAL_SERVER_ERROR,
|
||||||
|
"Failed to verify two-factor authentication code",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,8 +6,18 @@ import * as target from "./target";
|
||||||
import * as user from "./user";
|
import * as user from "./user";
|
||||||
import * as auth from "./auth";
|
import * as auth from "./auth";
|
||||||
import HttpCode from "@server/types/HttpCode";
|
import HttpCode from "@server/types/HttpCode";
|
||||||
import { verifySessionMiddleware } from "@server/middlewares";
|
import {
|
||||||
import { verifyOrgAccess, getUserOrgs, verifySiteAccess, verifyResourceAccess, verifyTargetAccess } from "./auth";
|
rateLimitMiddleware,
|
||||||
|
verifySessionMiddleware,
|
||||||
|
verifySessionUserMiddleware,
|
||||||
|
} from "@server/middlewares";
|
||||||
|
import {
|
||||||
|
verifyOrgAccess,
|
||||||
|
getUserOrgs,
|
||||||
|
verifySiteAccess,
|
||||||
|
verifyResourceAccess,
|
||||||
|
verifyTargetAccess,
|
||||||
|
} from "./auth";
|
||||||
|
|
||||||
// Root routes
|
// Root routes
|
||||||
export const unauthenticated = Router();
|
export const unauthenticated = Router();
|
||||||
|
@ -18,7 +28,17 @@ unauthenticated.get("/", (_, res) => {
|
||||||
|
|
||||||
// Authenticated Root routes
|
// Authenticated Root routes
|
||||||
export const authenticated = Router();
|
export const authenticated = Router();
|
||||||
authenticated.use(verifySessionMiddleware);
|
authenticated.use(verifySessionUserMiddleware);
|
||||||
|
unauthenticated.use(
|
||||||
|
rateLimitMiddleware({
|
||||||
|
windowMin: 60,
|
||||||
|
max: 5,
|
||||||
|
type: "IP_AND_PATH",
|
||||||
|
skipCondition: (req) => {
|
||||||
|
return !["/auth/request-email-code"].includes(req.path);
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
authenticated.put("/org", getUserOrgs, org.createOrg);
|
authenticated.put("/org", getUserOrgs, org.createOrg);
|
||||||
authenticated.get("/orgs", getUserOrgs, org.listOrgs); // TODO we need to check the orgs here
|
authenticated.get("/orgs", getUserOrgs, org.listOrgs); // TODO we need to check the orgs here
|
||||||
|
@ -32,18 +52,54 @@ authenticated.get("/site/:siteId", verifySiteAccess, site.getSite);
|
||||||
authenticated.post("/site/:siteId", verifySiteAccess, site.updateSite);
|
authenticated.post("/site/:siteId", verifySiteAccess, site.updateSite);
|
||||||
authenticated.delete("/site/:siteId", verifySiteAccess, site.deleteSite);
|
authenticated.delete("/site/:siteId", verifySiteAccess, site.deleteSite);
|
||||||
|
|
||||||
authenticated.put("/org/:orgId/site/:siteId/resource", verifyOrgAccess, resource.createResource);
|
authenticated.put(
|
||||||
|
"/org/:orgId/site/:siteId/resource",
|
||||||
|
verifyOrgAccess,
|
||||||
|
resource.createResource,
|
||||||
|
);
|
||||||
authenticated.get("/site/:siteId/resources", resource.listResources);
|
authenticated.get("/site/:siteId/resources", resource.listResources);
|
||||||
authenticated.get("/org/:orgId/resources", verifyOrgAccess, resource.listResources);
|
authenticated.get(
|
||||||
authenticated.get("/resource/:resourceId", verifyResourceAccess, resource.getResource);
|
"/org/:orgId/resources",
|
||||||
authenticated.post("/resource/:resourceId", verifyResourceAccess, resource.updateResource);
|
verifyOrgAccess,
|
||||||
authenticated.delete("/resource/:resourceId", verifyResourceAccess, resource.deleteResource);
|
resource.listResources,
|
||||||
|
);
|
||||||
|
authenticated.get(
|
||||||
|
"/resource/:resourceId",
|
||||||
|
verifyResourceAccess,
|
||||||
|
resource.getResource,
|
||||||
|
);
|
||||||
|
authenticated.post(
|
||||||
|
"/resource/:resourceId",
|
||||||
|
verifyResourceAccess,
|
||||||
|
resource.updateResource,
|
||||||
|
);
|
||||||
|
authenticated.delete(
|
||||||
|
"/resource/:resourceId",
|
||||||
|
verifyResourceAccess,
|
||||||
|
resource.deleteResource,
|
||||||
|
);
|
||||||
|
|
||||||
authenticated.put("/resource/:resourceId/target", verifyResourceAccess, target.createTarget);
|
authenticated.put(
|
||||||
authenticated.get("/resource/:resourceId/targets", verifyResourceAccess, target.listTargets);
|
"/resource/:resourceId/target",
|
||||||
|
verifyResourceAccess,
|
||||||
|
target.createTarget,
|
||||||
|
);
|
||||||
|
authenticated.get(
|
||||||
|
"/resource/:resourceId/targets",
|
||||||
|
verifyResourceAccess,
|
||||||
|
target.listTargets,
|
||||||
|
);
|
||||||
authenticated.get("/target/:targetId", verifyTargetAccess, target.getTarget);
|
authenticated.get("/target/:targetId", verifyTargetAccess, target.getTarget);
|
||||||
authenticated.post("/target/:targetId", verifyTargetAccess, target.updateTarget);
|
authenticated.post(
|
||||||
authenticated.delete("/target/:targetId", verifyTargetAccess, target.deleteTarget);
|
"/target/:targetId",
|
||||||
|
verifyTargetAccess,
|
||||||
|
target.updateTarget,
|
||||||
|
);
|
||||||
|
authenticated.delete(
|
||||||
|
"/target/:targetId",
|
||||||
|
verifyTargetAccess,
|
||||||
|
target.deleteTarget,
|
||||||
|
);
|
||||||
|
|
||||||
authenticated.get("/users", user.listUsers);
|
authenticated.get("/users", user.listUsers);
|
||||||
// authenticated.get("/org/:orgId/users", user.???); // TODO: Implement this
|
// authenticated.get("/org/:orgId/users", user.???); // TODO: Implement this
|
||||||
|
@ -57,3 +113,13 @@ unauthenticated.post("/auth/logout", auth.logout);
|
||||||
authenticated.post("/auth/verify-totp", auth.verifyTotp);
|
authenticated.post("/auth/verify-totp", auth.verifyTotp);
|
||||||
authenticated.post("/auth/request-totp-secret", auth.requestTotpSecret);
|
authenticated.post("/auth/request-totp-secret", auth.requestTotpSecret);
|
||||||
authenticated.post("/auth/disable-2fa", auth.disable2fa);
|
authenticated.post("/auth/disable-2fa", auth.disable2fa);
|
||||||
|
unauthenticated.post(
|
||||||
|
"/auth/verify-email",
|
||||||
|
verifySessionMiddleware,
|
||||||
|
auth.verifyEmail,
|
||||||
|
);
|
||||||
|
unauthenticated.post(
|
||||||
|
"/auth/request-email-code",
|
||||||
|
verifySessionMiddleware,
|
||||||
|
auth.requestEmailVerificationCode,
|
||||||
|
);
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue