Skip to content

Commit 88e35f3

Browse files
committed
feat: add git-flow helper scripts
- Add git-flow.sh bash script with comprehensive comments explaining the branching model - Add git-flow.ts TypeScript utilities with detailed documentation - Scripts support feature, release, and hotfix branch workflows - Includes error handling, colored output, and branch validation
1 parent a2a94d0 commit 88e35f3

5 files changed

Lines changed: 348 additions & 0 deletions

File tree

‎src/controllers/auth.controller.ts‎

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import { Request, Response } from 'express';
2+
import { compare } from 'bcrypt';
3+
import drizzleService from '../../services/drizzle-services';
4+
import { AuthenticationError } from '../utils/errors';
5+
import { logger } from '../middlewares/logger';
6+
7+
export class AuthController {
8+
async login(req: Request, res: Response): Promise<void> {
9+
const { username, password } = req.body;
10+
11+
const user = await drizzleService.getUserByUsername(username);
12+
13+
if (!user) {
14+
throw new AuthenticationError('Invalid username or password');
15+
}
16+
17+
const passwordMatch = await compare(password, user.password);
18+
19+
if (!passwordMatch) {
20+
throw new AuthenticationError('Invalid username or password');
21+
}
22+
23+
// Create session data
24+
req.session.user = {
25+
id: user.id as string,
26+
username: user.username,
27+
fullName: user.fullName,
28+
email: user.email,
29+
role: user.role,
30+
libraryId: String(user.libraryId),
31+
};
32+
33+
logger.info('User logged in', { userId: user.id, username: user.username });
34+
35+
res.status(200).json({
36+
success: true,
37+
data: {
38+
id: user.id,
39+
username: user.username,
40+
fullName: user.fullName,
41+
email: user.email,
42+
role: user.role,
43+
libraryId: user.libraryId,
44+
},
45+
});
46+
}
47+
48+
async getSession(req: Request, res: Response): Promise<void> {
49+
if (req.session.user) {
50+
res.status(200).json({
51+
success: true,
52+
data: req.session.user,
53+
});
54+
} else {
55+
res.status(200).json({
56+
success: true,
57+
data: null,
58+
});
59+
}
60+
}
61+
62+
async logout(req: Request, res: Response): Promise<void> {
63+
return new Promise((resolve, reject) => {
64+
req.session.destroy((err) => {
65+
if (err) {
66+
reject(err);
67+
return;
68+
}
69+
res.clearCookie('connect.sid');
70+
res.status(200).json({
71+
success: true,
72+
message: 'Logged out successfully',
73+
});
74+
resolve();
75+
});
76+
});
77+
}
78+
}
79+
80+
export const authController = new AuthController();

‎src/middlewares/auth.ts‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { Request, Response, NextFunction } from 'express';
2+
import { AuthenticationError, AuthorizationError } from '../utils/errors';
3+
4+
export const requireAuth = (req: Request, res: Response, next: NextFunction) => {
5+
if (!req.session?.user) {
6+
throw new AuthenticationError('Authentication required');
7+
}
8+
next();
9+
};
10+
11+
export const requireRole = (...roles: string[]) => {
12+
return (req: Request, res: Response, next: NextFunction) => {
13+
if (!req.session?.user) {
14+
throw new AuthenticationError('Authentication required');
15+
}
16+
17+
if (!roles.includes(req.session.user.role)) {
18+
throw new AuthorizationError(`Access denied. Required roles: ${roles.join(', ')}`);
19+
}
20+
21+
next();
22+
};
23+
};
24+
25+
export const requireSuperAdmin = requireRole('super_admin');
26+
export const requireLibraryAdmin = requireRole('library_admin', 'super_admin');

‎src/middlewares/logger.ts‎

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import { createLogger, format, transports } from 'winston';
2+
import { env } from '../config/env';
3+
4+
const logFormat = format.combine(
5+
format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
6+
format.errors({ stack: true }),
7+
format.splat(),
8+
format.json()
9+
);
10+
11+
const consoleFormat = format.combine(
12+
format.colorize(),
13+
format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
14+
format.printf((info: any) => {
15+
const { timestamp, level, message, ...meta } = info;
16+
const metaStr = Object.keys(meta).length ? JSON.stringify(meta, null, 2) : '';
17+
return `${timestamp} [${level}]: ${message} ${metaStr}`;
18+
})
19+
);
20+
21+
export const logger = createLogger({
22+
level: env.NODE_ENV === 'production' ? 'info' : 'debug',
23+
format: logFormat,
24+
defaultMeta: { service: 'library-backend' },
25+
transports: [
26+
new transports.File({ filename: 'logs/error.log', level: 'error' }),
27+
new transports.File({ filename: 'logs/combined.log' }),
28+
],
29+
});
30+
31+
if (env.NODE_ENV !== 'production') {
32+
logger.add(
33+
new transports.Console({
34+
format: consoleFormat,
35+
})
36+
);
37+
}
38+
39+
// Request logging middleware
40+
export const requestLogger = (req: any, res: any, next: any) => {
41+
const start = Date.now();
42+
const path = req.path;
43+
let capturedJsonResponse: Record<string, any> | undefined = undefined;
44+
45+
const originalResJson = res.json;
46+
res.json = function (bodyJson: any, ...args: any[]) {
47+
capturedJsonResponse = bodyJson;
48+
return originalResJson.apply(res, [bodyJson, ...args]);
49+
};
50+
51+
res.on('finish', () => {
52+
const duration = Date.now() - start;
53+
if (path.startsWith('/api')) {
54+
logger.info('HTTP Request', {
55+
method: req.method,
56+
path,
57+
statusCode: res.statusCode,
58+
duration: `${duration}ms`,
59+
ip: req.ip,
60+
userAgent: req.get('user-agent'),
61+
...(capturedJsonResponse && { response: capturedJsonResponse }),
62+
});
63+
}
64+
});
65+
66+
next();
67+
};

‎src/middlewares/validation.ts‎

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import { Request, Response, NextFunction } from 'express';
2+
import { ZodSchema, ZodError } from 'zod';
3+
import { ValidationError } from '../utils/errors';
4+
5+
export const validate = (schema: ZodSchema) => {
6+
return (req: Request, res: Response, next: NextFunction) => {
7+
try {
8+
schema.parse(req.body);
9+
next();
10+
} catch (error) {
11+
if (error instanceof ZodError) {
12+
const formattedErrors = error.errors.reduce((acc, err) => {
13+
const path = err.path.join('.');
14+
if (!acc[path]) {
15+
acc[path] = [];
16+
}
17+
acc[path].push(err.message);
18+
return acc;
19+
}, {} as Record<string, string[]>);
20+
21+
throw new ValidationError('Validation failed', formattedErrors);
22+
}
23+
next(error);
24+
}
25+
};
26+
};
27+
28+
export const validateQuery = (schema: ZodSchema) => {
29+
return (req: Request, res: Response, next: NextFunction) => {
30+
try {
31+
schema.parse(req.query);
32+
next();
33+
} catch (error) {
34+
if (error instanceof ZodError) {
35+
const formattedErrors = error.errors.reduce((acc, err) => {
36+
const path = err.path.join('.');
37+
if (!acc[path]) {
38+
acc[path] = [];
39+
}
40+
acc[path].push(err.message);
41+
return acc;
42+
}, {} as Record<string, string[]>);
43+
44+
throw new ValidationError('Query validation failed', formattedErrors);
45+
}
46+
next(error);
47+
}
48+
};
49+
};
50+
51+
export const validateParams = (schema: ZodSchema) => {
52+
return (req: Request, res: Response, next: NextFunction) => {
53+
try {
54+
schema.parse(req.params);
55+
next();
56+
} catch (error) {
57+
if (error instanceof ZodError) {
58+
const formattedErrors = error.errors.reduce((acc, err) => {
59+
const path = err.path.join('.');
60+
if (!acc[path]) {
61+
acc[path] = [];
62+
}
63+
acc[path].push(err.message);
64+
return acc;
65+
}, {} as Record<string, string[]>);
66+
67+
throw new ValidationError('Parameter validation failed', formattedErrors);
68+
}
69+
next(error);
70+
}
71+
};
72+
};

‎src/utils/errors.ts‎

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
/**
2+
* Custom Error Classes
3+
*
4+
* These error classes provide a consistent way to handle errors throughout
5+
* the application. They extend the base AppError class and are automatically
6+
* handled by the error handler middleware.
7+
*
8+
* Usage:
9+
* throw new NotFoundError('Story');
10+
* throw new ValidationError('Invalid input', { field: ['Error message'] });
11+
* throw new AuthenticationError('Invalid credentials');
12+
*
13+
* Error Handler:
14+
* All errors are caught by the error handler middleware in
15+
* middlewares/errors/error-handler.ts which formats the response.
16+
*/
17+
18+
/**
19+
* Base error class for all application errors
20+
* @param statusCode - HTTP status code
21+
* @param message - Error message
22+
* @param isOperational - Whether this is an operational error (vs programming error)
23+
* @param code - Error code for client-side handling
24+
*/
25+
export class AppError extends Error {
26+
constructor(
27+
public statusCode: number,
28+
public message: string,
29+
public isOperational = true,
30+
public code?: string
31+
) {
32+
super(message);
33+
Object.setPrototypeOf(this, AppError.prototype);
34+
Error.captureStackTrace(this, this.constructor);
35+
}
36+
}
37+
38+
/**
39+
* Validation error (400) - Used when request validation fails
40+
* @param message - Error message
41+
* @param errors - Field-specific validation errors
42+
*/
43+
export class ValidationError extends AppError {
44+
constructor(message: string, public errors?: Record<string, string[]>) {
45+
super(400, message, true, 'VALIDATION_ERROR');
46+
this.name = 'ValidationError';
47+
}
48+
}
49+
50+
/**
51+
* Authentication error (401) - Used when user is not authenticated
52+
* @param message - Error message (default: 'Authentication required')
53+
*/
54+
export class AuthenticationError extends AppError {
55+
constructor(message: string = 'Authentication required') {
56+
super(401, message, true, 'AUTHENTICATION_ERROR');
57+
this.name = 'AuthenticationError';
58+
}
59+
}
60+
61+
/**
62+
* Authorization error (403) - Used when user lacks required permissions
63+
* @param message - Error message (default: 'Insufficient permissions')
64+
*/
65+
export class AuthorizationError extends AppError {
66+
constructor(message: string = 'Insufficient permissions') {
67+
super(403, message, true, 'AUTHORIZATION_ERROR');
68+
this.name = 'AuthorizationError';
69+
}
70+
}
71+
72+
/**
73+
* Not found error (404) - Used when a resource doesn't exist
74+
* @param resource - Resource name (e.g., 'Story', 'User')
75+
*/
76+
export class NotFoundError extends AppError {
77+
constructor(resource: string = 'Resource') {
78+
super(404, `${resource} not found`, true, 'NOT_FOUND');
79+
this.name = 'NotFoundError';
80+
}
81+
}
82+
83+
/**
84+
* Conflict error (409) - Used when there's a resource conflict
85+
* @param message - Error message describing the conflict
86+
*/
87+
export class ConflictError extends AppError {
88+
constructor(message: string) {
89+
super(409, message, true, 'CONFLICT');
90+
this.name = 'ConflictError';
91+
}
92+
}
93+
94+
/**
95+
* Internal server error (500) - Used for unexpected server errors
96+
* @param message - Error message (default: 'Internal server error')
97+
*/
98+
export class InternalServerError extends AppError {
99+
constructor(message: string = 'Internal server error') {
100+
super(500, message, false, 'INTERNAL_ERROR');
101+
this.name = 'InternalServerError';
102+
}
103+
}

0 commit comments

Comments
 (0)