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:
Rayyan
2026-08-02 00:25:36 +07:00
parent fdbfb34842
commit 7ce0de4e91
132 changed files with 7754 additions and 2037 deletions
+52 -8
View File
@@ -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>,
) {}
}