feat: Complete Z.CRM system with all 6 modules
✨ Features: - Complete authentication system with JWT - Dashboard with all 6 modules visible - Contact Management module (Salesforce-style) - CRM & Sales Pipeline module (Pipedrive-style) - Inventory & Assets module (SAP-style) - Tasks & Projects module (Jira/Asana-style) - HR Management module (BambooHR-style) - Marketing Management module (HubSpot-style) - Admin Panel with user management and role matrix - World-class UI/UX with RTL Arabic support - Cairo font (headings) + Readex Pro font (body) - Sample data for all modules - Protected routes and authentication flow - Backend API with Prisma + PostgreSQL - Comprehensive documentation 🎨 Design: - Color-coded modules - Professional data tables - Stats cards with metrics - Progress bars and status badges - Search and filters - Responsive layout 📊 Tech Stack: - Frontend: Next.js 14, TypeScript, Tailwind CSS - Backend: Node.js, Express, Prisma - Database: PostgreSQL - Auth: JWT with bcrypt 🚀 Production-ready frontend with all features accessible
This commit is contained in:
108
backend/src/shared/middleware/auth.ts
Normal file
108
backend/src/shared/middleware/auth.ts
Normal file
@@ -0,0 +1,108 @@
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import { config } from '../../config';
|
||||
import { AppError } from './errorHandler';
|
||||
import prisma from '../../config/database';
|
||||
|
||||
export interface AuthRequest extends Request {
|
||||
user?: {
|
||||
id: string;
|
||||
email: string;
|
||||
employeeId?: string;
|
||||
employee?: any;
|
||||
};
|
||||
}
|
||||
|
||||
export const authenticate = async (
|
||||
req: AuthRequest,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
try {
|
||||
// Get token from header
|
||||
const authHeader = req.headers.authorization;
|
||||
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
||||
throw new AppError(401, 'غير مصرح - Unauthorized');
|
||||
}
|
||||
|
||||
const token = authHeader.split(' ')[1];
|
||||
|
||||
// Verify token
|
||||
const decoded = jwt.verify(token, config.jwt.secret) as {
|
||||
id: string;
|
||||
email: string;
|
||||
};
|
||||
|
||||
// Get user with employee info
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: decoded.id },
|
||||
include: {
|
||||
employee: {
|
||||
include: {
|
||||
position: {
|
||||
include: {
|
||||
permissions: true,
|
||||
},
|
||||
},
|
||||
department: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!user || !user.isActive) {
|
||||
throw new AppError(401, 'غير مصرح - Unauthorized');
|
||||
}
|
||||
|
||||
// HR module requirement: User must have an active employee record
|
||||
if (!user.employee || user.employee.status !== 'ACTIVE') {
|
||||
throw new AppError(403, 'الوصول مرفوض - Access denied. Active employee record required.');
|
||||
}
|
||||
|
||||
// Attach user to request
|
||||
req.user = {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
employeeId: user.employeeId || undefined,
|
||||
employee: user.employee,
|
||||
};
|
||||
|
||||
next();
|
||||
} catch (error) {
|
||||
if (error instanceof jwt.JsonWebTokenError) {
|
||||
return next(new AppError(401, 'رمز غير صالح - Invalid token'));
|
||||
}
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
// Permission checking middleware
|
||||
export const authorize = (module: string, resource: string, action: string) => {
|
||||
return async (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
if (!req.user?.employee?.position?.permissions) {
|
||||
throw new AppError(403, 'الوصول مرفوض - Access denied');
|
||||
}
|
||||
|
||||
// Find permission for this module and resource
|
||||
const permission = req.user.employee.position.permissions.find(
|
||||
(p: any) => p.module === module && p.resource === resource
|
||||
);
|
||||
|
||||
if (!permission) {
|
||||
throw new AppError(403, 'الوصول مرفوض - Access denied');
|
||||
}
|
||||
|
||||
// Check if action is allowed
|
||||
const actions = permission.actions as string[];
|
||||
if (!actions.includes(action) && !actions.includes('*')) {
|
||||
throw new AppError(403, 'الوصول مرفوض - Access denied');
|
||||
}
|
||||
|
||||
next();
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
66
backend/src/shared/middleware/errorHandler.ts
Normal file
66
backend/src/shared/middleware/errorHandler.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
import { Prisma } from '@prisma/client';
|
||||
|
||||
export class AppError extends Error {
|
||||
constructor(
|
||||
public statusCode: number,
|
||||
public message: string,
|
||||
public isOperational = true
|
||||
) {
|
||||
super(message);
|
||||
Object.setPrototypeOf(this, AppError.prototype);
|
||||
}
|
||||
}
|
||||
|
||||
export const errorHandler = (
|
||||
err: Error | AppError,
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
console.error('Error:', err);
|
||||
|
||||
// Handle Prisma errors
|
||||
if (err instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
if (err.code === 'P2002') {
|
||||
return res.status(409).json({
|
||||
success: false,
|
||||
message: 'سجل مكرر - Duplicate record',
|
||||
error: 'DUPLICATE_RECORD',
|
||||
});
|
||||
}
|
||||
if (err.code === 'P2025') {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: 'السجل غير موجود - Record not found',
|
||||
error: 'RECORD_NOT_FOUND',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Handle validation errors
|
||||
if (err instanceof Prisma.PrismaClientValidationError) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'بيانات غير صالحة - Invalid data',
|
||||
error: 'VALIDATION_ERROR',
|
||||
});
|
||||
}
|
||||
|
||||
// Handle custom app errors
|
||||
if (err instanceof AppError) {
|
||||
return res.status(err.statusCode).json({
|
||||
success: false,
|
||||
message: err.message,
|
||||
error: err.message,
|
||||
});
|
||||
}
|
||||
|
||||
// Default error
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'خطأ في الخادم - Internal server error',
|
||||
error: process.env.NODE_ENV === 'development' ? err.message : 'INTERNAL_ERROR',
|
||||
});
|
||||
};
|
||||
|
||||
11
backend/src/shared/middleware/notFoundHandler.ts
Normal file
11
backend/src/shared/middleware/notFoundHandler.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { Request, Response } from 'express';
|
||||
|
||||
export const notFoundHandler = (req: Request, res: Response) => {
|
||||
res.status(404).json({
|
||||
success: false,
|
||||
message: 'الصفحة غير موجودة - Route not found',
|
||||
error: 'NOT_FOUND',
|
||||
path: req.originalUrl,
|
||||
});
|
||||
};
|
||||
|
||||
16
backend/src/shared/middleware/requestLogger.ts
Normal file
16
backend/src/shared/middleware/requestLogger.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
|
||||
export const requestLogger = (req: Request, res: Response, next: NextFunction) => {
|
||||
const start = Date.now();
|
||||
|
||||
res.on('finish', () => {
|
||||
const duration = Date.now() - start;
|
||||
const { method, originalUrl } = req;
|
||||
const { statusCode } = res;
|
||||
|
||||
console.log(`[${new Date().toISOString()}] ${method} ${originalUrl} ${statusCode} - ${duration}ms`);
|
||||
});
|
||||
|
||||
next();
|
||||
};
|
||||
|
||||
17
backend/src/shared/middleware/validation.ts
Normal file
17
backend/src/shared/middleware/validation.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
import { validationResult } from 'express-validator';
|
||||
|
||||
export const validate = (req: Request, res: Response, next: NextFunction) => {
|
||||
const errors = validationResult(req);
|
||||
|
||||
if (!errors.isEmpty()) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'بيانات غير صالحة - Validation error',
|
||||
errors: errors.array(),
|
||||
});
|
||||
}
|
||||
|
||||
next();
|
||||
};
|
||||
|
||||
56
backend/src/shared/utils/auditLogger.ts
Normal file
56
backend/src/shared/utils/auditLogger.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import prisma from '../../config/database';
|
||||
|
||||
interface AuditLogData {
|
||||
entityType: string;
|
||||
entityId: string;
|
||||
action: string;
|
||||
userId: string;
|
||||
changes?: any;
|
||||
ipAddress?: string;
|
||||
userAgent?: string;
|
||||
reason?: string;
|
||||
}
|
||||
|
||||
export class AuditLogger {
|
||||
static async log(data: AuditLogData): Promise<void> {
|
||||
try {
|
||||
await prisma.auditLog.create({
|
||||
data: {
|
||||
entityType: data.entityType,
|
||||
entityId: data.entityId,
|
||||
action: data.action,
|
||||
userId: data.userId,
|
||||
changes: data.changes || {},
|
||||
ipAddress: data.ipAddress,
|
||||
userAgent: data.userAgent,
|
||||
reason: data.reason,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to create audit log:', error);
|
||||
// Don't throw - audit logging should not break the main flow
|
||||
}
|
||||
}
|
||||
|
||||
static async getEntityHistory(entityType: string, entityId: string) {
|
||||
return prisma.auditLog.findMany({
|
||||
where: {
|
||||
entityType,
|
||||
entityId,
|
||||
},
|
||||
include: {
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
username: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: 'desc',
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
31
backend/src/shared/utils/responseFormatter.ts
Normal file
31
backend/src/shared/utils/responseFormatter.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
export class ResponseFormatter {
|
||||
static success(data: any, message?: string) {
|
||||
return {
|
||||
success: true,
|
||||
message: message || 'تم بنجاح - Success',
|
||||
data,
|
||||
};
|
||||
}
|
||||
|
||||
static paginated(data: any[], total: number, page: number, pageSize: number) {
|
||||
return {
|
||||
success: true,
|
||||
data,
|
||||
pagination: {
|
||||
total,
|
||||
page,
|
||||
pageSize,
|
||||
totalPages: Math.ceil(total / pageSize),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
static error(message: string, error?: string) {
|
||||
return {
|
||||
success: false,
|
||||
message,
|
||||
error,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user