reorg to domain and api
This commit is contained in:
1 parent
0fb210e9e1
commit
d022f7d849
190 files changed
+643
-830
No files matched your search
Executable
+23
@@ -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 };
|
||||
Executable
+29
@@ -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 };
|
||||
@@ -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,
|
||||
});
|
||||
@@ -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();
|
||||
};
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
};
|
||||
Reference in new issue
Block a user