هذا الالتزام موجود في:
2026-04-20 15:12:16 +03:00
التزام 28f7241bcd
172 ملفات معدلة مع 21907 إضافات و0 حذوفات

عرض الملف

@@ -0,0 +1,6 @@
import { ExecutionContext, createParamDecorator } from '@nestjs/common';
export const CurrentUser = createParamDecorator((_: unknown, context: ExecutionContext) => {
const request = context.switchToHttp().getRequest();
return request.user;
});

عرض الملف

@@ -0,0 +1,4 @@
import { SetMetadata } from '@nestjs/common';
export const IS_PUBLIC_KEY = 'isPublic';
export const Public = (): ReturnType<typeof SetMetadata> => SetMetadata(IS_PUBLIC_KEY, true);

عرض الملف

@@ -0,0 +1,5 @@
import { SetMetadata } from '@nestjs/common';
export const ROLES_KEY = 'roles';
export const Roles = (...roles: string[]): ReturnType<typeof SetMetadata> =>
SetMetadata(ROLES_KEY, roles);

عرض الملف

@@ -0,0 +1,11 @@
import { SetMetadata } from '@nestjs/common';
export const THROTTLE_META_KEY = 'throttle_meta_key';
export type ThrottleMeta = {
limit: number;
windowMs: number;
};
export const Throttle = (limit: number, windowMs: number) =>
SetMetadata(THROTTLE_META_KEY, { limit, windowMs } satisfies ThrottleMeta);

عرض الملف

@@ -0,0 +1,6 @@
import { IsMongoId } from 'class-validator';
export class ObjectIdParamDto {
@IsMongoId()
id!: string;
}

عرض الملف

@@ -0,0 +1,22 @@
import { Type } from 'class-transformer';
import { IsInt, IsOptional, IsString, Max, Min } from 'class-validator';
import { APP_CONSTANTS } from '../../config/constants';
export class PaginationQueryDto {
@IsOptional()
@Type(() => Number)
@IsInt()
@Min(1)
page?: number = APP_CONSTANTS.DEFAULT_PAGE;
@IsOptional()
@Type(() => Number)
@IsInt()
@Min(1)
@Max(APP_CONSTANTS.MAX_LIMIT)
limit?: number = APP_CONSTANTS.DEFAULT_LIMIT;
@IsOptional()
@IsString()
cursor?: string;
}

عرض الملف

@@ -0,0 +1,6 @@
export enum ExperienceLevel {
BEGINNER = 'beginner',
INTERMEDIATE = 'intermediate',
ADVANCED = 'advanced',
PROFESSIONAL = 'professional',
}

عرض الملف

@@ -0,0 +1,8 @@
export enum MusicRole {
INSTRUMENTALIST = 'instrumentalist',
SINGER = 'singer',
COMPOSER = 'composer',
LYRICIST = 'lyricist',
PRODUCER = 'producer',
ARRANGER = 'arranger',
}

عرض الملف

@@ -0,0 +1,6 @@
export enum NotificationType {
LIKE = 'like',
COMMENT = 'comment',
FOLLOW = 'follow',
MESSAGE = 'message',
}

عرض الملف

@@ -0,0 +1,5 @@
export enum PostType {
TEXT = 'text',
VIDEO = 'video',
AUDIO = 'audio',
}

عرض الملف

@@ -0,0 +1,5 @@
export enum PostVisibility {
PUBLIC = 'public',
FOLLOWERS = 'followers',
PRIVATE = 'private',
}

عرض الملف

@@ -0,0 +1,4 @@
export enum TokenType {
ACCESS = 'access',
REFRESH = 'refresh',
}

عرض الملف

@@ -0,0 +1,5 @@
export enum UserRole {
USER = 'user',
ADMIN = 'admin',
SUPERADMIN = 'superadmin',
}

عرض الملف

@@ -0,0 +1,5 @@
import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {}

عرض الملف

@@ -0,0 +1,5 @@
import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
@Injectable()
export class JwtRefreshGuard extends AuthGuard('jwt-refresh') {}

عرض الملف

@@ -0,0 +1,29 @@
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { ROLES_KEY } from '../decorators/roles.decorator';
@Injectable()
export class RolesGuard implements CanActivate {
constructor(private readonly reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
const requiredRoles = this.reflector.getAllAndOverride<string[]>(ROLES_KEY, [
context.getHandler(),
context.getClass(),
]);
if (!requiredRoles || requiredRoles.length === 0) {
return true;
}
const request = context.switchToHttp().getRequest();
const payload = request.user ?? {};
const userRoles: string[] = Array.isArray(payload.roles)
? payload.roles
: payload.role
? [payload.role]
: [];
return requiredRoles.some((role) => userRoles.includes(role));
}
}

عرض الملف

@@ -0,0 +1,5 @@
import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
@Injectable()
export class SuperAdminJwtAuthGuard extends AuthGuard('superadmin-jwt') {}

عرض الملف

@@ -0,0 +1,50 @@
import {
CanActivate,
ExecutionContext,
HttpException,
HttpStatus,
Injectable,
} from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { THROTTLE_META_KEY, ThrottleMeta } from '../decorators/throttle.decorator';
type Bucket = {
count: number;
resetAt: number;
};
@Injectable()
export class ThrottleGuard implements CanActivate {
private readonly buckets = new Map<string, Bucket>();
constructor(private readonly reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
const meta = this.reflector.getAllAndOverride<ThrottleMeta>(THROTTLE_META_KEY, [
context.getHandler(),
context.getClass(),
]);
if (!meta) {
return true;
}
const req = context.switchToHttp().getRequest<Request & { ip?: string; originalUrl?: string }>();
const ip = req.ip ?? 'unknown';
const route = req.originalUrl ?? 'unknown-route';
const key = `${ip}:${route}`;
const now = Date.now();
const existing = this.buckets.get(key);
if (!existing || now > existing.resetAt) {
this.buckets.set(key, { count: 1, resetAt: now + meta.windowMs });
return true;
}
if (existing.count >= meta.limit) {
throw new HttpException('Too many requests, please try again later', HttpStatus.TOO_MANY_REQUESTS);
}
existing.count += 1;
return true;
}
}

عرض الملف

@@ -0,0 +1,25 @@
import {
CallHandler,
ExecutionContext,
Injectable,
NestInterceptor,
} from '@nestjs/common';
import { map, Observable } from 'rxjs';
@Injectable()
export class ResponseEnvelopeInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<unknown> {
const http = context.switchToHttp();
const response = http.getResponse<{ statusCode?: number }>();
return next.handle().pipe(
map((data) => ({
data,
meta: {
statusCode: response.statusCode ?? 200,
timestamp: new Date().toISOString(),
},
})),
);
}
}

عرض الملف

@@ -0,0 +1,7 @@
export interface JwtPayload {
sub: string;
username: string;
role?: string;
tokenType: 'access' | 'refresh' | 'superadmin_access' | 'superadmin_refresh';
email?: string;
}

عرض الملف

@@ -0,0 +1,13 @@
import { decodeOffsetCursor, encodeOffsetCursor } from './cursor.util';
describe('cursor util', () => {
it('encodes and decodes cursor offsets', () => {
const cursor = encodeOffsetCursor(40);
expect(decodeOffsetCursor(cursor)).toBe(40);
});
it('returns null on invalid cursor', () => {
expect(decodeOffsetCursor('%%%invalid%%%')).toBeNull();
expect(decodeOffsetCursor(encodeOffsetCursor(-1))).toBeNull();
});
});

عرض الملف

@@ -0,0 +1,19 @@
export const encodeOffsetCursor = (offset: number): string =>
Buffer.from(String(offset), 'utf8').toString('base64url');
export const decodeOffsetCursor = (cursor?: string): number | null => {
if (!cursor) {
return null;
}
try {
const raw = Buffer.from(cursor, 'base64url').toString('utf8');
const parsed = Number(raw);
if (!Number.isInteger(parsed) || parsed < 0) {
return null;
}
return parsed;
} catch {
return null;
}
};

عرض الملف

@@ -0,0 +1,7 @@
import * as bcrypt from 'bcrypt';
export const hashValue = async (value: string, saltRounds: number): Promise<string> =>
bcrypt.hash(value, saltRounds);
export const compareHash = async (value: string, hashedValue: string): Promise<boolean> =>
bcrypt.compare(value, hashedValue);