yantr-js 0.1.0-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1023 -0
- package/dist/index.js.map +1 -0
- package/package.json +57 -0
- package/registry/registry.json +58 -0
- package/registry/templates/auth/auth.controller.ts +125 -0
- package/registry/templates/auth/auth.middleware.ts +116 -0
- package/registry/templates/auth/auth.routes.ts +34 -0
- package/registry/templates/auth/auth.service.ts +140 -0
- package/registry/templates/base/error-handler.ts +127 -0
- package/registry/templates/base/zod-middleware.ts +104 -0
- package/registry/templates/database/db.ts +83 -0
- package/registry/templates/database/prisma.ts +47 -0
- package/registry/templates/logger/http-logger.ts +61 -0
- package/registry/templates/logger/logger.ts +60 -0
- package/registry/templates/security/helmet.ts +88 -0
- package/registry/templates/security/rate-limiter.ts +79 -0
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import bcrypt from 'bcryptjs';
|
|
2
|
+
import {
|
|
3
|
+
generateAccessToken,
|
|
4
|
+
generateRefreshToken,
|
|
5
|
+
verifyToken,
|
|
6
|
+
type TokenPayload
|
|
7
|
+
} from './auth.middleware';
|
|
8
|
+
|
|
9
|
+
// Types
|
|
10
|
+
export interface RegisterInput {
|
|
11
|
+
email: string;
|
|
12
|
+
password: string;
|
|
13
|
+
name?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface LoginInput {
|
|
17
|
+
email: string;
|
|
18
|
+
password: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface AuthTokens {
|
|
22
|
+
accessToken: string;
|
|
23
|
+
refreshToken: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface UserData {
|
|
27
|
+
id: string;
|
|
28
|
+
email: string;
|
|
29
|
+
name?: string;
|
|
30
|
+
role?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Constants
|
|
34
|
+
const SALT_ROUNDS = 12;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Hash a password
|
|
38
|
+
*/
|
|
39
|
+
export async function hashPassword(password: string): Promise<string> {
|
|
40
|
+
return bcrypt.hash(password, SALT_ROUNDS);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Compare password with hash
|
|
45
|
+
*/
|
|
46
|
+
export async function comparePassword(
|
|
47
|
+
password: string,
|
|
48
|
+
hash: string
|
|
49
|
+
): Promise<boolean> {
|
|
50
|
+
return bcrypt.compare(password, hash);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Generate auth tokens for a user
|
|
55
|
+
*/
|
|
56
|
+
export function generateAuthTokens(user: UserData): AuthTokens {
|
|
57
|
+
const payload: TokenPayload = {
|
|
58
|
+
userId: user.id,
|
|
59
|
+
email: user.email,
|
|
60
|
+
role: user.role,
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
accessToken: generateAccessToken(payload),
|
|
65
|
+
refreshToken: generateRefreshToken(payload),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Refresh access token using refresh token
|
|
71
|
+
*/
|
|
72
|
+
export function refreshAccessToken(refreshToken: string): AuthTokens {
|
|
73
|
+
const payload = verifyToken(refreshToken);
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
accessToken: generateAccessToken(payload),
|
|
77
|
+
refreshToken: generateRefreshToken(payload),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Example: Register a new user
|
|
83
|
+
*
|
|
84
|
+
* NOTE: This is a template. You need to implement your own user storage logic.
|
|
85
|
+
* Replace the TODO comments with your database operations.
|
|
86
|
+
*/
|
|
87
|
+
export async function registerUser(input: RegisterInput): Promise<UserData> {
|
|
88
|
+
const { email, password, name } = input;
|
|
89
|
+
|
|
90
|
+
// TODO: Check if user already exists in your database
|
|
91
|
+
// const existingUser = await prisma.user.findUnique({ where: { email } });
|
|
92
|
+
// if (existingUser) throw new ConflictError('Email already registered');
|
|
93
|
+
|
|
94
|
+
// Hash password
|
|
95
|
+
const hashedPassword = await hashPassword(password);
|
|
96
|
+
|
|
97
|
+
// TODO: Create user in your database
|
|
98
|
+
// const user = await prisma.user.create({
|
|
99
|
+
// data: { email, password: hashedPassword, name }
|
|
100
|
+
// });
|
|
101
|
+
|
|
102
|
+
// Placeholder - replace with actual user creation
|
|
103
|
+
const user: UserData = {
|
|
104
|
+
id: 'user_' + Date.now(), // Replace with actual ID
|
|
105
|
+
email,
|
|
106
|
+
name,
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
return user;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Example: Login user
|
|
114
|
+
*
|
|
115
|
+
* NOTE: This is a template. Implement your own user lookup logic.
|
|
116
|
+
*/
|
|
117
|
+
export async function loginUser(
|
|
118
|
+
input: LoginInput
|
|
119
|
+
): Promise<{ user: UserData; tokens: AuthTokens }> {
|
|
120
|
+
const { email, password } = input;
|
|
121
|
+
|
|
122
|
+
// TODO: Find user by email in your database
|
|
123
|
+
// const user = await prisma.user.findUnique({ where: { email } });
|
|
124
|
+
// if (!user) throw new UnauthorizedError('Invalid credentials');
|
|
125
|
+
|
|
126
|
+
// TODO: Verify password
|
|
127
|
+
// const isValid = await comparePassword(password, user.password);
|
|
128
|
+
// if (!isValid) throw new UnauthorizedError('Invalid credentials');
|
|
129
|
+
|
|
130
|
+
// Placeholder - replace with actual user lookup
|
|
131
|
+
const user: UserData = {
|
|
132
|
+
id: 'user_1',
|
|
133
|
+
email,
|
|
134
|
+
name: 'User',
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const tokens = generateAuthTokens(user);
|
|
138
|
+
|
|
139
|
+
return { user, tokens };
|
|
140
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import type { Request, Response, NextFunction } from 'express';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Custom error class for application errors
|
|
5
|
+
*/
|
|
6
|
+
export class AppError extends Error {
|
|
7
|
+
public readonly statusCode: number;
|
|
8
|
+
public readonly isOperational: boolean;
|
|
9
|
+
public readonly code?: string;
|
|
10
|
+
|
|
11
|
+
constructor(
|
|
12
|
+
message: string,
|
|
13
|
+
statusCode: number = 500,
|
|
14
|
+
isOperational: boolean = true,
|
|
15
|
+
code?: string
|
|
16
|
+
) {
|
|
17
|
+
super(message);
|
|
18
|
+
this.statusCode = statusCode;
|
|
19
|
+
this.isOperational = isOperational;
|
|
20
|
+
this.code = code;
|
|
21
|
+
|
|
22
|
+
Object.setPrototypeOf(this, AppError.prototype);
|
|
23
|
+
Error.captureStackTrace(this, this.constructor);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Common HTTP error classes
|
|
29
|
+
*/
|
|
30
|
+
export class NotFoundError extends AppError {
|
|
31
|
+
constructor(message: string = 'Resource not found') {
|
|
32
|
+
super(message, 404, true, 'NOT_FOUND');
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export class BadRequestError extends AppError {
|
|
37
|
+
constructor(message: string = 'Bad request') {
|
|
38
|
+
super(message, 400, true, 'BAD_REQUEST');
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export class UnauthorizedError extends AppError {
|
|
43
|
+
constructor(message: string = 'Unauthorized') {
|
|
44
|
+
super(message, 401, true, 'UNAUTHORIZED');
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export class ForbiddenError extends AppError {
|
|
49
|
+
constructor(message: string = 'Forbidden') {
|
|
50
|
+
super(message, 403, true, 'FORBIDDEN');
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export class ConflictError extends AppError {
|
|
55
|
+
constructor(message: string = 'Conflict') {
|
|
56
|
+
super(message, 409, true, 'CONFLICT');
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export class ValidationError extends AppError {
|
|
61
|
+
public readonly errors: Record<string, string[]>;
|
|
62
|
+
|
|
63
|
+
constructor(errors: Record<string, string[]>) {
|
|
64
|
+
super('Validation failed', 422, true, 'VALIDATION_ERROR');
|
|
65
|
+
this.errors = errors;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Global error handler middleware
|
|
71
|
+
*/
|
|
72
|
+
export function errorHandler(
|
|
73
|
+
err: Error,
|
|
74
|
+
_req: Request,
|
|
75
|
+
res: Response,
|
|
76
|
+
_next: NextFunction
|
|
77
|
+
): void {
|
|
78
|
+
// Log error for debugging
|
|
79
|
+
console.error('[Error]', err);
|
|
80
|
+
|
|
81
|
+
// Handle AppError instances
|
|
82
|
+
if (err instanceof AppError) {
|
|
83
|
+
const response: Record<string, unknown> = {
|
|
84
|
+
success: false,
|
|
85
|
+
error: {
|
|
86
|
+
message: err.message,
|
|
87
|
+
code: err.code,
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
// Include validation errors if present
|
|
92
|
+
if (err instanceof ValidationError) {
|
|
93
|
+
response.error = {
|
|
94
|
+
...response.error as object,
|
|
95
|
+
details: err.errors,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
res.status(err.statusCode).json(response);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Handle unexpected errors
|
|
104
|
+
const statusCode = 500;
|
|
105
|
+
const message = process.env.NODE_ENV === 'production'
|
|
106
|
+
? 'Internal server error'
|
|
107
|
+
: err.message;
|
|
108
|
+
|
|
109
|
+
res.status(statusCode).json({
|
|
110
|
+
success: false,
|
|
111
|
+
error: {
|
|
112
|
+
message,
|
|
113
|
+
code: 'INTERNAL_ERROR',
|
|
114
|
+
},
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Async handler wrapper to catch errors in async route handlers
|
|
120
|
+
*/
|
|
121
|
+
export function asyncHandler(
|
|
122
|
+
fn: (req: Request, res: Response, next: NextFunction) => Promise<void>
|
|
123
|
+
) {
|
|
124
|
+
return (req: Request, res: Response, next: NextFunction) => {
|
|
125
|
+
Promise.resolve(fn(req, res, next)).catch(next);
|
|
126
|
+
};
|
|
127
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import type { Request, Response, NextFunction } from 'express';
|
|
2
|
+
import { z, ZodError, ZodSchema } from 'zod';
|
|
3
|
+
import { ValidationError } from './error-handler';
|
|
4
|
+
|
|
5
|
+
type RequestLocation = 'body' | 'query' | 'params';
|
|
6
|
+
|
|
7
|
+
interface ValidateOptions {
|
|
8
|
+
body?: ZodSchema;
|
|
9
|
+
query?: ZodSchema;
|
|
10
|
+
params?: ZodSchema;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Format Zod errors into a readable format
|
|
15
|
+
*/
|
|
16
|
+
function formatZodErrors(error: ZodError): Record<string, string[]> {
|
|
17
|
+
const errors: Record<string, string[]> = {};
|
|
18
|
+
|
|
19
|
+
for (const issue of error.issues) {
|
|
20
|
+
const path = issue.path.join('.');
|
|
21
|
+
const key = path || 'root';
|
|
22
|
+
|
|
23
|
+
if (!errors[key]) {
|
|
24
|
+
errors[key] = [];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
errors[key].push(issue.message);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return errors;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Validate request data against Zod schemas
|
|
35
|
+
*
|
|
36
|
+
* @example
|
|
37
|
+
* ```typescript
|
|
38
|
+
* import { z } from 'zod';
|
|
39
|
+
*
|
|
40
|
+
* const createUserSchema = z.object({
|
|
41
|
+
* email: z.string().email(),
|
|
42
|
+
* password: z.string().min(8),
|
|
43
|
+
* });
|
|
44
|
+
*
|
|
45
|
+
* router.post('/users', validate({ body: createUserSchema }), createUser);
|
|
46
|
+
* ```
|
|
47
|
+
*/
|
|
48
|
+
export function validate(schemas: ValidateOptions) {
|
|
49
|
+
return async (req: Request, _res: Response, next: NextFunction) => {
|
|
50
|
+
const allErrors: Record<string, string[]> = {};
|
|
51
|
+
|
|
52
|
+
const locations: RequestLocation[] = ['body', 'query', 'params'];
|
|
53
|
+
|
|
54
|
+
for (const location of locations) {
|
|
55
|
+
const schema = schemas[location];
|
|
56
|
+
|
|
57
|
+
if (schema) {
|
|
58
|
+
const result = await schema.safeParseAsync(req[location]);
|
|
59
|
+
|
|
60
|
+
if (!result.success) {
|
|
61
|
+
const errors = formatZodErrors(result.error);
|
|
62
|
+
|
|
63
|
+
for (const [key, messages] of Object.entries(errors)) {
|
|
64
|
+
const prefixedKey = `${location}.${key}`;
|
|
65
|
+
allErrors[prefixedKey] = messages;
|
|
66
|
+
}
|
|
67
|
+
} else {
|
|
68
|
+
// Replace request data with parsed data (for type coercion)
|
|
69
|
+
req[location] = result.data;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (Object.keys(allErrors).length > 0) {
|
|
75
|
+
return next(new ValidationError(allErrors));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
next();
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Validate only request body
|
|
84
|
+
*/
|
|
85
|
+
export function validateBody<T extends ZodSchema>(schema: T) {
|
|
86
|
+
return validate({ body: schema });
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Validate only query parameters
|
|
91
|
+
*/
|
|
92
|
+
export function validateQuery<T extends ZodSchema>(schema: T) {
|
|
93
|
+
return validate({ query: schema });
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Validate only route parameters
|
|
98
|
+
*/
|
|
99
|
+
export function validateParams<T extends ZodSchema>(schema: T) {
|
|
100
|
+
return validate({ params: schema });
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Re-export Zod for convenience
|
|
104
|
+
export { z };
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { prisma, disconnectDb, checkDbConnection } from './prisma';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Database utilities and helpers
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Transaction wrapper for database operations
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```typescript
|
|
12
|
+
* const result = await withTransaction(async (tx) => {
|
|
13
|
+
* const user = await tx.user.create({ data: { email: 'test@example.com' } });
|
|
14
|
+
* const profile = await tx.profile.create({ data: { userId: user.id } });
|
|
15
|
+
* return { user, profile };
|
|
16
|
+
* });
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
export async function withTransaction<T>(
|
|
20
|
+
fn: (tx: typeof prisma) => Promise<T>
|
|
21
|
+
): Promise<T> {
|
|
22
|
+
return prisma.$transaction(async (tx) => {
|
|
23
|
+
return fn(tx as typeof prisma);
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Pagination helper
|
|
29
|
+
*/
|
|
30
|
+
export interface PaginationOptions {
|
|
31
|
+
page?: number;
|
|
32
|
+
limit?: number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface PaginatedResult<T> {
|
|
36
|
+
data: T[];
|
|
37
|
+
meta: {
|
|
38
|
+
total: number;
|
|
39
|
+
page: number;
|
|
40
|
+
limit: number;
|
|
41
|
+
totalPages: number;
|
|
42
|
+
hasNextPage: boolean;
|
|
43
|
+
hasPrevPage: boolean;
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Create pagination parameters for Prisma queries
|
|
49
|
+
*/
|
|
50
|
+
export function getPaginationParams(options: PaginationOptions = {}) {
|
|
51
|
+
const page = Math.max(1, options.page || 1);
|
|
52
|
+
const limit = Math.min(100, Math.max(1, options.limit || 10));
|
|
53
|
+
const skip = (page - 1) * limit;
|
|
54
|
+
|
|
55
|
+
return { skip, take: limit, page, limit };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Format paginated response
|
|
60
|
+
*/
|
|
61
|
+
export function formatPaginatedResponse<T>(
|
|
62
|
+
data: T[],
|
|
63
|
+
total: number,
|
|
64
|
+
page: number,
|
|
65
|
+
limit: number
|
|
66
|
+
): PaginatedResult<T> {
|
|
67
|
+
const totalPages = Math.ceil(total / limit);
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
data,
|
|
71
|
+
meta: {
|
|
72
|
+
total,
|
|
73
|
+
page,
|
|
74
|
+
limit,
|
|
75
|
+
totalPages,
|
|
76
|
+
hasNextPage: page < totalPages,
|
|
77
|
+
hasPrevPage: page > 1,
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Re-export prisma utilities
|
|
83
|
+
export { prisma, disconnectDb, checkDbConnection };
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { PrismaClient } from '@prisma/client';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Prisma Client Singleton
|
|
5
|
+
*
|
|
6
|
+
* This ensures only one instance of PrismaClient is created,
|
|
7
|
+
* even during hot reloading in development.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const globalForPrisma = globalThis as unknown as {
|
|
11
|
+
prisma: PrismaClient | undefined;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export const prisma =
|
|
15
|
+
globalForPrisma.prisma ??
|
|
16
|
+
new PrismaClient({
|
|
17
|
+
log:
|
|
18
|
+
process.env.NODE_ENV === 'development'
|
|
19
|
+
? ['query', 'error', 'warn']
|
|
20
|
+
: ['error'],
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
24
|
+
globalForPrisma.prisma = prisma;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Graceful shutdown handler
|
|
29
|
+
* Call this when your app is shutting down
|
|
30
|
+
*/
|
|
31
|
+
export async function disconnectDb(): Promise<void> {
|
|
32
|
+
await prisma.$disconnect();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Health check for database connection
|
|
37
|
+
*/
|
|
38
|
+
export async function checkDbConnection(): Promise<boolean> {
|
|
39
|
+
try {
|
|
40
|
+
await prisma.$queryRaw`SELECT 1`;
|
|
41
|
+
return true;
|
|
42
|
+
} catch {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export default prisma;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import pinoHttp from 'pino-http';
|
|
2
|
+
import { logger } from './logger';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* HTTP request logging middleware
|
|
6
|
+
*
|
|
7
|
+
* Logs all incoming requests with:
|
|
8
|
+
* - Method, URL, status code
|
|
9
|
+
* - Response time
|
|
10
|
+
* - Request ID for tracing
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```typescript
|
|
14
|
+
* import express from 'express';
|
|
15
|
+
* import { httpLogger } from './lib/setu/logger/http-logger';
|
|
16
|
+
*
|
|
17
|
+
* const app = express();
|
|
18
|
+
* app.use(httpLogger);
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
export const httpLogger = pinoHttp({
|
|
22
|
+
logger,
|
|
23
|
+
|
|
24
|
+
// Generate unique request ID
|
|
25
|
+
genReqId: (req) => {
|
|
26
|
+
return req.headers['x-request-id'] as string || crypto.randomUUID();
|
|
27
|
+
},
|
|
28
|
+
|
|
29
|
+
// Custom log level based on status code
|
|
30
|
+
customLogLevel: (_req, res, err) => {
|
|
31
|
+
if (res.statusCode >= 500 || err) return 'error';
|
|
32
|
+
if (res.statusCode >= 400) return 'warn';
|
|
33
|
+
return 'info';
|
|
34
|
+
},
|
|
35
|
+
|
|
36
|
+
// Custom success message
|
|
37
|
+
customSuccessMessage: (req, res) => {
|
|
38
|
+
return `${req.method} ${req.url} ${res.statusCode}`;
|
|
39
|
+
},
|
|
40
|
+
|
|
41
|
+
// Custom error message
|
|
42
|
+
customErrorMessage: (req, res, err) => {
|
|
43
|
+
return `${req.method} ${req.url} ${res.statusCode} - ${err.message}`;
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
// Properties to include in logs
|
|
47
|
+
customProps: (req) => ({
|
|
48
|
+
userAgent: req.headers['user-agent'],
|
|
49
|
+
ip: req.headers['x-forwarded-for'] || req.socket.remoteAddress,
|
|
50
|
+
}),
|
|
51
|
+
|
|
52
|
+
// Don't log these paths (health checks, etc.)
|
|
53
|
+
autoLogging: {
|
|
54
|
+
ignore: (req) => {
|
|
55
|
+
const ignorePaths = ['/health', '/ready', '/metrics'];
|
|
56
|
+
return ignorePaths.includes(req.url || '');
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
export default httpLogger;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import pino from 'pino';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Logger configuration
|
|
5
|
+
*
|
|
6
|
+
* In production, logs are JSON formatted for easy parsing.
|
|
7
|
+
* In development, logs are pretty-printed for readability.
|
|
8
|
+
*/
|
|
9
|
+
const isProduction = process.env.NODE_ENV === 'production';
|
|
10
|
+
|
|
11
|
+
export const logger = pino({
|
|
12
|
+
level: process.env.LOG_LEVEL || (isProduction ? 'info' : 'debug'),
|
|
13
|
+
|
|
14
|
+
// Base properties added to every log
|
|
15
|
+
base: {
|
|
16
|
+
pid: process.pid,
|
|
17
|
+
env: process.env.NODE_ENV || 'development',
|
|
18
|
+
},
|
|
19
|
+
|
|
20
|
+
// Timestamp configuration
|
|
21
|
+
timestamp: pino.stdTimeFunctions.isoTime,
|
|
22
|
+
|
|
23
|
+
// Pretty print in development
|
|
24
|
+
transport: isProduction
|
|
25
|
+
? undefined
|
|
26
|
+
: {
|
|
27
|
+
target: 'pino-pretty',
|
|
28
|
+
options: {
|
|
29
|
+
colorize: true,
|
|
30
|
+
translateTime: 'SYS:standard',
|
|
31
|
+
ignore: 'pid,hostname',
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Create a child logger with additional context
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* ```typescript
|
|
41
|
+
* const userLogger = createLogger({ module: 'user-service' });
|
|
42
|
+
* userLogger.info({ userId: '123' }, 'User logged in');
|
|
43
|
+
* ```
|
|
44
|
+
*/
|
|
45
|
+
export function createLogger(context: Record<string, unknown>) {
|
|
46
|
+
return logger.child(context);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Log levels available:
|
|
51
|
+
* - trace: Very detailed debugging information
|
|
52
|
+
* - debug: Debugging information
|
|
53
|
+
* - info: General information
|
|
54
|
+
* - warn: Warning messages
|
|
55
|
+
* - error: Error messages
|
|
56
|
+
* - fatal: Critical errors
|
|
57
|
+
*/
|
|
58
|
+
export type LogLevel = 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'fatal';
|
|
59
|
+
|
|
60
|
+
export default logger;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import helmet from 'helmet';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Helmet security middleware configuration
|
|
5
|
+
*
|
|
6
|
+
* Sets various HTTP headers to protect your app from common vulnerabilities.
|
|
7
|
+
*
|
|
8
|
+
* @see https://helmetjs.github.io/
|
|
9
|
+
*/
|
|
10
|
+
export const helmetConfig = helmet({
|
|
11
|
+
// Content Security Policy
|
|
12
|
+
contentSecurityPolicy: {
|
|
13
|
+
directives: {
|
|
14
|
+
defaultSrc: ["'self'"],
|
|
15
|
+
styleSrc: ["'self'", "'unsafe-inline'"],
|
|
16
|
+
scriptSrc: ["'self'"],
|
|
17
|
+
imgSrc: ["'self'", 'data:', 'https:'],
|
|
18
|
+
connectSrc: ["'self'"],
|
|
19
|
+
fontSrc: ["'self'"],
|
|
20
|
+
objectSrc: ["'none'"],
|
|
21
|
+
mediaSrc: ["'self'"],
|
|
22
|
+
frameSrc: ["'none'"],
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
|
|
26
|
+
// Cross-Origin Resource Policy
|
|
27
|
+
crossOriginResourcePolicy: { policy: 'same-origin' },
|
|
28
|
+
|
|
29
|
+
// Cross-Origin Opener Policy
|
|
30
|
+
crossOriginOpenerPolicy: { policy: 'same-origin' },
|
|
31
|
+
|
|
32
|
+
// Cross-Origin Embedder Policy
|
|
33
|
+
crossOriginEmbedderPolicy: false, // Enable if using SharedArrayBuffer
|
|
34
|
+
|
|
35
|
+
// DNS Prefetch Control
|
|
36
|
+
dnsPrefetchControl: { allow: false },
|
|
37
|
+
|
|
38
|
+
// Frameguard (X-Frame-Options)
|
|
39
|
+
frameguard: { action: 'deny' },
|
|
40
|
+
|
|
41
|
+
// Hide X-Powered-By header
|
|
42
|
+
hidePoweredBy: true,
|
|
43
|
+
|
|
44
|
+
// HTTP Strict Transport Security
|
|
45
|
+
hsts: {
|
|
46
|
+
maxAge: 31536000, // 1 year
|
|
47
|
+
includeSubDomains: true,
|
|
48
|
+
preload: true,
|
|
49
|
+
},
|
|
50
|
+
|
|
51
|
+
// IE No Open
|
|
52
|
+
ieNoOpen: true,
|
|
53
|
+
|
|
54
|
+
// Don't Sniff Mimetype
|
|
55
|
+
noSniff: true,
|
|
56
|
+
|
|
57
|
+
// Origin Agent Cluster
|
|
58
|
+
originAgentCluster: true,
|
|
59
|
+
|
|
60
|
+
// Permitted Cross-Domain Policies
|
|
61
|
+
permittedCrossDomainPolicies: { permittedPolicies: 'none' },
|
|
62
|
+
|
|
63
|
+
// Referrer Policy
|
|
64
|
+
referrerPolicy: { policy: 'strict-origin-when-cross-origin' },
|
|
65
|
+
|
|
66
|
+
// X-XSS-Protection (deprecated but still useful for older browsers)
|
|
67
|
+
xssFilter: true,
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Relaxed helmet config for development
|
|
72
|
+
* Disables CSP which can be annoying during development
|
|
73
|
+
*/
|
|
74
|
+
export const helmetDevConfig = helmet({
|
|
75
|
+
contentSecurityPolicy: false,
|
|
76
|
+
crossOriginEmbedderPolicy: false,
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Get appropriate helmet config based on environment
|
|
81
|
+
*/
|
|
82
|
+
export function getHelmetConfig() {
|
|
83
|
+
return process.env.NODE_ENV === 'production'
|
|
84
|
+
? helmetConfig
|
|
85
|
+
: helmetDevConfig;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export default helmetConfig;
|