reorg to domain and api

This commit is contained in:
Alex Holliday committed 2026-06-09 12:34:15 -07:00
1 parent 0fb210e9e1
commit d022f7d849
190 files changed
+643 -830

No files matched your search

+23
View File
@@ -0,0 +1,23 @@
import type { NextFunction, Request, Response } from "express";
import { logger } from "@/utils/logger.js";
import { AppError } from "@/utils/AppError.js";
const handleErrors = (error: unknown, req: Request, res: Response, _next: NextFunction) => {
const status = error instanceof AppError ? error.status || 500 : 500;
const message = error instanceof AppError ? error.message : error instanceof Error ? error.message : "Server error";
const service = error instanceof AppError ? error.service : "unknownService";
const method = error instanceof AppError ? error.method : "unknownMethod";
logger.error({
message: message,
service: service,
method: method,
stack: error instanceof Error ? error.stack : undefined,
details: error instanceof AppError ? error.details : undefined,
});
res.status(status).json({
status,
msg: message,
});
};
export { handleErrors };
+29
View File
@@ -0,0 +1,29 @@
import type { Request, Response, NextFunction } from "express";
const SERVICE_NAME = "allowedRoles";
import { AppError } from "@/utils/AppError.js";
import type { UserRole } from "@/domain/users/user.type.js";
const isAllowed = (allowedRoles: UserRole[]) => {
return (req: Request, res: Response, next: NextFunction) => {
try {
const user = req.user;
if (!user) {
throw new AppError({ message: "Unauthorized", status: 403, service: SERVICE_NAME });
}
const userRoles = req.user?.role || [];
// Check if the user has the required role
if (userRoles.some((role) => allowedRoles.includes(role))) {
next();
return;
} else {
throw new AppError({ message: "Unauthorized", status: 403, service: SERVICE_NAME });
}
} catch (error) {
next(error);
return;
}
};
};
export { isAllowed };
+17
View File
@@ -0,0 +1,17 @@
import rateLimit from "express-rate-limit";
export const generalApiLimiter = rateLimit({
windowMs: 60 * 1000,
limit: 600,
standardHeaders: true,
legacyHeaders: false,
ipv6Subnet: 64,
});
export const authApiLimiter = rateLimit({
windowMs: 60 * 1000,
limit: 15,
standardHeaders: true,
legacyHeaders: false,
ipv6Subnet: 64,
});
+66
View File
@@ -0,0 +1,66 @@
import type { Request, Response, NextFunction } from "express";
import DOMPurify from "isomorphic-dompurify";
export function sanitizeInput(input: string, options?: object): string;
export function sanitizeInput(input: unknown, options?: object): unknown;
export function sanitizeInput(input: unknown, options = {}): unknown {
if (typeof input !== "string") {
return input;
}
// Default configuration - remove all HTML tags and attributes
const defaultConfig = {
ALLOWED_TAGS: [] as string[],
ALLOWED_ATTR: [] as string[],
KEEP_CONTENT: true,
...options,
};
return DOMPurify.sanitize(input, defaultConfig);
}
export const sanitizeObject = (obj: unknown, options = {}): unknown => {
if (typeof obj !== "object" || obj === null) {
return obj;
}
if (Array.isArray(obj)) {
return obj.map((item) => sanitizeObject(item, options));
}
const sanitized: Record<string, unknown> = {};
for (const [key, value] of Object.entries(obj)) {
if (typeof value === "string") {
sanitized[key] = sanitizeInput(value, options);
} else if (typeof value === "object" && value !== null) {
sanitized[key] = sanitizeObject(value, options);
} else {
sanitized[key] = value;
}
}
return sanitized;
};
export const sanitizeBody = (options = {}): ((req: Request, res: Response, next: NextFunction) => void) => {
return (req: Request, res: Response, next: NextFunction) => {
if (req.body && typeof req.body === "object") {
req.body = sanitizeObject(req.body, options);
}
next();
};
};
export const sanitizeQuery = (options = {}): ((req: Request, res: Response, next: NextFunction) => void) => {
return (req: Request, res: Response, next: NextFunction) => {
if (req.query && typeof req.query === "object") {
for (const key of Object.keys(req.query)) {
const value = req.query[key];
if (typeof value === "string") {
req.query[key] = sanitizeInput(value, options);
}
}
}
next();
};
};
+58
View File
@@ -0,0 +1,58 @@
import { NextFunction, Request, Response } from "express";
import jwt from "jsonwebtoken";
import { AppError } from "@/utils/AppError.js";
import type { ISettingsService } from "@/domain/app-settings/app-settings.service.js";
import type { User } from "@/domain/users/user.type.js";
const SERVICE_NAME = "verifyJWT";
const TOKEN_PREFIX = "Bearer ";
const isUser = (payload: unknown): payload is User => {
return typeof payload === "object" && payload !== null && "id" in payload && "teamId" in payload && "role" in payload;
};
export const createVerifyJWT = (settingsService: ISettingsService) => {
return (req: Request, res: Response, next: NextFunction) => {
const token = req.headers["authorization"];
// Make sure a token is provided
if (!token) {
const error = new AppError({ message: "No token provided", status: 401, service: SERVICE_NAME });
next(error);
return;
}
// Make sure it is properly formatted
if (!token.startsWith(TOKEN_PREFIX)) {
const error = new AppError({ message: "Invalid token format", status: 401, service: SERVICE_NAME, method: "verifyJWT" });
next(error);
return;
}
const parsedToken = token.slice(TOKEN_PREFIX.length, token.length);
// Verify the token's authenticity
const { jwtSecret } = settingsService.getSettings();
if (!jwtSecret) {
const error = new AppError({ message: "JWT secret not configured", status: 500, service: SERVICE_NAME });
next(error);
return;
}
jwt.verify(parsedToken, jwtSecret, (err: jwt.VerifyErrors | null, decoded: jwt.JwtPayload | string | undefined) => {
if (err) {
const error = new AppError({
message: err instanceof Error ? err.message : "Failed to authenticate token",
status: 401,
service: SERVICE_NAME,
details: err instanceof Error ? { error: err } : undefined,
method: "verifyJWT",
});
next(error);
return;
} else if (isUser(decoded)) {
req.user = decoded;
next();
} else {
next(new AppError({ message: "Invalid token payload", status: 401, service: SERVICE_NAME, method: "verifyJWT" }));
}
});
};
};
@@ -0,0 +1,22 @@
import { NextFunction, Request, RequestHandler, Response } from "express";
import { IStatusPagesRepository } from "@/domain/status-pages/status-page-repository.interface.js";
import { AppError } from "@/utils/AppError.js";
export const createVerifyStatusPageAccess = (statusPagesRepository: IStatusPagesRepository, verifyJWT: RequestHandler) => {
return async (req: Request, res: Response, next: NextFunction) => {
try {
const url = Array.isArray(req.params.url) ? req.params.url[0] : req.params.url;
if (!url) {
throw new AppError({ message: "Status page URL is required", status: 400 });
}
const statusPage = await statusPagesRepository.findByUrl(url);
if (statusPage.isPublished) {
next(); // Published — no auth needed
} else {
verifyJWT(req, res, next); // Unpublished — require JWT
}
} catch (error) {
next(error);
}
};
};