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:
@@ -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,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user