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
@@ -1,52 +1,196 @@
export class User {
private constructor(
import { RoleData } from "./role.entity";
import { PermissionData } from "./permission.entity";
export class UserData {
private constructor(
public readonly id: string,
public name: string,
public authentikId: string | null,
public username: string,
public email: string,
public password: string,
public metadata?: Record<string, any>,
public password: string | null,
public roles: RoleData[],
public permissions: PermissionData[],
public isActive: boolean,
public deletedAt: Date | null,
public lastSeenAt: Date | null,
public storageQuota: number,
public storageUsed: number,
) {}
static create(data: {
name: string;
//#region Create
static create(data: {
username: string;
email: string;
password: string;
metadata?: Record<string, any>;
}): User {
return new User(
password?: string | null;
authentikId?: string | null;
storageQuota?: number;
storageUsed?: number;
}): UserData {
const quota = data.storageQuota !== undefined ? data.storageQuota : 10737418240; // 10 GB
const used = data.storageUsed !== undefined ? data.storageUsed : 0;
return new UserData(
crypto.randomUUID(),
data.name,
data.authentikId || null,
data.username,
data.email,
data.password,
data.metadata,
data.password ?? null,
[],
[],
true,
null,
null,
quota,
used,
);
}
delete() {
// Business Rule
//#endregion
//#region Read
hasRole(roleId: string) : boolean {
return this.roles.some(x => x.id.toLowerCase() === roleId.toLowerCase())
}
changeEmail(email: string) {
assignRole(roleData : RoleData) : void {
if (this.hasRole(roleData.id))
throw new Error("Role Sudah dimiliki.");
this.roles.push(roleData);
}
hasPermissions(permissionId : string) : boolean {
return this.permissions.some(x => x.id.toLowerCase() === permissionId.toLowerCase())
}
hasPermission(permission: string) : boolean {
const byPerm = this.permissions.some(x => x.id.toLowerCase() === permission.toLowerCase() || x.name.toLowerCase() === permission.toLowerCase());
if (byPerm) return true;
// check roles
return this.roles.some(r => r.permissions.some(p => p.id.toLowerCase() === permission.toLowerCase() || p.name.toLowerCase() === permission.toLowerCase()));
}
assignPermission(permissionData : PermissionData) : void {
if (this.hasPermissions(permissionData.id))
throw new Error("Permission sudah dimiliki.")
this.permissions.push(permissionData);
}
//#endregion
//#region Update
changeEmail(email: string) {
this.email = email;
}
changeName(name: string) {
this.name = name;
changeUsername(username: string) {
this.username = username;
}
static restore(data: {
id: string;
name: string;
email: string;
password: string;
metadata?: Record<string, any>;
}): User {
return new User(
data.id,
data.name,
data.email,
data.password,
data.metadata,
);
touchLastSeen() {
this.lastSeenAt = new Date();
}
enable() {
this.isActive = true;
}
disable() {
this.isActive = false;
}
//#endregion
//#region Delete
softDelete() {
this.deletedAt = new Date();
this.isActive = false;
}
restoreInstance() {
this.deletedAt = null;
}
//#endregion
public static restore(props: {
id: string;
authentikId?: string | null;
username: string;
email: string;
password: string | null;
roles?: RoleData[];
permissions?: PermissionData[];
isActive?: boolean;
deletedAt?: Date | null;
lastSeenAt?: Date | null;
storageQuota?: number | null;
storageUsed?: number | null;
}): UserData {
return new UserData(
props.id,
props.authentikId || null,
props.username,
props.email,
props.password,
props.roles || [],
props.permissions || [],
props.isActive !== undefined ? props.isActive : true,
props.deletedAt || null,
props.lastSeenAt || null,
props.storageQuota !== undefined && props.storageQuota !== null ? props.storageQuota : 10737418240,
props.storageUsed !== undefined && props.storageUsed !== null ? props.storageUsed : 0,
);
}
removeRole(roleId: string) {
const idx = this.roles.findIndex(r => r.id.toLowerCase() === roleId.toLowerCase());
if (idx === -1) throw new Error('Role tidak ditemukan pada user.');
this.roles.splice(idx, 1);
}
removePermission(permissionId: string) {
const idx = this.permissions.findIndex(p => p.id.toLowerCase() === permissionId.toLowerCase());
if (idx === -1) throw new Error('Permission tidak ditemukan pada user.');
this.permissions.splice(idx, 1);
}
// Storage helpers
setStorageQuota(bytes: number) {
if (bytes < 0) throw new Error('storageQuota must be >= 0');
if (this.storageUsed > bytes) throw new Error('storageQuota cannot be less than storageUsed');
this.storageQuota = bytes;
}
setStorageUsed(bytes: number) {
if (bytes < 0) throw new Error('storageUsed must be >= 0');
if (bytes > this.storageQuota) throw new Error('storageUsed cannot exceed storageQuota');
this.storageUsed = bytes;
}
// Response helper for API
toResponse() {
const remaining = this.storageQuota - this.storageUsed;
const usagePercentage = this.storageQuota > 0 ? Math.round((this.storageUsed / this.storageQuota) * 100) : 0;
return {
id: this.id,
authentikId: this.authentikId,
username: this.username,
email: this.email,
roles: this.roles,
permissions: this.permissions,
isActive: this.isActive,
deletedAt: this.deletedAt,
lastSeenAt: this.lastSeenAt,
storage: {
quota: this.storageQuota,
used: this.storageUsed,
remaining,
usagePercentage,
},
};
}
}