feat: add admin guard and JWT role support

- Create AdminGuard to check user role
- Update JWT strategy to include role in payload
- Update auth service to include role in token generation
- Prepare admin module structure
- TypeScript will resolve lint errors after server restart
This commit is contained in:
dwindown
2025-10-11 14:15:34 +07:00
parent c3bc181063
commit 9b789b333f
26 changed files with 117 additions and 15 deletions

View File

@@ -0,0 +1 @@
export {};

View File

@@ -0,0 +1,3 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
//# sourceMappingURL=admin-plans.controller.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"admin-plans.controller.js","sourceRoot":"","sources":["../../src/admin/admin-plans.controller.ts"],"names":[],"mappings":""}

View File

@@ -0,0 +1 @@
export {};

View File

@@ -0,0 +1,3 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
//# sourceMappingURL=admin-plans.service.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"admin-plans.service.js","sourceRoot":"","sources":["../../src/admin/admin-plans.service.ts"],"names":[],"mappings":""}

1
apps/api/dist/admin/admin.module.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
export {};

3
apps/api/dist/admin/admin.module.js vendored Normal file
View File

@@ -0,0 +1,3 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
//# sourceMappingURL=admin.module.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"admin.module.js","sourceRoot":"","sources":["../../src/admin/admin.module.ts"],"names":[],"mappings":""}

View File

@@ -0,0 +1,4 @@
import { CanActivate, ExecutionContext } from '@nestjs/common';
export declare class AdminGuard implements CanActivate {
canActivate(context: ExecutionContext): boolean;
}

View File

@@ -0,0 +1,28 @@
"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.AdminGuard = void 0;
const common_1 = require("@nestjs/common");
let AdminGuard = class AdminGuard {
canActivate(context) {
const request = context.switchToHttp().getRequest();
const user = request.user;
if (!user) {
throw new common_1.ForbiddenException('Authentication required');
}
if (user.role !== 'admin') {
throw new common_1.ForbiddenException('Admin access required');
}
return true;
}
};
exports.AdminGuard = AdminGuard;
exports.AdminGuard = AdminGuard = __decorate([
(0, common_1.Injectable)()
], AdminGuard);
//# sourceMappingURL=admin.guard.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"admin.guard.js","sourceRoot":"","sources":["../../../src/admin/guards/admin.guard.ts"],"names":[],"mappings":";;;;;;;;;AAAA,2CAKwB;AAUjB,IAAM,UAAU,GAAhB,MAAM,UAAU;IACrB,WAAW,CAAC,OAAyB;QACnC,MAAM,OAAO,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC,UAAU,EAAmB,CAAC;QACrE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;QAE1B,IAAI,CAAC,IAAI,EAAE,CAAC;YACV,MAAM,IAAI,2BAAkB,CAAC,yBAAyB,CAAC,CAAC;QAC1D,CAAC;QAED,IAAI,IAAI,CAAC,IAAI,KAAK,OAAO,EAAE,CAAC;YAC1B,MAAM,IAAI,2BAAkB,CAAC,uBAAuB,CAAC,CAAC;QACxD,CAAC;QAED,OAAO,IAAI,CAAC;IACd,CAAC;CACF,CAAA;AAfY,gCAAU;qBAAV,UAAU;IADtB,IAAA,mBAAU,GAAE;GACA,UAAU,CAetB"}

View File

@@ -21,7 +21,7 @@ export declare class AuthController {
avatarUrl: string | null;
emailVerified: boolean;
};
token: string;
token: Promise<string>;
}>;
login(body: {
email: string;
@@ -44,7 +44,7 @@ export declare class AuthController {
avatarUrl: string | null;
emailVerified: boolean;
};
token: string;
token: Promise<string>;
requiresOtp?: undefined;
availableMethods?: undefined;
tempToken?: undefined;
@@ -61,7 +61,7 @@ export declare class AuthController {
avatarUrl: string | null;
emailVerified: boolean;
};
token: string;
token: Promise<string>;
}>;
googleAuth(): Promise<void>;
googleAuthCallback(req: any, res: Response): Promise<void>;

View File

@@ -14,7 +14,7 @@ export declare class AuthService {
avatarUrl: string | null;
emailVerified: boolean;
};
token: string;
token: Promise<string>;
}>;
login(email: string, password: string): Promise<{
requiresOtp: boolean;
@@ -34,7 +34,7 @@ export declare class AuthService {
avatarUrl: string | null;
emailVerified: boolean;
};
token: string;
token: Promise<string>;
requiresOtp?: undefined;
availableMethods?: undefined;
tempToken?: undefined;
@@ -62,7 +62,7 @@ export declare class AuthService {
avatarUrl: string | null;
emailVerified: boolean;
};
token: string;
token: Promise<string>;
requiresOtp?: undefined;
availableMethods?: undefined;
tempToken?: undefined;
@@ -75,7 +75,7 @@ export declare class AuthService {
avatarUrl: string | null;
emailVerified: boolean;
};
token: string;
token: Promise<string>;
}>;
private generateToken;
private generateTempToken;

View File

@@ -317,10 +317,15 @@ let AuthService = class AuthService {
token,
};
}
generateToken(userId, email) {
async generateToken(userId, email) {
const user = await this.prisma.user.findUnique({
where: { id: userId },
select: { role: true },
});
return this.jwtService.sign({
sub: userId,
email,
role: user?.role || 'user',
});
}
generateTempToken(userId, email) {

File diff suppressed because one or more lines are too long

View File

@@ -2,10 +2,11 @@ import { Strategy } from 'passport-jwt';
export interface JwtPayload {
sub: string;
email: string;
role?: string;
iat?: number;
exp?: number;
}
declare const JwtStrategy_base: new (...args: [opt: import("passport-jwt").StrategyOptionsWithRequest] | [opt: import("passport-jwt").StrategyOptionsWithoutRequest]) => Strategy & {
declare const JwtStrategy_base: new (...args: [opt: import("passport-jwt").StrategyOptionsWithoutRequest] | [opt: import("passport-jwt").StrategyOptionsWithRequest]) => Strategy & {
validate(...args: any[]): unknown;
};
export declare class JwtStrategy extends JwtStrategy_base {
@@ -13,6 +14,7 @@ export declare class JwtStrategy extends JwtStrategy_base {
validate(payload: JwtPayload): Promise<{
userId: string;
email: string;
role: string;
}>;
}
export {};

View File

@@ -22,7 +22,11 @@ let JwtStrategy = class JwtStrategy extends (0, passport_1.PassportStrategy)(pas
});
}
async validate(payload) {
return { userId: payload.sub, email: payload.email };
return {
userId: payload.sub,
email: payload.email,
role: payload.role || 'user'
};
}
};
exports.JwtStrategy = JwtStrategy;

View File

@@ -1 +1 @@
{"version":3,"file":"jwt.strategy.js","sourceRoot":"","sources":["../../src/auth/jwt.strategy.ts"],"names":[],"mappings":";;;;;;;;;;;;AAAA,2CAA4C;AAC5C,+CAAoD;AACpD,+CAAoD;AAU7C,IAAM,WAAW,GAAjB,MAAM,WAAY,SAAQ,IAAA,2BAAgB,EAAC,uBAAQ,CAAC;IACzD;QACE,KAAK,CAAC;YACJ,cAAc,EAAE,yBAAU,CAAC,2BAA2B,EAAE;YACxD,gBAAgB,EAAE,KAAK;YACvB,WAAW,EAAE,OAAO,CAAC,GAAG,CAAC,UAAU,IAAI,6BAA6B;SACrE,CAAC,CAAC;IACL,CAAC;IAED,KAAK,CAAC,QAAQ,CAAC,OAAmB;QAChC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,GAAG,EAAE,KAAK,EAAE,OAAO,CAAC,KAAK,EAAE,CAAC;IACvD,CAAC;CACF,CAAA;AAZY,kCAAW;sBAAX,WAAW;IADvB,IAAA,mBAAU,GAAE;;GACA,WAAW,CAYvB"}
{"version":3,"file":"jwt.strategy.js","sourceRoot":"","sources":["../../src/auth/jwt.strategy.ts"],"names":[],"mappings":";;;;;;;;;;;;AAAA,2CAA4C;AAC5C,+CAAoD;AACpD,+CAAoD;AAW7C,IAAM,WAAW,GAAjB,MAAM,WAAY,SAAQ,IAAA,2BAAgB,EAAC,uBAAQ,CAAC;IACzD;QACE,KAAK,CAAC;YACJ,cAAc,EAAE,yBAAU,CAAC,2BAA2B,EAAE;YACxD,gBAAgB,EAAE,KAAK;YACvB,WAAW,EAAE,OAAO,CAAC,GAAG,CAAC,UAAU,IAAI,6BAA6B;SACrE,CAAC,CAAC;IACL,CAAC;IAED,KAAK,CAAC,QAAQ,CAAC,OAAmB;QAChC,OAAO;YACL,MAAM,EAAE,OAAO,CAAC,GAAG;YACnB,KAAK,EAAE,OAAO,CAAC,KAAK;YACpB,IAAI,EAAE,OAAO,CAAC,IAAI,IAAI,MAAM;SAC7B,CAAC;IACJ,CAAC;CACF,CAAA;AAhBY,kCAAW;sBAAX,WAAW;IADvB,IAAA,mBAAU,GAAE;;GACA,WAAW,CAgBvB"}

File diff suppressed because one or more lines are too long

View File

View File

@@ -0,0 +1,31 @@
import {
Injectable,
CanActivate,
ExecutionContext,
ForbiddenException,
} from '@nestjs/common';
interface RequestWithUser {
user: {
userId: string;
role?: string;
};
}
@Injectable()
export class AdminGuard implements CanActivate {
canActivate(context: ExecutionContext): boolean {
const request = context.switchToHttp().getRequest<RequestWithUser>();
const user = request.user;
if (!user) {
throw new ForbiddenException('Authentication required');
}
if (user.role !== 'admin') {
throw new ForbiddenException('Admin access required');
}
return true;
}
}

View File

@@ -359,10 +359,17 @@ export class AuthService {
};
}
private generateToken(userId: string, email: string): string {
private async generateToken(userId: string, email: string): Promise<string> {
// Get user role
const user = await this.prisma.user.findUnique({
where: { id: userId },
select: { role: true },
});
return this.jwtService.sign({
sub: userId,
email,
role: user?.role || 'user',
});
}

View File

@@ -5,6 +5,7 @@ import { ExtractJwt, Strategy } from 'passport-jwt';
export interface JwtPayload {
sub: string; // user ID
email: string;
role?: string; // user role
iat?: number;
exp?: number;
}
@@ -20,6 +21,10 @@ export class JwtStrategy extends PassportStrategy(Strategy) {
}
async validate(payload: JwtPayload) {
return { userId: payload.sub, email: payload.email };
return {
userId: payload.sub,
email: payload.email,
role: payload.role || 'user'
};
}
}