feat: updade workspace

This commit is contained in:
thanhnv
2026-07-11 15:56:31 +09:00
parent 4fc72332f5
commit 193a449829
120 changed files with 868 additions and 350 deletions
@@ -0,0 +1,9 @@
export interface ApiResponse<T> {
success: true;
data: T;
meta?: Record<string, unknown>;
}
export function ok<T>(data: T, meta?: Record<string, unknown>): ApiResponse<T> {
return meta === undefined ? { success: true, data } : { success: true, data, meta };
}
+14
View File
@@ -0,0 +1,14 @@
import type { Request } from 'express';
export type Role = 'ADMIN' | 'MANAGER' | 'EMPLOYEE';
export interface JwtUser {
sub: number;
email: string;
role: Role;
name: string;
}
export interface AuthenticatedRequest extends Request {
user: JwtUser;
}
@@ -0,0 +1,7 @@
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
import type { AuthenticatedRequest, JwtUser } from './auth.types.js';
export const CurrentUser = createParamDecorator((_data: unknown, context: ExecutionContext): JwtUser => {
const request = context.switchToHttp().getRequest<AuthenticatedRequest>();
return request.user;
});
@@ -0,0 +1,38 @@
import { CanActivate, ExecutionContext, Inject, Injectable, UnauthorizedException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import type { Request } from 'express';
import type { AuthenticatedRequest, JwtUser } from './auth.types.js';
function readCookieToken(request: Request): string | undefined {
const header = request.headers.cookie;
if (header === undefined) {
return undefined;
}
const tokenPair = header
.split(';')
.map((part) => part.trim())
.find((part) => part.startsWith('okr_token='));
return tokenPair?.slice('okr_token='.length);
}
@Injectable()
export class JwtAuthGuard implements CanActivate {
constructor(@Inject(JwtService) private readonly jwtService: JwtService) {}
canActivate(context: ExecutionContext): boolean {
const request = context.switchToHttp().getRequest<AuthenticatedRequest>();
const authHeader = request.headers.authorization;
const bearer = authHeader?.startsWith('Bearer ') === true ? authHeader.slice(7) : undefined;
const token = bearer ?? readCookieToken(request);
if (token === undefined || token.length === 0) {
throw new UnauthorizedException('Authentication required');
}
try {
request.user = this.jwtService.verify<JwtUser>(token);
return true;
} catch {
throw new UnauthorizedException('Invalid or expired token');
}
}
}
@@ -0,0 +1,5 @@
import { SetMetadata } from '@nestjs/common';
import type { Role } from './auth.types.js';
export const ROLES_KEY = 'roles';
export const Roles = (...roles: Role[]): ReturnType<typeof SetMetadata> => SetMetadata(ROLES_KEY, roles);
@@ -0,0 +1,25 @@
import { CanActivate, ExecutionContext, ForbiddenException, Inject, Injectable } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import type { AuthenticatedRequest, Role } from './auth.types.js';
import { ROLES_KEY } from './roles.decorator.js';
@Injectable()
export class RolesGuard implements CanActivate {
constructor(@Inject(Reflector) private readonly reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
const roles = this.reflector.getAllAndOverride<Role[]>(ROLES_KEY, [
context.getHandler(),
context.getClass(),
]);
if (roles === undefined || roles.length === 0) {
return true;
}
const request = context.switchToHttp().getRequest<AuthenticatedRequest>();
if (!roles.includes(request.user.role)) {
throw new ForbiddenException('Insufficient role');
}
return true;
}
}