verify email workflow working

This commit is contained in:
Milo Schwartz 2024-10-04 23:14:40 -04:00
parent e176295593
commit 76eeb335a3
No known key found for this signature in database
23 changed files with 16363 additions and 15802 deletions

31336
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -15,7 +15,6 @@
"dependencies": {
"@lucia-auth/adapter-drizzle": "1.1.0",
"@node-rs/argon2": "1.8.3",
"@node-rs/argon2-linux-x64-gnu": "1.8.3",
"@react-email/components": "0.0.25",
"@react-email/tailwind": "0.1.0",
"axios": "1.7.7",

View file

@ -15,6 +15,7 @@ export const lucia = new Lucia(adapter, {
email: attributes.email,
twoFactorEnabled: attributes.twoFactorEnabled,
twoFactorSecret: attributes.twoFactorSecret,
emailVerified: attributes.emailVerified,
};
},
// getSessionAttributes: (attributes) => {
@ -49,6 +50,7 @@ interface DatabaseUserAttributes {
passwordHash: string;
twoFactorEnabled: boolean;
twoFactorSecret: string | null;
emailVerified: boolean;
}
interface DatabaseSessionAttributes {

View file

@ -1 +0,0 @@
names.json

View file

@ -78,6 +78,9 @@ export const users = sqliteTable("user", {
.notNull()
.default(false),
twoFactorSecret: text("twoFactorSecret"),
emailVerified: integer("emailVerified", { mode: "boolean" })
.notNull()
.default(false),
});
// Sessions table
@ -85,7 +88,7 @@ export const sessions = sqliteTable("session", {
id: text("id").primaryKey(), // has to be id not sessionId for lucia
userId: text("userId")
.notNull()
.references(() => users.id),
.references(() => users.id, { onDelete: "cascade" }),
expiresAt: integer("expiresAt").notNull(),
});
@ -99,6 +102,16 @@ export const userOrgs = sqliteTable("userOrgs", {
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
export type Org = InferSelectModel<typeof orgs>;
export type User = InferSelectModel<typeof users>;
@ -108,3 +121,6 @@ export type ExitNode = InferSelectModel<typeof exitNodes>;
export type Route = InferSelectModel<typeof routes>;
export type Target = InferSelectModel<typeof targets>;
export type Session = InferSelectModel<typeof sessions>;
export type EmailVerificationCode = InferSelectModel<
typeof emailVerificationCodes
>;

View file

@ -62,6 +62,7 @@ const environment = {
EMAIL_SMTP_PORT: process.env.EMAIL_SMTP_PORT as string,
EMAIL_SMTP_USER: process.env.EMAIL_SMTP_USER 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);

View file

@ -30,7 +30,13 @@ app.prepare().then(() => {
externalServer.use(cors());
externalServer.use(cookieParser());
externalServer.use(express.json());
externalServer.use(rateLimitMiddleware);
externalServer.use(
rateLimitMiddleware({
windowMin: 1,
max: 100,
type: "IP_ONLY",
}),
);
const prefix = `/api/v1`;
externalServer.use(prefix, unauthenticated);

View file

@ -11,6 +11,9 @@ export const errorHandlerMiddleware: ErrorRequestHandler = (
next: NextFunction,
) => {
const statusCode = error.statusCode || HttpCode.INTERNAL_SERVER_ERROR;
if (environment.ENVIRONMENT !== "prod") {
logger.error(error);
}
res?.status(statusCode).send({
data: null,
success: false,

View file

@ -2,3 +2,4 @@ export * from "./notFound";
export * from "./rateLimit";
export * from "./formatError";
export * from "./verifySession";
export * from "./verifyUser";

View file

@ -3,19 +3,47 @@ import createHttpError from "http-errors";
import { NextFunction, Request, Response } from "express";
import logger from "@server/logger";
import HttpCode from "@server/types/HttpCode";
import environment from "@server/environment";
const limit = environment.RATE_LIMIT_MAX;
const minutes = environment.RATE_LIMIT_WINDOW_MIN;
export const rateLimitMiddleware = rateLimit({
windowMs: minutes * 60 * 1000,
limit,
handler: (req: Request, res: Response, next: NextFunction) => {
const message = `Rate limit exceeded. You can make ${limit} requests every ${minutes} minute(s).`;
logger.warn(`Rate limit exceeded for IP ${req.ip}`);
return next(createHttpError(HttpCode.TOO_MANY_REQUESTS, message));
},
});
export function rateLimitMiddleware({
windowMin,
max,
type,
skipCondition,
}: {
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) => {
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}`);
return next(createHttpError(HttpCode.TOO_MANY_REQUESTS, message));
},
});
}
export default rateLimitMiddleware;

View file

@ -30,4 +30,6 @@ export const verifySessionMiddleware = async (
req.user = existingUser[0];
req.session = session;
next();
};

View 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();
};

View file

@ -35,36 +35,45 @@ export async function disable2fa(
const { password } = parsedBody.data;
const user = req.user as User;
const validPassword = await verify(user.passwordHash, password, {
memoryCost: 19456,
timeCost: 2,
outputLen: 32,
parallelism: 1,
});
if (!validPassword) {
await new Promise((resolve) => setTimeout(resolve, 250)); // delay to prevent brute force attacks
return next(unauthorized());
}
try {
const validPassword = await verify(user.passwordHash, password, {
memoryCost: 19456,
timeCost: 2,
outputLen: 32,
parallelism: 1,
});
if (!validPassword) {
await new Promise((resolve) => setTimeout(resolve, 250)); // delay to prevent brute force attacks
return next(unauthorized());
}
if (!user.twoFactorEnabled) {
if (!user.twoFactorEnabled) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
"Two-factor authentication is already disabled",
),
);
}
await db
.update(users)
.set({ twoFactorEnabled: false })
.where(eq(users.id, user.id));
return response<null>(res, {
data: null,
success: true,
error: false,
message: "Two-factor authentication disabled",
status: HttpCode.OK,
});
} catch (error) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
"Two-factor authentication is already disabled",
HttpCode.INTERNAL_SERVER_ERROR,
"Failed to disable two-factor authentication",
),
);
}
await db
.update(users)
.set({ twoFactorEnabled: false })
.where(eq(users.id, user.id));
return response<null>(res, {
data: null,
success: true,
error: false,
message: "Two-factor authentication disabled",
status: HttpCode.OK,
});
}

View file

@ -9,3 +9,5 @@ export * from "./getUserOrgs";
export * from "./verifySiteAccess";
export * from "./verifyResourceAccess";
export * from "./verifyTargetAccess";
export * from "./verifyEmail";
export * from "./requestEmailVerificationCode";

View file

@ -4,6 +4,7 @@ import db from "@server/db";
import { users } from "@server/db/schema";
import { sendEmail } from "@server/emails";
import { VerifyEmail } from "@server/emails/templates/verifyEmailCode";
import logger from "@server/logger";
import HttpCode from "@server/types/HttpCode";
import response from "@server/utils/response";
import { eq } from "drizzle-orm";
@ -24,6 +25,7 @@ export type LoginBody = z.infer<typeof loginBodySchema>;
export type LoginResponse = {
codeRequested?: boolean;
emailVerificationRequired?: boolean;
};
export async function login(
@ -44,95 +46,118 @@ export async function login(
const { email, password, code } = parsedBody.data;
const { session: existingSession } = await verifySession(req);
if (existingSession) {
return response<null>(res, {
data: null,
success: true,
error: false,
message: "Already logged in",
status: HttpCode.OK,
});
}
const existingUserRes = await db
.select()
.from(users)
.where(eq(users.email, email));
if (!existingUserRes || !existingUserRes.length) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
"Username or password is incorrect",
),
);
}
const existingUser = existingUserRes[0];
const validPassword = await verify(existingUser.passwordHash, password, {
memoryCost: 19456,
timeCost: 2,
outputLen: 32,
parallelism: 1,
});
if (!validPassword) {
await new Promise((resolve) => setTimeout(resolve, 250)); // delay to prevent brute force attacks
return next(
createHttpError(
HttpCode.BAD_REQUEST,
"Username or password is incorrect",
),
);
}
if (existingUser.twoFactorEnabled) {
if (!code) {
return response<{ codeRequested: boolean }>(res, {
data: { codeRequested: true },
try {
const { session: existingSession } = await verifySession(req);
if (existingSession) {
return response<null>(res, {
data: null,
success: true,
error: false,
message: "Two-factor authentication required",
status: HttpCode.ACCEPTED,
message: "Already logged in",
status: HttpCode.OK,
});
}
if (!existingUser.twoFactorSecret) {
const existingUserRes = await db
.select()
.from(users)
.where(eq(users.email, email));
if (!existingUserRes || !existingUserRes.length) {
return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
"Failed to authenticate user",
HttpCode.BAD_REQUEST,
"Username or password is incorrect",
),
);
}
const validOTP = await new TOTPController().verify(
code,
decodeHex(existingUser.twoFactorSecret),
);
const existingUser = existingUserRes[0];
if (!validOTP) {
const validPassword = await verify(
existingUser.passwordHash,
password,
{
memoryCost: 19456,
timeCost: 2,
outputLen: 32,
parallelism: 1,
},
);
if (!validPassword) {
await new Promise((resolve) => setTimeout(resolve, 250)); // delay to prevent brute force attacks
return next(
createHttpError(
HttpCode.BAD_REQUEST,
"The two-factor code you entered is incorrect",
"Username or password is incorrect",
),
);
}
if (existingUser.twoFactorEnabled) {
if (!code) {
return response<{ codeRequested: boolean }>(res, {
data: { codeRequested: true },
success: true,
error: false,
message: "Two-factor authentication required",
status: HttpCode.ACCEPTED,
});
}
if (!existingUser.twoFactorSecret) {
return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
"Failed to authenticate user",
),
);
}
const validOTP = await new TOTPController().verify(
code,
decodeHex(existingUser.twoFactorSecret),
);
if (!validOTP) {
await new Promise((resolve) => setTimeout(resolve, 250)); // delay to prevent brute force attacks
return next(
createHttpError(
HttpCode.BAD_REQUEST,
"The two-factor code you entered is incorrect",
),
);
}
}
const session = await lucia.createSession(existingUser.id, {});
res.appendHeader(
"Set-Cookie",
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, {
data: null,
success: true,
error: false,
message: "Logged in successfully",
status: HttpCode.OK,
});
} catch (e) {
return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
"Failed to authenticate user",
),
);
}
const session = await lucia.createSession(existingUser.id, {});
res.appendHeader(
"Set-Cookie",
lucia.createSessionCookie(session.id).serialize(),
);
return response<null>(res, {
data: null,
success: true,
error: false,
message: "Logged in successfully",
status: HttpCode.OK,
});
}

View file

@ -3,6 +3,7 @@ import { lucia } from "@server/auth";
import createHttpError from "http-errors";
import HttpCode from "@server/types/HttpCode";
import response from "@server/utils/response";
import logger from "@server/logger";
export async function logout(
req: Request,
@ -20,14 +21,26 @@ export async function logout(
);
}
await lucia.invalidateSession(sessionId);
res.setHeader("Set-Cookie", lucia.createBlankSessionCookie().serialize());
try {
await lucia.invalidateSession(sessionId);
res.setHeader(
"Set-Cookie",
lucia.createBlankSessionCookie().serialize(),
);
return response<null>(res, {
data: null,
success: true,
error: false,
message: "Logged out successfully",
status: HttpCode.OK,
});
return response<null>(res, {
data: null,
success: true,
error: false,
message: "Logged out successfully",
status: HttpCode.OK,
});
} catch (error) {
return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
"Failed to log out",
),
);
}
}

View 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;

View file

@ -43,44 +43,53 @@ export async function requestTotpSecret(
const user = req.user as User;
const validPassword = await verify(user.passwordHash, password, {
memoryCost: 19456,
timeCost: 2,
outputLen: 32,
parallelism: 1,
});
if (!validPassword) {
await new Promise((resolve) => setTimeout(resolve, 250)); // delay to prevent brute force attacks
return next(unauthorized());
}
try {
const validPassword = await verify(user.passwordHash, password, {
memoryCost: 19456,
timeCost: 2,
outputLen: 32,
parallelism: 1,
});
if (!validPassword) {
await new Promise((resolve) => setTimeout(resolve, 250)); // delay to prevent brute force attacks
return next(unauthorized());
}
if (user.twoFactorEnabled) {
if (user.twoFactorEnabled) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
"User has already enabled two-factor authentication",
),
);
}
const hex = crypto.getRandomValues(new Uint8Array(20));
const secret = encodeHex(hex);
const uri = createTOTPKeyURI(env.APP_NAME, user.email, hex);
await db
.update(users)
.set({
twoFactorSecret: secret,
})
.where(eq(users.id, user.id));
return response<RequestTotpSecretResponse>(res, {
data: {
secret: uri,
},
success: true,
error: false,
message: "TOTP secret generated successfully",
status: HttpCode.OK,
});
} catch (error) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
"User has already enabled two-factor authentication",
HttpCode.INTERNAL_SERVER_ERROR,
"Failed to generate TOTP secret",
),
);
}
const hex = crypto.getRandomValues(new Uint8Array(20));
const secret = encodeHex(hex);
const uri = createTOTPKeyURI(env.APP_NAME, user.email, hex);
await db
.update(users)
.set({
twoFactorSecret: secret,
})
.where(eq(users.id, user.id));
return response<RequestTotpSecretResponse>(res, {
data: {
secret: uri,
},
success: true,
error: false,
message: "TOTP secret generated successfully",
status: HttpCode.OK,
});
}

View 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;
}

View file

@ -10,6 +10,8 @@ import { fromError } from "zod-validation-error";
import createHttpError from "http-errors";
import response from "@server/utils/response";
import { SqliteError } from "better-sqlite3";
import { sendEmailVerificationCode } from "./sendEmailVerificationCode";
import logger from "@server/logger";
export const signupBodySchema = z.object({
email: z.string().email(),
@ -28,6 +30,10 @@ export const signupBodySchema = z.object({
export type SignUpBody = z.infer<typeof signupBodySchema>;
export type SignUpResponse = {
emailVerificationRequired: boolean;
};
export async function signup(
req: Request,
res: Response,
@ -68,11 +74,15 @@ export async function signup(
lucia.createSessionCookie(session.id).serialize(),
);
return response<null>(res, {
data: null,
sendEmailVerificationCode(email, userId);
return response<SignUpResponse>(res, {
data: {
emailVerificationRequired: true,
},
success: true,
error: false,
message: "User created successfully",
message: `User created successfully. We sent an email to ${email} with a verification code.`,
status: HttpCode.OK,
});
} catch (e) {

View 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;
}

View file

@ -58,27 +58,36 @@ export async function verifyTotp(
);
}
const totpController = new TOTPController();
const valid = await totpController.verify(
code,
decodeHex(user.twoFactorSecret),
);
try {
const totpController = new TOTPController();
const valid = await totpController.verify(
code,
decodeHex(user.twoFactorSecret),
);
if (valid) {
// if valid, enable two-factor authentication; the totp secret is no longer temporary
await db
.update(users)
.set({ twoFactorEnabled: true })
.where(eq(users.id, user.id));
if (valid) {
// if valid, enable two-factor authentication; the totp secret is no longer temporary
await db
.update(users)
.set({ twoFactorEnabled: true })
.where(eq(users.id, user.id));
}
return response<{ valid: boolean }>(res, {
data: { valid },
success: true,
error: false,
message: valid
? "Code is valid. Two-factor is now enabled"
: "Code is invalid",
status: HttpCode.OK,
});
} catch (error) {
return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
"Failed to verify two-factor authentication code",
),
);
}
return response<{ valid: boolean }>(res, {
data: { valid },
success: true,
error: false,
message: valid
? "Code is valid. Two-factor is now enabled"
: "Code is invalid",
status: HttpCode.OK,
});
}

View file

@ -6,8 +6,18 @@ import * as target from "./target";
import * as user from "./user";
import * as auth from "./auth";
import HttpCode from "@server/types/HttpCode";
import { verifySessionMiddleware } from "@server/middlewares";
import { verifyOrgAccess, getUserOrgs, verifySiteAccess, verifyResourceAccess, verifyTargetAccess } from "./auth";
import {
rateLimitMiddleware,
verifySessionMiddleware,
verifySessionUserMiddleware,
} from "@server/middlewares";
import {
verifyOrgAccess,
getUserOrgs,
verifySiteAccess,
verifyResourceAccess,
verifyTargetAccess,
} from "./auth";
// Root routes
export const unauthenticated = Router();
@ -18,7 +28,17 @@ unauthenticated.get("/", (_, res) => {
// Authenticated Root routes
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.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.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("/org/:orgId/resources", verifyOrgAccess, resource.listResources);
authenticated.get("/resource/:resourceId", verifyResourceAccess, resource.getResource);
authenticated.post("/resource/:resourceId", verifyResourceAccess, resource.updateResource);
authenticated.delete("/resource/:resourceId", verifyResourceAccess, resource.deleteResource);
authenticated.get(
"/org/:orgId/resources",
verifyOrgAccess,
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.get("/resource/:resourceId/targets", verifyResourceAccess, target.listTargets);
authenticated.put(
"/resource/:resourceId/target",
verifyResourceAccess,
target.createTarget,
);
authenticated.get(
"/resource/:resourceId/targets",
verifyResourceAccess,
target.listTargets,
);
authenticated.get("/target/:targetId", verifyTargetAccess, target.getTarget);
authenticated.post("/target/:targetId", verifyTargetAccess, target.updateTarget);
authenticated.delete("/target/:targetId", verifyTargetAccess, target.deleteTarget);
authenticated.post(
"/target/:targetId",
verifyTargetAccess,
target.updateTarget,
);
authenticated.delete(
"/target/:targetId",
verifyTargetAccess,
target.deleteTarget,
);
authenticated.get("/users", user.listUsers);
// 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/request-totp-secret", auth.requestTotpSecret);
authenticated.post("/auth/disable-2fa", auth.disable2fa);
unauthenticated.post(
"/auth/verify-email",
verifySessionMiddleware,
auth.verifyEmail,
);
unauthenticated.post(
"/auth/request-email-code",
verifySessionMiddleware,
auth.requestEmailVerificationCode,
);