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:
+53
-7
@@ -1,10 +1,56 @@
|
|||||||
APP_ENV=development #development / test / prod
|
# Application
|
||||||
PORT=3069
|
BASE_URL=http://localhost:3069 PORT=3000 NODE_ENV=development
|
||||||
DATABASE_URL=postgresql://USERNAME:PASSWORD@HOST:5432/DATABASE?schema=public
|
|
||||||
|
|
||||||
JWT_SECRET=GANTI_DENGAN_SECRET
|
# Database (PostgreSQL)
|
||||||
JWT_REFRESH_SECRET=GANTI_DENGAN_REFRESH_SECRET
|
DATABASE_URL=postgresql://user:password@localhost:5432/Database?shema=public
|
||||||
|
|
||||||
|
# Authentik (OIDC discovery base)
|
||||||
|
# Used by OIDC discovery and token exchange (AuthService)
|
||||||
|
AUTHENTIK_URL=https://auth.raylab.site
|
||||||
|
|
||||||
|
# Authentik Admin API (provisioning identities & groups)
|
||||||
|
# Example: https://auth.raylab.site/api
|
||||||
|
AUTHENTIK_API_BASE=https://auth.raylab.site/api
|
||||||
|
|
||||||
|
# Bearer token for Authentik Admin API (admin service account)
|
||||||
|
AUTHENTIK_API_TOKEN=changeme_admin_api_token
|
||||||
|
|
||||||
|
# Default group name format: RL-{object}
|
||||||
|
# RayLab will use this group to gate access (default RL-RayLab-Users)
|
||||||
|
AUTHENTIK_DEFAULT_GROUP=RL-RayLab-Users
|
||||||
|
|
||||||
|
# OIDC / JWKS / Issuer settings for verifying Authentik tokens (if needed)
|
||||||
|
AUTHENTIK_JWKS_URI=https://auth.raylab.site/.well-known/jwks.json AUTHENTIK_ISSUER=https://auth.raylab.site/application/o/ray-lab-core/ AUTHENTIK_AUDIENCE=ray-lab-core
|
||||||
|
|
||||||
|
# OIDC client credentials (used when RayLab needs to call token endpoint)
|
||||||
|
AUTHENTIK_CLIENT_ID=raylab-client AUTHENTIK_CLIENT_SECRET=raylab-client-secret
|
||||||
|
|
||||||
|
# RayLab internal JWT (used to sign internal tokens)
|
||||||
|
RAYLAB_JWT_SECRET=replace_with_a_long_random_secret
|
||||||
|
|
||||||
|
# Token lifetime in seconds
|
||||||
|
RAYLAB_JWT_EXPIRES_IN=3600
|
||||||
|
|
||||||
|
# Sync / reconciliation
|
||||||
|
# Interval in seconds for reconciler to retry PENDING/FAILED syncs
|
||||||
|
AUTH_RECONCILE_INTERVAL_SECONDS=3600
|
||||||
|
|
||||||
|
# Auth / sync behavior
|
||||||
|
# Disable auto-creation of users on login (must remain false per design)
|
||||||
|
AUTH_AUTO_CREATE_USER=false AUTH_SYNC_EMAIL=true AUTH_SYNC_USERNAME=false
|
||||||
|
|
||||||
|
# Test accounts for integration tests (must exist in Authentik and RayLab DB before tests,
|
||||||
|
# or be provisioned via RayLab Admin endpoints during test run)
|
||||||
|
ADMIN_USERNAME=admin@example.com ADMIN_PASSWORD=changeme_admin_password
|
||||||
|
|
||||||
|
OWNER_USERNAME=owner@example.com OWNER_PASSWORD=changeme_owner_password
|
||||||
|
|
||||||
|
EMPLOYEE_USERNAME=employee@example.com EMPLOYEE_PASSWORD=changeme_employee_password
|
||||||
|
|
||||||
|
# Optional: integration test user (overrides defaults used by tests)
|
||||||
|
TEST_USER_EMAIL=test-integration@example.com TEST_USER_USERNAME=test-integration TEST_USER_PASSWORD=StrongP@ssw0rd!
|
||||||
|
|
||||||
|
# Optional: adjust logging or other runtime flags
|
||||||
|
LOG_LEVEL=info
|
||||||
|
|
||||||
SWAGGER_ENABLED=true
|
SWAGGER_ENABLED=true
|
||||||
|
|
||||||
LOG_LEVEL=debug
|
|
||||||
Vendored
+20
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"name": "Debug NestJS",
|
||||||
|
"type": "node",
|
||||||
|
"request": "launch",
|
||||||
|
"runtimeExecutable": "npm",
|
||||||
|
"runtimeArgs": [
|
||||||
|
"run",
|
||||||
|
"start:debug"
|
||||||
|
],
|
||||||
|
"console": "integratedTerminal",
|
||||||
|
"restart": true,
|
||||||
|
"skipFiles": [
|
||||||
|
"<node_internals>/**"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
RayLab Core
|
||||||
|
│
|
||||||
|
├── Identity
|
||||||
|
│ ├── User
|
||||||
|
│ ├── Group
|
||||||
|
│ ├── Role
|
||||||
|
│ └── Permission
|
||||||
|
│
|
||||||
|
├── Authentication
|
||||||
|
│
|
||||||
|
├── Authorization
|
||||||
|
│
|
||||||
|
├── Audit
|
||||||
|
│
|
||||||
|
├── Scheduler
|
||||||
|
│
|
||||||
|
├── Media
|
||||||
|
│
|
||||||
|
├── Storage
|
||||||
|
│
|
||||||
|
├── Configuration
|
||||||
|
│
|
||||||
|
├── Workflow / Events
|
||||||
|
│
|
||||||
|
└── Registry
|
||||||
Binary file not shown.
@@ -0,0 +1,46 @@
|
|||||||
|
Note :
|
||||||
|
- pastikan berada di branch yang benar.
|
||||||
|
-
|
||||||
|
|
||||||
|
Requirement :
|
||||||
|
- Node.js 22 LTS
|
||||||
|
|
||||||
|
Step :
|
||||||
|
1. jalankan "NPM Install".
|
||||||
|
2. generate prisma client "npx prisma generate".
|
||||||
|
3. jalankan "cp env.example env" atau buat file .env baru.
|
||||||
|
4. sesuaikan isi dari .env
|
||||||
|
|
||||||
|
Run Server :
|
||||||
|
- npm run start:dev
|
||||||
|
|
||||||
|
Command Dev :
|
||||||
|
- npm test
|
||||||
|
- npm run lint
|
||||||
|
- npm run format
|
||||||
|
- npx prisma studio {untuk cek db}
|
||||||
|
|
||||||
|
Test API :
|
||||||
|
- menggunakan postman :
|
||||||
|
arahkan ke file /postman/RayLab.postman_collection.json
|
||||||
|
- menggunakan swagger :
|
||||||
|
- masuk ke http://localhost:port/apilist
|
||||||
|
- klik authorize dan masukkan jwt token
|
||||||
|
|
||||||
|
Controller Requirement :
|
||||||
|
- diatas deklarasi class controller pastikan ada ini :
|
||||||
|
- @ApiTags('object')
|
||||||
|
- @Controller('object')
|
||||||
|
- @UseGuards(JwtAuthGuard, PermissionGuard)
|
||||||
|
|
||||||
|
- diatas deklarasi method controller pastikan ada ini :
|
||||||
|
- tipe api : @Post(), @Get(':id'), @Patch(':id'), @Delete(':id')
|
||||||
|
- @Permissions(PermissionType.SOMETHING)
|
||||||
|
- @ApiOperation({ summary: 'penjelasan singkat api' })
|
||||||
|
|
||||||
|
Table :
|
||||||
|
- untuk table akan dibuat otomatis oleh schema.prisma, disana hanya perlu ditambahkan model atau diubah, jalankan commandnya, setelah itu otomatis tablenya dibuat
|
||||||
|
- command : npx prisma migrate dev --name {Nama Perubahan}
|
||||||
|
|
||||||
|
Use case:
|
||||||
|
- penentuan use case ditentukan siapa pemiliknya dahulu, dan harus konsisten.
|
||||||
Binary file not shown.
Generated
+3617
-1296
File diff suppressed because it is too large
Load Diff
+6
-1
@@ -11,6 +11,7 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "node dist/main.js",
|
"start": "node dist/main.js",
|
||||||
"start:dev": "ts-node-dev --respawn --pretty --transpile-only src/main.ts",
|
"start:dev": "ts-node-dev --respawn --pretty --transpile-only src/main.ts",
|
||||||
|
"start:debug": "nest start --debug --watch",
|
||||||
"build": "tsc -p tsconfig.json",
|
"build": "tsc -p tsconfig.json",
|
||||||
"lint": "eslint \"src/**/*.ts\"",
|
"lint": "eslint \"src/**/*.ts\"",
|
||||||
"format": "prettier --write \"src/**/*.ts\" \"tests/**/*.ts\"",
|
"format": "prettier --write \"src/**/*.ts\" \"tests/**/*.ts\"",
|
||||||
@@ -37,6 +38,8 @@
|
|||||||
"class-transformer": "^0.5.1",
|
"class-transformer": "^0.5.1",
|
||||||
"class-validator": "^0.14.2",
|
"class-validator": "^0.14.2",
|
||||||
"dotenv": "^16.6.1",
|
"dotenv": "^16.6.1",
|
||||||
|
"jose": "^6.2.6",
|
||||||
|
"openid-client": "^6.5.0",
|
||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
"passport": "^0.7.0",
|
"passport": "^0.7.0",
|
||||||
"passport-jwt": "^4.0.1",
|
"passport-jwt": "^4.0.1",
|
||||||
@@ -47,6 +50,9 @@
|
|||||||
"uuid": "^11.1.0"
|
"uuid": "^11.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@eslint/js": "^9.21.0",
|
||||||
|
"@nestjs/testing": "^10.4.22",
|
||||||
|
"@playwright/test": "^1.62.1",
|
||||||
"@types/bcrypt": "^5.0.2",
|
"@types/bcrypt": "^5.0.2",
|
||||||
"@types/jest": "^29.5.14",
|
"@types/jest": "^29.5.14",
|
||||||
"@types/node": "^22.13.10",
|
"@types/node": "^22.13.10",
|
||||||
@@ -56,7 +62,6 @@
|
|||||||
"@typescript-eslint/eslint-plugin": "^8.25.0",
|
"@typescript-eslint/eslint-plugin": "^8.25.0",
|
||||||
"@typescript-eslint/parser": "^8.25.0",
|
"@typescript-eslint/parser": "^8.25.0",
|
||||||
"eslint": "^9.21.0",
|
"eslint": "^9.21.0",
|
||||||
"@eslint/js": "^9.21.0",
|
|
||||||
"globals": "^16.0.0",
|
"globals": "^16.0.0",
|
||||||
"jest": "^29.7.0",
|
"jest": "^29.7.0",
|
||||||
"prettier": "^3.5.3",
|
"prettier": "^3.5.3",
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import { defineConfig, devices } from '@playwright/test';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
// only run Playwright tests in the integration folder to avoid running Jest unit tests
|
||||||
|
testDir: './tests/integration',
|
||||||
|
timeout: 30 * 1000,
|
||||||
|
expect: {
|
||||||
|
timeout: 5000,
|
||||||
|
},
|
||||||
|
forbidOnly: !!process.env.CI,
|
||||||
|
retries: process.env.CI ? 2 : 0,
|
||||||
|
workers: process.env.CI ? 1 : undefined,
|
||||||
|
reporter: [["list"], ["./tests/reporter/custom-reporter.js"]],
|
||||||
|
use: {
|
||||||
|
baseURL: process.env.BASE_URL || 'http://localhost:3000',
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
{
|
||||||
|
"info": {
|
||||||
|
"_postman_id": "e1e605cc-a2c6-4410-a7aa-4692b594d42d",
|
||||||
|
"name": "RayLab",
|
||||||
|
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json",
|
||||||
|
"_exporter_id": "45313893"
|
||||||
|
},
|
||||||
|
"item": [
|
||||||
|
{
|
||||||
|
"name": "User",
|
||||||
|
"item": [
|
||||||
|
{
|
||||||
|
"name": "Create User",
|
||||||
|
"request": {
|
||||||
|
"method": "POST",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Content-Type",
|
||||||
|
"value": "application/json",
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Authorization",
|
||||||
|
"value": "Bearer ",
|
||||||
|
"description": "Bearer <JWT_Token>",
|
||||||
|
"type": "text",
|
||||||
|
"disabled": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"body": {
|
||||||
|
"mode": "raw",
|
||||||
|
"raw": "{\r\n \"username\": \"rayyan\",\r\n \"email\": \"rayyan@example.com\",\r\n \"password\": \"Password123!\",\r\n \"fullName\": \"Rayyan\"\r\n}",
|
||||||
|
"options": {
|
||||||
|
"raw": {
|
||||||
|
"language": "json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"raw": "http://localhost:3000/api/v1/users",
|
||||||
|
"protocol": "http",
|
||||||
|
"host": [
|
||||||
|
"localhost"
|
||||||
|
],
|
||||||
|
"port": "3000",
|
||||||
|
"path": [
|
||||||
|
"api",
|
||||||
|
"v1",
|
||||||
|
"users"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "User" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"username" TEXT NOT NULL,
|
||||||
|
"email" TEXT NOT NULL,
|
||||||
|
"password" TEXT NOT NULL,
|
||||||
|
"isActive" BOOLEAN NOT NULL DEFAULT true,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "User_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "Role" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"code" TEXT NOT NULL,
|
||||||
|
"name" TEXT NOT NULL,
|
||||||
|
"description" TEXT,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "Role_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "Permission" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"code" TEXT NOT NULL,
|
||||||
|
"name" TEXT NOT NULL,
|
||||||
|
"description" TEXT,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "Permission_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "UserRole" (
|
||||||
|
"userId" TEXT NOT NULL,
|
||||||
|
"roleId" TEXT NOT NULL,
|
||||||
|
"assignedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
CONSTRAINT "UserRole_pkey" PRIMARY KEY ("userId","roleId")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "RolePermission" (
|
||||||
|
"roleId" TEXT NOT NULL,
|
||||||
|
"permissionId" TEXT NOT NULL,
|
||||||
|
"assignedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
CONSTRAINT "RolePermission_pkey" PRIMARY KEY ("roleId","permissionId")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "UserPermission" (
|
||||||
|
"userId" TEXT NOT NULL,
|
||||||
|
"permissionId" TEXT NOT NULL,
|
||||||
|
"assignedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
CONSTRAINT "UserPermission_pkey" PRIMARY KEY ("userId","permissionId")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "User_username_key" ON "User"("username");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "Role_code_key" ON "Role"("code");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "Permission_code_key" ON "Permission"("code");
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "UserRole" ADD CONSTRAINT "UserRole_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "UserRole" ADD CONSTRAINT "UserRole_roleId_fkey" FOREIGN KEY ("roleId") REFERENCES "Role"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "RolePermission" ADD CONSTRAINT "RolePermission_roleId_fkey" FOREIGN KEY ("roleId") REFERENCES "Role"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "RolePermission" ADD CONSTRAINT "RolePermission_permissionId_fkey" FOREIGN KEY ("permissionId") REFERENCES "Permission"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "UserPermission" ADD CONSTRAINT "UserPermission_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "UserPermission" ADD CONSTRAINT "UserPermission_permissionId_fkey" FOREIGN KEY ("permissionId") REFERENCES "Permission"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- Added the required column `assignedBy` to the `RolePermission` table without a default value. This is not possible if the table is not empty.
|
||||||
|
- Added the required column `assignedBy` to the `UserPermission` table without a default value. This is not possible if the table is not empty.
|
||||||
|
- Added the required column `assignedBy` to the `UserRole` table without a default value. This is not possible if the table is not empty.
|
||||||
|
|
||||||
|
*/
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "RolePermission" ADD COLUMN "assignedBy" TEXT NOT NULL;
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "UserPermission" ADD COLUMN "assignedBy" TEXT NOT NULL;
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "UserRole" ADD COLUMN "assignedBy" TEXT NOT NULL;
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Role" ADD COLUMN "isDefault" BOOLEAN NOT NULL DEFAULT false;
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- A unique constraint covering the columns `[authentikId]` on the table `User` will be added. If there are existing duplicate values, this will fail.
|
||||||
|
|
||||||
|
*/
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "User" ADD COLUMN "authentikId" TEXT;
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "User_authentikId_key" ON "User"("authentikId");
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- You are about to drop the column `password` on the `User` table. All the data in the column will be lost.
|
||||||
|
|
||||||
|
*/
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "User" DROP COLUMN "password",
|
||||||
|
ADD COLUMN "deletedAt" TIMESTAMP(3),
|
||||||
|
ADD COLUMN "lastSeenAt" TIMESTAMP(3);
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- A unique constraint covering the columns `[authentikUserId]` on the table `User` will be added. If there are existing duplicate values, this will fail.
|
||||||
|
- A unique constraint covering the columns `[authentikSubject]` on the table `User` will be added. If there are existing duplicate values, this will fail.
|
||||||
|
|
||||||
|
*/
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "User" ALTER COLUMN "lastSyncedAt" SET DATA TYPE TIMESTAMP(3);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "User_authentikUserId_key" ON "User"("authentikUserId");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "User_authentikSubject_key" ON "User"("authentikSubject");
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
-- Migration: add_authentik_fields
|
||||||
|
|
||||||
|
ALTER TABLE "User"
|
||||||
|
ADD COLUMN IF NOT EXISTS "authentikUserId" TEXT;
|
||||||
|
|
||||||
|
ALTER TABLE "User"
|
||||||
|
ADD COLUMN IF NOT EXISTS "authentikSubject" TEXT;
|
||||||
|
|
||||||
|
ALTER TABLE "User"
|
||||||
|
ADD COLUMN IF NOT EXISTS "lastSyncedAt" TIMESTAMP;
|
||||||
|
|
||||||
|
ALTER TABLE "User"
|
||||||
|
ADD COLUMN IF NOT EXISTS "syncStatus" TEXT;
|
||||||
|
|
||||||
|
-- Optional: add unique constraints if desired
|
||||||
|
-- DO NOT add unique constraints without verifying existing data
|
||||||
|
-- ALTER TABLE "User" ADD CONSTRAINT "User_authentikUserId_key" UNIQUE ("authentikUserId");
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
-- Migration: add_storage
|
||||||
|
|
||||||
|
ALTER TABLE "User"
|
||||||
|
ADD COLUMN IF NOT EXISTS "storageQuota" bigint NOT NULL DEFAULT 10737418240;
|
||||||
|
|
||||||
|
ALTER TABLE "User"
|
||||||
|
ADD COLUMN IF NOT EXISTS "storageUsed" bigint NOT NULL DEFAULT 0;
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
# Please do not edit this file manually
|
||||||
|
# It should be added in your version-control system (i.e. Git)
|
||||||
|
provider = "postgresql"
|
||||||
+93
-7
@@ -12,12 +12,98 @@ datasource db {
|
|||||||
|
|
||||||
model User {
|
model User {
|
||||||
id String @id @default(uuid())
|
id String @id @default(uuid())
|
||||||
name String
|
|
||||||
|
authentikId String? @unique
|
||||||
|
authentikUserId String? @unique
|
||||||
|
authentikSubject String? @unique
|
||||||
|
|
||||||
|
username String @unique
|
||||||
email String @unique
|
email String @unique
|
||||||
password String
|
|
||||||
status String @default("ACTIVE")
|
|
||||||
metadata Json?
|
isActive Boolean @default(true)
|
||||||
created_at DateTime @default(now())
|
|
||||||
updated_at DateTime @updatedAt
|
// storage (in bytes)
|
||||||
deleted_at DateTime?
|
storageQuota BigInt @default(10737418240)
|
||||||
|
storageUsed BigInt @default(0)
|
||||||
|
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
deletedAt DateTime?
|
||||||
|
lastSeenAt DateTime?
|
||||||
|
lastSyncedAt DateTime?
|
||||||
|
syncStatus String?
|
||||||
|
|
||||||
|
roles UserRole[]
|
||||||
|
permissions UserPermission[]
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
model Role {
|
||||||
|
id String @id @default(uuid())
|
||||||
|
|
||||||
|
code String @unique
|
||||||
|
name String
|
||||||
|
description String?
|
||||||
|
|
||||||
|
isDefault Boolean @default(false)
|
||||||
|
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
users UserRole[]
|
||||||
|
permissions RolePermission[]
|
||||||
|
}
|
||||||
|
|
||||||
|
model Permission {
|
||||||
|
id String @id @default(uuid())
|
||||||
|
|
||||||
|
code String @unique
|
||||||
|
name String
|
||||||
|
description String?
|
||||||
|
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
roles RolePermission[]
|
||||||
|
users UserPermission[]
|
||||||
|
}
|
||||||
|
|
||||||
|
model UserRole {
|
||||||
|
userId String
|
||||||
|
roleId String
|
||||||
|
|
||||||
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
|
role Role @relation(fields: [roleId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
|
assignedAt DateTime @default(now())
|
||||||
|
assignedBy String
|
||||||
|
|
||||||
|
@@id([userId, roleId])
|
||||||
|
}
|
||||||
|
|
||||||
|
model RolePermission {
|
||||||
|
roleId String
|
||||||
|
permissionId String
|
||||||
|
|
||||||
|
role Role @relation(fields: [roleId], references: [id], onDelete: Cascade)
|
||||||
|
permission Permission @relation(fields: [permissionId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
|
assignedAt DateTime @default(now())
|
||||||
|
assignedBy String
|
||||||
|
|
||||||
|
@@id([roleId, permissionId])
|
||||||
|
}
|
||||||
|
|
||||||
|
model UserPermission {
|
||||||
|
userId String
|
||||||
|
permissionId String
|
||||||
|
|
||||||
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
|
permission Permission @relation(fields: [permissionId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
|
assignedAt DateTime @default(now())
|
||||||
|
assignedBy String
|
||||||
|
|
||||||
|
@@id([userId, permissionId])
|
||||||
}
|
}
|
||||||
@@ -2,6 +2,7 @@ import { Module } from '@nestjs/common';
|
|||||||
import { ConfigModule } from '@nestjs/config';
|
import { ConfigModule } from '@nestjs/config';
|
||||||
|
|
||||||
import { IdentityModule } from './modules/identity/identity.module';
|
import { IdentityModule } from './modules/identity/identity.module';
|
||||||
|
import { AuthModule } from './modules/auth/auth.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
@@ -10,6 +11,7 @@ import { IdentityModule } from './modules/identity/identity.module';
|
|||||||
}),
|
}),
|
||||||
|
|
||||||
IdentityModule,
|
IdentityModule,
|
||||||
|
AuthModule,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class AppModule {}
|
export class AppModule {}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
export const PermissionType = {
|
||||||
|
USER_CREATE: 'USER_CREATE',
|
||||||
|
USER_READ: 'USER_READ',
|
||||||
|
USER_UPDATE: 'USER_UPDATE',
|
||||||
|
USER_DELETE: 'USER_DELETE',
|
||||||
|
|
||||||
|
ROLE_CREATE: 'ROLE_CREATE',
|
||||||
|
ROLE_READ: 'ROLE_READ',
|
||||||
|
ROLE_UPDATE: 'ROLE_UPDATE',
|
||||||
|
ROLE_DELETE: 'ROLE_DELETE',
|
||||||
|
|
||||||
|
PERMISSION_CREATE: 'PERMISSION_CREATE',
|
||||||
|
PERMISSION_READ: 'PERMISSION_READ',
|
||||||
|
PERMISSION_UPDATE: 'PERMISSION_UPDATE',
|
||||||
|
PERMISSION_DELETE: 'PERMISSION_DELETE',
|
||||||
|
} as const;
|
||||||
@@ -4,17 +4,23 @@ import {
|
|||||||
ExecutionContext,
|
ExecutionContext,
|
||||||
UnauthorizedException,
|
UnauthorizedException,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { JwtService } from '@nestjs/jwt';
|
|
||||||
import { Request } from 'express';
|
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()
|
@Injectable()
|
||||||
export class JwtAuthGuard implements CanActivate {
|
export class JwtAuthGuard implements CanActivate {
|
||||||
constructor(
|
private jwks: ReturnType<typeof createRemoteJWKSet> | null = null;
|
||||||
private readonly jwtService: JwtService,
|
|
||||||
) {}
|
constructor() {}
|
||||||
|
|
||||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
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;
|
const authHeader = request.headers.authorization;
|
||||||
|
|
||||||
@@ -28,13 +34,51 @@ export class JwtAuthGuard implements CanActivate {
|
|||||||
throw new UnauthorizedException('Invalid authorization header.');
|
throw new UnauthorizedException('Invalid authorization header.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const jwksUri = process.env.AUTHENTIK_JWKS_URI;
|
||||||
|
|
||||||
|
// First try verifying with external JWKS (Authentik)
|
||||||
|
if (jwksUri) {
|
||||||
try {
|
try {
|
||||||
const payload = await this.jwtService.verifyAsync(token);
|
if (!this.jwks) this.jwks = createRemoteJWKSet(new URL(jwksUri));
|
||||||
|
|
||||||
request['user'] = payload;
|
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;
|
return true;
|
||||||
} catch {
|
} 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 = jwt.verify(token, secret) as any;
|
||||||
|
|
||||||
|
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 (err: any) {
|
||||||
throw new UnauthorizedException('Invalid or expired token.');
|
throw new UnauthorizedException('Invalid or expired token.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import { Request } from 'express';
|
import { Request } from 'express';
|
||||||
|
import { UserData } from '../../../modules/identity/domain/entities/user.entity';
|
||||||
export interface JwtPayload {
|
import { IdentityData } from './identity-data';
|
||||||
sub: string;
|
|
||||||
email: string;
|
|
||||||
role: string;
|
|
||||||
permissions: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AuthenticatedRequest extends Request {
|
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>,
|
||||||
|
) {}
|
||||||
|
}
|
||||||
+3
-4
@@ -10,7 +10,7 @@ async function bootstrap() {
|
|||||||
|
|
||||||
const config = app.get(ConfigService);
|
const config = app.get(ConfigService);
|
||||||
|
|
||||||
app.setGlobalPrefix('api');
|
app.setGlobalPrefix('');
|
||||||
|
|
||||||
app.useGlobalPipes(
|
app.useGlobalPipes(
|
||||||
new ValidationPipe({
|
new ValidationPipe({
|
||||||
@@ -22,8 +22,7 @@ async function bootstrap() {
|
|||||||
|
|
||||||
app.enableCors();
|
app.enableCors();
|
||||||
|
|
||||||
const swaggerEnabled =
|
const swaggerEnabled = config.get<string>('SWAGGER_ENABLED') === 'true';
|
||||||
config.get<string>('SWAGGER_ENABLED') === 'true';
|
|
||||||
|
|
||||||
if (swaggerEnabled) {
|
if (swaggerEnabled) {
|
||||||
const swaggerConfig = new DocumentBuilder()
|
const swaggerConfig = new DocumentBuilder()
|
||||||
@@ -35,7 +34,7 @@ async function bootstrap() {
|
|||||||
|
|
||||||
const document = SwaggerModule.createDocument(app, swaggerConfig);
|
const document = SwaggerModule.createDocument(app, swaggerConfig);
|
||||||
|
|
||||||
SwaggerModule.setup('docs', app, document);
|
SwaggerModule.setup('ApiList', app, document);
|
||||||
}
|
}
|
||||||
|
|
||||||
const port = config.get<number>('PORT') || 3000;
|
const port = config.get<number>('PORT') || 3000;
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
|
||||||
|
import { Controller, Get, Post, Query, Res, Req, Body, HttpCode, HttpStatus } from '@nestjs/common';
|
||||||
|
import { ApiTags, ApiOperation } from '@nestjs/swagger';
|
||||||
|
import { AuthService } from './auth.service';
|
||||||
|
import { Response, Request } from 'express';
|
||||||
|
|
||||||
|
@ApiTags('Auth')
|
||||||
|
@Controller('auth')
|
||||||
|
export class AuthController {
|
||||||
|
constructor(private readonly authService: AuthService) {}
|
||||||
|
|
||||||
|
@Get('login')
|
||||||
|
@ApiOperation({ summary: 'Start Authorization Code + PKCE login (redirect to Identity Provider)' })
|
||||||
|
async login(@Query('returnTo') returnTo: string | undefined, @Res() res: Response) {
|
||||||
|
const redirect = await this.authService.createAuthorizationRedirect(returnTo);
|
||||||
|
return res.redirect(302, redirect);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('callback')
|
||||||
|
@ApiOperation({ summary: 'OIDC callback endpoint' })
|
||||||
|
async callback(@Query('code') code: string, @Query('state') state: string, @Res() res: Response) {
|
||||||
|
const result = await this.authService.handleCallback(code, state);
|
||||||
|
|
||||||
|
// set cookies
|
||||||
|
const cookieOptions: any = {
|
||||||
|
httpOnly: true,
|
||||||
|
secure: process.env.NODE_ENV === 'production',
|
||||||
|
sameSite: 'lax',
|
||||||
|
path: '/',
|
||||||
|
};
|
||||||
|
|
||||||
|
// access token cookie (internal JWT)
|
||||||
|
res.cookie('raylab_jwt', result.accessToken, { ...cookieOptions, maxAge: result.expiresIn * 1000 });
|
||||||
|
|
||||||
|
// refresh token cookie
|
||||||
|
res.cookie('raylab_refresh', result.refreshToken, { ...cookieOptions, maxAge: result.refreshTtl * 1000 });
|
||||||
|
|
||||||
|
return res.redirect(302, result.returnTo || '/');
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('logout')
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
@ApiOperation({ summary: 'Logout (invalidate internal session and redirect to identity provider logout)' })
|
||||||
|
async logout(@Req() req: Request, @Res() res: Response) {
|
||||||
|
const refreshToken = req.cookies?.raylab_refresh || req.body?.refreshToken;
|
||||||
|
const redirect = await this.authService.logout(refreshToken);
|
||||||
|
return res.redirect(302, redirect);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('refresh')
|
||||||
|
@ApiOperation({ summary: 'Refresh internal JWT using internal refresh token' })
|
||||||
|
async refresh(@Req() req: Request) {
|
||||||
|
const refreshToken = req.cookies?.raylab_refresh || req.body?.refreshToken;
|
||||||
|
const result = await this.authService.refresh(refreshToken);
|
||||||
|
return { success: true, data: result };
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('me')
|
||||||
|
@ApiOperation({ summary: 'Get current user from internal JWT (cookie or Authorization header)' })
|
||||||
|
async me(@Req() req: Request) {
|
||||||
|
const token = (req.cookies?.raylab_jwt) || (req.headers.authorization && (req.headers.authorization as string).replace(/^Bearer\s+/i, ''));
|
||||||
|
const user = await this.authService.me(token);
|
||||||
|
return { success: true, data: user };
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { AuthController } from './auth.controller';
|
||||||
|
import { AuthService } from './auth.service';
|
||||||
|
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||||
|
import { JwtModule } from '@nestjs/jwt';
|
||||||
|
import { PrismaService } from '../../shared/prisma.service';
|
||||||
|
import { IUser } from '../identity/domain/repositories/user.interface';
|
||||||
|
import { PrismaUserRepository } from '../identity/infrastructure/repositories/prisma-user.repository';
|
||||||
|
import { SyncIdentityHandler } from '../identity/application/handlers/user/sync-identity.handler';
|
||||||
|
import { IRole } from '../identity/domain/repositories/role.interface';
|
||||||
|
import { PrismaRoleRepository } from '../identity/infrastructure/repositories/prisma-role.repository';
|
||||||
|
import { IAuthConfig } from '../identity/application/config/i-auth-config';
|
||||||
|
import { EnvAuthConfig } from '../identity/application/config/env-auth-config';
|
||||||
|
import { RedisPkceStore } from './pkce/redis-pkce.store';
|
||||||
|
import { InMemoryRefreshStore } from './refresh/inmemory-refresh.store';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [
|
||||||
|
ConfigModule,
|
||||||
|
JwtModule.registerAsync({
|
||||||
|
imports: [ConfigModule],
|
||||||
|
useFactory: async (config: ConfigService) => ({
|
||||||
|
secret: config.get('RAYLAB_JWT_SECRET') || 'raylab-secret',
|
||||||
|
signOptions: { algorithm: 'HS256' },
|
||||||
|
}),
|
||||||
|
inject: [ConfigService],
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
controllers: [AuthController],
|
||||||
|
providers: [
|
||||||
|
AuthService,
|
||||||
|
PrismaService,
|
||||||
|
PrismaUserRepository,
|
||||||
|
PrismaRoleRepository,
|
||||||
|
SyncIdentityHandler,
|
||||||
|
{ provide: IUser, useClass: PrismaUserRepository },
|
||||||
|
{ provide: IRole, useClass: PrismaRoleRepository },
|
||||||
|
{ provide: IAuthConfig, useClass: EnvAuthConfig },
|
||||||
|
|
||||||
|
// In-memory PKCE and Refresh stores (Redis removed)
|
||||||
|
RedisPkceStore,
|
||||||
|
InMemoryRefreshStore,
|
||||||
|
],
|
||||||
|
exports: [AuthService],
|
||||||
|
})
|
||||||
|
export class AuthModule {}
|
||||||
@@ -0,0 +1,193 @@
|
|||||||
|
import { Injectable, UnauthorizedException, Inject } from '@nestjs/common';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import { JwtService } from '@nestjs/jwt';
|
||||||
|
import { SyncIdentityHandler } from '../identity/application/handlers/user/sync-identity.handler';
|
||||||
|
import { IUser } from '../identity/domain/repositories/user.interface';
|
||||||
|
import { PrismaService } from '../../shared/prisma.service';
|
||||||
|
import { RedisPkceStore } from './pkce/redis-pkce.store';
|
||||||
|
import { InMemoryRefreshStore } from './refresh/inmemory-refresh.store';
|
||||||
|
|
||||||
|
import { Issuer, generators, TokenSet } from 'openid-client';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AuthService {
|
||||||
|
constructor(
|
||||||
|
private readonly config: ConfigService,
|
||||||
|
private readonly jwtService: JwtService,
|
||||||
|
private readonly prisma: PrismaService,
|
||||||
|
@Inject(IUser) private readonly userRepository: IUser,
|
||||||
|
private readonly syncIdentityHandler: SyncIdentityHandler,
|
||||||
|
private readonly pkceStore: RedisPkceStore,
|
||||||
|
private readonly refreshStore: InMemoryRefreshStore,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
private issuer: any = null;
|
||||||
|
private client: any = null;
|
||||||
|
|
||||||
|
private async getIssuer() {
|
||||||
|
if (this.issuer) return this.issuer;
|
||||||
|
const issuerUrl = this.config.get<string>('AUTHENTIK_ISSUER');
|
||||||
|
if (!issuerUrl) throw new Error('AUTHENTIK_ISSUER not configured');
|
||||||
|
this.issuer = await Issuer.discover(issuerUrl);
|
||||||
|
return this.issuer;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getClient() {
|
||||||
|
if (this.client) return this.client;
|
||||||
|
const issuer = await this.getIssuer();
|
||||||
|
const clientId = this.config.get<string>('AUTHENTIK_CLIENT_ID');
|
||||||
|
const clientSecret = this.config.get<string>('AUTHENTIK_CLIENT_SECRET');
|
||||||
|
if (!clientId) throw new Error('AUTHENTIK_CLIENT_ID not configured');
|
||||||
|
this.client = new issuer.Client({ client_id: clientId, client_secret: clientSecret });
|
||||||
|
return this.client;
|
||||||
|
}
|
||||||
|
|
||||||
|
async createAuthorizationRedirect(returnTo?: string) {
|
||||||
|
const client = await this.getClient();
|
||||||
|
const redirectUri = this.config.get<string>('AUTH_CALLBACK_URL') || `${this.config.get('APP_URL') || ''}/auth/callback`;
|
||||||
|
|
||||||
|
const state = require('crypto').randomUUID();
|
||||||
|
const code_verifier = generators.codeVerifier();
|
||||||
|
const code_challenge = await generators.codeChallenge(code_verifier);
|
||||||
|
const nonce = generators.nonce();
|
||||||
|
|
||||||
|
await this.pkceStore.save(state, { code_verifier, nonce, returnTo }, 300);
|
||||||
|
|
||||||
|
const url = client.authorizationUrl({
|
||||||
|
redirect_uri: redirectUri,
|
||||||
|
scope: this.config.get<string>('AUTHENTIK_DEFAULT_SCOPE') || 'openid email profile',
|
||||||
|
response_type: 'code',
|
||||||
|
code_challenge,
|
||||||
|
code_challenge_method: 'S256',
|
||||||
|
state,
|
||||||
|
nonce,
|
||||||
|
});
|
||||||
|
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleCallback(code: string, state: string) {
|
||||||
|
const client = await this.getClient();
|
||||||
|
const pkce = await this.pkceStore.get(state);
|
||||||
|
if (!pkce) throw new UnauthorizedException('Invalid or expired state');
|
||||||
|
|
||||||
|
// remove one-time state
|
||||||
|
await this.pkceStore.remove(state);
|
||||||
|
|
||||||
|
const redirectUri = this.config.get<string>('AUTH_CALLBACK_URL') || `${this.config.get('APP_URL') || ''}/auth/callback`;
|
||||||
|
|
||||||
|
// exchange code
|
||||||
|
const tokenSet: TokenSet = await client.callback(redirectUri, { code, state }, { code_verifier: pkce.code_verifier, nonce: pkce.nonce });
|
||||||
|
|
||||||
|
// verify id_token and get claims
|
||||||
|
const claims = tokenSet.claims();
|
||||||
|
|
||||||
|
// fetch userinfo if available
|
||||||
|
let userInfo = null;
|
||||||
|
try {
|
||||||
|
if (tokenSet.access_token && client.userinfo) {
|
||||||
|
userInfo = await client.userinfo(tokenSet.access_token);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
|
||||||
|
const identity = {
|
||||||
|
sub: userInfo?.sub || claims.sub || null,
|
||||||
|
preferred_username: userInfo?.preferred_username || userInfo?.username || userInfo?.email || claims.preferred_username || claims.email,
|
||||||
|
email: userInfo?.email || claims.email,
|
||||||
|
raw: { tokenSet, userInfo, claims },
|
||||||
|
} as any;
|
||||||
|
|
||||||
|
// Sync identity to local user (create if needed)
|
||||||
|
const domainUser = await this.syncIdentityHandler.execute(identity as any);
|
||||||
|
|
||||||
|
// ensure active/not deleted
|
||||||
|
if (!domainUser.isActive) throw new UnauthorizedException('User is not active');
|
||||||
|
if (domainUser.deletedAt) throw new UnauthorizedException('User is deleted');
|
||||||
|
|
||||||
|
// create internal JWT
|
||||||
|
const jwtPayload = {
|
||||||
|
sub: domainUser.id,
|
||||||
|
preferred_username: domainUser.username,
|
||||||
|
email: domainUser.email,
|
||||||
|
} as any;
|
||||||
|
|
||||||
|
const expiresIn = Number(this.config.get('RAYLAB_JWT_EXPIRES_IN') || 3600);
|
||||||
|
const access = this.jwtService.sign(jwtPayload, { expiresIn });
|
||||||
|
|
||||||
|
// create internal refresh token
|
||||||
|
const refreshToken = require('crypto').randomUUID();
|
||||||
|
const refreshTtl = Number(this.config.get('RAYLAB_REFRESH_EXPIRES_IN') || 30 * 24 * 3600); // default 30 days
|
||||||
|
|
||||||
|
await this.refreshStore.set(refreshToken, { userId: domainUser.id }, refreshTtl);
|
||||||
|
|
||||||
|
return {
|
||||||
|
accessToken: access,
|
||||||
|
refreshToken,
|
||||||
|
expiresIn,
|
||||||
|
refreshTtl,
|
||||||
|
user: {
|
||||||
|
id: domainUser.id,
|
||||||
|
username: domainUser.username,
|
||||||
|
email: domainUser.email,
|
||||||
|
roles: domainUser.roles || [],
|
||||||
|
},
|
||||||
|
returnTo: pkce.returnTo,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async refresh(refreshToken?: string) {
|
||||||
|
if (!refreshToken) throw new UnauthorizedException('Missing refresh token');
|
||||||
|
const data = await this.refreshStore.get(refreshToken);
|
||||||
|
if (!data) throw new UnauthorizedException('Invalid refresh token');
|
||||||
|
|
||||||
|
const userId = data.userId;
|
||||||
|
// load user
|
||||||
|
const domainUser = await this.userRepository.findById(userId);
|
||||||
|
if (!domainUser) throw new UnauthorizedException('User not found');
|
||||||
|
if (!domainUser.isActive) throw new UnauthorizedException('User is not active');
|
||||||
|
|
||||||
|
// rotate refresh token
|
||||||
|
await this.refreshStore.del(refreshToken);
|
||||||
|
const newRefresh = require('crypto').randomUUID();
|
||||||
|
const refreshTtl = Number(this.config.get('RAYLAB_REFRESH_EXPIRES_IN') || 30 * 24 * 3600);
|
||||||
|
await this.refreshStore.set(newRefresh, { userId }, refreshTtl);
|
||||||
|
|
||||||
|
const expiresIn = Number(this.config.get('RAYLAB_JWT_EXPIRES_IN') || 3600);
|
||||||
|
const access = this.jwtService.sign({ sub: domainUser.id, preferred_username: domainUser.username, email: domainUser.email }, { expiresIn });
|
||||||
|
|
||||||
|
return { accessToken: access, refreshToken: newRefresh, expiresIn, refreshTtl };
|
||||||
|
}
|
||||||
|
|
||||||
|
async logout(refreshToken?: string) {
|
||||||
|
if (refreshToken) {
|
||||||
|
await this.refreshStore.del(refreshToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
const issuer = await this.getIssuer();
|
||||||
|
const endSession = issuer.metadata.end_session_endpoint;
|
||||||
|
const postLogout = this.config.get<string>('APP_URL') || '/';
|
||||||
|
|
||||||
|
if (endSession) {
|
||||||
|
// Redirect to identity provider logout
|
||||||
|
const url = new URL(endSession);
|
||||||
|
if (postLogout) url.searchParams.set('post_logout_redirect_uri', postLogout);
|
||||||
|
return url.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
return postLogout;
|
||||||
|
}
|
||||||
|
|
||||||
|
async me(token?: string) {
|
||||||
|
if (!token) throw new UnauthorizedException('Missing token');
|
||||||
|
try {
|
||||||
|
const payload: any = this.jwtService.verify(token);
|
||||||
|
const user = await this.userRepository.findById(payload.sub);
|
||||||
|
if (!user) throw new UnauthorizedException('User not found');
|
||||||
|
return { id: user.id, username: user.username, email: user.email, roles: user.roles || [] };
|
||||||
|
} catch (e) {
|
||||||
|
throw new UnauthorizedException('Invalid token');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
// Login DTO removed. Password grant has been removed in favor of Authorization Code + PKCE flow.
|
||||||
|
// Formerly contained username/password properties.
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import { Injectable, OnModuleDestroy } from '@nestjs/common';
|
||||||
|
|
||||||
|
type PkceEntry = { code_verifier: string; nonce: string; returnTo?: string; expiresAt: number };
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class RedisPkceStore implements OnModuleDestroy {
|
||||||
|
// In-memory PKCE store replacing Redis-backed implementation
|
||||||
|
private map = new Map<string, PkceEntry>();
|
||||||
|
private cleanupInterval?: NodeJS.Timeout;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
// periodic cleanup
|
||||||
|
this.cleanupInterval = setInterval(() => {
|
||||||
|
const now = Date.now();
|
||||||
|
for (const [k, v] of this.map.entries()) {
|
||||||
|
if (v.expiresAt <= now) this.map.delete(k);
|
||||||
|
}
|
||||||
|
}, 60 * 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
private key(state: string) { return state; }
|
||||||
|
|
||||||
|
async save(state: string, data: { code_verifier: string; nonce: string; returnTo?: string }, ttlSeconds = 300) {
|
||||||
|
const expiresAt = Date.now() + ttlSeconds * 1000;
|
||||||
|
this.map.set(this.key(state), { ...data, expiresAt });
|
||||||
|
}
|
||||||
|
|
||||||
|
async get(state: string) {
|
||||||
|
const v = this.map.get(this.key(state));
|
||||||
|
if (!v) return null;
|
||||||
|
if (v.expiresAt <= Date.now()) { this.map.delete(this.key(state)); return null; }
|
||||||
|
return { code_verifier: v.code_verifier, nonce: v.nonce, returnTo: v.returnTo };
|
||||||
|
}
|
||||||
|
|
||||||
|
async remove(state: string) {
|
||||||
|
this.map.delete(this.key(state));
|
||||||
|
}
|
||||||
|
|
||||||
|
onModuleDestroy() {
|
||||||
|
if (this.cleanupInterval) clearInterval(this.cleanupInterval);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import { Injectable, OnModuleDestroy } from '@nestjs/common';
|
||||||
|
|
||||||
|
type RefreshEntry = { userId: string; expiresAt: number };
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class InMemoryRefreshStore implements OnModuleDestroy {
|
||||||
|
private map = new Map<string, RefreshEntry>();
|
||||||
|
private cleanupInterval?: NodeJS.Timeout;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.cleanupInterval = setInterval(() => {
|
||||||
|
const now = Date.now();
|
||||||
|
for (const [k, v] of this.map.entries()) {
|
||||||
|
if (v.expiresAt <= now) this.map.delete(k);
|
||||||
|
}
|
||||||
|
}, 60 * 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
async set(token: string, data: { userId: string }, ttlSeconds: number) {
|
||||||
|
const expiresAt = Date.now() + ttlSeconds * 1000;
|
||||||
|
this.map.set(token, { userId: data.userId, expiresAt });
|
||||||
|
}
|
||||||
|
|
||||||
|
async get(token: string) {
|
||||||
|
const v = this.map.get(token);
|
||||||
|
if (!v) return null;
|
||||||
|
if (v.expiresAt <= Date.now()) { this.map.delete(token); return null; }
|
||||||
|
return { userId: v.userId };
|
||||||
|
}
|
||||||
|
|
||||||
|
async del(token: string) {
|
||||||
|
this.map.delete(token);
|
||||||
|
}
|
||||||
|
|
||||||
|
onModuleDestroy() {
|
||||||
|
if (this.cleanupInterval) clearInterval(this.cleanupInterval);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,8 @@ import {
|
|||||||
Injectable,
|
Injectable,
|
||||||
CanActivate,
|
CanActivate,
|
||||||
ExecutionContext,
|
ExecutionContext,
|
||||||
|
UnauthorizedException,
|
||||||
|
ForbiddenException,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { Reflector } from '@nestjs/core';
|
import { Reflector } from '@nestjs/core';
|
||||||
|
|
||||||
@@ -25,12 +27,20 @@ export class PermissionGuard implements CanActivate {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const request = context.switchToHttp().getRequest();
|
const request = context.switchToHttp().getRequest() as any;
|
||||||
|
|
||||||
const user = request.user;
|
const user = request.currentUser;
|
||||||
|
|
||||||
return permissions.every(permission =>
|
if (!user) {
|
||||||
user.permissions.includes(permission),
|
throw new UnauthorizedException('Current user is missing.');
|
||||||
);
|
}
|
||||||
|
|
||||||
|
const hasAll = permissions.every((permission) => {
|
||||||
|
return user.hasPermission(permission);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!hasAll) throw new ForbiddenException('Insufficient permissions.');
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,12 +1 @@
|
|||||||
modules/identity/application/
|
ini adalah manager, semua kegiatan diarahkan dari sini, disini bukan mengakses db, menghitung, dll, dari controller akan masuk ke dalam sini dan dijalankan prosesnya, tapi tidak tau http dan databasenya.
|
||||||
|
|
||||||
Penjelasan:
|
|
||||||
Layer application berisi use-case (services/commands/queries) yang mengorkestrasi domain dan infrastruktur.
|
|
||||||
|
|
||||||
Contoh file:
|
|
||||||
- services/get-user.service.ts
|
|
||||||
- commands/create-user.command.ts
|
|
||||||
|
|
||||||
Aturan:
|
|
||||||
- Application service boleh memanggil repository interface, domain services, dan event publisher.
|
|
||||||
- Application menangani transaksi jika diperlukan.
|
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { IAuthConfig } from './i-auth-config';
|
||||||
|
|
||||||
|
export class EnvAuthConfig implements IAuthConfig {
|
||||||
|
autoCreateUser(): boolean {
|
||||||
|
return (process.env.AUTH_AUTO_CREATE_USER ?? 'true') === 'true';
|
||||||
|
}
|
||||||
|
|
||||||
|
syncEmail(): boolean {
|
||||||
|
return (process.env.AUTH_SYNC_EMAIL ?? 'true') === 'true';
|
||||||
|
}
|
||||||
|
|
||||||
|
syncUsername(): boolean {
|
||||||
|
return (process.env.AUTH_SYNC_USERNAME ?? 'false') === 'true';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
export abstract class IAuthConfig {
|
||||||
|
abstract autoCreateUser(): boolean;
|
||||||
|
abstract syncEmail(): boolean;
|
||||||
|
abstract syncUsername(): boolean;
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
semua method disini didapat dari interface, itu ada di domain/repositories.
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
import {
|
|
||||||
Injectable,
|
|
||||||
ConflictException,
|
|
||||||
} from '@nestjs/common';
|
|
||||||
|
|
||||||
import { CreateUserDto } from '../../presentation/dto/create-user.dto';
|
|
||||||
import { User } from '../../domain/entities/user.entity';
|
|
||||||
import { UserRepository } from '../../domain/repositories/user.repository.interface';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class CreateUserHandler {
|
|
||||||
constructor(
|
|
||||||
private readonly userRepository: UserRepository,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
async execute(dto: CreateUserDto): Promise<User> {
|
|
||||||
const exists = await this.userRepository.existsByEmail(dto.email);
|
|
||||||
|
|
||||||
if (exists) {
|
|
||||||
throw new ConflictException('Email already exists.');
|
|
||||||
}
|
|
||||||
|
|
||||||
const user = User.create({
|
|
||||||
name: dto.name,
|
|
||||||
email: dto.email,
|
|
||||||
password: dto.password,
|
|
||||||
metadata: dto.metadata,
|
|
||||||
});
|
|
||||||
|
|
||||||
await this.userRepository.create(user);
|
|
||||||
|
|
||||||
// TODO:
|
|
||||||
// this.eventDispatcher.publish(new UserCreatedEvent(user));
|
|
||||||
|
|
||||||
return user;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
import {
|
|
||||||
Injectable,
|
|
||||||
NotFoundException,
|
|
||||||
} from '@nestjs/common';
|
|
||||||
|
|
||||||
import { UserRepository } from '../../domain/repositories/user.repository.interface';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class DeleteUserHandler {
|
|
||||||
constructor(
|
|
||||||
private readonly userRepository: UserRepository,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
async execute(id: string): Promise<void> {
|
|
||||||
const user = await this.userRepository.findById(id);
|
|
||||||
|
|
||||||
if (!user) {
|
|
||||||
throw new NotFoundException('User not found.');
|
|
||||||
}
|
|
||||||
|
|
||||||
user.delete();
|
|
||||||
|
|
||||||
await this.userRepository.update(user);
|
|
||||||
|
|
||||||
// TODO:
|
|
||||||
// Publish UserDeletedEvent
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
import {
|
|
||||||
Injectable,
|
|
||||||
NotFoundException,
|
|
||||||
} from '@nestjs/common';
|
|
||||||
|
|
||||||
import { UserRepository } from '../../domain/repositories/user.repository.interface';
|
|
||||||
import { User } from '../../domain/entities/user.entity';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class FindUserHandler {
|
|
||||||
constructor(
|
|
||||||
private readonly userRepository: UserRepository,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
async execute(id: string): Promise<User> {
|
|
||||||
const user = await this.userRepository.findById(id);
|
|
||||||
|
|
||||||
if (!user) {
|
|
||||||
throw new NotFoundException('User not found.');
|
|
||||||
}
|
|
||||||
|
|
||||||
return user;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
|
||||||
|
|
||||||
import { UserRepository } from '../../domain/repositories/user.repository.interface';
|
|
||||||
import { User } from '../../domain/entities/user.entity';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class FindUsersHandler {
|
|
||||||
constructor(
|
|
||||||
private readonly userRepository: UserRepository,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
async execute(): Promise<User[]> {
|
|
||||||
return await this.userRepository.findAll();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { Injectable, ConflictException } from '@nestjs/common';
|
||||||
|
import { IPermission } from '../../../domain/repositories/permission.interface';
|
||||||
|
import { PermissionData } from '../../../domain/entities/permission.entity';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class CreatePermissionHandler {
|
||||||
|
constructor(private readonly permissionRepository: IPermission) {}
|
||||||
|
|
||||||
|
async execute(dto: any) {
|
||||||
|
const perm = PermissionData.restore({
|
||||||
|
id: crypto.randomUUID(),
|
||||||
|
code: dto.code || dto.name,
|
||||||
|
name: dto.name,
|
||||||
|
description: dto.description || '',
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date(),
|
||||||
|
} as any);
|
||||||
|
|
||||||
|
return this.permissionRepository.create(perm);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||||
|
import { IPermission } from '../../../domain/repositories/permission.interface';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class DeletePermissionHandler {
|
||||||
|
constructor(private readonly permissionRepository: IPermission) {}
|
||||||
|
|
||||||
|
async execute(id: string) {
|
||||||
|
const p = await this.permissionRepository.getById(id);
|
||||||
|
if (!p) throw new NotFoundException('Permission not found.');
|
||||||
|
|
||||||
|
await this.permissionRepository.delete(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { IPermission } from '../../../domain/repositories/permission.interface';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class GetPermissionHandler {
|
||||||
|
constructor(private readonly permissionRepository: IPermission) {}
|
||||||
|
|
||||||
|
async execute(id: string) {
|
||||||
|
return this.permissionRepository.getById(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { IPermission } from '../../../domain/repositories/permission.interface';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class GetPermissionsHandler {
|
||||||
|
constructor(private readonly permissionRepository: IPermission) {}
|
||||||
|
|
||||||
|
async execute(query: { page?: number; limit?: number; search?: string }) {
|
||||||
|
return this.permissionRepository.find({ page: query.page, limit: query.limit, search: query.search || null });
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||||
|
import { IPermission } from '../../../domain/repositories/permission.interface';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class UpdatePermissionHandler {
|
||||||
|
constructor(private readonly permissionRepository: IPermission) {}
|
||||||
|
|
||||||
|
async execute(id: string, dto: any) {
|
||||||
|
const perm = await this.permissionRepository.getById(id);
|
||||||
|
if (!perm) throw new NotFoundException('Permission not found.');
|
||||||
|
|
||||||
|
if (dto.name) perm.changeName(dto.name);
|
||||||
|
if (dto.description !== undefined) perm.changeDescription(dto.description);
|
||||||
|
if (dto.code) perm.changeCode(dto.code);
|
||||||
|
|
||||||
|
return this.permissionRepository.update(perm);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||||
|
import { IRole } from '../../../domain/repositories/role.interface';
|
||||||
|
import { IPermission } from '../../../domain/repositories/permission.interface';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class RoleAssignPermissionHandler {
|
||||||
|
constructor(
|
||||||
|
private readonly roleRepository: IRole,
|
||||||
|
private readonly permissionRepository: IPermission,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async execute(roleId: string, permissionId: string) {
|
||||||
|
const role = await this.roleRepository.findById(roleId);
|
||||||
|
if (!role) throw new NotFoundException('Role not found.');
|
||||||
|
|
||||||
|
const perm = await this.permissionRepository.getById(permissionId);
|
||||||
|
if (!perm) throw new NotFoundException('Permission not found.');
|
||||||
|
|
||||||
|
role.assignPermission(perm);
|
||||||
|
|
||||||
|
return this.roleRepository.update(role);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import { Injectable, ConflictException } from '@nestjs/common';
|
||||||
|
import { IRole } from '../../../domain/repositories/role.interface';
|
||||||
|
import { RoleData } from '../../../domain/entities/role.entity';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class CreateRoleHandler {
|
||||||
|
constructor(private readonly roleRepository: IRole) {}
|
||||||
|
|
||||||
|
async execute(dto: any) {
|
||||||
|
// check uniqueness by code
|
||||||
|
// simple check
|
||||||
|
try {
|
||||||
|
// attempt to create; repository may enforce uniqueness
|
||||||
|
const role = RoleData.restore({
|
||||||
|
id: crypto.randomUUID(),
|
||||||
|
code: dto.code,
|
||||||
|
name: dto.name,
|
||||||
|
description: dto.description || '',
|
||||||
|
permissions: [],
|
||||||
|
isDefault: dto.isDefault || false,
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date(),
|
||||||
|
} as any);
|
||||||
|
|
||||||
|
return this.roleRepository.create(role);
|
||||||
|
} catch (e) {
|
||||||
|
throw new ConflictException('Role creation failed.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||||
|
import { IRole } from '../../../domain/repositories/role.interface';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class DeleteRoleHandler {
|
||||||
|
constructor(private readonly roleRepository: IRole) {}
|
||||||
|
|
||||||
|
async execute(id: string) {
|
||||||
|
const role = await this.roleRepository.findById(id);
|
||||||
|
if (!role) throw new NotFoundException('Role not found.');
|
||||||
|
|
||||||
|
await this.roleRepository.delete(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { IRole } from '../../../domain/repositories/role.interface';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class GetRoleHandler {
|
||||||
|
constructor(private readonly roleRepository: IRole) {}
|
||||||
|
|
||||||
|
async execute(id: string) {
|
||||||
|
return this.roleRepository.findById(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { IRole } from '../../../domain/repositories/role.interface';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class GetRolesHandler {
|
||||||
|
constructor(private readonly roleRepository: IRole) {}
|
||||||
|
|
||||||
|
async execute(query: { page?: number; limit?: number; search?: string }) {
|
||||||
|
return this.roleRepository.find({ page: query.page, limit: query.limit, search: query.search || null });
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||||
|
import { IRole } from '../../../domain/repositories/role.interface';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class RoleRemovePermissionHandler {
|
||||||
|
constructor(private readonly roleRepository: IRole) {}
|
||||||
|
|
||||||
|
async execute(roleId: string, permissionId: string) {
|
||||||
|
const role = await this.roleRepository.findById(roleId);
|
||||||
|
if (!role) throw new NotFoundException('Role not found.');
|
||||||
|
|
||||||
|
role.removePermission(permissionId);
|
||||||
|
|
||||||
|
return this.roleRepository.update(role);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||||
|
import { IRole } from '../../../domain/repositories/role.interface';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class UpdateRoleHandler {
|
||||||
|
constructor(private readonly roleRepository: IRole) {}
|
||||||
|
|
||||||
|
async execute(id: string, dto: any) {
|
||||||
|
const role = await this.roleRepository.findById(id);
|
||||||
|
if (!role) throw new NotFoundException('Role not found.');
|
||||||
|
|
||||||
|
if (dto.name) role.changeName(dto.name);
|
||||||
|
if (dto.description !== undefined) role.changeDescription(dto.description);
|
||||||
|
if (dto.isDefault !== undefined) role.setDefault(!!dto.isDefault);
|
||||||
|
|
||||||
|
return this.roleRepository.update(role);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
import {
|
|
||||||
Injectable,
|
|
||||||
ConflictException,
|
|
||||||
NotFoundException,
|
|
||||||
} from '@nestjs/common';
|
|
||||||
|
|
||||||
import { UpdateUserDto } from '../../presentation/dto/update-user.dto';
|
|
||||||
import { UserRepository } from '../../domain/repositories/user.repository.interface';
|
|
||||||
import { User } from '../../domain/entities/user.entity';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class UpdateUserHandler {
|
|
||||||
constructor(
|
|
||||||
private readonly userRepository: UserRepository,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
async execute(
|
|
||||||
id: string,
|
|
||||||
dto: UpdateUserDto,
|
|
||||||
): Promise<User> {
|
|
||||||
const user = await this.userRepository.findById(id);
|
|
||||||
|
|
||||||
if (!user) {
|
|
||||||
throw new NotFoundException('User not found.');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
dto.email &&
|
|
||||||
dto.email !== user.email
|
|
||||||
) {
|
|
||||||
const exists =
|
|
||||||
await this.userRepository.existsByEmail(dto.email);
|
|
||||||
|
|
||||||
if (exists) {
|
|
||||||
throw new ConflictException(
|
|
||||||
'Email already exists.',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
user.changeEmail(dto.email);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (dto.name) {
|
|
||||||
user.changeName(dto.name);
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.userRepository.update(user);
|
|
||||||
|
|
||||||
return user;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||||
|
import { IUser } from '../../../domain/repositories/user.interface';
|
||||||
|
import { PermissionData } from '../../../domain/entities/permission.entity';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AssignPermissionHandler {
|
||||||
|
constructor(private readonly userRepository: IUser) {}
|
||||||
|
|
||||||
|
async execute(userId: string, permissionId: string) {
|
||||||
|
const user = await this.userRepository.getById(userId);
|
||||||
|
if (!user) throw new NotFoundException('User not found.');
|
||||||
|
|
||||||
|
// Permission repository is not available; create a minimal PermissionData
|
||||||
|
const perm = PermissionData.restore({
|
||||||
|
id: permissionId,
|
||||||
|
name: permissionId,
|
||||||
|
description: '',
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date(),
|
||||||
|
});
|
||||||
|
|
||||||
|
user.assignPermission(perm);
|
||||||
|
|
||||||
|
return this.userRepository.update(user);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||||
|
import { IUser } from '../../../domain/repositories/user.interface';
|
||||||
|
import { IRole } from '../../../domain/repositories/role.interface';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AssignRoleHandler {
|
||||||
|
constructor(
|
||||||
|
private readonly userRepository: IUser,
|
||||||
|
private readonly roleRepository: IRole,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async execute(userId: string, roleId: string) {
|
||||||
|
const user = await this.userRepository.getById(userId);
|
||||||
|
if (!user) throw new NotFoundException('User not found.');
|
||||||
|
|
||||||
|
const role = await this.roleRepository.findById(roleId);
|
||||||
|
if (!role) throw new NotFoundException('Role not found.');
|
||||||
|
|
||||||
|
user.assignRole(role);
|
||||||
|
|
||||||
|
return this.userRepository.update(user);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import {
|
||||||
|
Injectable,
|
||||||
|
ConflictException,
|
||||||
|
BadRequestException,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
|
||||||
|
import { CreateUserDto } from '../../../presentation/dto/create-user.dto';
|
||||||
|
import { IUser } from '../../../domain/repositories/user.interface';
|
||||||
|
import { UserData } from '../../../domain/entities/user.entity';
|
||||||
|
import { UserService } from '../../services/user.service';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class CreateUserHandler {
|
||||||
|
constructor(
|
||||||
|
private readonly userRepository: IUser,
|
||||||
|
private readonly userService: UserService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async execute(dto: CreateUserDto): Promise<UserData> {
|
||||||
|
|
||||||
|
const exists = await this.userRepository.existByEmail(dto.email);
|
||||||
|
|
||||||
|
if (exists) {
|
||||||
|
throw new ConflictException('Email already exists.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Provisioning disabled: Do not create users in external IdP from RayLab.
|
||||||
|
// Reject attempts to create users via API to enforce creation-at-Authentik policy.
|
||||||
|
throw new BadRequestException('User creation via RayLab API is disabled. Create users in Authentik and then login to sync.');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||||
|
import { IUser } from '../../../domain/repositories/user.interface';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class DeleteUserHandler {
|
||||||
|
constructor(private readonly userRepository: IUser) {}
|
||||||
|
|
||||||
|
async execute(id: string) {
|
||||||
|
const exists = await this.userRepository.existsById(id);
|
||||||
|
if (!exists) throw new NotFoundException('User not found.');
|
||||||
|
|
||||||
|
await this.userRepository.softDelete(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||||
|
import { IUser } from '../../../domain/repositories/user.interface';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class DisableUserHandler {
|
||||||
|
constructor(private readonly userRepository: IUser) {}
|
||||||
|
|
||||||
|
async execute(id: string) {
|
||||||
|
const exists = await this.userRepository.existsById(id);
|
||||||
|
if (!exists) throw new NotFoundException('User not found.');
|
||||||
|
|
||||||
|
return this.userRepository.disable(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||||
|
import { IUser } from '../../../domain/repositories/user.interface';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class EnableUserHandler {
|
||||||
|
constructor(private readonly userRepository: IUser) {}
|
||||||
|
|
||||||
|
async execute(id: string) {
|
||||||
|
const exists = await this.userRepository.existsById(id);
|
||||||
|
if (!exists) throw new NotFoundException('User not found.');
|
||||||
|
|
||||||
|
return this.userRepository.enable(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { UserData } from '../../../domain/entities/user.entity';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class GetCurrentUserHandler {
|
||||||
|
async execute(currentUser: UserData) {
|
||||||
|
// currentUser is already domain user attached by CurrentUserGuard
|
||||||
|
return currentUser;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { IUser } from '../../../domain/repositories/user.interface';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class GetUserHandler {
|
||||||
|
constructor(private readonly userRepository: IUser) {}
|
||||||
|
|
||||||
|
async execute(id: string) {
|
||||||
|
return this.userRepository.getById(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { IUser } from '../../../domain/repositories/user.interface';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class GetUsersHandler {
|
||||||
|
constructor(private readonly userRepository: IUser) {}
|
||||||
|
|
||||||
|
async execute(query: {
|
||||||
|
page?: number;
|
||||||
|
limit?: number;
|
||||||
|
search?: string;
|
||||||
|
isActive?: boolean;
|
||||||
|
deleted?: boolean;
|
||||||
|
roleId?: string;
|
||||||
|
}) {
|
||||||
|
const res = await this.userRepository.find({
|
||||||
|
page: query.page,
|
||||||
|
limit: query.limit,
|
||||||
|
search: query.search || null,
|
||||||
|
isActive: query.isActive !== undefined ? query.isActive : null,
|
||||||
|
deleted: query.deleted !== undefined ? query.deleted : null,
|
||||||
|
roleId: query.roleId || null,
|
||||||
|
});
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||||
|
import { IUser } from '../../../domain/repositories/user.interface';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class RemovePermissionHandler {
|
||||||
|
constructor(private readonly userRepository: IUser) {}
|
||||||
|
|
||||||
|
async execute(userId: string, permissionId: string) {
|
||||||
|
const user = await this.userRepository.getById(userId);
|
||||||
|
if (!user) throw new NotFoundException('User not found.');
|
||||||
|
|
||||||
|
user.removePermission(permissionId);
|
||||||
|
|
||||||
|
return this.userRepository.update(user);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||||
|
import { IUser } from '../../../domain/repositories/user.interface';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class RemoveRoleHandler {
|
||||||
|
constructor(private readonly userRepository: IUser) {}
|
||||||
|
|
||||||
|
async execute(userId: string, roleId: string) {
|
||||||
|
const user = await this.userRepository.getById(userId);
|
||||||
|
if (!user) throw new NotFoundException('User not found.');
|
||||||
|
|
||||||
|
user.removeRole(roleId);
|
||||||
|
|
||||||
|
return this.userRepository.update(user);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||||
|
import { IUser } from '../../../domain/repositories/user.interface';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class RestoreUserHandler {
|
||||||
|
constructor(private readonly userRepository: IUser) {}
|
||||||
|
|
||||||
|
async execute(id: string) {
|
||||||
|
const exists = await this.userRepository.existsById(id);
|
||||||
|
if (!exists) throw new NotFoundException('User not found.');
|
||||||
|
|
||||||
|
return this.userRepository.restore(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
import { Injectable, UnauthorizedException, ForbiddenException, Inject } from '@nestjs/common';
|
||||||
|
import { IUser } from '../../../domain/repositories/user.interface';
|
||||||
|
import { IRole } from '../../../domain/repositories/role.interface';
|
||||||
|
import { IAuthConfig } from '../../config/i-auth-config';
|
||||||
|
import { UserData } from '../../../domain/entities/user.entity';
|
||||||
|
import { IdentityData } from '../../../../../core/auth/interfaces/identity-data';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class SyncIdentityHandler {
|
||||||
|
constructor(
|
||||||
|
private readonly userRepository: IUser,
|
||||||
|
private readonly roleRepository: IRole,
|
||||||
|
private readonly authConfig: IAuthConfig,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async execute(identity: IdentityData): Promise<UserData> {
|
||||||
|
const { sub, preferred_username, email } = identity as any;
|
||||||
|
|
||||||
|
if (!sub) throw new UnauthorizedException('Invalid identity payload.');
|
||||||
|
|
||||||
|
// Try find by authentikId
|
||||||
|
let user = await this.userRepository.findByAuthentikId(sub);
|
||||||
|
|
||||||
|
const syncEmail = this.authConfig.syncEmail();
|
||||||
|
const syncUsername = this.authConfig.syncUsername();
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
// User does not exist locally: create minimal local record per new architecture
|
||||||
|
const username = preferred_username || email || sub;
|
||||||
|
const userEntity = UserData.create({ username, email: email || '', authentikId: sub });
|
||||||
|
|
||||||
|
// assign default role if available
|
||||||
|
try {
|
||||||
|
const defaultRole = await this.roleRepository.getDefaultRole();
|
||||||
|
if (defaultRole) {
|
||||||
|
userEntity.assignRole(defaultRole);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// ignore if role repo not available or no default role
|
||||||
|
}
|
||||||
|
|
||||||
|
// persist local user
|
||||||
|
user = await this.userRepository.create(userEntity);
|
||||||
|
|
||||||
|
// return freshly created user
|
||||||
|
return user;
|
||||||
|
} else {
|
||||||
|
let changed = false;
|
||||||
|
|
||||||
|
if (syncEmail && email && user.email !== email) {
|
||||||
|
user.changeEmail(email);
|
||||||
|
changed = true;
|
||||||
|
|
||||||
|
// prepare event class instance if needed (no dispatch)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (syncUsername && preferred_username && user.username !== preferred_username) {
|
||||||
|
user.changeUsername(preferred_username);
|
||||||
|
changed = true;
|
||||||
|
|
||||||
|
// prepare event class instance if needed (no dispatch)
|
||||||
|
}
|
||||||
|
|
||||||
|
// update last seen
|
||||||
|
user.touchLastSeen();
|
||||||
|
changed = true;
|
||||||
|
|
||||||
|
if (changed) {
|
||||||
|
user = await this.userRepository.update(user);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validations
|
||||||
|
if (!user.isActive) {
|
||||||
|
// Authentication succeeded but user is disabled
|
||||||
|
throw new ForbiddenException('User is not active.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user.deletedAt) {
|
||||||
|
throw new ForbiddenException('User is deleted.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure roles/permissions loaded (repo should return includes)
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
services/
|
|
||||||
|
|
||||||
Penjelasan:
|
|
||||||
Application services (use-cases) untuk module identity.
|
|
||||||
|
|
||||||
Contoh file:
|
|
||||||
- get-user.service.ts
|
|
||||||
- create-user.service.ts
|
|
||||||
|
|
||||||
Aturan:
|
|
||||||
- Application service mengorkestrasi domain services dan repository.
|
|
||||||
- Menangani transaction boundary jika perlu.
|
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { Injectable, BadRequestException, Logger } from '@nestjs/common';
|
||||||
|
import { PrismaService } from '../../../../shared/prisma.service';
|
||||||
|
import { IUser } from '../../domain/repositories/user.interface';
|
||||||
|
import { UserData } from '../../domain/entities/user.entity';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class UserService {
|
||||||
|
private readonly logger = new Logger(UserService.name);
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly prisma: PrismaService,
|
||||||
|
private readonly userRepository: IUser,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async createUser(input: { username: string; email: string; roles?: string[]; isActive?: boolean }) {
|
||||||
|
// Provisioning to external Identity Provider (Authentik) has been disabled.
|
||||||
|
// All users must be created in Authentik first. RayLab will create local record on first successful login.
|
||||||
|
throw new BadRequestException('Provisioning disabled: create users in Authentik and login to sync to RayLab');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,11 +1 @@
|
|||||||
entities/
|
ini adalah tempat class objek dibuat, objek user, role, dll. disini juga ada method dan harus diingat yang boleh mengubah objek ini hanya objek ini sendiri.
|
||||||
|
|
||||||
Penjelasan:
|
|
||||||
Entity domain untuk identity, mis. User, Profile.
|
|
||||||
|
|
||||||
Contoh file:
|
|
||||||
- user.entity.ts
|
|
||||||
- profile.entity.ts
|
|
||||||
|
|
||||||
Aturan:
|
|
||||||
- Entity berisi atribut dan mungkin method domain kecil (invariants), bukan orchestration.
|
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
export class PermissionData {
|
||||||
|
private constructor(
|
||||||
|
public readonly id: string,
|
||||||
|
public code: string,
|
||||||
|
public name: string,
|
||||||
|
public description: string,
|
||||||
|
public createdAt: Date,
|
||||||
|
public updatedAt: Date,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
changeName(name: string) {
|
||||||
|
this.name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
changeDescription(description: string) {
|
||||||
|
this.description = description;
|
||||||
|
}
|
||||||
|
|
||||||
|
changeCode(code: string) {
|
||||||
|
this.code = code;
|
||||||
|
}
|
||||||
|
|
||||||
|
static restore(props: {
|
||||||
|
id: string;
|
||||||
|
code?: string;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
|
}) {
|
||||||
|
return new PermissionData(
|
||||||
|
props.id,
|
||||||
|
props.code || props.name,
|
||||||
|
props.name,
|
||||||
|
props.description,
|
||||||
|
props.createdAt,
|
||||||
|
props.updatedAt,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
import { PermissionData } from "./permission.entity";
|
||||||
|
|
||||||
|
export class RoleData {
|
||||||
|
private constructor(
|
||||||
|
public readonly id: string,
|
||||||
|
public code: string,
|
||||||
|
public name: string,
|
||||||
|
public description: string,
|
||||||
|
public permissions: PermissionData[],
|
||||||
|
public isDefault: boolean,
|
||||||
|
public createdAt: Date,
|
||||||
|
public updatedAt: Date,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
hasPermissions(permissionId: string): boolean {
|
||||||
|
return this.permissions.some(x => x.id.toLowerCase() === permissionId.toLowerCase());
|
||||||
|
}
|
||||||
|
|
||||||
|
assignPermission(permissionData: PermissionData): void {
|
||||||
|
if (this.hasPermissions(permissionData.id)) throw new Error("Permission sudah dimiliki.");
|
||||||
|
|
||||||
|
this.permissions.push(permissionData);
|
||||||
|
}
|
||||||
|
|
||||||
|
removePermission(permissionId: string): void {
|
||||||
|
const idx = this.permissions.findIndex(p => p.id.toLowerCase() === permissionId.toLowerCase());
|
||||||
|
if (idx === -1) throw new Error('Permission tidak ditemukan pada role.');
|
||||||
|
this.permissions.splice(idx, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
changeName(name: string) {
|
||||||
|
this.name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
changeDescription(description: string) {
|
||||||
|
this.description = description;
|
||||||
|
}
|
||||||
|
|
||||||
|
setDefault(isDefault: boolean) {
|
||||||
|
this.isDefault = isDefault;
|
||||||
|
}
|
||||||
|
|
||||||
|
static restore(props: {
|
||||||
|
id: string;
|
||||||
|
code: string;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
permissions?: PermissionData[];
|
||||||
|
isDefault?: boolean;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
|
}) {
|
||||||
|
return new RoleData(
|
||||||
|
props.id,
|
||||||
|
props.code,
|
||||||
|
props.name,
|
||||||
|
props.description,
|
||||||
|
props.permissions || [],
|
||||||
|
props.isDefault ?? false,
|
||||||
|
props.createdAt,
|
||||||
|
props.updatedAt,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,52 +1,196 @@
|
|||||||
export class User {
|
import { RoleData } from "./role.entity";
|
||||||
|
import { PermissionData } from "./permission.entity";
|
||||||
|
|
||||||
|
export class UserData {
|
||||||
private constructor(
|
private constructor(
|
||||||
public readonly id: string,
|
public readonly id: string,
|
||||||
public name: string,
|
public authentikId: string | null,
|
||||||
|
public username: string,
|
||||||
public email: string,
|
public email: string,
|
||||||
public password: string,
|
public password: string | null,
|
||||||
public metadata?: Record<string, any>,
|
public roles: RoleData[],
|
||||||
|
public permissions: PermissionData[],
|
||||||
|
public isActive: boolean,
|
||||||
|
public deletedAt: Date | null,
|
||||||
|
public lastSeenAt: Date | null,
|
||||||
|
public storageQuota: number,
|
||||||
|
public storageUsed: number,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
//#region Create
|
||||||
|
|
||||||
static create(data: {
|
static create(data: {
|
||||||
name: string;
|
username: string;
|
||||||
email: string;
|
email: string;
|
||||||
password: string;
|
password?: string | null;
|
||||||
metadata?: Record<string, any>;
|
authentikId?: string | null;
|
||||||
}): User {
|
storageQuota?: number;
|
||||||
return new User(
|
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(),
|
crypto.randomUUID(),
|
||||||
data.name,
|
data.authentikId || null,
|
||||||
|
data.username,
|
||||||
data.email,
|
data.email,
|
||||||
data.password,
|
data.password ?? null,
|
||||||
data.metadata,
|
[],
|
||||||
|
[],
|
||||||
|
true,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
quota,
|
||||||
|
used,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
delete() {
|
//#endregion
|
||||||
// Business Rule
|
|
||||||
|
//#region Read
|
||||||
|
|
||||||
|
hasRole(roleId: string) : boolean {
|
||||||
|
return this.roles.some(x => x.id.toLowerCase() === roleId.toLowerCase())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
changeEmail(email: string) {
|
||||||
this.email = email;
|
this.email = email;
|
||||||
}
|
}
|
||||||
|
|
||||||
changeName(name: string) {
|
changeUsername(username: string) {
|
||||||
this.name = name;
|
this.username = username;
|
||||||
}
|
}
|
||||||
|
|
||||||
static restore(data: {
|
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;
|
id: string;
|
||||||
name: string;
|
authentikId?: string | null;
|
||||||
|
username: string;
|
||||||
email: string;
|
email: string;
|
||||||
password: string;
|
password: string | null;
|
||||||
metadata?: Record<string, any>;
|
roles?: RoleData[];
|
||||||
}): User {
|
permissions?: PermissionData[];
|
||||||
return new User(
|
isActive?: boolean;
|
||||||
data.id,
|
deletedAt?: Date | null;
|
||||||
data.name,
|
lastSeenAt?: Date | null;
|
||||||
data.email,
|
storageQuota?: number | null;
|
||||||
data.password,
|
storageUsed?: number | null;
|
||||||
data.metadata,
|
}): 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,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -2,5 +2,6 @@ import { DomainEvent } from '../../../../core/events/event.interface';
|
|||||||
|
|
||||||
export class UserCreatedEvent implements DomainEvent {
|
export class UserCreatedEvent implements DomainEvent {
|
||||||
readonly name = 'UserCreated';
|
readonly name = 'UserCreated';
|
||||||
constructor(public readonly payload: any, public readonly occurredAt: Date = new Date()) {}
|
readonly occurredAt: Date = new Date();
|
||||||
|
constructor(public readonly payload: { userId: string; authentikId: string; email: string; username: string }) {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
import { DomainEvent } from '../../../../core/events/event.interface';
|
|
||||||
|
|
||||||
export class UserDeletedEvent implements DomainEvent {
|
|
||||||
readonly name = 'UserDeleted';
|
|
||||||
constructor(public readonly payload: any, public readonly occurredAt: Date = new Date()) {}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import { DomainEvent } from '../../../../core/events/event.interface';
|
||||||
|
|
||||||
|
export class UserDisabledEvent implements DomainEvent {
|
||||||
|
readonly name = 'UserDisabled';
|
||||||
|
readonly occurredAt: Date = new Date();
|
||||||
|
constructor(public readonly payload: { userId: string }) {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import { DomainEvent } from '../../../../core/events/event.interface';
|
||||||
|
|
||||||
|
export class UserEmailChangedEvent implements DomainEvent {
|
||||||
|
readonly name = 'UserEmailChanged';
|
||||||
|
readonly occurredAt: Date = new Date();
|
||||||
|
constructor(public readonly payload: { userId: string; oldEmail: string; newEmail: string }) {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import { DomainEvent } from '../../../../core/events/event.interface';
|
||||||
|
|
||||||
|
export class UserEnabledEvent implements DomainEvent {
|
||||||
|
readonly name = 'UserEnabled';
|
||||||
|
readonly occurredAt: Date = new Date();
|
||||||
|
constructor(public readonly payload: { userId: string }) {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import { DomainEvent } from '../../../../core/events/event.interface';
|
||||||
|
|
||||||
|
export class UserUsernameChangedEvent implements DomainEvent {
|
||||||
|
readonly name = 'UserUsernameChanged';
|
||||||
|
readonly occurredAt: Date = new Date();
|
||||||
|
constructor(public readonly payload: { userId: string; oldUsername: string; newUsername: string }) {}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
ini adalah interface, semua yang akan dibuat oleh application/handler harus dibuat disini dulu.
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { PermissionData } from '../entities/permission.entity';
|
||||||
|
|
||||||
|
export abstract class IPermission {
|
||||||
|
abstract find(params: { page?: number; limit?: number; search?: string | null }): Promise<{ data: PermissionData[]; total: number }>;
|
||||||
|
abstract getById(id: string): Promise<PermissionData>;
|
||||||
|
abstract create(permission: PermissionData): Promise<PermissionData>;
|
||||||
|
abstract update(permission: PermissionData): Promise<PermissionData>;
|
||||||
|
abstract delete(id: string): Promise<void>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { RoleData } from '../entities/role.entity';
|
||||||
|
|
||||||
|
export abstract class IRole {
|
||||||
|
abstract getDefaultRole(): Promise<RoleData | null>;
|
||||||
|
abstract findById(roleId: string): Promise<RoleData>;
|
||||||
|
|
||||||
|
abstract find(params: { page?: number; limit?: number; search?: string | null }): Promise<{ data: RoleData[]; total: number }>;
|
||||||
|
|
||||||
|
abstract create(role: RoleData): Promise<RoleData>;
|
||||||
|
abstract update(role: RoleData): Promise<RoleData>;
|
||||||
|
abstract delete(roleId: string): Promise<void>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import { UserData } from '../entities/user.entity';
|
||||||
|
|
||||||
|
export abstract class IUser {
|
||||||
|
abstract create(user: UserData): Promise<UserData>;
|
||||||
|
|
||||||
|
abstract existsById(userId: string): Promise<boolean>;
|
||||||
|
abstract existByEmail(userEmail: string): Promise<boolean>;
|
||||||
|
abstract getById(userId: string): Promise<UserData>;
|
||||||
|
abstract getByEmail(userEmail: string): Promise<UserData>;
|
||||||
|
|
||||||
|
abstract findByAuthentikId(authentikId: string): Promise<UserData | null>;
|
||||||
|
|
||||||
|
abstract update(user: UserData): Promise<UserData>;
|
||||||
|
|
||||||
|
abstract find(params: {
|
||||||
|
page?: number;
|
||||||
|
limit?: number;
|
||||||
|
search?: string | null;
|
||||||
|
isActive?: boolean | null;
|
||||||
|
deleted?: boolean | null;
|
||||||
|
roleId?: string | null;
|
||||||
|
}): Promise<{ data: UserData[]; total: number }>;
|
||||||
|
|
||||||
|
abstract softDelete(userId: string): Promise<void>;
|
||||||
|
abstract restore(userId: string): Promise<UserData>;
|
||||||
|
|
||||||
|
abstract enable(userId: string): Promise<UserData>;
|
||||||
|
abstract disable(userId: string): Promise<UserData>;
|
||||||
|
}
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
import { User } from '../entities/user.entity';
|
|
||||||
|
|
||||||
export abstract class UserRepository {
|
|
||||||
abstract create(user: User): Promise<User>;
|
|
||||||
|
|
||||||
abstract update(user: User): Promise<User>;
|
|
||||||
|
|
||||||
abstract findById(
|
|
||||||
id: string,
|
|
||||||
): Promise<User | null>;
|
|
||||||
|
|
||||||
abstract findAll(): Promise<User[]>;
|
|
||||||
|
|
||||||
abstract existsByEmail(
|
|
||||||
email: string,
|
|
||||||
): Promise<boolean>;
|
|
||||||
}
|
|
||||||
@@ -1,52 +1,115 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { UsersController } from './presentation/controllers/users.controller';
|
import { UsersController } from './presentation/controllers/users.controller';
|
||||||
|
import { RolesController } from './presentation/controllers/roles.controller';
|
||||||
|
import { PermissionsController } from './presentation/controllers/permissions.controller';
|
||||||
import { PrismaService } from '../../shared/prisma.service';
|
import { PrismaService } from '../../shared/prisma.service';
|
||||||
|
import { UserService } from './application/services/user.service';
|
||||||
import { PrismaUserRepository } from './infrastructure/repositories/prisma-user.repository';
|
import { PrismaUserRepository } from './infrastructure/repositories/prisma-user.repository';
|
||||||
import { UserRepository } from './domain/repositories/user.repository.interface';
|
import { IUser } from './domain/repositories/user.interface';
|
||||||
import { EventDispatcher } from '../../core/events/event-dispatcher';
|
|
||||||
|
|
||||||
import { JwtAuthGuard } from '../../core/auth/guards/jwt-auth.guard';
|
import { JwtAuthGuard } from '../../core/auth/guards/jwt-auth.guard';
|
||||||
import { JwtModule } from '@nestjs/jwt';
|
import { CreateUserHandler } from './application/handlers/user/create-user.handler';
|
||||||
import { CreateUserHandler } from './application/handlers/create-user.handler';
|
|
||||||
import { FindUserHandler } from './application/handlers/find-user.handler';
|
|
||||||
import { DeleteUserHandler } from './application/handlers/delete-user.handler';
|
|
||||||
import { FindUsersHandler } from './application/handlers/find-users.handler';
|
|
||||||
import { UpdateUserHandler } from './application/handlers/update-user.handler';
|
|
||||||
import { PermissionGuard } from '../authorization/presentation/guards/permission.guard';
|
import { PermissionGuard } from '../authorization/presentation/guards/permission.guard';
|
||||||
import { Reflector } from '@nestjs/core';
|
import { Reflector } from '@nestjs/core';
|
||||||
|
import { SyncIdentityHandler } from './application/handlers/user/sync-identity.handler';
|
||||||
|
import { CurrentUserGuard } from './presentation/guards/current-user.guard';
|
||||||
|
import { PrismaRoleRepository } from './infrastructure/repositories/prisma-role.repository';
|
||||||
|
import { PrismaPermissionRepository } from './infrastructure/repositories/prisma-permission.repository';
|
||||||
|
import { IRole } from './domain/repositories/role.interface';
|
||||||
|
import { IPermission } from './domain/repositories/permission.interface';
|
||||||
|
import { IAuthConfig } from './application/config/i-auth-config';
|
||||||
|
import { EnvAuthConfig } from './application/config/env-auth-config';
|
||||||
|
|
||||||
|
import { GetUsersHandler } from './application/handlers/user/get-users.handler';
|
||||||
|
import { GetUserHandler } from './application/handlers/user/get-user.handler';
|
||||||
|
import { GetCurrentUserHandler } from './application/handlers/user/get-current-user.handler';
|
||||||
|
import { EnableUserHandler } from './application/handlers/user/enable-user.handler';
|
||||||
|
import { DisableUserHandler } from './application/handlers/user/disable-user.handler';
|
||||||
|
import { DeleteUserHandler } from './application/handlers/user/delete-user.handler';
|
||||||
|
import { RestoreUserHandler } from './application/handlers/user/restore-user.handler';
|
||||||
|
import { AssignRoleHandler } from './application/handlers/user/assign-role.handler';
|
||||||
|
import { RemoveRoleHandler } from './application/handlers/user/remove-role.handler';
|
||||||
|
import { AssignPermissionHandler } from './application/handlers/user/assign-permission.handler';
|
||||||
|
import { RemovePermissionHandler } from './application/handlers/user/remove-permission.handler';
|
||||||
|
|
||||||
|
import { GetRolesHandler } from './application/handlers/role/get-roles.handler';
|
||||||
|
import { GetRoleHandler } from './application/handlers/role/get-role.handler';
|
||||||
|
import { CreateRoleHandler } from './application/handlers/role/create-role.handler';
|
||||||
|
import { UpdateRoleHandler } from './application/handlers/role/update-role.handler';
|
||||||
|
import { DeleteRoleHandler } from './application/handlers/role/delete-role.handler';
|
||||||
|
import { RoleAssignPermissionHandler } from './application/handlers/role/assign-permission.handler';
|
||||||
|
import { RoleRemovePermissionHandler } from './application/handlers/role/remove-permission.handler';
|
||||||
|
|
||||||
|
import { GetPermissionsHandler } from './application/handlers/permission/get-permissions.handler';
|
||||||
|
import { GetPermissionHandler } from './application/handlers/permission/get-permission.handler';
|
||||||
|
import { CreatePermissionHandler } from './application/handlers/permission/create-permission.handler';
|
||||||
|
import { UpdatePermissionHandler } from './application/handlers/permission/update-permission.handler';
|
||||||
|
import { DeletePermissionHandler } from './application/handlers/permission/delete-permission.handler';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
|
||||||
JwtModule.register({
|
|
||||||
secret: process.env.JWT_SECRET,
|
|
||||||
signOptions: {
|
|
||||||
expiresIn: '1d',
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
controllers: [UsersController],
|
|
||||||
providers: [
|
providers: [
|
||||||
//#region User
|
//#region User
|
||||||
CreateUserHandler,
|
// CreateUserHandler has been removed: provisioning disabled; users must be created in Authentik.
|
||||||
FindUserHandler,
|
SyncIdentityHandler,
|
||||||
FindUsersHandler,
|
GetUsersHandler,
|
||||||
UpdateUserHandler,
|
|
||||||
|
GetUserHandler,
|
||||||
|
GetCurrentUserHandler,
|
||||||
|
EnableUserHandler,
|
||||||
|
DisableUserHandler,
|
||||||
DeleteUserHandler,
|
DeleteUserHandler,
|
||||||
|
RestoreUserHandler,
|
||||||
|
AssignRoleHandler,
|
||||||
|
RemoveRoleHandler,
|
||||||
|
AssignPermissionHandler,
|
||||||
|
RemovePermissionHandler,
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
|
// role & permission handlers
|
||||||
|
GetRolesHandler,
|
||||||
|
GetRoleHandler,
|
||||||
|
CreateRoleHandler,
|
||||||
|
UpdateRoleHandler,
|
||||||
|
DeleteRoleHandler,
|
||||||
|
RoleAssignPermissionHandler,
|
||||||
|
RoleRemovePermissionHandler,
|
||||||
|
|
||||||
|
GetPermissionsHandler,
|
||||||
|
GetPermissionHandler,
|
||||||
|
CreatePermissionHandler,
|
||||||
|
UpdatePermissionHandler,
|
||||||
|
DeletePermissionHandler,
|
||||||
|
|
||||||
JwtAuthGuard,
|
JwtAuthGuard,
|
||||||
|
CurrentUserGuard,
|
||||||
PermissionGuard,
|
PermissionGuard,
|
||||||
Reflector,
|
Reflector,
|
||||||
PrismaService,
|
PrismaService,
|
||||||
PrismaUserRepository,
|
PrismaUserRepository,
|
||||||
|
PrismaRoleRepository,
|
||||||
|
PrismaPermissionRepository,
|
||||||
|
UserService,
|
||||||
{
|
{
|
||||||
provide: UserRepository,
|
provide: IUser,
|
||||||
useClass: PrismaUserRepository,
|
useClass: PrismaUserRepository,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
provide: 'EVENT_DISPATCHER',
|
provide: IRole,
|
||||||
useValue: new EventDispatcher(),
|
useClass: PrismaRoleRepository,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: IPermission,
|
||||||
|
useClass: PrismaPermissionRepository,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: IAuthConfig,
|
||||||
|
useClass: EnvAuthConfig,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
imports: [],
|
||||||
|
controllers: [UsersController, RolesController, PermissionsController],
|
||||||
})
|
})
|
||||||
export class IdentityModule {}
|
export class IdentityModule {}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1 @@
|
|||||||
modules/identity/infrastructure/
|
infrastrucute adalah tempat yang langsung berhubungan dengan dunia luar, misal db, server lain, JWT, dsb.
|
||||||
|
|
||||||
Penjelasan:
|
|
||||||
Implementasi teknis untuk module identity, seperti Prisma repository, adapter implementations, dan data mappers.
|
|
||||||
|
|
||||||
Contoh file:
|
|
||||||
- prisma/user.repository.ts (mengimplementasikan domain repository interface)
|
|
||||||
- adapter/identity-adapter.ts
|
|
||||||
|
|
||||||
Aturan:
|
|
||||||
- Infrastruktur hanya mengimplementasikan interface domain; jangan memuat business rules.
|
|
||||||
- Import dari infrastructure ke domain harus satu arah: infrastructure -> domain (implementasi).
|
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
disini emngubah dari domain/entity menjadi format di database dan sebaliknya.
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { PermissionData } from '../../domain/entities/permission.entity';
|
||||||
|
|
||||||
|
export class PrismaPermissionMapper {
|
||||||
|
static toDomain(model: any): PermissionData {
|
||||||
|
return PermissionData.restore({
|
||||||
|
id: model.id,
|
||||||
|
code: model.code || model.name,
|
||||||
|
name: model.name,
|
||||||
|
description: model.description,
|
||||||
|
createdAt: model.createdAt,
|
||||||
|
updatedAt: model.updatedAt,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
static toPersistence(permission: PermissionData) {
|
||||||
|
return {
|
||||||
|
id: permission.id,
|
||||||
|
code: permission.code,
|
||||||
|
name: permission.name,
|
||||||
|
description: permission.description,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import { RoleData } from '../../domain/entities/role.entity';
|
||||||
|
import { PermissionData } from '../../domain/entities/permission.entity';
|
||||||
|
|
||||||
|
export class PrismaRoleMapper {
|
||||||
|
static toDomain(model: any): RoleData {
|
||||||
|
const permissions: PermissionData[] = (model.permissions || []).map((p: any) =>
|
||||||
|
PermissionData.restore({
|
||||||
|
id: p.id,
|
||||||
|
name: p.name,
|
||||||
|
description: p.description,
|
||||||
|
createdAt: p.createdAt,
|
||||||
|
updatedAt: p.updatedAt,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
return RoleData.restore({
|
||||||
|
id: model.id,
|
||||||
|
code: model.code,
|
||||||
|
name: model.name,
|
||||||
|
description: model.description,
|
||||||
|
permissions,
|
||||||
|
isDefault: model.isDefault ?? false,
|
||||||
|
createdAt: model.createdAt,
|
||||||
|
updatedAt: model.updatedAt,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,27 +1,73 @@
|
|||||||
import { User } from '../../domain/entities/user.entity';
|
import { UserData } from '../../domain/entities/user.entity';
|
||||||
|
import { RoleData } from '../../domain/entities/role.entity';
|
||||||
|
import { PermissionData } from '../../domain/entities/permission.entity';
|
||||||
|
|
||||||
export class PrismaUserMapper {
|
export class PrismaUserMapper {
|
||||||
static toDomain(model: any): User | null {
|
static toDomain(model: any): UserData {
|
||||||
if (!model) {
|
const roles: RoleData[] = (model.roles || []).map((ur: any) => {
|
||||||
return null;
|
const r = ur.role;
|
||||||
}
|
const permissions: PermissionData[] = (r?.permissions || []).map((rp: any) =>
|
||||||
|
PermissionData.restore({
|
||||||
|
id: rp.permission.id,
|
||||||
|
name: rp.permission.name,
|
||||||
|
description: rp.permission.description,
|
||||||
|
createdAt: rp.permission.createdAt,
|
||||||
|
updatedAt: rp.permission.updatedAt,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
return User.restore({
|
return RoleData.restore({
|
||||||
|
id: r.id,
|
||||||
|
code: r.code,
|
||||||
|
name: r.name,
|
||||||
|
description: r.description,
|
||||||
|
permissions,
|
||||||
|
createdAt: r.createdAt,
|
||||||
|
updatedAt: r.updatedAt,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const permissions: PermissionData[] = (model.permissions || []).map((up: any) =>
|
||||||
|
PermissionData.restore({
|
||||||
|
id: up.permission.id,
|
||||||
|
name: up.permission.name,
|
||||||
|
description: up.permission.description,
|
||||||
|
createdAt: up.permission.createdAt,
|
||||||
|
updatedAt: up.permission.updatedAt,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
return UserData.restore({
|
||||||
id: model.id,
|
id: model.id,
|
||||||
name: model.name,
|
authentikId: model.authentikUserId || model.authentikId || null,
|
||||||
|
username: model.username || model.name || model.username,
|
||||||
email: model.email,
|
email: model.email,
|
||||||
password: model.password,
|
password: model.password ?? null,
|
||||||
metadata: model.metadata,
|
roles,
|
||||||
|
permissions,
|
||||||
|
isActive: model.isActive ?? true,
|
||||||
|
deletedAt: model.deletedAt || null,
|
||||||
|
lastSeenAt: model.lastSeenAt || null,
|
||||||
|
storageQuota: model.storageQuota !== undefined && model.storageQuota !== null ? Number(model.storageQuota) : undefined,
|
||||||
|
storageUsed: model.storageUsed !== undefined && model.storageUsed !== null ? Number(model.storageUsed) : undefined,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
static toPersistence(user: User) {
|
static toPersistence(userData: UserData) {
|
||||||
return {
|
return {
|
||||||
id: user.id,
|
id: userData.id,
|
||||||
name: user.name,
|
authentikUserId: (userData as any).authentikUserId || userData.authentikId,
|
||||||
email: user.email,
|
authentikId: userData.authentikId,
|
||||||
password: user.password,
|
username: userData.username,
|
||||||
metadata: user.metadata ?? {},
|
email: userData.email,
|
||||||
|
password: userData.password ?? null,
|
||||||
|
isActive: userData.isActive,
|
||||||
|
deletedAt: userData.deletedAt,
|
||||||
|
lastSeenAt: userData.lastSeenAt,
|
||||||
|
lastSyncedAt: (userData as any).lastSyncedAt,
|
||||||
|
syncStatus: (userData as any).syncStatus,
|
||||||
|
storageQuota: (userData as any).storageQuota,
|
||||||
|
storageUsed: (userData as any).storageUsed,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
ini adalah tempat konfigurasi database.
|
||||||
@@ -1,11 +1 @@
|
|||||||
repositories/
|
disini yang menjalankan logika bisnisnya.
|
||||||
|
|
||||||
Penjelasan:
|
|
||||||
Implementasi repository di layer infrastructure. Biasanya berisi Prisma queries dan mapping antara DB model dan domain entity.
|
|
||||||
|
|
||||||
Contoh file:
|
|
||||||
- prisma/user.repository.ts
|
|
||||||
|
|
||||||
Aturan:
|
|
||||||
- Repository mengimplementasikan interface di domain layer.
|
|
||||||
- Hindari business logic di repository.
|
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { PrismaService } from '../../../../shared/prisma.service';
|
||||||
|
import { IPermission } from '../../domain/repositories/permission.interface';
|
||||||
|
import { PrismaPermissionMapper } from '../mappers/prisma-permission.mapper';
|
||||||
|
import { PermissionData } from '../../domain/entities/permission.entity';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class PrismaPermissionRepository implements IPermission {
|
||||||
|
constructor(private readonly prisma: PrismaService) {}
|
||||||
|
|
||||||
|
async find(params: { page?: number; limit?: number; search?: string | null }) {
|
||||||
|
const page = params.page && params.page > 0 ? params.page : 1;
|
||||||
|
const limit = params.limit && params.limit > 0 ? params.limit : 10;
|
||||||
|
|
||||||
|
const where: any = {};
|
||||||
|
if (params.search) {
|
||||||
|
where.OR = [
|
||||||
|
{ name: { contains: params.search, mode: 'insensitive' } },
|
||||||
|
{ code: { contains: params.search, mode: 'insensitive' } },
|
||||||
|
{ description: { contains: params.search, mode: 'insensitive' } },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
const [total, items] = await Promise.all([
|
||||||
|
this.prisma.permission.count({ where }),
|
||||||
|
this.prisma.permission.findMany({ where, skip: (page - 1) * limit, take: limit, orderBy: { createdAt: 'desc' } }),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return { data: items.map(i => PrismaPermissionMapper.toDomain(i)!), total };
|
||||||
|
}
|
||||||
|
|
||||||
|
async getById(id: string) {
|
||||||
|
const p = await this.prisma.permission.findUnique({ where: { id } });
|
||||||
|
if (!p) throw new Error('Permission not found.');
|
||||||
|
return PrismaPermissionMapper.toDomain(p);
|
||||||
|
}
|
||||||
|
|
||||||
|
async create(permission: PermissionData) {
|
||||||
|
const base = PrismaPermissionMapper.toPersistence(permission);
|
||||||
|
const created = await this.prisma.permission.create({ data: base });
|
||||||
|
return PrismaPermissionMapper.toDomain(created)!;
|
||||||
|
}
|
||||||
|
|
||||||
|
async update(permission: PermissionData) {
|
||||||
|
const base = PrismaPermissionMapper.toPersistence(permission);
|
||||||
|
const updated = await this.prisma.permission.update({ where: { id: permission.id }, data: base });
|
||||||
|
return PrismaPermissionMapper.toDomain(updated)!;
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete(id: string) {
|
||||||
|
await this.prisma.permission.delete({ where: { id } });
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { PrismaService } from '../../../../shared/prisma.service';
|
||||||
|
import { IRole } from '../../domain/repositories/role.interface';
|
||||||
|
import { PrismaRoleMapper } from '../mappers/prisma-role.mapper';
|
||||||
|
import { RoleData } from '../../domain/entities/role.entity';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class PrismaRoleRepository implements IRole {
|
||||||
|
constructor(private readonly prisma: PrismaService) {}
|
||||||
|
|
||||||
|
async getDefaultRole() {
|
||||||
|
const role = await this.prisma.role.findFirst({
|
||||||
|
where: { isDefault: true },
|
||||||
|
include: { permissions: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!role) return null;
|
||||||
|
|
||||||
|
return PrismaRoleMapper.toDomain(role);
|
||||||
|
}
|
||||||
|
|
||||||
|
async findById(roleId: string) {
|
||||||
|
const role = await this.prisma.role.findUnique({
|
||||||
|
where: { id: roleId },
|
||||||
|
include: { permissions: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!role) throw new Error('Role not found.');
|
||||||
|
|
||||||
|
return PrismaRoleMapper.toDomain(role);
|
||||||
|
}
|
||||||
|
|
||||||
|
async find(params: { page?: number; limit?: number; search?: string | null }) {
|
||||||
|
const page = params.page && params.page > 0 ? params.page : 1;
|
||||||
|
const limit = params.limit && params.limit > 0 ? params.limit : 10;
|
||||||
|
|
||||||
|
const where: any = {};
|
||||||
|
if (params.search) {
|
||||||
|
where.OR = [
|
||||||
|
{ name: { contains: params.search, mode: 'insensitive' } },
|
||||||
|
{ code: { contains: params.search, mode: 'insensitive' } },
|
||||||
|
{ description: { contains: params.search, mode: 'insensitive' } },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
const [total, items] = await Promise.all([
|
||||||
|
this.prisma.role.count({ where }),
|
||||||
|
this.prisma.role.findMany({ where, skip: (page - 1) * limit, take: limit, orderBy: { createdAt: 'desc' }, include: { permissions: true } }),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return { data: items.map(i => PrismaRoleMapper.toDomain(i)!), total };
|
||||||
|
}
|
||||||
|
|
||||||
|
async create(role: RoleData) {
|
||||||
|
const base: any = {
|
||||||
|
id: role.id,
|
||||||
|
code: role.code,
|
||||||
|
name: role.name,
|
||||||
|
description: role.description,
|
||||||
|
isDefault: role.isDefault,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (role.permissions && role.permissions.length > 0) {
|
||||||
|
base.permissions = {
|
||||||
|
create: role.permissions.map(p => ({ permission: { connect: { id: p.id } }, assignedBy: 'system' })),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const created = await this.prisma.role.create({ data: base, include: { permissions: true } });
|
||||||
|
return PrismaRoleMapper.toDomain(created)!;
|
||||||
|
}
|
||||||
|
|
||||||
|
async update(role: RoleData) {
|
||||||
|
const base: any = {
|
||||||
|
code: role.code,
|
||||||
|
name: role.name,
|
||||||
|
description: role.description,
|
||||||
|
isDefault: role.isDefault,
|
||||||
|
};
|
||||||
|
|
||||||
|
// sync permissions via transaction
|
||||||
|
const permIds = role.permissions ? role.permissions.map(p => p.id) : [];
|
||||||
|
|
||||||
|
await this.prisma.$transaction(async (prisma) => {
|
||||||
|
const currentPerms = await prisma.rolePermission.findMany({ where: { roleId: role.id } });
|
||||||
|
const currentIds = currentPerms.map(p => p.permissionId);
|
||||||
|
|
||||||
|
const toAdd = permIds.filter(id => !currentIds.includes(id));
|
||||||
|
const toRemove = currentIds.filter(id => !permIds.includes(id));
|
||||||
|
|
||||||
|
if (toRemove.length > 0) {
|
||||||
|
await prisma.rolePermission.deleteMany({ where: { roleId: role.id, permissionId: { in: toRemove } } });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (toAdd.length > 0) {
|
||||||
|
await prisma.rolePermission.createMany({ data: toAdd.map(pid => ({ roleId: role.id, permissionId: pid, assignedBy: 'system' })) as any, skipDuplicates: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
await prisma.role.update({ where: { id: role.id }, data: base });
|
||||||
|
});
|
||||||
|
|
||||||
|
const updated = await this.prisma.role.findUnique({ where: { id: role.id }, include: { permissions: true } });
|
||||||
|
return PrismaRoleMapper.toDomain(updated)!;
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete(roleId: string) {
|
||||||
|
await this.prisma.role.delete({ where: { id: roleId } });
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,64 +1,257 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { PrismaService } from '../../../../shared/prisma.service';
|
import { PrismaService } from '../../../../shared/prisma.service';
|
||||||
import { UserRepository } from '../../domain/repositories/user.repository.interface';
|
import { IUser } from '../../domain/repositories/user.interface';
|
||||||
import { User } from '../../domain/entities/user.entity';
|
import { UserData } from '../../domain/entities/user.entity';
|
||||||
import { PrismaUserMapper } from '../mappers/prisma-user.mapper';
|
import { PrismaUserMapper } from '../mappers/prisma-user.mapper';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class PrismaUserRepository implements UserRepository {
|
export class PrismaUserRepository implements IUser {
|
||||||
constructor(private readonly prisma: PrismaService) {}
|
constructor(private readonly prisma: PrismaService) {}
|
||||||
|
|
||||||
async findById(id: string) {
|
//#region Create
|
||||||
const user = await this.prisma.user.findUnique({
|
|
||||||
where: { id },
|
|
||||||
});
|
|
||||||
|
|
||||||
return PrismaUserMapper.toDomain(user);
|
async create(userData: UserData): Promise<UserData> {
|
||||||
|
const base = PrismaUserMapper.toPersistence(userData);
|
||||||
|
|
||||||
|
const data: any = { ...base };
|
||||||
|
|
||||||
|
if (userData.roles && userData.roles.length > 0) {
|
||||||
|
data.roles = {
|
||||||
|
create: userData.roles.map(r => ({ role: { connect: { id: r.id } }, assignedBy: 'system' })),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async findAll(): Promise<User[]> {
|
|
||||||
const users = await this.prisma.user.findMany({
|
|
||||||
where: { deleted_at: null },
|
|
||||||
orderBy: { created_at: 'desc' },
|
|
||||||
});
|
|
||||||
|
|
||||||
return users.map(PrismaUserMapper.toDomain);
|
|
||||||
}
|
|
||||||
|
|
||||||
async create(user: User): Promise<User> {
|
|
||||||
const created = await this.prisma.user.create({
|
const created = await this.prisma.user.create({
|
||||||
data: PrismaUserMapper.toPersistence(user),
|
data,
|
||||||
|
include: {
|
||||||
|
roles: { include: { role: { include: { permissions: { include: { permission: true } } } } } },
|
||||||
|
permissions: { include: { permission: true } },
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return PrismaUserMapper.toDomain(created)!;
|
return PrismaUserMapper.toDomain(created)!;
|
||||||
}
|
}
|
||||||
|
|
||||||
async update(user: User): Promise<User> {
|
//#endregion
|
||||||
|
|
||||||
|
//#region Read
|
||||||
|
|
||||||
|
async existsById(userId: string): Promise<boolean> {
|
||||||
|
return (
|
||||||
|
(await this.prisma.user.findUnique({
|
||||||
|
where: { id: userId },
|
||||||
|
select: { id: true },
|
||||||
|
})) !== null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async existByEmail(userEmail: string): Promise<boolean> {
|
||||||
|
return (
|
||||||
|
(await this.prisma.user.findUnique({
|
||||||
|
where: { email: userEmail },
|
||||||
|
select: { email: true },
|
||||||
|
})) !== null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getById(userId: string): Promise<UserData> {
|
||||||
|
const userData = await this.prisma.user.findUnique({
|
||||||
|
where: { id: userId },
|
||||||
|
include: {
|
||||||
|
roles: { include: { role: { include: { permissions: { include: { permission: true } } } } } },
|
||||||
|
permissions: { include: { permission: true } },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (userData === null) throw new Error('User tidak ditemukan.');
|
||||||
|
|
||||||
|
return PrismaUserMapper.toDomain(userData);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getByEmail(userEmail: string): Promise<UserData> {
|
||||||
|
const userData = await this.prisma.user.findUnique({
|
||||||
|
where: { email: userEmail },
|
||||||
|
include: {
|
||||||
|
roles: { include: { role: { include: { permissions: { include: { permission: true } } } } } },
|
||||||
|
permissions: { include: { permission: true } },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (userData === null) throw new Error('User tidak ditemukan.');
|
||||||
|
|
||||||
|
return PrismaUserMapper.toDomain(userData);
|
||||||
|
}
|
||||||
|
|
||||||
|
async findByAuthentikId(authentikId: string): Promise<UserData | null> {
|
||||||
|
const userData = await this.prisma.user.findUnique({
|
||||||
|
where: { authentikId },
|
||||||
|
include: {
|
||||||
|
roles: { include: { role: { include: { permissions: { include: { permission: true } } } } } },
|
||||||
|
permissions: { include: { permission: true } },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!userData) return null;
|
||||||
|
|
||||||
|
return PrismaUserMapper.toDomain(userData);
|
||||||
|
}
|
||||||
|
|
||||||
|
async find(params: {
|
||||||
|
page?: number;
|
||||||
|
limit?: number;
|
||||||
|
search?: string | null;
|
||||||
|
isActive?: boolean | null;
|
||||||
|
deleted?: boolean | null;
|
||||||
|
roleId?: string | null;
|
||||||
|
}): Promise<{ data: UserData[]; total: number }> {
|
||||||
|
const page = params.page && params.page > 0 ? params.page : 1;
|
||||||
|
const limit = params.limit && params.limit > 0 ? params.limit : 10;
|
||||||
|
|
||||||
|
const where: any = {};
|
||||||
|
|
||||||
|
if (params.search) {
|
||||||
|
where.OR = [
|
||||||
|
{ username: { contains: params.search, mode: 'insensitive' } },
|
||||||
|
{ email: { contains: params.search, mode: 'insensitive' } },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (params.isActive !== undefined && params.isActive !== null) {
|
||||||
|
where.isActive = params.isActive;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (params.deleted !== undefined && params.deleted !== null) {
|
||||||
|
if (params.deleted) where.deletedAt = { not: null };
|
||||||
|
else where.deletedAt = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (params.roleId) {
|
||||||
|
where.roles = { some: { roleId: params.roleId } };
|
||||||
|
}
|
||||||
|
|
||||||
|
const [total, items] = await Promise.all([
|
||||||
|
this.prisma.user.count({ where }),
|
||||||
|
this.prisma.user.findMany({
|
||||||
|
where,
|
||||||
|
include: {
|
||||||
|
roles: { include: { role: { include: { permissions: { include: { permission: true } } } } } },
|
||||||
|
permissions: { include: { permission: true } },
|
||||||
|
},
|
||||||
|
skip: (page - 1) * limit,
|
||||||
|
take: limit,
|
||||||
|
orderBy: { createdAt: 'desc' },
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return { data: items.map(i => PrismaUserMapper.toDomain(i)!), total };
|
||||||
|
}
|
||||||
|
|
||||||
|
async softDelete(userId: string): Promise<void> {
|
||||||
|
await this.prisma.user.update({ where: { id: userId }, data: { deletedAt: new Date(), isActive: false } });
|
||||||
|
}
|
||||||
|
|
||||||
|
async restore(userId: string): Promise<UserData> {
|
||||||
const updated = await this.prisma.user.update({
|
const updated = await this.prisma.user.update({
|
||||||
where: {
|
where: { id: userId },
|
||||||
id: user.id,
|
data: { deletedAt: null },
|
||||||
|
include: {
|
||||||
|
roles: { include: { role: { include: { permissions: { include: { permission: true } } } } } },
|
||||||
|
permissions: { include: { permission: true } },
|
||||||
},
|
},
|
||||||
data: PrismaUserMapper.toPersistence(user),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return PrismaUserMapper.toDomain(updated)!;
|
return PrismaUserMapper.toDomain(updated)!;
|
||||||
}
|
}
|
||||||
|
|
||||||
async delete(id: string) {
|
async enable(userId: string): Promise<UserData> {
|
||||||
const user = await this.prisma.user.update({
|
const updated = await this.prisma.user.update({
|
||||||
where: { id },
|
where: { id: userId },
|
||||||
data: { deleted_at: new Date() },
|
data: { isActive: true },
|
||||||
|
include: {
|
||||||
|
roles: { include: { role: { include: { permissions: { include: { permission: true } } } } } },
|
||||||
|
permissions: { include: { permission: true } },
|
||||||
|
},
|
||||||
});
|
});
|
||||||
return PrismaUserMapper.toDomain(user);
|
|
||||||
|
return PrismaUserMapper.toDomain(updated)!;
|
||||||
}
|
}
|
||||||
|
|
||||||
async existsByEmail(email: string, excludeId?: string | null) {
|
async disable(userId: string): Promise<UserData> {
|
||||||
const where: any = { email };
|
const updated = await this.prisma.user.update({
|
||||||
if (excludeId) {
|
where: { id: userId },
|
||||||
where.id = { not: excludeId };
|
data: { isActive: false },
|
||||||
|
include: {
|
||||||
|
roles: { include: { role: { include: { permissions: { include: { permission: true } } } } } },
|
||||||
|
permissions: { include: { permission: true } },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return PrismaUserMapper.toDomain(updated)!;
|
||||||
}
|
}
|
||||||
|
|
||||||
const user = await this.prisma.user.findFirst({ where });
|
async update(userData: UserData): Promise<UserData> {
|
||||||
return !!user;
|
const base = PrismaUserMapper.toPersistence(userData);
|
||||||
|
|
||||||
|
const data: any = { ...base };
|
||||||
|
|
||||||
|
// sync roles and permissions using transaction
|
||||||
|
const roleIds = userData.roles ? userData.roles.map(r => r.id) : [];
|
||||||
|
const permissionIds = userData.permissions ? userData.permissions.map(p => p.id) : [];
|
||||||
|
|
||||||
|
// perform transaction: delete removed relations, create missing ones, update user
|
||||||
|
await this.prisma.$transaction(async (prisma) => {
|
||||||
|
// current roles
|
||||||
|
const currentRoles = await prisma.userRole.findMany({ where: { userId: userData.id } });
|
||||||
|
const currentRoleIds = currentRoles.map(r => r.roleId);
|
||||||
|
|
||||||
|
const toAddRoles = roleIds.filter(id => !currentRoleIds.includes(id));
|
||||||
|
const toRemoveRoles = currentRoleIds.filter(id => !roleIds.includes(id));
|
||||||
|
|
||||||
|
if (toRemoveRoles.length > 0) {
|
||||||
|
await prisma.userRole.deleteMany({ where: { userId: userData.id, roleId: { in: toRemoveRoles } } });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (toAddRoles.length > 0) {
|
||||||
|
await prisma.userRole.createMany({
|
||||||
|
data: toAddRoles.map(rid => ({ userId: userData.id, roleId: rid, assignedBy: 'system' })) as any,
|
||||||
|
skipDuplicates: true,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// permissions
|
||||||
|
const currentPerms = await prisma.userPermission.findMany({ where: { userId: userData.id } });
|
||||||
|
const currentPermIds = currentPerms.map(p => p.permissionId);
|
||||||
|
|
||||||
|
const toAddPerms = permissionIds.filter(id => !currentPermIds.includes(id));
|
||||||
|
const toRemovePerms = currentPermIds.filter(id => !permissionIds.includes(id));
|
||||||
|
|
||||||
|
if (toRemovePerms.length > 0) {
|
||||||
|
await prisma.userPermission.deleteMany({ where: { userId: userData.id, permissionId: { in: toRemovePerms } } });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (toAddPerms.length > 0) {
|
||||||
|
await prisma.userPermission.createMany({
|
||||||
|
data: toAddPerms.map(pid => ({ userId: userData.id, permissionId: pid, assignedBy: 'system' })) as any,
|
||||||
|
skipDuplicates: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// update base user fields
|
||||||
|
await prisma.user.update({ where: { id: userData.id }, data });
|
||||||
|
});
|
||||||
|
|
||||||
|
const updated = await this.prisma.user.findUnique({
|
||||||
|
where: { id: userData.id },
|
||||||
|
include: {
|
||||||
|
roles: { include: { role: { include: { permissions: { include: { permission: true } } } } } },
|
||||||
|
permissions: { include: { permission: true } },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return PrismaUserMapper.toDomain(updated)!;
|
||||||
|
}
|
||||||
|
|
||||||
|
//#endregion
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
import { Controller, Get, Post, Patch, Delete, Param, UseGuards, Body, Query } from '@nestjs/common';
|
||||||
|
import { ApiOperation, ApiTags } from '@nestjs/swagger';
|
||||||
|
|
||||||
|
import { JwtAuthGuard } from '../../../../core/auth/guards/jwt-auth.guard';
|
||||||
|
import { PermissionGuard } from '../../../authorization/presentation/guards/permission.guard';
|
||||||
|
import { CurrentUserGuard } from '../guards/current-user.guard';
|
||||||
|
import { Permissions } from '../../../authorization/presentation/decorators/permission.decorator';
|
||||||
|
import { PermissionType } from '../../../../common/constants/permission.constants';
|
||||||
|
|
||||||
|
import { GetPermissionsHandler } from '../../application/handlers/permission/get-permissions.handler';
|
||||||
|
import { GetPermissionHandler } from '../../application/handlers/permission/get-permission.handler';
|
||||||
|
import { CreatePermissionHandler } from '../../application/handlers/permission/create-permission.handler';
|
||||||
|
import { UpdatePermissionHandler } from '../../application/handlers/permission/update-permission.handler';
|
||||||
|
import { DeletePermissionHandler } from '../../application/handlers/permission/delete-permission.handler';
|
||||||
|
|
||||||
|
@ApiTags('Permissions')
|
||||||
|
@Controller('permissions')
|
||||||
|
@UseGuards(JwtAuthGuard, CurrentUserGuard, PermissionGuard)
|
||||||
|
export class PermissionsController {
|
||||||
|
constructor(
|
||||||
|
private readonly getPermissionsHandler: GetPermissionsHandler,
|
||||||
|
private readonly getPermissionHandler: GetPermissionHandler,
|
||||||
|
private readonly createPermissionHandler: CreatePermissionHandler,
|
||||||
|
private readonly updatePermissionHandler: UpdatePermissionHandler,
|
||||||
|
private readonly deletePermissionHandler: DeletePermissionHandler,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
@Get()
|
||||||
|
@Permissions(PermissionType.PERMISSION_READ)
|
||||||
|
@ApiOperation({ summary: 'List permissions' })
|
||||||
|
async findAll(@Query() query: any) {
|
||||||
|
const res = await this.getPermissionsHandler.execute(query);
|
||||||
|
return { success: true, data: res.data, meta: { total: res.total } };
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get(':id')
|
||||||
|
@Permissions(PermissionType.PERMISSION_READ)
|
||||||
|
@ApiOperation({ summary: 'Get permission' })
|
||||||
|
async findOne(@Param('id') id: string) {
|
||||||
|
const p = await this.getPermissionHandler.execute(id);
|
||||||
|
return { success: true, data: p, meta: {} };
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post()
|
||||||
|
@Permissions(PermissionType.PERMISSION_CREATE)
|
||||||
|
@ApiOperation({ summary: 'Create permission' })
|
||||||
|
async create(@Body() body: any) {
|
||||||
|
const created = await this.createPermissionHandler.execute(body);
|
||||||
|
return { success: true, data: created, meta: {} };
|
||||||
|
}
|
||||||
|
|
||||||
|
@Patch(':id')
|
||||||
|
@Permissions(PermissionType.PERMISSION_UPDATE)
|
||||||
|
@ApiOperation({ summary: 'Update permission' })
|
||||||
|
async update(@Param('id') id: string, @Body() body: any) {
|
||||||
|
const updated = await this.updatePermissionHandler.execute(id, body);
|
||||||
|
return { success: true, data: updated, meta: {} };
|
||||||
|
}
|
||||||
|
|
||||||
|
@Delete(':id')
|
||||||
|
@Permissions(PermissionType.PERMISSION_DELETE)
|
||||||
|
@ApiOperation({ summary: 'Delete permission' })
|
||||||
|
async remove(@Param('id') id: string) {
|
||||||
|
await this.deletePermissionHandler.execute(id);
|
||||||
|
return { success: true, data: null, meta: {} };
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
import { Controller, Get, Post, Patch, Delete, Param, UseGuards, Body, Query } from '@nestjs/common';
|
||||||
|
import { ApiOperation, ApiTags } from '@nestjs/swagger';
|
||||||
|
|
||||||
|
import { JwtAuthGuard } from '../../../../core/auth/guards/jwt-auth.guard';
|
||||||
|
import { PermissionGuard } from '../../../authorization/presentation/guards/permission.guard';
|
||||||
|
import { CurrentUserGuard } from '../guards/current-user.guard';
|
||||||
|
import { Permissions } from '../../../authorization/presentation/decorators/permission.decorator';
|
||||||
|
import { PermissionType } from '../../../../common/constants/permission.constants';
|
||||||
|
|
||||||
|
import { GetRolesHandler } from '../../application/handlers/role/get-roles.handler';
|
||||||
|
import { GetRoleHandler } from '../../application/handlers/role/get-role.handler';
|
||||||
|
import { CreateRoleHandler } from '../../application/handlers/role/create-role.handler';
|
||||||
|
import { UpdateRoleHandler } from '../../application/handlers/role/update-role.handler';
|
||||||
|
import { DeleteRoleHandler } from '../../application/handlers/role/delete-role.handler';
|
||||||
|
import { RoleAssignPermissionHandler } from '../../application/handlers/role/assign-permission.handler';
|
||||||
|
import { RoleRemovePermissionHandler } from '../../application/handlers/role/remove-permission.handler';
|
||||||
|
|
||||||
|
@ApiTags('Roles')
|
||||||
|
@Controller('roles')
|
||||||
|
@UseGuards(JwtAuthGuard, CurrentUserGuard, PermissionGuard)
|
||||||
|
export class RolesController {
|
||||||
|
constructor(
|
||||||
|
private readonly getRolesHandler: GetRolesHandler,
|
||||||
|
private readonly getRoleHandler: GetRoleHandler,
|
||||||
|
private readonly createRoleHandler: CreateRoleHandler,
|
||||||
|
private readonly updateRoleHandler: UpdateRoleHandler,
|
||||||
|
private readonly deleteRoleHandler: DeleteRoleHandler,
|
||||||
|
private readonly roleAssignPermissionHandler: RoleAssignPermissionHandler,
|
||||||
|
private readonly roleRemovePermissionHandler: RoleRemovePermissionHandler,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
@Get()
|
||||||
|
@Permissions(PermissionType.ROLE_READ)
|
||||||
|
@ApiOperation({ summary: 'List roles' })
|
||||||
|
async findAll(@Query() query: any) {
|
||||||
|
const res = await this.getRolesHandler.execute(query);
|
||||||
|
return { success: true, data: res.data, meta: { total: res.total } };
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get(':id')
|
||||||
|
@Permissions(PermissionType.ROLE_READ)
|
||||||
|
@ApiOperation({ summary: 'Get role' })
|
||||||
|
async findOne(@Param('id') id: string) {
|
||||||
|
const role = await this.getRoleHandler.execute(id);
|
||||||
|
return { success: true, data: role, meta: {} };
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post()
|
||||||
|
@Permissions(PermissionType.ROLE_CREATE)
|
||||||
|
@ApiOperation({ summary: 'Create role' })
|
||||||
|
async create(@Body() body: any) {
|
||||||
|
const created = await this.createRoleHandler.execute(body);
|
||||||
|
return { success: true, data: created, meta: {} };
|
||||||
|
}
|
||||||
|
|
||||||
|
@Patch(':id')
|
||||||
|
@Permissions(PermissionType.ROLE_UPDATE)
|
||||||
|
@ApiOperation({ summary: 'Update role' })
|
||||||
|
async update(@Param('id') id: string, @Body() body: any) {
|
||||||
|
const updated = await this.updateRoleHandler.execute(id, body);
|
||||||
|
return { success: true, data: updated, meta: {} };
|
||||||
|
}
|
||||||
|
|
||||||
|
@Delete(':id')
|
||||||
|
@Permissions(PermissionType.ROLE_DELETE)
|
||||||
|
@ApiOperation({ summary: 'Delete role' })
|
||||||
|
async remove(@Param('id') id: string) {
|
||||||
|
await this.deleteRoleHandler.execute(id);
|
||||||
|
return { success: true, data: null, meta: {} };
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post(':id/permissions')
|
||||||
|
@Permissions(PermissionType.ROLE_UPDATE)
|
||||||
|
@ApiOperation({ summary: 'Assign permission to role' })
|
||||||
|
async assignPermission(@Param('id') id: string, @Body() body: any) {
|
||||||
|
const permissionId = body.permissionId;
|
||||||
|
const updated = await this.roleAssignPermissionHandler.execute(id, permissionId);
|
||||||
|
return { success: true, data: updated, meta: {} };
|
||||||
|
}
|
||||||
|
|
||||||
|
@Delete(':id/permissions/:permissionId')
|
||||||
|
@Permissions(PermissionType.ROLE_UPDATE)
|
||||||
|
@ApiOperation({ summary: 'Remove permission from role' })
|
||||||
|
async removePermission(@Param('id') id: string, @Param('permissionId') permissionId: string) {
|
||||||
|
const updated = await this.roleRemovePermissionHandler.execute(id, permissionId);
|
||||||
|
return { success: true, data: updated, meta: {} };
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,101 +1,134 @@
|
|||||||
import {
|
import { Controller, Get, Param, UseGuards, Query, Patch, Delete, Post, Body, Req } from '@nestjs/common';
|
||||||
Controller,
|
|
||||||
Get,
|
|
||||||
Post,
|
|
||||||
Delete,
|
|
||||||
Param,
|
|
||||||
UseGuards,
|
|
||||||
Body,
|
|
||||||
Patch
|
|
||||||
} from '@nestjs/common';
|
|
||||||
import { ApiOperation, ApiTags } from '@nestjs/swagger';
|
import { ApiOperation, ApiTags } from '@nestjs/swagger';
|
||||||
|
|
||||||
import { JwtAuthGuard } from '../../../../core/auth/guards/jwt-auth.guard';
|
import { JwtAuthGuard } from '../../../../core/auth/guards/jwt-auth.guard';
|
||||||
import { PermissionGuard } from '../../../authorization/presentation/guards/permission.guard';
|
import { PermissionGuard } from '../../../authorization/presentation/guards/permission.guard';
|
||||||
|
import { CurrentUserGuard } from '../guards/current-user.guard';
|
||||||
import { Permissions } from '../../../authorization/presentation/decorators/permission.decorator';
|
import { Permissions } from '../../../authorization/presentation/decorators/permission.decorator';
|
||||||
|
import { PermissionType } from '../../../../common/constants/permission.constants';
|
||||||
|
|
||||||
import { FindUserHandler } from '../../application/handlers/find-user.handler';
|
import { GetUsersHandler } from '../../application/handlers/user/get-users.handler';
|
||||||
import { FindUsersHandler } from '../../application/handlers/find-users.handler';
|
import { GetUserHandler } from '../../application/handlers/user/get-user.handler';
|
||||||
import { CreateUserHandler } from '../../application/handlers/create-user.handler';
|
import { GetCurrentUserHandler } from '../../application/handlers/user/get-current-user.handler';
|
||||||
import { UpdateUserHandler } from '../../application/handlers/update-user.handler';
|
import { EnableUserHandler } from '../../application/handlers/user/enable-user.handler';
|
||||||
import { DeleteUserHandler } from '../../application/handlers/delete-user.handler';
|
import { DisableUserHandler } from '../../application/handlers/user/disable-user.handler';
|
||||||
import { CreateUserDto } from '../dto/create-user.dto';
|
import { DeleteUserHandler } from '../../application/handlers/user/delete-user.handler';
|
||||||
import { UpdateUserDto } from '../dto/update-user.dto';
|
import { RestoreUserHandler } from '../../application/handlers/user/restore-user.handler';
|
||||||
|
import { AssignRoleHandler } from '../../application/handlers/user/assign-role.handler';
|
||||||
|
import { RemoveRoleHandler } from '../../application/handlers/user/remove-role.handler';
|
||||||
|
import { AssignPermissionHandler } from '../../application/handlers/user/assign-permission.handler';
|
||||||
|
import { RemovePermissionHandler } from '../../application/handlers/user/remove-permission.handler';
|
||||||
|
|
||||||
@ApiTags('Users')
|
@ApiTags('Users')
|
||||||
@Controller('api/v1/users')
|
@Controller('users')
|
||||||
@UseGuards(JwtAuthGuard, PermissionGuard)
|
@UseGuards(JwtAuthGuard, CurrentUserGuard, PermissionGuard)
|
||||||
export class UsersController {
|
export class UsersController {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly createUserHandler: CreateUserHandler,
|
private readonly getUsersHandler: GetUsersHandler,
|
||||||
private readonly findUserHandler: FindUserHandler,
|
private readonly getUserHandler: GetUserHandler,
|
||||||
private readonly findUsersHandler: FindUsersHandler,
|
private readonly getCurrentUserHandler: GetCurrentUserHandler,
|
||||||
private readonly updateUserHandler: UpdateUserHandler,
|
private readonly enableUserHandler: EnableUserHandler,
|
||||||
|
private readonly disableUserHandler: DisableUserHandler,
|
||||||
private readonly deleteUserHandler: DeleteUserHandler,
|
private readonly deleteUserHandler: DeleteUserHandler,
|
||||||
|
private readonly restoreUserHandler: RestoreUserHandler,
|
||||||
|
private readonly assignRoleHandler: AssignRoleHandler,
|
||||||
|
private readonly removeRoleHandler: RemoveRoleHandler,
|
||||||
|
private readonly assignPermissionHandler: AssignPermissionHandler,
|
||||||
|
private readonly removePermissionHandler: RemovePermissionHandler,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@Post()
|
@Get()
|
||||||
@Permissions('USER_CREATE')
|
@Permissions(PermissionType.USER_READ)
|
||||||
@ApiOperation({ summary: 'Create user' })
|
@ApiOperation({ summary: 'List users' })
|
||||||
async create(@Body() dto: CreateUserDto) {
|
async findAll(@Query() query: any) {
|
||||||
const data = await this.createUserHandler.execute(dto);
|
const res = await this.getUsersHandler.execute(query);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
data,
|
data: res.data.map(u => (u as any).toResponse ? (u as any).toResponse() : u),
|
||||||
meta: {},
|
meta: { total: res.total },
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Get('me')
|
||||||
|
@ApiOperation({ summary: 'Get current user' })
|
||||||
|
async me(@Req() req: any) {
|
||||||
|
const user = await this.getCurrentUserHandler.execute(req.currentUser);
|
||||||
|
|
||||||
|
return { success: true, data: (user as any).toResponse ? (user as any).toResponse() : user, meta: {} };
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get(':id')
|
@Get(':id')
|
||||||
@Permissions('USER_READ')
|
@Permissions(PermissionType.USER_READ)
|
||||||
@ApiOperation({ summary: 'Get user by id' })
|
@ApiOperation({ summary: 'Get user by id' })
|
||||||
async getById(@Param('id') id: string) {
|
async findOne(@Param('id') id: string) {
|
||||||
const data = await this.findUserHandler.execute(id);
|
const user = await this.getUserHandler.execute(id);
|
||||||
|
return { success: true, data: (user as any).toResponse ? (user as any).toResponse() : user, meta: {} };
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
data,
|
|
||||||
meta: {},
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get()
|
@Patch(':id/enable')
|
||||||
@Permissions('USER_READ_ADMIN')
|
@Permissions(PermissionType.USER_UPDATE)
|
||||||
@ApiOperation({ summary: 'Get All User' })
|
@ApiOperation({ summary: 'Enable user' })
|
||||||
async getAll() {
|
async enable(@Param('id') id: string) {
|
||||||
const data = await this.findUsersHandler.execute();
|
const updated = await this.enableUserHandler.execute(id);
|
||||||
|
return { success: true, data: (updated as any).toResponse ? (updated as any).toResponse() : updated, meta: {} };
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
data,
|
|
||||||
meta: {},
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Patch(':id')
|
@Patch(':id/disable')
|
||||||
@Permissions('USER_UPDATE')
|
@Permissions(PermissionType.USER_UPDATE)
|
||||||
@ApiOperation({ summary: 'Update User' })
|
@ApiOperation({ summary: 'Disable user' })
|
||||||
async update(@Param('id') id:string, @Body() dto:UpdateUserDto) {
|
async disable(@Param('id') id: string) {
|
||||||
const data = await this.updateUserHandler.execute(id, dto);
|
const updated = await this.disableUserHandler.execute(id);
|
||||||
|
return { success: true, data: (updated as any).toResponse ? (updated as any).toResponse() : updated, meta: {} };
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
data,
|
|
||||||
meta: {},
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Delete(':id')
|
@Delete(':id')
|
||||||
@Permissions('USER_DELETE')
|
@Permissions(PermissionType.USER_DELETE)
|
||||||
@ApiOperation({ summary: 'Delete user by id' })
|
@ApiOperation({ summary: 'Soft delete user' })
|
||||||
async delete(@Param('id') id: string) {
|
async remove(@Param('id') id: string) {
|
||||||
await this.deleteUserHandler.execute(id);
|
await this.deleteUserHandler.execute(id);
|
||||||
|
return { success: true, data: null, meta: {} };
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
@Post(':id/restore')
|
||||||
success: true,
|
@Permissions(PermissionType.USER_UPDATE)
|
||||||
data: null,
|
@ApiOperation({ summary: 'Restore user' })
|
||||||
meta: {},
|
async restore(@Param('id') id: string) {
|
||||||
};
|
const restored = await this.restoreUserHandler.execute(id);
|
||||||
|
return { success: true, data: (restored as any).toResponse ? (restored as any).toResponse() : restored, meta: {} };
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post(':id/roles')
|
||||||
|
@Permissions(PermissionType.USER_UPDATE)
|
||||||
|
@ApiOperation({ summary: 'Assign role to user' })
|
||||||
|
async assignRole(@Param('id') id: string, @Body() body: any) {
|
||||||
|
const roleId = body.roleId;
|
||||||
|
const updated = await this.assignRoleHandler.execute(id, roleId);
|
||||||
|
return { success: true, data: (updated as any).toResponse ? (updated as any).toResponse() : updated, meta: {} };
|
||||||
|
}
|
||||||
|
|
||||||
|
@Delete(':id/roles/:roleId')
|
||||||
|
@Permissions(PermissionType.USER_UPDATE)
|
||||||
|
@ApiOperation({ summary: 'Remove role from user' })
|
||||||
|
async removeRole(@Param('id') id: string, @Param('roleId') roleId: string) {
|
||||||
|
const updated = await this.removeRoleHandler.execute(id, roleId);
|
||||||
|
return { success: true, data: (updated as any).toResponse ? (updated as any).toResponse() : updated, meta: {} };
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post(':id/permissions')
|
||||||
|
@Permissions(PermissionType.USER_UPDATE)
|
||||||
|
@ApiOperation({ summary: 'Assign permission to user' })
|
||||||
|
async assignPermission(@Param('id') id: string, @Body() body: any) {
|
||||||
|
const permissionId = body.permissionId;
|
||||||
|
const updated = await this.assignPermissionHandler.execute(id, permissionId);
|
||||||
|
return { success: true, data: (updated as any).toResponse ? (updated as any).toResponse() : updated, meta: {} };
|
||||||
|
}
|
||||||
|
|
||||||
|
@Delete(':id/permissions/:permissionId')
|
||||||
|
@Permissions(PermissionType.USER_UPDATE)
|
||||||
|
@ApiOperation({ summary: 'Remove permission from user' })
|
||||||
|
async removePermission(@Param('id') id: string, @Param('permissionId') permissionId: string) {
|
||||||
|
const updated = await this.removePermissionHandler.execute(id, permissionId);
|
||||||
|
return { success: true, data: (updated as any).toResponse ? (updated as any).toResponse() : updated, meta: {} };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
import { IsString, IsNotEmpty, IsOptional } from 'class-validator';
|
||||||
|
|
||||||
|
export class CreatePermissionDto {
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
code!: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
name!: string;
|
||||||
|
|
||||||
|
@ApiProperty({ required: false })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
description?: string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
import { IsString, IsNotEmpty, IsOptional } from 'class-validator';
|
||||||
|
|
||||||
|
export class CreateRoleDto {
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
code!: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
name!: string;
|
||||||
|
|
||||||
|
@ApiProperty({ required: false })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
description?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ required: false })
|
||||||
|
@IsOptional()
|
||||||
|
isDefault?: boolean;
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user