🚀 Initialize Musadaq SaaS: Full Backend + AI + React Dashboard + Docker Setup
This commit is contained in:
14
backend/src/common/decorators/current-user.decorator.ts
Normal file
14
backend/src/common/decorators/current-user.decorator.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* ════════════════════════════════════════════════════════════
|
||||
* مُصادَق (Musadaq) — CurrentUser Decorator
|
||||
* ════════════════════════════════════════════════════════════
|
||||
*/
|
||||
|
||||
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
|
||||
|
||||
export const CurrentUser = createParamDecorator(
|
||||
(data: unknown, ctx: ExecutionContext) => {
|
||||
const request = ctx.switchToHttp().getRequest();
|
||||
return request.user;
|
||||
},
|
||||
);
|
||||
11
backend/src/common/decorators/roles.decorator.ts
Normal file
11
backend/src/common/decorators/roles.decorator.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* ════════════════════════════════════════════════════════════
|
||||
* مُصادَق (Musadaq) — Roles Decorator
|
||||
* ════════════════════════════════════════════════════════════
|
||||
*/
|
||||
|
||||
import { SetMetadata } from '@nestjs/common';
|
||||
import { UserRole } from '../../modules/users/enums/role.enum';
|
||||
|
||||
export const ROLES_KEY = 'roles';
|
||||
export const Roles = (...roles: UserRole[]) => SetMetadata(ROLES_KEY, roles);
|
||||
54
backend/src/common/filters/global-exception.filter.ts
Normal file
54
backend/src/common/filters/global-exception.filter.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
* ════════════════════════════════════════════════════════════
|
||||
* مُصادَق (Musadaq) — Global Exception Filter
|
||||
* ════════════════════════════════════════════════════════════
|
||||
*/
|
||||
|
||||
import {
|
||||
ExceptionFilter,
|
||||
Catch,
|
||||
ArgumentsHost,
|
||||
HttpException,
|
||||
HttpStatus,
|
||||
Logger,
|
||||
} from '@nestjs/common';
|
||||
import { Request, Response } from 'express';
|
||||
|
||||
@Catch()
|
||||
export class GlobalExceptionFilter implements ExceptionFilter {
|
||||
private readonly logger = new Logger(GlobalExceptionFilter.name);
|
||||
|
||||
catch(exception: unknown, host: ArgumentsHost) {
|
||||
const ctx = host.switchToHttp();
|
||||
const response = ctx.getResponse<Response>();
|
||||
const request = ctx.getRequest<Request>();
|
||||
|
||||
const status =
|
||||
exception instanceof HttpException
|
||||
? exception.getStatus()
|
||||
: HttpStatus.INTERNAL_SERVER_ERROR;
|
||||
|
||||
const message =
|
||||
exception instanceof HttpException
|
||||
? exception.getResponse()
|
||||
: 'Internal Server Error';
|
||||
|
||||
// Log the error
|
||||
if (status >= 500) {
|
||||
this.logger.error(
|
||||
`${request.method} ${request.url} - ${status} - ${JSON.stringify(message)}`,
|
||||
exception instanceof Error ? exception.stack : '',
|
||||
);
|
||||
} else {
|
||||
this.logger.warn(`${request.method} ${request.url} - ${status} - ${JSON.stringify(message)}`);
|
||||
}
|
||||
|
||||
response.status(status).json({
|
||||
statusCode: status,
|
||||
timestamp: new Date().toISOString(),
|
||||
path: request.url,
|
||||
message: typeof message === 'string' ? message : (message as any).message || message,
|
||||
error: typeof message === 'string' ? null : (message as any).error || null,
|
||||
});
|
||||
}
|
||||
}
|
||||
30
backend/src/common/guards/jwt-auth.guard.ts
Normal file
30
backend/src/common/guards/jwt-auth.guard.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* ════════════════════════════════════════════════════════════
|
||||
* مُصادَق (Musadaq) — JWT Auth Guard
|
||||
* ════════════════════════════════════════════════════════════
|
||||
*/
|
||||
|
||||
import {
|
||||
ExecutionContext,
|
||||
Injectable,
|
||||
UnauthorizedException,
|
||||
} from '@nestjs/common';
|
||||
import { AuthGuard } from '@nestjs/passport';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
@Injectable()
|
||||
export class JwtAuthGuard extends AuthGuard('jwt') {
|
||||
canActivate(
|
||||
context: ExecutionContext,
|
||||
): boolean | Promise<boolean> | Observable<boolean> {
|
||||
// Add custom authentication logic here if needed
|
||||
return super.canActivate(context);
|
||||
}
|
||||
|
||||
handleRequest(err: any, user: any, info: any) {
|
||||
if (err || !user) {
|
||||
throw err || new UnauthorizedException('Authentication Required');
|
||||
}
|
||||
return user;
|
||||
}
|
||||
}
|
||||
45
backend/src/common/guards/roles.guard.ts
Normal file
45
backend/src/common/guards/roles.guard.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
/**
|
||||
* ════════════════════════════════════════════════════════════
|
||||
* مُصادَق (Musadaq) — Roles Guard
|
||||
* ════════════════════════════════════════════════════════════
|
||||
*/
|
||||
|
||||
import {
|
||||
Injectable,
|
||||
CanActivate,
|
||||
ExecutionContext,
|
||||
ForbiddenException,
|
||||
} from '@nestjs/common';
|
||||
import { Reflector } from '@nestjs/core';
|
||||
import { UserRole } from '../../modules/users/enums/role.enum';
|
||||
import { ROLES_KEY } from '../decorators/roles.decorator';
|
||||
|
||||
@Injectable()
|
||||
export class RolesGuard implements CanActivate {
|
||||
constructor(private reflector: Reflector) {}
|
||||
|
||||
canActivate(context: ExecutionContext): boolean {
|
||||
const requiredRoles = this.reflector.getAllAndOverride<UserRole[]>(ROLES_KEY, [
|
||||
context.getHandler(),
|
||||
context.getClass(),
|
||||
]);
|
||||
|
||||
if (!requiredRoles) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const { user } = context.switchToHttp().getRequest();
|
||||
|
||||
if (!user) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const hasRole = requiredRoles.includes(user.role);
|
||||
|
||||
if (!hasRole) {
|
||||
throw new ForbiddenException('Required Role Missing');
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
32
backend/src/common/guards/tenant.guard.ts
Normal file
32
backend/src/common/guards/tenant.guard.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* ════════════════════════════════════════════════════════════
|
||||
* مُصادَق (Musadaq) — Tenant Guard
|
||||
* ════════════════════════════════════════════════════════════
|
||||
* يضمن أن المستخدم الحالي يحمل نفس tenant_id الكيان المطلوب.
|
||||
* يمنع الوصول العشوائي للبيانات عبر المستأجرين.
|
||||
* ════════════════════════════════════════════════════════════
|
||||
*/
|
||||
|
||||
import {
|
||||
Injectable,
|
||||
CanActivate,
|
||||
ExecutionContext,
|
||||
ForbiddenException,
|
||||
} from '@nestjs/common';
|
||||
|
||||
@Injectable()
|
||||
export class TenantGuard implements CanActivate {
|
||||
canActivate(context: ExecutionContext): boolean {
|
||||
const request = context.switchToHttp().getRequest();
|
||||
const { user, params } = request;
|
||||
|
||||
if (!user || !user.tenantId) {
|
||||
throw new ForbiddenException('Invalid User Context');
|
||||
}
|
||||
|
||||
// This is a stub: real implementation will check the target entity's tenant_id
|
||||
// against the user's tenant_id if applicable.
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
37
backend/src/common/interceptors/audit-log.interceptor.ts
Normal file
37
backend/src/common/interceptors/audit-log.interceptor.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* ════════════════════════════════════════════════════════════
|
||||
* مُصادَق (Musadaq) — Audit Log Interceptor
|
||||
* ════════════════════════════════════════════════════════════
|
||||
*/
|
||||
|
||||
import {
|
||||
Injectable,
|
||||
NestInterceptor,
|
||||
ExecutionContext,
|
||||
CallHandler,
|
||||
Logger,
|
||||
} from '@nestjs/common';
|
||||
import { Observable } from 'rxjs';
|
||||
import { tap } from 'rxjs/operators';
|
||||
|
||||
@Injectable()
|
||||
export class AuditLogInterceptor implements NestInterceptor {
|
||||
private readonly logger = new Logger(AuditLogInterceptor.name);
|
||||
|
||||
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
|
||||
const request = context.switchToHttp().getRequest();
|
||||
const { user, method, url } = request;
|
||||
|
||||
return next.handle().pipe(
|
||||
tap(() => {
|
||||
// Only log non-GET requests (mutative operations)
|
||||
if (method !== 'GET') {
|
||||
this.logger.log(
|
||||
`Audit: User ${user?.id || 'Anonymous'} - ${method} ${url}`,
|
||||
);
|
||||
// Phase 2: Save to Database
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
32
backend/src/common/middleware/tenant.middleware.ts
Normal file
32
backend/src/common/middleware/tenant.middleware.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* ════════════════════════════════════════════════════════════
|
||||
* مُصادَق (Musadaq) — Tenant Middleware
|
||||
* ════════════════════════════════════════════════════════════
|
||||
* يستخرج tenantId من JWT ويضعه في request.
|
||||
* يستخدم في تصفية البيانات (Query filtering).
|
||||
* ════════════════════════════════════════════════════════════
|
||||
*/
|
||||
|
||||
import {
|
||||
Injectable,
|
||||
NestMiddleware,
|
||||
UnauthorizedException,
|
||||
} from '@nestjs/common';
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
|
||||
@Injectable()
|
||||
export class TenantMiddleware implements NestMiddleware {
|
||||
use(req: Request, res: Response, next: NextFunction) {
|
||||
const { user } = req as any;
|
||||
|
||||
if (!user || !user.tenantId) {
|
||||
// Phase 1: Not always required (registration, login)
|
||||
return next();
|
||||
}
|
||||
|
||||
// Set tenant context for the request
|
||||
(req as any).tenantId = user.tenantId;
|
||||
|
||||
next();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user