feat: Cấu hình lại ứng dụng IAM, cập nhật cấu trúc routes, tích hợp RBAC và thêm các bài kiểm tra tích hợp.
This commit is contained in:
@@ -117,7 +117,7 @@ services:
|
||||
- microservices-network
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:5001/health/live"]
|
||||
test: ["CMD", "curl", "-f", "http://localhost:5001/health"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
@@ -128,7 +128,7 @@ services:
|
||||
- "traefik.http.routers.iam-service.rule=PathPrefix(`/api/v1/auth`) || PathPrefix(`/api/v1/users`) || PathPrefix(`/api/v1/identity`) || PathPrefix(`/api/v1/access`) || PathPrefix(`/api/v1/governance`) || PathPrefix(`/api/v1/rbac`) || PathPrefix(`/api/v1/mfa`) || PathPrefix(`/api/v1/sessions`)"
|
||||
- "traefik.http.routers.iam-service.entrypoints=web"
|
||||
- "traefik.http.services.iam-service.loadbalancer.server.port=5001"
|
||||
- "traefik.http.services.iam-service.loadbalancer.healthcheck.path=/health/live"
|
||||
- "traefik.http.services.iam-service.loadbalancer.healthcheck.path=/health"
|
||||
- "traefik.http.services.iam-service.loadbalancer.healthcheck.interval=10s"
|
||||
|
||||
# ===========================================================================
|
||||
|
||||
@@ -2,13 +2,11 @@ http:
|
||||
middlewares:
|
||||
secure-headers:
|
||||
headers:
|
||||
sslRedirect: true
|
||||
sslRedirect: false # EN: Disabled for local development / VI: Tắt cho local development
|
||||
stsSeconds: 31536000
|
||||
contentTypeNosniff: true
|
||||
browserXssFilter: true
|
||||
frameDeny: true
|
||||
customRequestHeaders:
|
||||
X-Forwarded-Proto: "https"
|
||||
|
||||
cors:
|
||||
headers:
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
http:
|
||||
routers:
|
||||
iam-service-router:
|
||||
rule: "Host(`api.goodgo.vn`) && PathPrefix(`/api/v1/auth`)"
|
||||
rule: "PathPrefix(`/api/v1/auth`)"
|
||||
service: iam-service
|
||||
priority: 100
|
||||
middlewares:
|
||||
- auth-ratelimit
|
||||
- cors
|
||||
@@ -11,8 +12,9 @@ http:
|
||||
- web
|
||||
|
||||
iam-service-users-router:
|
||||
rule: "Host(`api.goodgo.vn`) && PathPrefix(`/api/v1/users`)"
|
||||
rule: "PathPrefix(`/api/v1/users`) || PathPrefix(`/api/v1/identity`) || PathPrefix(`/api/v1/access`) || PathPrefix(`/api/v1/governance`) || PathPrefix(`/api/v1/rbac`) || PathPrefix(`/api/v1/mfa`) || PathPrefix(`/api/v1/sessions`)"
|
||||
service: iam-service
|
||||
priority: 100
|
||||
middlewares:
|
||||
- auth-ratelimit
|
||||
- cors
|
||||
|
||||
8
pnpm-lock.yaml
generated
8
pnpm-lock.yaml
generated
@@ -618,6 +618,9 @@ importers:
|
||||
specifier: ^3.22.4
|
||||
version: 3.25.76
|
||||
devDependencies:
|
||||
'@faker-js/faker':
|
||||
specifier: ^10.1.0
|
||||
version: 10.1.0
|
||||
'@goodgo/eslint-config':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/config/eslint-config
|
||||
@@ -1479,6 +1482,11 @@ packages:
|
||||
optional: true
|
||||
dev: false
|
||||
|
||||
/@faker-js/faker@10.1.0:
|
||||
resolution: {integrity: sha512-C3mrr3b5dRVlKPJdfrAXS8+dq+rq8Qm5SNRazca0JKgw1HQERFmrVb0towvMmw5uu8hHKNiQasMaR/tydf3Zsg==}
|
||||
engines: {node: ^20.19.0 || ^22.13.0 || ^23.5.0 || >=24.0.0, npm: '>=10'}
|
||||
dev: true
|
||||
|
||||
/@fingerprintjs/fingerprintjs@5.0.1:
|
||||
resolution: {integrity: sha512-KbaeE/rk2WL8MfpRP6jTI4lSr42SJPjvkyrjP3QU6uUDkOMWWYC2Ts1sNSYcegHC8avzOoYTHBj+2fTqvZWQBA==}
|
||||
dev: false
|
||||
|
||||
@@ -35,7 +35,10 @@ const config: Config = {
|
||||
clearMocks: true,
|
||||
// EN: Reset modules between tests for isolation
|
||||
// VI: Reset modules giữa các test để cô lập
|
||||
resetModules: true
|
||||
resetModules: true,
|
||||
transformIgnorePatterns: [
|
||||
'node_modules/(?!(@faker-js/faker)/)'
|
||||
]
|
||||
};
|
||||
|
||||
export default config;
|
||||
17
services/iam-service/jest.integration.config.ts
Normal file
17
services/iam-service/jest.integration.config.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import type { Config } from 'jest';
|
||||
import defaultConfig from './jest.config';
|
||||
|
||||
const config: Config = {
|
||||
...defaultConfig,
|
||||
testMatch: [
|
||||
'**/__tests__/**/*.integration.test.ts',
|
||||
'**/__tests__/**/*.e2e.ts'
|
||||
],
|
||||
// Override setupFilesAfterEnv to avoid loading global mocks
|
||||
setupFilesAfterEnv: ['<rootDir>/src/__tests__/setupIntegrationTests.ts'],
|
||||
// Ensure we don't carry over mocks
|
||||
resetMocks: true,
|
||||
restoreMocks: true,
|
||||
};
|
||||
|
||||
export default config;
|
||||
@@ -60,6 +60,7 @@
|
||||
"zod": "^3.22.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@faker-js/faker": "^10.1.0",
|
||||
"@goodgo/eslint-config": "workspace:*",
|
||||
"@goodgo/tsconfig": "workspace:*",
|
||||
"@jest/globals": "^29.7.0",
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
-- Add account lockout fields to users table
|
||||
-- Migration: add_account_lockout_fields
|
||||
|
||||
-- Add failedLoginAttempts column with default value 0
|
||||
ALTER TABLE "users" ADD COLUMN "failedLoginAttempts" INTEGER NOT NULL DEFAULT 0;
|
||||
|
||||
-- Add lockedUntil column for account lockout timestamp
|
||||
ALTER TABLE "users" ADD COLUMN "lockedUntil" TIMESTAMP(3);
|
||||
|
||||
-- Add index on failedLoginAttempts for potential queries
|
||||
CREATE INDEX "users_failedLoginAttempts_idx" ON "users"("failedLoginAttempts");
|
||||
|
||||
-- Add index on lockedUntil for cleanup queries
|
||||
CREATE INDEX "users_lockedUntil_idx" ON "users"("lockedUntil");
|
||||
@@ -1,5 +0,0 @@
|
||||
-- Add MFA backup codes field to users table
|
||||
-- Migration: add_mfa_backup_codes
|
||||
|
||||
-- Add mfaBackupCodes column to store encrypted backup codes
|
||||
ALTER TABLE "users" ADD COLUMN "mfaBackupCodes" TEXT;
|
||||
@@ -0,0 +1,723 @@
|
||||
-- CreateEnum
|
||||
CREATE TYPE "Provider" AS ENUM ('GOOGLE', 'FACEBOOK', 'GITHUB', 'APPLE', 'MICROSOFT');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "MFAType" AS ENUM ('TOTP', 'WEBAUTHN');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "VerificationType" AS ENUM ('EMAIL', 'PHONE', 'DOCUMENT', 'BIOMETRIC');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "VerificationStatus" AS ENUM ('PENDING', 'VERIFIED', 'REJECTED', 'EXPIRED');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "RequestStatus" AS ENUM ('PENDING', 'APPROVED', 'REJECTED', 'EXPIRED', 'CANCELLED');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "ApprovalStatus" AS ENUM ('PENDING', 'APPROVED', 'REJECTED');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "ReviewType" AS ENUM ('PERIODIC', 'ADHOC', 'CERTIFICATION');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "ReviewStatus" AS ENUM ('DRAFT', 'ACTIVE', 'COMPLETED', 'CANCELLED');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "ReviewItemStatus" AS ENUM ('PENDING', 'APPROVED', 'REVOKED', 'NO_CHANGE');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "ComplianceType" AS ENUM ('GDPR', 'SOC2', 'ISO27001', 'HIPAA', 'CUSTOM');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "ReportStatus" AS ENUM ('DRAFT', 'GENERATED', 'PUBLISHED', 'ARCHIVED');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "PolicyCategory" AS ENUM ('SECURITY', 'ACCESS', 'COMPLIANCE', 'DATA', 'PRIVACY');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "RiskLevel" AS ENUM ('LOW', 'MEDIUM', 'HIGH', 'CRITICAL');
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "users" (
|
||||
"id" TEXT NOT NULL,
|
||||
"email" TEXT NOT NULL,
|
||||
"username" TEXT,
|
||||
"passwordHash" TEXT,
|
||||
"isActive" BOOLEAN NOT NULL DEFAULT true,
|
||||
"emailVerified" BOOLEAN NOT NULL DEFAULT false,
|
||||
"mfaEnabled" BOOLEAN NOT NULL DEFAULT false,
|
||||
"mfaSecret" TEXT,
|
||||
"mfaBackupCodes" TEXT,
|
||||
"lastLoginAt" TIMESTAMP(3),
|
||||
"loginCount" INTEGER NOT NULL DEFAULT 0,
|
||||
"failedLoginAttempts" INTEGER NOT NULL DEFAULT 0,
|
||||
"lockedUntil" TIMESTAMP(3),
|
||||
"organizationId" TEXT,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "users_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "roles" (
|
||||
"id" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"displayName" TEXT NOT NULL,
|
||||
"description" TEXT,
|
||||
"isSystem" BOOLEAN NOT NULL DEFAULT false,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "roles_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "permissions" (
|
||||
"id" TEXT NOT NULL,
|
||||
"resource" TEXT NOT NULL,
|
||||
"action" TEXT NOT NULL,
|
||||
"scope" TEXT,
|
||||
"description" TEXT,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "permissions_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "user_roles" (
|
||||
"id" TEXT NOT NULL,
|
||||
"userId" TEXT NOT NULL,
|
||||
"roleId" TEXT NOT NULL,
|
||||
"grantedBy" TEXT,
|
||||
"expiresAt" TIMESTAMP(3),
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "user_roles_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "role_permissions" (
|
||||
"id" TEXT NOT NULL,
|
||||
"roleId" TEXT NOT NULL,
|
||||
"permissionId" TEXT NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "role_permissions_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "user_permissions" (
|
||||
"id" TEXT NOT NULL,
|
||||
"userId" TEXT NOT NULL,
|
||||
"permissionId" TEXT NOT NULL,
|
||||
"granted" BOOLEAN NOT NULL DEFAULT true,
|
||||
"grantedBy" TEXT,
|
||||
"expiresAt" TIMESTAMP(3),
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "user_permissions_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "sessions" (
|
||||
"id" TEXT NOT NULL,
|
||||
"userId" TEXT NOT NULL,
|
||||
"deviceId" TEXT NOT NULL,
|
||||
"deviceName" TEXT,
|
||||
"ipAddress" TEXT NOT NULL,
|
||||
"userAgent" TEXT,
|
||||
"fingerprint" TEXT,
|
||||
"isActive" BOOLEAN NOT NULL DEFAULT true,
|
||||
"expiresAt" TIMESTAMP(3) NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"lastActivityAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "sessions_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "refresh_tokens" (
|
||||
"id" TEXT NOT NULL,
|
||||
"userId" TEXT NOT NULL,
|
||||
"token" TEXT NOT NULL,
|
||||
"family" TEXT,
|
||||
"deviceId" TEXT,
|
||||
"ipAddress" TEXT,
|
||||
"userAgent" TEXT,
|
||||
"expiresAt" TIMESTAMP(3) NOT NULL,
|
||||
"revokedAt" TIMESTAMP(3),
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "refresh_tokens_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "social_accounts" (
|
||||
"id" TEXT NOT NULL,
|
||||
"userId" TEXT NOT NULL,
|
||||
"provider" "Provider" NOT NULL,
|
||||
"providerId" TEXT NOT NULL,
|
||||
"email" TEXT,
|
||||
"name" TEXT,
|
||||
"avatar" TEXT,
|
||||
"accessToken" TEXT,
|
||||
"refreshToken" TEXT,
|
||||
"expiresAt" TIMESTAMP(3),
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "social_accounts_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "mfa_devices" (
|
||||
"id" TEXT NOT NULL,
|
||||
"userId" TEXT NOT NULL,
|
||||
"type" "MFAType" NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"secret" TEXT,
|
||||
"credentialId" TEXT,
|
||||
"publicKey" TEXT,
|
||||
"counter" INTEGER,
|
||||
"isActive" BOOLEAN NOT NULL DEFAULT true,
|
||||
"lastUsedAt" TIMESTAMP(3),
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "mfa_devices_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "auth_events" (
|
||||
"id" TEXT NOT NULL,
|
||||
"userId" TEXT,
|
||||
"eventType" TEXT NOT NULL,
|
||||
"eventData" JSONB NOT NULL,
|
||||
"ipAddress" TEXT,
|
||||
"userAgent" TEXT,
|
||||
"success" BOOLEAN NOT NULL DEFAULT true,
|
||||
"errorMessage" TEXT,
|
||||
"timestamp" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "auth_events_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "features" (
|
||||
"id" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"title" TEXT,
|
||||
"description" TEXT,
|
||||
"config" JSONB,
|
||||
"enabled" BOOLEAN NOT NULL DEFAULT true,
|
||||
"version" TEXT NOT NULL DEFAULT '1.0.0',
|
||||
"tags" JSONB,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "features_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "policies" (
|
||||
"id" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"description" TEXT,
|
||||
"resource" TEXT NOT NULL,
|
||||
"condition" JSONB NOT NULL,
|
||||
"effect" TEXT NOT NULL,
|
||||
"priority" INTEGER NOT NULL DEFAULT 0,
|
||||
"isActive" BOOLEAN NOT NULL DEFAULT true,
|
||||
"organizationId" TEXT,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "policies_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "organizations" (
|
||||
"id" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"domain" TEXT,
|
||||
"parentId" TEXT,
|
||||
"settings" JSONB,
|
||||
"isActive" BOOLEAN NOT NULL DEFAULT true,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "organizations_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "groups" (
|
||||
"id" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"organizationId" TEXT,
|
||||
"description" TEXT,
|
||||
"isActive" BOOLEAN NOT NULL DEFAULT true,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "groups_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "group_members" (
|
||||
"id" TEXT NOT NULL,
|
||||
"userId" TEXT NOT NULL,
|
||||
"groupId" TEXT NOT NULL,
|
||||
"role" TEXT NOT NULL DEFAULT 'member',
|
||||
"joinedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"expiresAt" TIMESTAMP(3),
|
||||
|
||||
CONSTRAINT "group_members_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "group_permissions" (
|
||||
"id" TEXT NOT NULL,
|
||||
"groupId" TEXT NOT NULL,
|
||||
"permissionId" TEXT NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "group_permissions_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "user_profiles" (
|
||||
"id" TEXT NOT NULL,
|
||||
"userId" TEXT NOT NULL,
|
||||
"firstName" TEXT,
|
||||
"lastName" TEXT,
|
||||
"phone" TEXT,
|
||||
"phoneVerified" BOOLEAN NOT NULL DEFAULT false,
|
||||
"avatarUrl" TEXT,
|
||||
"customFields" JSONB,
|
||||
"preferences" JSONB,
|
||||
"metadata" JSONB,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "user_profiles_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "identity_verifications" (
|
||||
"id" TEXT NOT NULL,
|
||||
"userId" TEXT NOT NULL,
|
||||
"type" "VerificationType" NOT NULL,
|
||||
"status" "VerificationStatus" NOT NULL DEFAULT 'PENDING',
|
||||
"method" TEXT,
|
||||
"token" TEXT,
|
||||
"verifiedAt" TIMESTAMP(3),
|
||||
"expiresAt" TIMESTAMP(3),
|
||||
"metadata" JSONB,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "identity_verifications_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "access_requests" (
|
||||
"id" TEXT NOT NULL,
|
||||
"userId" TEXT NOT NULL,
|
||||
"resource" TEXT NOT NULL,
|
||||
"action" TEXT NOT NULL,
|
||||
"reason" TEXT,
|
||||
"status" "RequestStatus" NOT NULL DEFAULT 'PENDING',
|
||||
"requestedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"reviewedAt" TIMESTAMP(3),
|
||||
"reviewedBy" TEXT,
|
||||
"expiresAt" TIMESTAMP(3),
|
||||
"metadata" JSONB,
|
||||
|
||||
CONSTRAINT "access_requests_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "access_request_approvers" (
|
||||
"id" TEXT NOT NULL,
|
||||
"requestId" TEXT NOT NULL,
|
||||
"approverId" TEXT NOT NULL,
|
||||
"status" "ApprovalStatus" NOT NULL DEFAULT 'PENDING',
|
||||
"comments" TEXT,
|
||||
"reviewedAt" TIMESTAMP(3),
|
||||
|
||||
CONSTRAINT "access_request_approvers_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "access_reviews" (
|
||||
"id" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"description" TEXT,
|
||||
"type" "ReviewType" NOT NULL,
|
||||
"status" "ReviewStatus" NOT NULL DEFAULT 'DRAFT',
|
||||
"startDate" TIMESTAMP(3) NOT NULL,
|
||||
"endDate" TIMESTAMP(3) NOT NULL,
|
||||
"createdBy" TEXT NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"completedAt" TIMESTAMP(3),
|
||||
|
||||
CONSTRAINT "access_reviews_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "access_review_items" (
|
||||
"id" TEXT NOT NULL,
|
||||
"reviewId" TEXT NOT NULL,
|
||||
"userId" TEXT NOT NULL,
|
||||
"resource" TEXT NOT NULL,
|
||||
"access" JSONB NOT NULL,
|
||||
"status" "ReviewItemStatus" NOT NULL DEFAULT 'PENDING',
|
||||
"reviewedBy" TEXT,
|
||||
"reviewedAt" TIMESTAMP(3),
|
||||
"comments" TEXT,
|
||||
|
||||
CONSTRAINT "access_review_items_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "compliance_reports" (
|
||||
"id" TEXT NOT NULL,
|
||||
"type" "ComplianceType" NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"periodStart" TIMESTAMP(3) NOT NULL,
|
||||
"periodEnd" TIMESTAMP(3) NOT NULL,
|
||||
"status" "ReportStatus" NOT NULL DEFAULT 'DRAFT',
|
||||
"data" JSONB,
|
||||
"generatedAt" TIMESTAMP(3),
|
||||
"generatedBy" TEXT,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "compliance_reports_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "policy_templates" (
|
||||
"id" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"category" "PolicyCategory" NOT NULL,
|
||||
"description" TEXT,
|
||||
"content" JSONB NOT NULL,
|
||||
"version" TEXT NOT NULL DEFAULT '1.0.0',
|
||||
"isActive" BOOLEAN NOT NULL DEFAULT true,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "policy_templates_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "risk_scores" (
|
||||
"id" TEXT NOT NULL,
|
||||
"userId" TEXT NOT NULL,
|
||||
"score" INTEGER NOT NULL,
|
||||
"level" "RiskLevel" NOT NULL,
|
||||
"factors" JSONB NOT NULL,
|
||||
"calculatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "risk_scores_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "users_email_key" ON "users"("email");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "users_username_key" ON "users"("username");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "users_email_idx" ON "users"("email");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "users_username_idx" ON "users"("username");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "users_createdAt_idx" ON "users"("createdAt");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "users_organizationId_idx" ON "users"("organizationId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "roles_name_key" ON "roles"("name");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "permissions_resource_idx" ON "permissions"("resource");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "permissions_resource_action_idx" ON "permissions"("resource", "action");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "permissions_resource_action_scope_key" ON "permissions"("resource", "action", "scope");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "user_roles_userId_idx" ON "user_roles"("userId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "user_roles_roleId_idx" ON "user_roles"("roleId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "user_roles_expiresAt_idx" ON "user_roles"("expiresAt");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "user_roles_userId_roleId_key" ON "user_roles"("userId", "roleId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "role_permissions_roleId_idx" ON "role_permissions"("roleId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "role_permissions_roleId_permissionId_key" ON "role_permissions"("roleId", "permissionId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "user_permissions_userId_idx" ON "user_permissions"("userId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "user_permissions_expiresAt_idx" ON "user_permissions"("expiresAt");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "user_permissions_userId_permissionId_key" ON "user_permissions"("userId", "permissionId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "sessions_userId_idx" ON "sessions"("userId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "sessions_deviceId_idx" ON "sessions"("deviceId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "sessions_expiresAt_idx" ON "sessions"("expiresAt");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "sessions_isActive_idx" ON "sessions"("isActive");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "refresh_tokens_token_key" ON "refresh_tokens"("token");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "refresh_tokens_userId_idx" ON "refresh_tokens"("userId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "refresh_tokens_token_idx" ON "refresh_tokens"("token");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "refresh_tokens_family_idx" ON "refresh_tokens"("family");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "refresh_tokens_expiresAt_idx" ON "refresh_tokens"("expiresAt");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "social_accounts_userId_idx" ON "social_accounts"("userId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "social_accounts_provider_idx" ON "social_accounts"("provider");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "social_accounts_provider_providerId_key" ON "social_accounts"("provider", "providerId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "mfa_devices_userId_idx" ON "mfa_devices"("userId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "mfa_devices_credentialId_idx" ON "mfa_devices"("credentialId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "auth_events_userId_timestamp_idx" ON "auth_events"("userId", "timestamp");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "auth_events_eventType_timestamp_idx" ON "auth_events"("eventType", "timestamp");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "auth_events_timestamp_idx" ON "auth_events"("timestamp");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "features_name_key" ON "features"("name");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "features_name_idx" ON "features"("name");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "features_enabled_idx" ON "features"("enabled");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "policies_name_key" ON "policies"("name");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "policies_resource_idx" ON "policies"("resource");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "policies_isActive_priority_idx" ON "policies"("isActive", "priority");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "policies_organizationId_idx" ON "policies"("organizationId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "organizations_domain_key" ON "organizations"("domain");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "organizations_domain_idx" ON "organizations"("domain");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "groups_organizationId_idx" ON "groups"("organizationId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "groups_organizationId_name_key" ON "groups"("organizationId", "name");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "group_members_userId_idx" ON "group_members"("userId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "group_members_groupId_idx" ON "group_members"("groupId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "group_members_userId_groupId_key" ON "group_members"("userId", "groupId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "group_permissions_groupId_idx" ON "group_permissions"("groupId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "group_permissions_groupId_permissionId_key" ON "group_permissions"("groupId", "permissionId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "user_profiles_userId_key" ON "user_profiles"("userId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "user_profiles_phone_idx" ON "user_profiles"("phone");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "identity_verifications_token_key" ON "identity_verifications"("token");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "identity_verifications_userId_type_idx" ON "identity_verifications"("userId", "type");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "identity_verifications_token_idx" ON "identity_verifications"("token");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "identity_verifications_status_idx" ON "identity_verifications"("status");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "access_requests_userId_idx" ON "access_requests"("userId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "access_requests_status_idx" ON "access_requests"("status");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "access_requests_resource_idx" ON "access_requests"("resource");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "access_request_approvers_approverId_idx" ON "access_request_approvers"("approverId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "access_request_approvers_requestId_approverId_key" ON "access_request_approvers"("requestId", "approverId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "access_reviews_status_idx" ON "access_reviews"("status");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "access_reviews_type_idx" ON "access_reviews"("type");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "access_review_items_reviewId_idx" ON "access_review_items"("reviewId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "access_review_items_userId_idx" ON "access_review_items"("userId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "access_review_items_status_idx" ON "access_review_items"("status");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "compliance_reports_type_idx" ON "compliance_reports"("type");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "compliance_reports_status_idx" ON "compliance_reports"("status");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "policy_templates_category_idx" ON "policy_templates"("category");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "policy_templates_isActive_idx" ON "policy_templates"("isActive");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "risk_scores_userId_idx" ON "risk_scores"("userId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "risk_scores_score_idx" ON "risk_scores"("score");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "risk_scores_level_idx" ON "risk_scores"("level");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "users" ADD CONSTRAINT "users_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "organizations"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "user_roles" ADD CONSTRAINT "user_roles_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "user_roles" ADD CONSTRAINT "user_roles_roleId_fkey" FOREIGN KEY ("roleId") REFERENCES "roles"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "role_permissions" ADD CONSTRAINT "role_permissions_roleId_fkey" FOREIGN KEY ("roleId") REFERENCES "roles"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "role_permissions" ADD CONSTRAINT "role_permissions_permissionId_fkey" FOREIGN KEY ("permissionId") REFERENCES "permissions"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "user_permissions" ADD CONSTRAINT "user_permissions_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "user_permissions" ADD CONSTRAINT "user_permissions_permissionId_fkey" FOREIGN KEY ("permissionId") REFERENCES "permissions"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "sessions" ADD CONSTRAINT "sessions_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "refresh_tokens" ADD CONSTRAINT "refresh_tokens_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "social_accounts" ADD CONSTRAINT "social_accounts_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "mfa_devices" ADD CONSTRAINT "mfa_devices_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "auth_events" ADD CONSTRAINT "auth_events_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "policies" ADD CONSTRAINT "policies_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "organizations"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "organizations" ADD CONSTRAINT "organizations_parentId_fkey" FOREIGN KEY ("parentId") REFERENCES "organizations"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "groups" ADD CONSTRAINT "groups_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "organizations"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "group_members" ADD CONSTRAINT "group_members_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "group_members" ADD CONSTRAINT "group_members_groupId_fkey" FOREIGN KEY ("groupId") REFERENCES "groups"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "group_permissions" ADD CONSTRAINT "group_permissions_groupId_fkey" FOREIGN KEY ("groupId") REFERENCES "groups"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "group_permissions" ADD CONSTRAINT "group_permissions_permissionId_fkey" FOREIGN KEY ("permissionId") REFERENCES "permissions"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "user_profiles" ADD CONSTRAINT "user_profiles_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "identity_verifications" ADD CONSTRAINT "identity_verifications_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "access_requests" ADD CONSTRAINT "access_requests_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "access_request_approvers" ADD CONSTRAINT "access_request_approvers_requestId_fkey" FOREIGN KEY ("requestId") REFERENCES "access_requests"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "access_review_items" ADD CONSTRAINT "access_review_items_reviewId_fkey" FOREIGN KEY ("reviewId") REFERENCES "access_reviews"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "risk_scores" ADD CONSTRAINT "risk_scores_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
@@ -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"
|
||||
@@ -43,12 +43,14 @@ export TEST_DATABASE_URL="postgresql://test:test@localhost:5433/test_iam_db"
|
||||
export TEST_REDIS_URL="redis://localhost:6380"
|
||||
export NODE_ENV="test"
|
||||
export LOG_LEVEL="error" # Reduce log noise during tests
|
||||
# Export DATABASE_URL for Prisma
|
||||
export DATABASE_URL=$TEST_DATABASE_URL
|
||||
|
||||
print_status "Setting up test infrastructure..."
|
||||
|
||||
# Start test database containers
|
||||
print_status "Starting PostgreSQL and Redis test containers..."
|
||||
docker-compose -f docker-compose.test.yml up -d
|
||||
docker compose -f docker-compose.test.yml up -d
|
||||
|
||||
# Wait for databases to be ready
|
||||
print_status "Waiting for PostgreSQL to be ready..."
|
||||
@@ -57,7 +59,7 @@ counter=0
|
||||
while ! docker exec postgres-test-iam pg_isready -U test -d test_iam_db >/dev/null 2>&1; do
|
||||
if [ $counter -gt $timeout ]; then
|
||||
print_error "PostgreSQL failed to start within ${timeout} seconds"
|
||||
docker-compose -f docker-compose.test.yml logs postgres-test
|
||||
docker compose -f docker-compose.test.yml logs postgres-test
|
||||
exit 1
|
||||
fi
|
||||
counter=$((counter + 1))
|
||||
@@ -69,7 +71,7 @@ counter=0
|
||||
while ! docker exec redis-test-iam redis-cli ping >/dev/null 2>&1; do
|
||||
if [ $counter -gt $timeout ]; then
|
||||
print_error "Redis failed to start within ${timeout} seconds"
|
||||
docker-compose -f docker-compose.test.yml logs redis-test
|
||||
docker compose -f docker-compose.test.yml logs redis-test
|
||||
exit 1
|
||||
fi
|
||||
counter=$((counter + 1))
|
||||
@@ -80,21 +82,26 @@ print_success "Test infrastructure is ready!"
|
||||
|
||||
# Run database migrations
|
||||
print_status "Running database migrations..."
|
||||
cd "$(dirname "$0")/../.." # Go to iam-service directory
|
||||
if ! pnpm prisma migrate deploy --schema=prisma/schema.prisma; then
|
||||
# Assuming script is run from services/iam-service root if invoked via pnpm
|
||||
# If invoked directly, we might need adjustment. But existing script does cd relative to script path.
|
||||
# Let's keep existing cd logic but verify path.
|
||||
SERVICE_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
cd "$SERVICE_ROOT"
|
||||
|
||||
if ! npx prisma migrate deploy --schema=prisma/schema.prisma; then
|
||||
print_error "Database migration failed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Seed test data (optional - comment out if not needed)
|
||||
print_status "Seeding test data..."
|
||||
if ! pnpm prisma db seed; then
|
||||
print_warning "Test data seeding failed, but continuing..."
|
||||
fi
|
||||
# print_status "Seeding test data..."
|
||||
# if ! pnpm prisma db seed; then
|
||||
# print_warning "Test data seeding failed, but continuing..."
|
||||
# fi
|
||||
|
||||
# Run integration tests
|
||||
print_status "Running integration tests..."
|
||||
if npm test -- --testPathPattern="integration" --testTimeout=60000 --verbose; then
|
||||
if npx jest -c jest.integration.config.ts --runInBand --verbose; then
|
||||
print_success "Integration tests passed!"
|
||||
TEST_RESULT=0
|
||||
else
|
||||
@@ -104,7 +111,7 @@ fi
|
||||
|
||||
# Cleanup
|
||||
print_status "Cleaning up test infrastructure..."
|
||||
docker-compose -f docker-compose.test.yml down -v
|
||||
docker compose -f docker-compose.test.yml down -v
|
||||
|
||||
if [ $TEST_RESULT -eq 0 ]; then
|
||||
print_success "All integration tests completed successfully! 🎉"
|
||||
|
||||
86
services/iam-service/src/__tests__/factories.ts
Normal file
86
services/iam-service/src/__tests__/factories.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import { User, Organization, AccessRequest, Role, Permission } from '@prisma/client';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
const randomString = (prefix: string = 'text') => `${prefix}-${Math.random().toString(36).substring(7)}`;
|
||||
const randomEmail = () => `${randomString('user')}@example.com`;
|
||||
const randomDate = () => new Date();
|
||||
|
||||
export class TestFactory {
|
||||
static createUser(overrides: Partial<User> = {}): User {
|
||||
return {
|
||||
id: overrides.id || uuidv4(),
|
||||
email: overrides.email || randomEmail(),
|
||||
username: overrides.username || randomString('user'),
|
||||
passwordHash: overrides.passwordHash || 'hashed-password',
|
||||
isActive: overrides.isActive ?? true,
|
||||
emailVerified: overrides.emailVerified ?? false,
|
||||
mfaEnabled: overrides.mfaEnabled ?? false,
|
||||
mfaSecret: overrides.mfaSecret || null,
|
||||
mfaBackupCodes: overrides.mfaBackupCodes || null,
|
||||
lastLoginAt: overrides.lastLoginAt || null,
|
||||
loginCount: overrides.loginCount ?? 0,
|
||||
failedLoginAttempts: overrides.failedLoginAttempts ?? 0,
|
||||
lockedUntil: overrides.lockedUntil || null,
|
||||
organizationId: overrides.organizationId || null,
|
||||
createdAt: overrides.createdAt || randomDate(),
|
||||
updatedAt: overrides.updatedAt || randomDate(),
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
static createOrganization(overrides: Partial<Organization> = {}): Organization {
|
||||
return {
|
||||
id: overrides.id || uuidv4(),
|
||||
name: overrides.name || randomString('org'),
|
||||
domain: overrides.domain || `${randomString('org')}.com`,
|
||||
parentId: overrides.parentId || null,
|
||||
settings: overrides.settings || {},
|
||||
isActive: overrides.isActive ?? true,
|
||||
createdAt: overrides.createdAt || randomDate(),
|
||||
updatedAt: overrides.updatedAt || randomDate(),
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
static createRole(overrides: Partial<Role> = {}): Role {
|
||||
return {
|
||||
id: overrides.id || uuidv4(),
|
||||
name: overrides.name || randomString('ROLE').toUpperCase(),
|
||||
displayName: overrides.displayName || randomString('Role Name'),
|
||||
description: overrides.description || randomString('Description'),
|
||||
isSystem: overrides.isSystem ?? false,
|
||||
createdAt: overrides.createdAt || randomDate(),
|
||||
updatedAt: overrides.updatedAt || randomDate(),
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
static createPermission(overrides: Partial<Permission> = {}): Permission {
|
||||
return {
|
||||
id: overrides.id || uuidv4(),
|
||||
resource: overrides.resource || 'users',
|
||||
action: overrides.action || 'create',
|
||||
scope: overrides.scope || 'all',
|
||||
description: overrides.description || randomString('desc'),
|
||||
createdAt: overrides.createdAt || randomDate(),
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
static createAccessRequest(overrides: Partial<AccessRequest> = {}): AccessRequest {
|
||||
return {
|
||||
id: overrides.id || uuidv4(),
|
||||
userId: overrides.userId || uuidv4(),
|
||||
resource: overrides.resource || 'resource',
|
||||
action: overrides.action || 'read',
|
||||
reason: overrides.reason || randomString('reason'),
|
||||
status: overrides.status || 'PENDING',
|
||||
requestedAt: overrides.requestedAt || randomDate(),
|
||||
reviewedAt: overrides.reviewedAt || null,
|
||||
reviewedBy: overrides.reviewedBy || null,
|
||||
expiresAt: overrides.expiresAt || null,
|
||||
metadata: overrides.metadata || {},
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
}
|
||||
12
services/iam-service/src/__tests__/setupIntegrationTests.ts
Normal file
12
services/iam-service/src/__tests__/setupIntegrationTests.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
// EN: Setup file for integration tests
|
||||
// VI: File setup cho integration tests
|
||||
|
||||
import { config } from 'dotenv';
|
||||
|
||||
// Load env vars
|
||||
config();
|
||||
|
||||
// Increase timeout for integration tests
|
||||
jest.setTimeout(30000);
|
||||
|
||||
// We do NOT mock Prisma here. We want real DB connection.
|
||||
@@ -39,63 +39,56 @@ jest.mock('@goodgo/tracing', () => ({
|
||||
|
||||
// EN: Mock database client to avoid real DB connections in unit tests
|
||||
// VI: Mock database client để tránh kết nối DB thật trong unit tests
|
||||
// EN: Create a reusable mock for Prisma models
|
||||
const mockModel = () => ({
|
||||
create: jest.fn(),
|
||||
findMany: jest.fn(),
|
||||
findUnique: jest.fn(),
|
||||
findFirst: jest.fn(),
|
||||
update: jest.fn(),
|
||||
delete: jest.fn(),
|
||||
deleteMany: jest.fn(),
|
||||
count: jest.fn(),
|
||||
updateMany: jest.fn(),
|
||||
});
|
||||
|
||||
const mockPrismaClient = {
|
||||
$queryRaw: jest.fn(),
|
||||
$disconnect: jest.fn(),
|
||||
$connect: jest.fn(),
|
||||
$transaction: jest.fn((args: any) => (Array.isArray(args) ? Promise.all(args) : args(mockPrismaClient))),
|
||||
user: mockModel(),
|
||||
role: mockModel(),
|
||||
permission: mockModel(),
|
||||
feature: mockModel(),
|
||||
session: mockModel(),
|
||||
refreshToken: mockModel(),
|
||||
authEvent: mockModel(),
|
||||
organization: mockModel(),
|
||||
group: mockModel(),
|
||||
groupMember: mockModel(),
|
||||
groupPermission: mockModel(),
|
||||
userProfile: mockModel(),
|
||||
identityVerification: mockModel(),
|
||||
accessRequest: mockModel(),
|
||||
accessRequestApprover: mockModel(),
|
||||
accessReview: mockModel(),
|
||||
accessReviewItem: mockModel(),
|
||||
complianceReport: mockModel(),
|
||||
policyTemplate: mockModel(),
|
||||
policy: mockModel(),
|
||||
riskScore: mockModel(),
|
||||
socialAccount: mockModel(),
|
||||
mfaDevice: mockModel(),
|
||||
userRole: mockModel(),
|
||||
userPermission: mockModel(),
|
||||
rolePermission: mockModel(),
|
||||
};
|
||||
|
||||
jest.mock('../config/database.config', () => ({
|
||||
connectDatabase: jest.fn(),
|
||||
prisma: {
|
||||
$queryRaw: jest.fn(),
|
||||
$disconnect: jest.fn(),
|
||||
$connect: jest.fn(),
|
||||
user: {
|
||||
create: jest.fn(),
|
||||
findMany: jest.fn(),
|
||||
findUnique: jest.fn(),
|
||||
update: jest.fn(),
|
||||
delete: jest.fn(),
|
||||
},
|
||||
role: {
|
||||
create: jest.fn(),
|
||||
findMany: jest.fn(),
|
||||
findUnique: jest.fn(),
|
||||
update: jest.fn(),
|
||||
delete: jest.fn(),
|
||||
},
|
||||
permission: {
|
||||
create: jest.fn(),
|
||||
findMany: jest.fn(),
|
||||
findUnique: jest.fn(),
|
||||
update: jest.fn(),
|
||||
delete: jest.fn(),
|
||||
},
|
||||
feature: {
|
||||
create: jest.fn(),
|
||||
findMany: jest.fn(),
|
||||
findUnique: jest.fn(),
|
||||
update: jest.fn(),
|
||||
delete: jest.fn(),
|
||||
count: jest.fn(),
|
||||
},
|
||||
session: {
|
||||
create: jest.fn(),
|
||||
findMany: jest.fn(),
|
||||
findUnique: jest.fn(),
|
||||
update: jest.fn(),
|
||||
delete: jest.fn(),
|
||||
},
|
||||
refreshToken: {
|
||||
create: jest.fn(),
|
||||
findMany: jest.fn(),
|
||||
findUnique: jest.fn(),
|
||||
update: jest.fn(),
|
||||
delete: jest.fn(),
|
||||
},
|
||||
authEvent: {
|
||||
create: jest.fn(),
|
||||
findMany: jest.fn(),
|
||||
findUnique: jest.fn(),
|
||||
update: jest.fn(),
|
||||
delete: jest.fn(),
|
||||
},
|
||||
},
|
||||
getPrismaClient: jest.fn(() => mockPrismaClient),
|
||||
prisma: mockPrismaClient,
|
||||
}));
|
||||
|
||||
// EN: Set up default mock implementations
|
||||
|
||||
103
services/iam-service/src/app.ts
Normal file
103
services/iam-service/src/app.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
import { logger } from '@goodgo/logger';
|
||||
import { initTracing } from '@goodgo/tracing';
|
||||
import cookieParser from 'cookie-parser';
|
||||
import cors from 'cors';
|
||||
import express, { Express } from 'express';
|
||||
import { rateLimit } from 'express-rate-limit';
|
||||
import helmet from 'helmet';
|
||||
|
||||
import { appConfig } from './config/app.config';
|
||||
import { setupSwagger } from './docs/swagger';
|
||||
import { correlationMiddleware } from './middlewares/correlation.middleware';
|
||||
import { errorHandler, notFoundHandler } from './middlewares/error.middleware';
|
||||
import { requestLogger } from './middlewares/logger.middleware';
|
||||
import { metricsMiddleware } from './middlewares/metrics.middleware';
|
||||
import { createRouter } from './routes';
|
||||
|
||||
// EN: Initialize tracing
|
||||
// VI: Khởi tạo tracing
|
||||
if (process.env.TRACING_ENABLED === 'true') {
|
||||
initTracing({
|
||||
serviceName: process.env.SERVICE_NAME || 'microservice',
|
||||
jaegerEndpoint: process.env.JAEGER_ENDPOINT,
|
||||
enabled: true,
|
||||
});
|
||||
}
|
||||
|
||||
const app: Express = express();
|
||||
|
||||
// EN: Security middleware
|
||||
// VI: Middleware bảo mật
|
||||
app.use(helmet());
|
||||
app.use(
|
||||
cors({
|
||||
origin: appConfig.corsOrigin,
|
||||
credentials: true,
|
||||
})
|
||||
);
|
||||
|
||||
// EN: Rate limiting
|
||||
// VI: Giới hạn số lượng request
|
||||
const limiter = rateLimit({
|
||||
windowMs: 15 * 60 * 1000,
|
||||
max: 100,
|
||||
});
|
||||
app.use('/api', limiter);
|
||||
|
||||
// EN: Correlation ID middleware (must be early)
|
||||
// VI: Correlation ID middleware (phải đặt sớm)
|
||||
app.use(correlationMiddleware());
|
||||
|
||||
// EN: Body parsing
|
||||
// VI: Phân tích body request
|
||||
app.use(express.json());
|
||||
app.use(express.urlencoded({ extended: true }));
|
||||
app.use(cookieParser());
|
||||
|
||||
// EN: Request logging
|
||||
// VI: Ghi log request
|
||||
app.use(requestLogger);
|
||||
|
||||
// EN: Metrics
|
||||
// VI: Metrics
|
||||
app.use(metricsMiddleware);
|
||||
|
||||
// EN: Routes with async error handling
|
||||
// VI: Routes với async error handling
|
||||
app.use(createRouter());
|
||||
// In main.ts it was `app.use(createRouter())`. Let's check routes/index.ts to see if it adds prefix.
|
||||
// The integration test used `createRouter` directly in main.
|
||||
// Let's keep it as `app.use(createRouter())` unless I know better.
|
||||
// Actually `routes/index.ts` creates a router.
|
||||
// Looking at previous `main.ts`: `app.use(createRouter())`
|
||||
// I'll stick to that. Or check auth.routes.ts.
|
||||
// auth.routes.ts says: `const router = express.Router(); router.post('/register'...)`
|
||||
// So mounting at root means `/auth/register`? Or `/api/v1/auth`?
|
||||
// I need to check `routes/index.ts`.
|
||||
|
||||
// Checking routes/index.ts content (from memory/context):
|
||||
// It refactored to import feature modules.
|
||||
// Usually: router.use('/auth', authRoutes).
|
||||
// If createsRouter returns a router with paths, then `app.use(createRouter())` mounts them at `/`.
|
||||
// Let's assume `createRouter()` returns the main router.
|
||||
|
||||
// RE-READING main.ts view:
|
||||
// app.use(createRouter());
|
||||
|
||||
// BUT usually API is at `/api/v1/...`
|
||||
// The `routes/index.ts` might handle `/api/v1` prefix or `main.ts` should.
|
||||
// Let's keep `app.use(createRouter())` for now to match main.ts. Use exact logic.
|
||||
|
||||
// Wait, I am overwriting app.ts.
|
||||
// I will verify `routes/index.ts` content later if tests fail 404.
|
||||
|
||||
// EN: Setup Swagger documentation
|
||||
// VI: Thiết lập tài liệu Swagger
|
||||
setupSwagger(app, '/api-docs');
|
||||
|
||||
// EN: Error handling
|
||||
// VI: Xử lý lỗi
|
||||
app.use(notFoundHandler);
|
||||
app.use(errorHandler);
|
||||
|
||||
export { app };
|
||||
@@ -1,85 +1,7 @@
|
||||
import { logger } from '@goodgo/logger';
|
||||
import { initTracing } from '@goodgo/tracing';
|
||||
import cookieParser from 'cookie-parser';
|
||||
import cors from 'cors';
|
||||
import express from 'express';
|
||||
import { rateLimit } from 'express-rate-limit';
|
||||
import helmet from 'helmet';
|
||||
import { RedisStore } from 'rate-limit-redis';
|
||||
|
||||
import { prisma, connectDatabase } from './config/database.config';
|
||||
import { appConfig } from './config/app.config';
|
||||
import { connectDatabase, prisma } from './config/database.config';
|
||||
import { getRedisClient } from './config/redis.config';
|
||||
import { setupSwagger } from './docs/swagger';
|
||||
import { correlationMiddleware } from './middlewares/correlation.middleware';
|
||||
import { errorHandler, notFoundHandler } from './middlewares/error.middleware';
|
||||
import { requestLogger } from './middlewares/logger.middleware';
|
||||
import { metricsMiddleware } from './middlewares/metrics.middleware';
|
||||
import { createRouter } from './routes';
|
||||
|
||||
// EN: Initialize tracing
|
||||
// VI: Khởi tạo tracing
|
||||
if (process.env.TRACING_ENABLED === 'true') {
|
||||
initTracing({
|
||||
serviceName: process.env.SERVICE_NAME || 'microservice',
|
||||
jaegerEndpoint: process.env.JAEGER_ENDPOINT,
|
||||
enabled: true,
|
||||
});
|
||||
}
|
||||
|
||||
const app = express();
|
||||
|
||||
// EN: Security middleware
|
||||
// VI: Middleware bảo mật
|
||||
app.use(helmet());
|
||||
app.use(
|
||||
cors({
|
||||
origin: appConfig.corsOrigin,
|
||||
credentials: true,
|
||||
})
|
||||
);
|
||||
|
||||
// EN: Rate limiting (use memory store initially, Redis will be used after connection)
|
||||
// VI: Giới hạn số lượng request (dùng memory store ban đầu, Redis sẽ được dùng sau khi kết nối)
|
||||
const limiter = rateLimit({
|
||||
windowMs: 15 * 60 * 1000,
|
||||
max: 100,
|
||||
// EN: Use memory store by default, will upgrade to Redis when available
|
||||
// VI: Dùng memory store mặc định, sẽ nâng cấp lên Redis khi có sẵn
|
||||
});
|
||||
app.use('/api', limiter);
|
||||
|
||||
// EN: Correlation ID middleware (must be early)
|
||||
// VI: Correlation ID middleware (phải đặt sớm)
|
||||
app.use(correlationMiddleware());
|
||||
|
||||
// EN: Body parsing
|
||||
// VI: Phân tích body request
|
||||
app.use(express.json());
|
||||
app.use(express.urlencoded({ extended: true }));
|
||||
app.use(cookieParser());
|
||||
|
||||
// EN: Request logging
|
||||
// VI: Ghi log request
|
||||
app.use(requestLogger);
|
||||
|
||||
// EN: Metrics
|
||||
// VI: Metrics
|
||||
app.use(metricsMiddleware);
|
||||
|
||||
|
||||
// EN: Routes with async error handling
|
||||
// VI: Routes với async error handling
|
||||
app.use(createRouter());
|
||||
|
||||
// EN: Setup Swagger documentation
|
||||
// VI: Thiết lập tài liệu Swagger
|
||||
setupSwagger(app, '/api-docs');
|
||||
|
||||
// EN: Error handling
|
||||
// VI: Xử lý lỗi
|
||||
app.use(notFoundHandler);
|
||||
app.use(errorHandler);
|
||||
import { app } from './app';
|
||||
|
||||
const startServer = async () => {
|
||||
try {
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
import request from 'supertest';
|
||||
import { app } from '../../../app';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
describe('Auth API Integration Tests', () => {
|
||||
beforeAll(async () => {
|
||||
// Clean DB
|
||||
await prisma.$transaction([
|
||||
prisma.refreshToken.deleteMany(),
|
||||
prisma.user.deleteMany(),
|
||||
]);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await prisma.$disconnect();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await prisma.$transaction([
|
||||
prisma.refreshToken.deleteMany(),
|
||||
prisma.user.deleteMany(),
|
||||
]);
|
||||
});
|
||||
|
||||
describe('POST /auth/register', () => {
|
||||
it('should register a user with strong password', async () => {
|
||||
const res = await request(app)
|
||||
.post('/auth/register')
|
||||
.send({
|
||||
email: 'test@example.com',
|
||||
password: 'StrongPassword123!',
|
||||
username: 'testuser'
|
||||
});
|
||||
|
||||
expect(res.status).toBe(201);
|
||||
expect(res.body.success).toBe(true);
|
||||
expect(res.body.data.user.email).toBe('test@example.com');
|
||||
});
|
||||
|
||||
it('should fail with weak password (Zod validation)', async () => {
|
||||
const res = await request(app)
|
||||
.post('/auth/register')
|
||||
.send({
|
||||
email: 'weak@example.com',
|
||||
password: 'weak',
|
||||
username: 'weakuser'
|
||||
});
|
||||
|
||||
expect(res.status).toBe(400);
|
||||
expect(res.body.success).toBe(false);
|
||||
expect(res.body.error.code).toBe('VALIDATION_ERROR');
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /auth/login', () => {
|
||||
it('should login successfully', async () => {
|
||||
// Create user first
|
||||
await request(app)
|
||||
.post('/auth/register')
|
||||
.send({
|
||||
email: 'login@example.com',
|
||||
password: 'StrongPassword123!',
|
||||
username: 'loginuser'
|
||||
});
|
||||
|
||||
const res = await request(app)
|
||||
.post('/auth/login')
|
||||
.send({
|
||||
email: 'login@example.com',
|
||||
password: 'StrongPassword123!'
|
||||
});
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.body.success).toBe(true);
|
||||
expect(res.body.data.tokens).toBeDefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,117 @@
|
||||
import { AuthController } from '../auth.controller';
|
||||
import { authService } from '../auth.service';
|
||||
import { TestFactory } from '../../../__tests__/factories';
|
||||
|
||||
// Mock dependencies
|
||||
jest.mock('../auth.service');
|
||||
|
||||
describe('AuthController', () => {
|
||||
let authController: AuthController;
|
||||
let mockReq: any;
|
||||
let mockRes: any;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
authController = new AuthController();
|
||||
|
||||
mockReq = {
|
||||
body: {},
|
||||
ip: '127.0.0.1',
|
||||
headers: {},
|
||||
};
|
||||
|
||||
mockRes = {
|
||||
status: jest.fn().mockReturnThis(),
|
||||
json: jest.fn().mockReturnThis(),
|
||||
cookie: jest.fn().mockReturnThis(),
|
||||
};
|
||||
});
|
||||
|
||||
describe('register', () => {
|
||||
it('should register user and return 201', async () => {
|
||||
mockReq.body = { email: 'test@example.com', password: 'Password123!' };
|
||||
const mockResult = {
|
||||
user: TestFactory.createUser(),
|
||||
tokens: { accessToken: 'access', refreshToken: 'refresh' },
|
||||
};
|
||||
|
||||
(authService.register as jest.Mock).mockResolvedValue(mockResult);
|
||||
|
||||
await authController.register(mockReq, mockRes);
|
||||
|
||||
expect(authService.register).toHaveBeenCalledWith(expect.anything(), mockReq);
|
||||
expect(mockRes.status).toHaveBeenCalledWith(201);
|
||||
expect(mockRes.json).toHaveBeenCalledWith({
|
||||
success: true,
|
||||
data: expect.anything(),
|
||||
});
|
||||
});
|
||||
|
||||
it('should return 400 if registration fails', async () => {
|
||||
// Pass valid body to pass Zod, but fail later
|
||||
mockReq.body = { email: 'test@example.com', password: 'Password123!' };
|
||||
const error = new Error('Registration failed');
|
||||
(authService.register as jest.Mock).mockRejectedValue(error);
|
||||
|
||||
await authController.register(mockReq, mockRes);
|
||||
|
||||
expect(mockRes.status).toHaveBeenCalledWith(400);
|
||||
expect(mockRes.json).toHaveBeenCalledWith(expect.objectContaining({
|
||||
success: false,
|
||||
error: expect.objectContaining({ message: 'Registration failed' })
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
describe('login', () => {
|
||||
it('should login user and return 200', async () => {
|
||||
mockReq.body = { email: 'test@example.com', password: 'Password123!' };
|
||||
const mockResult = {
|
||||
user: TestFactory.createUser(),
|
||||
tokens: { accessToken: 'access', refreshToken: 'refresh' },
|
||||
};
|
||||
|
||||
(authService.login as jest.Mock).mockResolvedValue(mockResult);
|
||||
|
||||
await authController.login(mockReq, mockRes);
|
||||
|
||||
expect(authService.login).toHaveBeenCalledWith(expect.anything(), mockReq);
|
||||
expect(mockRes.status).toHaveBeenCalledWith(200);
|
||||
expect(mockRes.json).toHaveBeenCalledWith({
|
||||
success: true,
|
||||
data: expect.anything(),
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle MFA required response', async () => {
|
||||
mockReq.body = { email: 'test@example.com', password: 'Password123!' };
|
||||
const mockMfaResult = {
|
||||
user: { ...TestFactory.createUser(), mfaRequired: true },
|
||||
tokens: { accessToken: '', refreshToken: '' }, // Type requires tokens
|
||||
};
|
||||
(authService.login as jest.Mock).mockResolvedValue(mockMfaResult);
|
||||
|
||||
await authController.login(mockReq, mockRes);
|
||||
|
||||
expect(mockRes.status).toHaveBeenCalledWith(200);
|
||||
expect(mockRes.json).toHaveBeenCalledWith({
|
||||
success: true,
|
||||
data: expect.objectContaining({ mfaRequired: true }),
|
||||
});
|
||||
});
|
||||
|
||||
it('should return 401 if login fails', async () => {
|
||||
mockReq.body = { email: 'test@example.com', password: 'Password123!' };
|
||||
const error = new Error('Invalid credentials');
|
||||
(authService.login as jest.Mock).mockRejectedValue(error);
|
||||
|
||||
await authController.login(mockReq, mockRes);
|
||||
|
||||
expect(mockRes.status).toHaveBeenCalledWith(401);
|
||||
expect(mockRes.json).toHaveBeenCalledWith(expect.objectContaining({
|
||||
success: false,
|
||||
error: expect.objectContaining({ code: 'LOGIN_FAILED' })
|
||||
}));
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -252,57 +252,5 @@ describe('AuthService Integration Tests', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('Password Security', () => {
|
||||
it('should enforce password complexity during registration', async () => {
|
||||
const mockRequest = {
|
||||
ip: '127.0.0.1',
|
||||
headers: { 'user-agent': 'test-agent' },
|
||||
} as Partial<Request>;
|
||||
|
||||
// Test various weak passwords
|
||||
const weakPasswords = [
|
||||
'short', // Too short
|
||||
'nouppercase123!', // No uppercase
|
||||
'NOLOWERCASE123!', // No lowercase
|
||||
'NoNumbers!', // No numbers
|
||||
'NoSpecial123', // No special characters
|
||||
];
|
||||
|
||||
for (const weakPassword of weakPasswords) {
|
||||
const registerData = {
|
||||
email: `weak-pass-${Date.now()}-${Math.random()}@example.com`,
|
||||
password: weakPassword,
|
||||
username: `weak-user-${Date.now()}-${Math.random()}`,
|
||||
};
|
||||
|
||||
await expect(authService.register(registerData, mockRequest as Request))
|
||||
.rejects.toThrow(); // Should fail validation
|
||||
}
|
||||
});
|
||||
|
||||
it('should accept strong passwords', async () => {
|
||||
const mockRequest = {
|
||||
ip: '127.0.0.1',
|
||||
headers: { 'user-agent': 'test-agent' },
|
||||
} as Partial<Request>;
|
||||
|
||||
const strongPasswords = [
|
||||
'ValidPass123!',
|
||||
'Complex!Password#456',
|
||||
'Super@Secure$789',
|
||||
];
|
||||
|
||||
for (const strongPassword of strongPasswords) {
|
||||
const registerData = {
|
||||
email: `strong-pass-${Date.now()}-${Math.random()}@example.com`,
|
||||
password: strongPassword,
|
||||
username: `strong-user-${Date.now()}-${Math.random()}`,
|
||||
};
|
||||
|
||||
const result = await authService.register(registerData, mockRequest as Request);
|
||||
expect(result).toHaveProperty('user');
|
||||
expect(result).toHaveProperty('tokens');
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,83 +1,179 @@
|
||||
import crypto from 'crypto';
|
||||
import { AuthService } from '../auth.service';
|
||||
import { prisma } from '../../../config/database.config';
|
||||
import { rbacService } from '../../rbac/rbac.service';
|
||||
import { sessionService } from '../../session/session.service';
|
||||
import { jwtService } from '../../token/jwt.service';
|
||||
import { auditService } from '../../../core/events/audit.service';
|
||||
import { accountLockoutService } from '../account-lockout.service';
|
||||
import { TestFactory } from '../../../__tests__/factories';
|
||||
import bcrypt from 'bcryptjs';
|
||||
|
||||
import { AuthService } from '../auth.service';
|
||||
|
||||
// Mock only the database config to avoid complex dependencies
|
||||
jest.mock('../../../config/database.config');
|
||||
// Mock dependencies
|
||||
jest.mock('../../rbac/rbac.service');
|
||||
jest.mock('../../session/session.service');
|
||||
jest.mock('../../token/jwt.service');
|
||||
jest.mock('../../../core/events/audit.service');
|
||||
jest.mock('../account-lockout.service');
|
||||
jest.mock('bcryptjs');
|
||||
|
||||
describe('AuthService', () => {
|
||||
let authService: AuthService;
|
||||
let mockPrisma: any;
|
||||
// Create a mock request object that satisfies the interface and avoiding unsafe access
|
||||
const mockReq = {
|
||||
ip: '127.0.0.1',
|
||||
headers: { 'user-agent': 'Jest-Test' },
|
||||
socket: { remoteAddress: '127.0.0.1' },
|
||||
} as any;
|
||||
|
||||
beforeEach(() => {
|
||||
// Reset all mocks
|
||||
jest.clearAllMocks();
|
||||
|
||||
// Setup minimal Prisma mock
|
||||
mockPrisma = {
|
||||
user: {
|
||||
findUnique: jest.fn(),
|
||||
create: jest.fn(),
|
||||
update: jest.fn(),
|
||||
},
|
||||
refreshToken: {
|
||||
deleteMany: jest.fn(),
|
||||
},
|
||||
session: {
|
||||
deleteMany: jest.fn(),
|
||||
},
|
||||
};
|
||||
|
||||
// Mock the database client
|
||||
const { getPrismaClient } = require('../../../config/database.config');
|
||||
getPrismaClient.mockReturnValue(mockPrisma);
|
||||
|
||||
authService = new AuthService();
|
||||
});
|
||||
|
||||
describe('hashToken', () => {
|
||||
it('should hash refresh tokens using SHA-256', () => {
|
||||
// This is a private method, but we can test it by accessing it
|
||||
const token = 'refresh-token-123';
|
||||
const hashedToken = (authService as any).hashToken(token);
|
||||
describe('register', () => {
|
||||
it('should register a new user successfully', async () => {
|
||||
// Arrange
|
||||
const registerDto = {
|
||||
email: 'test@example.com',
|
||||
password: 'password123',
|
||||
username: 'testuser',
|
||||
};
|
||||
|
||||
expect(hashedToken).toBeDefined();
|
||||
expect(typeof hashedToken).toBe('string');
|
||||
expect(hashedToken.length).toBe(64); // SHA-256 produces 64 character hex string
|
||||
const mockUser = TestFactory.createUser({
|
||||
email: registerDto.email,
|
||||
username: registerDto.username
|
||||
});
|
||||
const mockTokens = { accessToken: 'access-token', refreshToken: 'refresh-token' };
|
||||
const mockDecodedRefresh = { tokenId: 'token-id', sub: mockUser.id };
|
||||
|
||||
// Verify it's actually SHA-256 of the input
|
||||
const expectedHash = crypto.createHash('sha256').update(token).digest('hex');
|
||||
expect(hashedToken).toBe(expectedHash);
|
||||
(prisma.user.findUnique as jest.Mock).mockResolvedValue(null);
|
||||
(bcrypt.hash as jest.Mock).mockResolvedValue('hashed-password' as never);
|
||||
(prisma.user.create as jest.Mock).mockResolvedValue(mockUser);
|
||||
(prisma.role.findFirst as jest.Mock).mockResolvedValue({ id: 'role-id' });
|
||||
(rbacService.assignRole as jest.Mock).mockResolvedValue(undefined);
|
||||
(rbacService.getUserRoles as jest.Mock).mockResolvedValue(['USER']);
|
||||
(rbacService.getUserPermissions as jest.Mock).mockResolvedValue([]);
|
||||
(jwtService.generateTokenSet as jest.Mock).mockResolvedValue(mockTokens);
|
||||
(jwtService.verifyRefreshToken as jest.Mock).mockReturnValue(mockDecodedRefresh);
|
||||
(prisma.refreshToken.create as jest.Mock).mockResolvedValue({} as any);
|
||||
(sessionService.createSession as jest.Mock).mockResolvedValue(undefined);
|
||||
|
||||
// Act
|
||||
const result = await authService.register(registerDto, mockReq);
|
||||
|
||||
// Assert
|
||||
expect(prisma.user.create).toHaveBeenCalledWith({
|
||||
data: expect.objectContaining({
|
||||
email: registerDto.email,
|
||||
username: registerDto.username,
|
||||
passwordHash: 'hashed-password',
|
||||
}),
|
||||
});
|
||||
expect(result).toHaveProperty('tokens', mockTokens);
|
||||
expect(result.user).toHaveProperty('email', registerDto.email);
|
||||
});
|
||||
|
||||
it('should produce consistent hashes for same input', () => {
|
||||
const token = 'test-token';
|
||||
const hash1 = (authService as any).hashToken(token);
|
||||
const hash2 = (authService as any).hashToken(token);
|
||||
it('should throw error if user already exists', async () => {
|
||||
const registerDto = { email: 'exists@example.com', password: '123' };
|
||||
(prisma.user.findUnique as jest.Mock).mockResolvedValue({ id: 'existing' });
|
||||
|
||||
expect(hash1).toBe(hash2);
|
||||
await expect(authService.register(registerDto, mockReq)).rejects.toThrow('User already exists');
|
||||
});
|
||||
});
|
||||
|
||||
describe('logout', () => {
|
||||
it('should logout user and invalidate sessions', async () => {
|
||||
// Arrange
|
||||
const userId = 'user-123';
|
||||
|
||||
mockPrisma.refreshToken.deleteMany.mockResolvedValue({ count: 1 });
|
||||
mockPrisma.session.deleteMany.mockResolvedValue({ count: 1 });
|
||||
|
||||
// Act
|
||||
await authService.logout(userId);
|
||||
|
||||
// Assert
|
||||
expect(mockPrisma.refreshToken.deleteMany).toHaveBeenCalledWith({
|
||||
where: { userId }
|
||||
});
|
||||
expect(mockPrisma.session.deleteMany).toHaveBeenCalledWith({
|
||||
where: { userId }
|
||||
describe('login', () => {
|
||||
it('should login successfully with valid credentials', async () => {
|
||||
const loginDto = { email: 'test@example.com', password: 'password123' };
|
||||
const mockUser = TestFactory.createUser({
|
||||
email: loginDto.email,
|
||||
passwordHash: 'hashed-password'
|
||||
});
|
||||
const mockTokens = { accessToken: 'access', refreshToken: 'refresh' };
|
||||
|
||||
(prisma.user.findUnique as jest.Mock).mockResolvedValue(mockUser);
|
||||
(accountLockoutService.isAccountLocked as jest.Mock).mockResolvedValue(false);
|
||||
(bcrypt.compare as jest.Mock).mockResolvedValue(true as never);
|
||||
(jwtService.generateTokenSet as jest.Mock).mockResolvedValue(mockTokens);
|
||||
(jwtService.verifyRefreshToken as jest.Mock).mockReturnValue({ tokenId: 'tid' });
|
||||
|
||||
const result = await authService.login(loginDto, mockReq);
|
||||
|
||||
expect(result).toEqual(expect.objectContaining({
|
||||
tokens: mockTokens,
|
||||
user: expect.objectContaining({ email: mockUser.email })
|
||||
}));
|
||||
expect(prisma.user.update).toHaveBeenCalled(); // Last login update
|
||||
});
|
||||
|
||||
it('should throw if user not found', async () => {
|
||||
(prisma.user.findUnique as jest.Mock).mockResolvedValue(null);
|
||||
await expect(authService.login({ email: '404', password: '123' }, mockReq))
|
||||
.rejects.toThrow('Invalid credentials');
|
||||
});
|
||||
|
||||
it('should throw if password invalid', async () => {
|
||||
const mockUser = TestFactory.createUser({ passwordHash: 'hash' });
|
||||
(prisma.user.findUnique as jest.Mock).mockResolvedValue(mockUser);
|
||||
(bcrypt.compare as jest.Mock).mockResolvedValue(false as never);
|
||||
|
||||
await expect(authService.login({ email: 'test', password: 'wrong' }, mockReq))
|
||||
.rejects.toThrow('Invalid credentials');
|
||||
expect(accountLockoutService.recordFailedAttempt).toHaveBeenCalledWith(mockUser.id);
|
||||
});
|
||||
|
||||
it('should throw if account is locked', async () => {
|
||||
const mockUser = TestFactory.createUser();
|
||||
(prisma.user.findUnique as jest.Mock).mockResolvedValue(mockUser);
|
||||
(accountLockoutService.isAccountLocked as jest.Mock).mockResolvedValue(true);
|
||||
(accountLockoutService.getLockoutStatus as jest.Mock).mockResolvedValue({ remainingMinutes: 15 });
|
||||
|
||||
await expect(authService.login({ email: 'locked', password: '123' }, mockReq))
|
||||
.rejects.toThrow(/Account is locked/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('refreshToken', () => {
|
||||
it('should refresh token successfully', async () => {
|
||||
const token = 'valid-refresh-token';
|
||||
const mockDecoded = { sub: 'user-id', tokenId: 'family-id' };
|
||||
const mockTokenRecord = {
|
||||
id: 'record-id',
|
||||
token: 'hashed-token',
|
||||
family: 'family-id',
|
||||
deviceId: 'device-id',
|
||||
user: TestFactory.createUser()
|
||||
};
|
||||
|
||||
(jwtService.verifyRefreshToken as jest.Mock).mockReturnValue(mockDecoded);
|
||||
(prisma.refreshToken.findFirst as jest.Mock).mockResolvedValue(mockTokenRecord);
|
||||
(jwtService.generateAccessToken as jest.Mock).mockReturnValue('new-access');
|
||||
(jwtService.generateRefreshToken as jest.Mock).mockReturnValue('new-refresh');
|
||||
// Mock transaction return
|
||||
(prisma.$transaction as jest.Mock).mockResolvedValue([{}, {}]);
|
||||
|
||||
const result = await authService.refreshToken(token, mockReq);
|
||||
|
||||
expect(result).toHaveProperty('accessToken', 'new-access');
|
||||
expect(result).toHaveProperty('refreshToken', 'new-refresh');
|
||||
});
|
||||
|
||||
it('should detect token reuse (family mismatch) and revoke chain', async () => {
|
||||
const token = 'stolen-token';
|
||||
const mockDecoded = { sub: 'user-id', tokenId: 'old-family' };
|
||||
const mockTokenRecord = {
|
||||
id: 'record-id',
|
||||
family: 'new-family' // Mismatch!
|
||||
};
|
||||
|
||||
(jwtService.verifyRefreshToken as jest.Mock).mockReturnValue(mockDecoded);
|
||||
(prisma.refreshToken.findFirst as jest.Mock).mockResolvedValue(mockTokenRecord);
|
||||
|
||||
await expect(authService.refreshToken(token, mockReq))
|
||||
.rejects.toThrow(/Token family mismatch/i);
|
||||
|
||||
expect(prisma.refreshToken.updateMany).toHaveBeenCalledWith(expect.objectContaining({
|
||||
where: { userId: 'user-id', family: 'new-family' }
|
||||
}));
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -156,7 +156,7 @@ export class AuthController {
|
||||
async refreshToken(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const refreshToken = req.body.refreshToken; // cookieService.getTokenFromCookie(req, 'refresh') || req.body.refreshToken;
|
||||
|
||||
|
||||
if (!refreshToken) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
@@ -189,6 +189,51 @@ export class AuthController {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* EN: Verify email endpoint
|
||||
* VI: Endpoint xác minh email
|
||||
*/
|
||||
async verifyEmail(req: Request, res: Response): Promise<void> {
|
||||
res.status(501).json({
|
||||
success: false,
|
||||
error: {
|
||||
code: 'NOT_IMPLEMENTED',
|
||||
message: 'Email verification is not yet implemented / Xác minh email chưa được triển khai'
|
||||
},
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* EN: Forgot password endpoint
|
||||
* VI: Endpoint quên mật khẩu
|
||||
*/
|
||||
async forgotPassword(req: Request, res: Response): Promise<void> {
|
||||
res.status(501).json({
|
||||
success: false,
|
||||
error: {
|
||||
code: 'NOT_IMPLEMENTED',
|
||||
message: 'Forgot password is not yet implemented / Quên mật khẩu chưa được triển khai'
|
||||
},
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* EN: Reset password endpoint
|
||||
* VI: Endpoint đặt lại mật khẩu
|
||||
*/
|
||||
async resetPassword(req: Request, res: Response): Promise<void> {
|
||||
res.status(501).json({
|
||||
success: false,
|
||||
error: {
|
||||
code: 'NOT_IMPLEMENTED',
|
||||
message: 'Reset password is not yet implemented / Đặt lại mật khẩu chưa được triển khai'
|
||||
},
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const authController = new AuthController();
|
||||
|
||||
@@ -238,6 +238,88 @@ export const createAuthRouter = (): Router => {
|
||||
});
|
||||
});
|
||||
|
||||
// =============================================================================
|
||||
// EN: Password Reset Endpoints
|
||||
// VI: Endpoints Đặt Lại Mật Khẩu
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/{version}/auth/verify-email:
|
||||
* post:
|
||||
* summary: Verify email address
|
||||
* description: Verify user email address with verification token
|
||||
* tags: [Authentication]
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* required: [token]
|
||||
* properties:
|
||||
* token:
|
||||
* type: string
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Email verified successfully
|
||||
* 400:
|
||||
* description: Invalid or expired token
|
||||
*/
|
||||
router.post('/verify-email', authController.verifyEmail.bind(authController));
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/{version}/auth/forgot-password:
|
||||
* post:
|
||||
* summary: Request password reset
|
||||
* description: Send password reset email to user
|
||||
* tags: [Authentication]
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* required: [email]
|
||||
* properties:
|
||||
* email:
|
||||
* type: string
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Reset email sent
|
||||
* 404:
|
||||
* description: User not found
|
||||
*/
|
||||
router.post('/forgot-password', authController.forgotPassword.bind(authController));
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/{version}/auth/reset-password:
|
||||
* post:
|
||||
* summary: Reset password
|
||||
* description: Reset user password with reset token
|
||||
* tags: [Authentication]
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* required: [token, password]
|
||||
* properties:
|
||||
* token:
|
||||
* type: string
|
||||
* password:
|
||||
* type: string
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Password reset successfully
|
||||
* 400:
|
||||
* description: Invalid or expired token
|
||||
*/
|
||||
router.post('/reset-password', authController.resetPassword.bind(authController));
|
||||
|
||||
// =============================================================================
|
||||
// EN: Social Authentication Endpoints
|
||||
// VI: Endpoints Xác Thực Mạng Xã Hội
|
||||
|
||||
@@ -28,7 +28,7 @@ export class RBACController {
|
||||
async getUserPermissions(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const userId = (req as any).user?.id || (req as any).user?.sub || req.params.userId;
|
||||
|
||||
|
||||
if (!userId) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
@@ -214,6 +214,35 @@ export class RBACController {
|
||||
});
|
||||
}
|
||||
}
|
||||
/**
|
||||
* EN: Get all roles
|
||||
* VI: Lấy tất cả roles
|
||||
*/
|
||||
async getRoles(req: Request, res: Response): Promise<void> {
|
||||
res.status(501).json({
|
||||
success: false,
|
||||
error: {
|
||||
code: 'NOT_IMPLEMENTED',
|
||||
message: 'Get roles is not yet implemented / Lấy roles chưa được triển khai'
|
||||
},
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* EN: Create new role
|
||||
* VI: Tạo role mới
|
||||
*/
|
||||
async createRole(req: Request, res: Response): Promise<void> {
|
||||
res.status(501).json({
|
||||
success: false,
|
||||
error: {
|
||||
code: 'NOT_IMPLEMENTED',
|
||||
message: 'Create role is not yet implemented / Tạo role chưa được triển khai'
|
||||
},
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const rbacController = new RBACController();
|
||||
|
||||
@@ -230,5 +230,12 @@ export const createRbacRouter = (): Router => {
|
||||
*/
|
||||
router.get('/permissions/check', authenticate(), rbacController.checkPermission.bind(rbacController));
|
||||
|
||||
|
||||
// EN: Role Management Routes
|
||||
// VI: Routes Quản Lý Role
|
||||
router.get("/roles", authenticate(), requirePermission("rbac", "read"), rbacController.getRoles.bind(rbacController));
|
||||
router.post("/roles", authenticate(), requirePermission("rbac", "create"), rbacController.createRole.bind(rbacController));
|
||||
router.post("/check", authenticate(), rbacController.checkPermission.bind(rbacController));
|
||||
|
||||
return router;
|
||||
};
|
||||
|
||||
234
services/iam-service/src/modules/rbac/rbac.routes.ts.bak
Normal file
234
services/iam-service/src/modules/rbac/rbac.routes.ts.bak
Normal file
@@ -0,0 +1,234 @@
|
||||
import { Router } from 'express';
|
||||
import { authenticate } from '../../middlewares/auth.middleware';
|
||||
import { requirePermission } from '../../middlewares/rbac.middleware';
|
||||
import { rbacController } from './rbac.controller';
|
||||
|
||||
/**
|
||||
* EN: Create and configure RBAC routes
|
||||
* VI: Tạo và cấu hình routes cho RBAC
|
||||
*/
|
||||
export const createRbacRouter = (): Router => {
|
||||
const router = Router();
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/{version}/rbac/permissions:
|
||||
* get:
|
||||
* summary: Get user permissions
|
||||
* description: Get list of permissions assigned to the current user
|
||||
* tags: [RBAC]
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: version
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* default: v1
|
||||
* description: API version
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Permissions retrieved successfully
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* allOf:
|
||||
* - $ref: '#/components/schemas/ApiResponse'
|
||||
* - type: object
|
||||
* properties:
|
||||
* data:
|
||||
* type: array
|
||||
* items:
|
||||
* type: string
|
||||
* 401:
|
||||
* description: Unauthorized
|
||||
*/
|
||||
router.get('/permissions', authenticate(), rbacController.getUserPermissions.bind(rbacController));
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/{version}/rbac/roles/assign:
|
||||
* post:
|
||||
* summary: Assign role to user
|
||||
* description: Assign a specific role to a user. Requires 'rbac:assign' permission.
|
||||
* tags: [RBAC]
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: version
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* default: v1
|
||||
* description: API version
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* required: [userId, roleId]
|
||||
* properties:
|
||||
* userId:
|
||||
* type: string
|
||||
* roleId:
|
||||
* type: string
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Role assigned successfully
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/ApiResponse'
|
||||
* 401:
|
||||
* description: Unauthorized
|
||||
* 403:
|
||||
* description: Forbidden - Insufficient permissions
|
||||
* 404:
|
||||
* description: User or Role not found
|
||||
*/
|
||||
router.post('/roles/assign', authenticate(), requirePermission('rbac', 'assign'), rbacController.assignRole.bind(rbacController));
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/{version}/rbac/roles/revoke:
|
||||
* post:
|
||||
* summary: Revoke role from user
|
||||
* description: Revoke a specific role from a user. Requires 'rbac:revoke' permission.
|
||||
* tags: [RBAC]
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: version
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* default: v1
|
||||
* description: API version
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* required: [userId, roleId]
|
||||
* properties:
|
||||
* userId:
|
||||
* type: string
|
||||
* roleId:
|
||||
* type: string
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Role revoked successfully
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/ApiResponse'
|
||||
* 401:
|
||||
* description: Unauthorized
|
||||
* 403:
|
||||
* description: Forbidden - Insufficient permissions
|
||||
* 404:
|
||||
* description: User or Role not found
|
||||
*/
|
||||
router.post('/roles/revoke', authenticate(), requirePermission('rbac', 'revoke'), rbacController.revokeRole.bind(rbacController));
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/{version}/rbac/permissions/grant:
|
||||
* post:
|
||||
* summary: Grant permission to role
|
||||
* description: Grant a specific permission to a role. Requires 'rbac:grant' permission.
|
||||
* tags: [RBAC]
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: version
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* default: v1
|
||||
* description: API version
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* required: [roleId, permissionId]
|
||||
* properties:
|
||||
* roleId:
|
||||
* type: string
|
||||
* permissionId:
|
||||
* type: string
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Permission granted successfully
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/ApiResponse'
|
||||
* 401:
|
||||
* description: Unauthorized
|
||||
* 403:
|
||||
* description: Forbidden - Insufficient permissions
|
||||
* 404:
|
||||
* description: Role or Permission not found
|
||||
*/
|
||||
router.post('/permissions/grant', authenticate(), requirePermission('rbac', 'grant'), rbacController.grantPermission.bind(rbacController));
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/{version}/rbac/permissions/check:
|
||||
* get:
|
||||
* summary: Check permission
|
||||
* description: Check if the current user has a specific permission
|
||||
* tags: [RBAC]
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: version
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* default: v1
|
||||
* description: API version
|
||||
* - in: query
|
||||
* name: resource
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* - in: query
|
||||
* name: action
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Permission check result
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* allOf:
|
||||
* - $ref: '#/components/schemas/ApiResponse'
|
||||
* - type: object
|
||||
* properties:
|
||||
* data:
|
||||
* type: object
|
||||
* properties:
|
||||
* allowed:
|
||||
* type: boolean
|
||||
* 401:
|
||||
* description: Unauthorized
|
||||
* 403:
|
||||
* description: Forbidden
|
||||
*/
|
||||
router.get('/permissions/check', authenticate(), rbacController.checkPermission.bind(rbacController));
|
||||
|
||||
return router;
|
||||
};
|
||||
@@ -15,43 +15,51 @@ import { createMfaRouter } from '../modules/mfa/mfa.routes';
|
||||
export const createRouter = (): Router => {
|
||||
const router = Router();
|
||||
|
||||
// EN: Health Check
|
||||
// VI: Kiểm tra trạng thái
|
||||
// EN: Health Check (root level, no API prefix needed)
|
||||
// VI: Kiểm tra trạng thái (ở root level, không cần prefix API)
|
||||
router.get('/health', (req, res) => {
|
||||
res.json({ status: 'ok', timestamp: new Date().toISOString() });
|
||||
});
|
||||
|
||||
// EN: API v1 routes with prefix
|
||||
// VI: Routes API v1 với prefix
|
||||
const apiV1Router = Router();
|
||||
|
||||
// EN: Auth Routes
|
||||
// VI: Routes xác thực
|
||||
router.use('/auth', createAuthRouter());
|
||||
apiV1Router.use('/auth', createAuthRouter());
|
||||
|
||||
// EN: Session Routes
|
||||
// VI: Routes phiên làm việc
|
||||
router.use('/session', createSessionRouter());
|
||||
apiV1Router.use('/sessions', createSessionRouter());
|
||||
|
||||
// EN: OIDC Routes
|
||||
// VI: Routes OpenID Connect
|
||||
router.use('/oidc', createOidcRouter());
|
||||
apiV1Router.use('/oidc', createOidcRouter());
|
||||
|
||||
// EN: RBAC Routes
|
||||
// VI: Routes phân quyền
|
||||
router.use('/rbac', createRbacRouter());
|
||||
apiV1Router.use('/rbac', createRbacRouter());
|
||||
|
||||
// EN: MFA Routes
|
||||
// VI: Routes xác thực đa yếu tố
|
||||
router.use('/mfa', createMfaRouter());
|
||||
apiV1Router.use('/mfa', createMfaRouter());
|
||||
|
||||
// EN: Identity Routes
|
||||
// VI: Routes định danh
|
||||
router.use('/identity', createIdentityRouter());
|
||||
apiV1Router.use('/identity', createIdentityRouter());
|
||||
|
||||
// EN: Access Routes
|
||||
// VI: Routes truy cập
|
||||
router.use('/access', createAccessRouter());
|
||||
apiV1Router.use('/access', createAccessRouter());
|
||||
|
||||
// EN: Governance Routes
|
||||
// VI: Routes quản trị
|
||||
router.use('/governance', createGovernanceRouter());
|
||||
apiV1Router.use('/governance', createGovernanceRouter());
|
||||
|
||||
// EN: Mount API v1 router with prefix
|
||||
// VI: Mount router API v1 với prefix
|
||||
router.use('/api/v1', apiV1Router);
|
||||
|
||||
return router;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user