feat(identity): redesign identity module and introduce RBAC foundation
- Redesign the Identity module with a richer domain model. - Extend the User entity to support username, Authentik integration, activity tracking, and storage information. - Add Role and Permission domain models with many-to-many relationships. - Implement RBAC foundation using UserRole, RolePermission, and UserPermission mappings. - Add user storage quota and usage fields with default values. - Introduce Authentik identifiers and synchronization metadata. - Refactor user domain logic for role and permission management. - Update Prisma schema to support the new identity architecture. - Improve JWT authentication and permission guard integration. - Update repositories, handlers, controllers, mappers, DTOs, and Swagger configuration. - Refresh environment configuration and project dependencies.
This commit is contained in:
@@ -4,17 +4,23 @@ import {
|
||||
ExecutionContext,
|
||||
UnauthorizedException,
|
||||
} from '@nestjs/common';
|
||||
import { JwtService } from '@nestjs/jwt';
|
||||
import { Request } from 'express';
|
||||
import { createRemoteJWKSet, jwtVerify } from 'jose';
|
||||
import { IdentityData } from '../interfaces/identity-data';
|
||||
import * as jwt from 'jsonwebtoken';
|
||||
|
||||
/**
|
||||
* JwtAuthGuard verifies JWTs issued by the external Identity Provider (Authentik)
|
||||
* using JWKS (RS256). It also accepts internal RayLab JWTs signed with a local secret.
|
||||
*/
|
||||
@Injectable()
|
||||
export class JwtAuthGuard implements CanActivate {
|
||||
constructor(
|
||||
private readonly jwtService: JwtService,
|
||||
) {}
|
||||
private jwks: ReturnType<typeof createRemoteJWKSet> | null = null;
|
||||
|
||||
constructor() {}
|
||||
|
||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
const request = context.switchToHttp().getRequest<Request>();
|
||||
const request = context.switchToHttp().getRequest<Request & { identity?: IdentityData }>();
|
||||
|
||||
const authHeader = request.headers.authorization;
|
||||
|
||||
@@ -28,13 +34,51 @@ export class JwtAuthGuard implements CanActivate {
|
||||
throw new UnauthorizedException('Invalid authorization header.');
|
||||
}
|
||||
|
||||
const jwksUri = process.env.AUTHENTIK_JWKS_URI;
|
||||
|
||||
// First try verifying with external JWKS (Authentik)
|
||||
if (jwksUri) {
|
||||
try {
|
||||
if (!this.jwks) this.jwks = createRemoteJWKSet(new URL(jwksUri));
|
||||
|
||||
const { payload } = await jwtVerify(token, this.jwks, {
|
||||
issuer: process.env.AUTHENTIK_ISSUER,
|
||||
audience: process.env.AUTHENTIK_AUDIENCE,
|
||||
});
|
||||
|
||||
const identity = new IdentityData(
|
||||
payload.sub as string,
|
||||
(payload as any).preferred_username as string | undefined,
|
||||
(payload as any).email as string | undefined,
|
||||
payload as Record<string, any>,
|
||||
);
|
||||
|
||||
request.identity = identity;
|
||||
return true;
|
||||
} catch (err) {
|
||||
// ignore and try internal verification
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: verify with internal symmetric secret
|
||||
const secret = process.env.RAYLAB_JWT_SECRET;
|
||||
if (!secret) {
|
||||
throw new UnauthorizedException('Invalid or expired token.');
|
||||
}
|
||||
|
||||
try {
|
||||
const payload = await this.jwtService.verifyAsync(token);
|
||||
const payload = jwt.verify(token, secret) as any;
|
||||
|
||||
request['user'] = payload;
|
||||
const identity = new IdentityData(
|
||||
payload.sub as string,
|
||||
payload.preferred_username as string | undefined,
|
||||
payload.email as string | undefined,
|
||||
payload as Record<string, any>,
|
||||
);
|
||||
|
||||
request.identity = identity;
|
||||
return true;
|
||||
} catch {
|
||||
} catch (err: any) {
|
||||
throw new UnauthorizedException('Invalid or expired token.');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { Request } from 'express';
|
||||
|
||||
export interface JwtPayload {
|
||||
sub: string;
|
||||
email: string;
|
||||
role: string;
|
||||
permissions: string[];
|
||||
}
|
||||
import { UserData } from '../../../modules/identity/domain/entities/user.entity';
|
||||
import { IdentityData } from './identity-data';
|
||||
|
||||
export interface AuthenticatedRequest extends Request {
|
||||
user: JwtPayload;
|
||||
// Identity comes from the external Identity Provider (Authentik)
|
||||
// JwtAuthGuard must set request.identity = payload
|
||||
identity?: IdentityData;
|
||||
|
||||
// After CurrentUserGuard resolves the user from repository, it must set request.currentUser = User Domain
|
||||
currentUser?: UserData;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
export class IdentityData {
|
||||
constructor(
|
||||
public readonly sub: string,
|
||||
public readonly preferred_username?: string,
|
||||
public readonly email?: string,
|
||||
public readonly claims?: Record<string, any>,
|
||||
) {}
|
||||
}
|
||||
Reference in New Issue
Block a user