@valentine-efagene/qshelter-common 2.0.88 → 2.0.90
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/generated/client/browser.d.ts +10 -2
- package/dist/generated/client/client.d.ts +10 -2
- package/dist/generated/client/commonInputTypes.d.ts +30 -0
- package/dist/generated/client/enums.d.ts +5 -0
- package/dist/generated/client/enums.js +4 -0
- package/dist/generated/client/internal/class.d.ts +11 -0
- package/dist/generated/client/internal/class.js +2 -2
- package/dist/generated/client/internal/prismaNamespace.d.ts +113 -16
- package/dist/generated/client/internal/prismaNamespace.js +38 -14
- package/dist/generated/client/internal/prismaNamespaceBrowser.d.ts +41 -15
- package/dist/generated/client/internal/prismaNamespaceBrowser.js +38 -14
- package/dist/generated/client/models/Permission.d.ts +333 -68
- package/dist/generated/client/models/Role.d.ts +403 -3
- package/dist/generated/client/models/Tenant.d.ts +761 -4
- package/dist/generated/client/models/TenantMembership.d.ts +1395 -0
- package/dist/generated/client/models/TenantMembership.js +1 -0
- package/dist/generated/client/models/User.d.ts +375 -0
- package/dist/generated/client/models/UserRole.d.ts +2 -1
- package/dist/generated/client/models/index.d.ts +1 -0
- package/dist/generated/client/models/index.js +1 -0
- package/dist/generated/client/models.d.ts +1 -0
- package/dist/src/events/policies/policy-event.d.ts +50 -5
- package/dist/src/events/policies/policy-event.js +4 -0
- package/dist/src/events/policies/policy-publisher.d.ts +13 -1
- package/dist/src/events/policies/policy-publisher.js +24 -0
- package/dist/src/prisma/tenant.js +13 -5
- package/package.json +1 -1
- package/prisma/migrations/20260111091027_rbac_redesign_federated_users/migration.sql +79 -0
- package/prisma/schema.prisma +84 -20
|
@@ -2,7 +2,8 @@ import type * as runtime from "@prisma/client/runtime/client";
|
|
|
2
2
|
import type * as Prisma from "../internal/prismaNamespace.js";
|
|
3
3
|
/**
|
|
4
4
|
* Model UserRole
|
|
5
|
-
*
|
|
5
|
+
* Legacy: Direct user-role assignment (global, not tenant-scoped)
|
|
6
|
+
* @deprecated Use TenantMembership for tenant-scoped role assignments
|
|
6
7
|
*/
|
|
7
8
|
export type UserRoleModel = runtime.Types.Result.DefaultSelection<Prisma.$UserRolePayload>;
|
|
8
9
|
export type AggregateUserRole = {
|
|
@@ -3,6 +3,7 @@ export type * from './models/Role.js';
|
|
|
3
3
|
export type * from './models/Permission.js';
|
|
4
4
|
export type * from './models/RolePermission.js';
|
|
5
5
|
export type * from './models/UserRole.js';
|
|
6
|
+
export type * from './models/TenantMembership.js';
|
|
6
7
|
export type * from './models/Tenant.js';
|
|
7
8
|
export type * from './models/ApiKey.js';
|
|
8
9
|
export type * from './models/RefreshToken.js';
|
|
@@ -13,29 +13,61 @@ export declare enum PolicyEventType {
|
|
|
13
13
|
PERMISSION_DELETED = "POLICY.PERMISSION_DELETED",
|
|
14
14
|
ROLE_PERMISSION_ASSIGNED = "POLICY.ROLE_PERMISSION_ASSIGNED",
|
|
15
15
|
ROLE_PERMISSION_REVOKED = "POLICY.ROLE_PERMISSION_REVOKED",
|
|
16
|
+
TENANT_MEMBERSHIP_CREATED = "POLICY.TENANT_MEMBERSHIP_CREATED",
|
|
17
|
+
TENANT_MEMBERSHIP_UPDATED = "POLICY.TENANT_MEMBERSHIP_UPDATED",
|
|
18
|
+
TENANT_MEMBERSHIP_DELETED = "POLICY.TENANT_MEMBERSHIP_DELETED",
|
|
16
19
|
FULL_SYNC_REQUESTED = "POLICY.FULL_SYNC_REQUESTED"
|
|
17
20
|
}
|
|
21
|
+
/**
|
|
22
|
+
* Role data with tenant scoping
|
|
23
|
+
*/
|
|
18
24
|
export interface RoleData {
|
|
19
25
|
id: string;
|
|
20
26
|
name: string;
|
|
21
27
|
description?: string | null;
|
|
28
|
+
tenantId?: string | null;
|
|
29
|
+
isSystem?: boolean;
|
|
30
|
+
isActive?: boolean;
|
|
22
31
|
}
|
|
32
|
+
/**
|
|
33
|
+
* Permission with path-based authorization
|
|
34
|
+
* Matches the authorizer's expected policy structure
|
|
35
|
+
*/
|
|
23
36
|
export interface PermissionData {
|
|
24
37
|
id: string;
|
|
25
38
|
name: string;
|
|
26
39
|
description?: string | null;
|
|
27
|
-
|
|
28
|
-
|
|
40
|
+
path: string;
|
|
41
|
+
methods: string[];
|
|
42
|
+
effect: 'ALLOW' | 'DENY';
|
|
43
|
+
tenantId?: string | null;
|
|
29
44
|
}
|
|
45
|
+
/**
|
|
46
|
+
* Role with full permission details for policy sync
|
|
47
|
+
*/
|
|
30
48
|
export interface RolePermissionData {
|
|
31
49
|
roleId: string;
|
|
32
50
|
roleName: string;
|
|
51
|
+
tenantId?: string | null;
|
|
33
52
|
permissions: Array<{
|
|
34
53
|
id: string;
|
|
35
|
-
|
|
36
|
-
|
|
54
|
+
path: string;
|
|
55
|
+
methods: string[];
|
|
56
|
+
effect: 'ALLOW' | 'DENY';
|
|
37
57
|
}>;
|
|
38
58
|
}
|
|
59
|
+
/**
|
|
60
|
+
* Tenant membership data for federated users
|
|
61
|
+
*/
|
|
62
|
+
export interface TenantMembershipData {
|
|
63
|
+
id: string;
|
|
64
|
+
userId: string;
|
|
65
|
+
tenantId: string;
|
|
66
|
+
roleId: string;
|
|
67
|
+
roleName: string;
|
|
68
|
+
isActive: boolean;
|
|
69
|
+
isDefault: boolean;
|
|
70
|
+
}
|
|
39
71
|
export interface PolicyEventMeta {
|
|
40
72
|
source: string;
|
|
41
73
|
timestamp: string;
|
|
@@ -86,4 +118,17 @@ export interface FullSyncRequestedEvent extends PolicyEvent<{
|
|
|
86
118
|
}> {
|
|
87
119
|
eventType: PolicyEventType.FULL_SYNC_REQUESTED;
|
|
88
120
|
}
|
|
89
|
-
export
|
|
121
|
+
export interface TenantMembershipCreatedEvent extends PolicyEvent<TenantMembershipData> {
|
|
122
|
+
eventType: PolicyEventType.TENANT_MEMBERSHIP_CREATED;
|
|
123
|
+
}
|
|
124
|
+
export interface TenantMembershipUpdatedEvent extends PolicyEvent<TenantMembershipData> {
|
|
125
|
+
eventType: PolicyEventType.TENANT_MEMBERSHIP_UPDATED;
|
|
126
|
+
}
|
|
127
|
+
export interface TenantMembershipDeletedEvent extends PolicyEvent<{
|
|
128
|
+
membershipId: string;
|
|
129
|
+
userId: string;
|
|
130
|
+
tenantId: string;
|
|
131
|
+
}> {
|
|
132
|
+
eventType: PolicyEventType.TENANT_MEMBERSHIP_DELETED;
|
|
133
|
+
}
|
|
134
|
+
export type AnyPolicyEvent = RoleCreatedEvent | RoleUpdatedEvent | RoleDeletedEvent | PermissionCreatedEvent | PermissionUpdatedEvent | PermissionDeletedEvent | RolePermissionAssignedEvent | RolePermissionRevokedEvent | FullSyncRequestedEvent | TenantMembershipCreatedEvent | TenantMembershipUpdatedEvent | TenantMembershipDeletedEvent;
|
|
@@ -17,6 +17,10 @@ export var PolicyEventType;
|
|
|
17
17
|
// Role-Permission association events
|
|
18
18
|
PolicyEventType["ROLE_PERMISSION_ASSIGNED"] = "POLICY.ROLE_PERMISSION_ASSIGNED";
|
|
19
19
|
PolicyEventType["ROLE_PERMISSION_REVOKED"] = "POLICY.ROLE_PERMISSION_REVOKED";
|
|
20
|
+
// Tenant membership events (for federated users)
|
|
21
|
+
PolicyEventType["TENANT_MEMBERSHIP_CREATED"] = "POLICY.TENANT_MEMBERSHIP_CREATED";
|
|
22
|
+
PolicyEventType["TENANT_MEMBERSHIP_UPDATED"] = "POLICY.TENANT_MEMBERSHIP_UPDATED";
|
|
23
|
+
PolicyEventType["TENANT_MEMBERSHIP_DELETED"] = "POLICY.TENANT_MEMBERSHIP_DELETED";
|
|
20
24
|
// Bulk sync events
|
|
21
25
|
PolicyEventType["FULL_SYNC_REQUESTED"] = "POLICY.FULL_SYNC_REQUESTED";
|
|
22
26
|
})(PolicyEventType || (PolicyEventType = {}));
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { PolicyEventType, PolicyEventMeta, RoleData, PermissionData, RolePermissionData } from './policy-event';
|
|
1
|
+
import { PolicyEventType, PolicyEventMeta, RoleData, PermissionData, RolePermissionData, TenantMembershipData } from './policy-event';
|
|
2
2
|
/**
|
|
3
3
|
* Configuration for the policy event publisher
|
|
4
4
|
*/
|
|
@@ -52,6 +52,18 @@ export declare class PolicyEventPublisher {
|
|
|
52
52
|
* Publish role permission revoked event
|
|
53
53
|
*/
|
|
54
54
|
publishRolePermissionRevoked(data: RolePermissionData, meta?: Partial<PolicyEventMeta>): Promise<string>;
|
|
55
|
+
/**
|
|
56
|
+
* Publish tenant membership created event
|
|
57
|
+
*/
|
|
58
|
+
publishTenantMembershipCreated(data: TenantMembershipData, meta?: Partial<PolicyEventMeta>): Promise<string>;
|
|
59
|
+
/**
|
|
60
|
+
* Publish tenant membership updated event
|
|
61
|
+
*/
|
|
62
|
+
publishTenantMembershipUpdated(data: TenantMembershipData, meta?: Partial<PolicyEventMeta>): Promise<string>;
|
|
63
|
+
/**
|
|
64
|
+
* Publish tenant membership deleted event
|
|
65
|
+
*/
|
|
66
|
+
publishTenantMembershipDeleted(membershipId: string, userId: string, tenantId: string, meta?: Partial<PolicyEventMeta>): Promise<string>;
|
|
55
67
|
/**
|
|
56
68
|
* Publish full sync requested event
|
|
57
69
|
*/
|
|
@@ -119,6 +119,30 @@ export class PolicyEventPublisher {
|
|
|
119
119
|
async publishRolePermissionRevoked(data, meta) {
|
|
120
120
|
return this.publish(PolicyEventType.ROLE_PERMISSION_REVOKED, data, meta);
|
|
121
121
|
}
|
|
122
|
+
/**
|
|
123
|
+
* Publish tenant membership created event
|
|
124
|
+
*/
|
|
125
|
+
async publishTenantMembershipCreated(data, meta) {
|
|
126
|
+
return this.publish(PolicyEventType.TENANT_MEMBERSHIP_CREATED, data, {
|
|
127
|
+
...meta,
|
|
128
|
+
tenantId: data.tenantId,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Publish tenant membership updated event
|
|
133
|
+
*/
|
|
134
|
+
async publishTenantMembershipUpdated(data, meta) {
|
|
135
|
+
return this.publish(PolicyEventType.TENANT_MEMBERSHIP_UPDATED, data, {
|
|
136
|
+
...meta,
|
|
137
|
+
tenantId: data.tenantId,
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Publish tenant membership deleted event
|
|
142
|
+
*/
|
|
143
|
+
async publishTenantMembershipDeleted(membershipId, userId, tenantId, meta) {
|
|
144
|
+
return this.publish(PolicyEventType.TENANT_MEMBERSHIP_DELETED, { membershipId, userId, tenantId }, { ...meta, tenantId });
|
|
145
|
+
}
|
|
122
146
|
/**
|
|
123
147
|
* Publish full sync requested event
|
|
124
148
|
*/
|
|
@@ -9,16 +9,18 @@
|
|
|
9
9
|
* These models either:
|
|
10
10
|
* - Don't have a tenantId field (system tables)
|
|
11
11
|
* - Have optional tenantId but are designed to work across tenants (User)
|
|
12
|
+
* - Are cross-tenant lookup/join tables (TenantMembership)
|
|
12
13
|
*/
|
|
13
14
|
const GLOBAL_MODELS = [
|
|
14
|
-
// User can exist across tenants or without a tenant
|
|
15
|
+
// User can exist across tenants or without a tenant (federated)
|
|
15
16
|
"user",
|
|
17
|
+
// TenantMembership is the user-tenant join table (queries by userId or tenantId)
|
|
18
|
+
"tenantMembership",
|
|
16
19
|
// System/infrastructure tables without tenantId
|
|
17
20
|
"tenant",
|
|
18
|
-
|
|
19
|
-
"permission",
|
|
20
|
-
"rolePermission",
|
|
21
|
+
// Legacy role assignment (global, not tenant-scoped)
|
|
21
22
|
"userRole",
|
|
23
|
+
"rolePermission",
|
|
22
24
|
"refreshToken",
|
|
23
25
|
"passwordReset",
|
|
24
26
|
"wallet",
|
|
@@ -29,7 +31,13 @@ const GLOBAL_MODELS = [
|
|
|
29
31
|
* These can be global templates (tenantId = null) or tenant-specific.
|
|
30
32
|
* Queries will return both global AND tenant-specific records.
|
|
31
33
|
*/
|
|
32
|
-
const OPTIONAL_TENANT_MODELS = [
|
|
34
|
+
const OPTIONAL_TENANT_MODELS = [
|
|
35
|
+
"paymentPlan",
|
|
36
|
+
// Role can be global template (tenantId = null) or tenant-specific
|
|
37
|
+
"role",
|
|
38
|
+
// Permission can be global template or tenant-specific
|
|
39
|
+
"permission",
|
|
40
|
+
];
|
|
33
41
|
function isGlobalModel(model) {
|
|
34
42
|
return GLOBAL_MODELS.includes(model);
|
|
35
43
|
}
|
package/package.json
CHANGED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Warnings:
|
|
3
|
+
|
|
4
|
+
- You are about to drop the column `action` on the `permissions` table. All the data in the column will be lost.
|
|
5
|
+
- You are about to drop the column `resource` on the `permissions` table. All the data in the column will be lost.
|
|
6
|
+
- A unique constraint covering the columns `[path,tenantId]` on the table `permissions` will be added. If there are existing duplicate values, this will fail.
|
|
7
|
+
- A unique constraint covering the columns `[name,tenantId]` on the table `roles` will be added. If there are existing duplicate values, this will fail.
|
|
8
|
+
- Added the required column `path` to the `permissions` table without a default value. This is not possible if the table is not empty.
|
|
9
|
+
|
|
10
|
+
*/
|
|
11
|
+
-- DropIndex
|
|
12
|
+
DROP INDEX `permissions_name_key` ON `permissions`;
|
|
13
|
+
|
|
14
|
+
-- DropIndex
|
|
15
|
+
DROP INDEX `permissions_resource_action_key` ON `permissions`;
|
|
16
|
+
|
|
17
|
+
-- DropIndex
|
|
18
|
+
DROP INDEX `permissions_resource_idx` ON `permissions`;
|
|
19
|
+
|
|
20
|
+
-- DropIndex
|
|
21
|
+
DROP INDEX `roles_name_key` ON `roles`;
|
|
22
|
+
|
|
23
|
+
-- AlterTable
|
|
24
|
+
ALTER TABLE `permissions` DROP COLUMN `action`,
|
|
25
|
+
DROP COLUMN `resource`,
|
|
26
|
+
ADD COLUMN `effect` ENUM('ALLOW', 'DENY') NOT NULL DEFAULT 'ALLOW',
|
|
27
|
+
ADD COLUMN `isSystem` BOOLEAN NOT NULL DEFAULT false,
|
|
28
|
+
ADD COLUMN `methods` JSON NOT NULL,
|
|
29
|
+
ADD COLUMN `path` VARCHAR(191) NOT NULL,
|
|
30
|
+
ADD COLUMN `tenantId` VARCHAR(191) NULL;
|
|
31
|
+
|
|
32
|
+
-- AlterTable
|
|
33
|
+
ALTER TABLE `roles` ADD COLUMN `isActive` BOOLEAN NOT NULL DEFAULT true,
|
|
34
|
+
ADD COLUMN `isSystem` BOOLEAN NOT NULL DEFAULT false,
|
|
35
|
+
ADD COLUMN `tenantId` VARCHAR(191) NULL;
|
|
36
|
+
|
|
37
|
+
-- CreateTable
|
|
38
|
+
CREATE TABLE `tenant_memberships` (
|
|
39
|
+
`id` VARCHAR(191) NOT NULL,
|
|
40
|
+
`userId` VARCHAR(191) NOT NULL,
|
|
41
|
+
`tenantId` VARCHAR(191) NOT NULL,
|
|
42
|
+
`roleId` VARCHAR(191) NOT NULL,
|
|
43
|
+
`isActive` BOOLEAN NOT NULL DEFAULT true,
|
|
44
|
+
`isDefault` BOOLEAN NOT NULL DEFAULT false,
|
|
45
|
+
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
|
46
|
+
`updatedAt` DATETIME(3) NOT NULL,
|
|
47
|
+
|
|
48
|
+
INDEX `tenant_memberships_tenantId_idx`(`tenantId`),
|
|
49
|
+
INDEX `tenant_memberships_userId_idx`(`userId`),
|
|
50
|
+
UNIQUE INDEX `tenant_memberships_userId_tenantId_key`(`userId`, `tenantId`),
|
|
51
|
+
PRIMARY KEY (`id`)
|
|
52
|
+
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
|
53
|
+
|
|
54
|
+
-- CreateIndex
|
|
55
|
+
CREATE INDEX `permissions_tenantId_idx` ON `permissions`(`tenantId`);
|
|
56
|
+
|
|
57
|
+
-- CreateIndex
|
|
58
|
+
CREATE UNIQUE INDEX `permissions_path_tenantId_key` ON `permissions`(`path`, `tenantId`);
|
|
59
|
+
|
|
60
|
+
-- CreateIndex
|
|
61
|
+
CREATE INDEX `roles_tenantId_idx` ON `roles`(`tenantId`);
|
|
62
|
+
|
|
63
|
+
-- CreateIndex
|
|
64
|
+
CREATE UNIQUE INDEX `roles_name_tenantId_key` ON `roles`(`name`, `tenantId`);
|
|
65
|
+
|
|
66
|
+
-- AddForeignKey
|
|
67
|
+
ALTER TABLE `roles` ADD CONSTRAINT `roles_tenantId_fkey` FOREIGN KEY (`tenantId`) REFERENCES `tenants`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
|
|
68
|
+
|
|
69
|
+
-- AddForeignKey
|
|
70
|
+
ALTER TABLE `permissions` ADD CONSTRAINT `permissions_tenantId_fkey` FOREIGN KEY (`tenantId`) REFERENCES `tenants`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
|
|
71
|
+
|
|
72
|
+
-- AddForeignKey
|
|
73
|
+
ALTER TABLE `tenant_memberships` ADD CONSTRAINT `tenant_memberships_userId_fkey` FOREIGN KEY (`userId`) REFERENCES `users`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
|
|
74
|
+
|
|
75
|
+
-- AddForeignKey
|
|
76
|
+
ALTER TABLE `tenant_memberships` ADD CONSTRAINT `tenant_memberships_tenantId_fkey` FOREIGN KEY (`tenantId`) REFERENCES `tenants`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
|
|
77
|
+
|
|
78
|
+
-- AddForeignKey
|
|
79
|
+
ALTER TABLE `tenant_memberships` ADD CONSTRAINT `tenant_memberships_roleId_fkey` FOREIGN KEY (`roleId`) REFERENCES `roles`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE;
|
package/prisma/schema.prisma
CHANGED
|
@@ -292,29 +292,39 @@ enum ExecutionStatus {
|
|
|
292
292
|
SKIPPED
|
|
293
293
|
}
|
|
294
294
|
|
|
295
|
+
/// Permission effect (Allow/Deny)
|
|
296
|
+
enum PermissionEffect {
|
|
297
|
+
ALLOW
|
|
298
|
+
DENY
|
|
299
|
+
}
|
|
300
|
+
|
|
295
301
|
// =============================================================================
|
|
296
302
|
// USER & AUTH DOMAIN
|
|
297
303
|
// =============================================================================
|
|
298
304
|
|
|
299
305
|
model User {
|
|
300
|
-
id String
|
|
301
|
-
email String
|
|
306
|
+
id String @id @default(cuid())
|
|
307
|
+
email String @unique
|
|
302
308
|
password String?
|
|
303
|
-
phone String?
|
|
309
|
+
phone String? @unique
|
|
304
310
|
firstName String?
|
|
305
311
|
lastName String?
|
|
306
|
-
isActive Boolean
|
|
307
|
-
isEmailVerified Boolean
|
|
312
|
+
isActive Boolean @default(true)
|
|
313
|
+
isEmailVerified Boolean @default(false)
|
|
308
314
|
googleId String?
|
|
309
315
|
avatar String?
|
|
316
|
+
// Legacy: Optional direct tenant association (for backward compatibility)
|
|
317
|
+
// New: Use tenantMemberships for multi-tenant federation
|
|
310
318
|
tenantId String?
|
|
311
|
-
tenant Tenant?
|
|
312
|
-
//
|
|
319
|
+
tenant Tenant? @relation(fields: [tenantId], references: [id], onDelete: SetNull)
|
|
320
|
+
// Federated: User can belong to multiple tenants with different roles
|
|
321
|
+
tenantMemberships TenantMembership[]
|
|
322
|
+
// Legacy: Support multiple roles via explicit join table `UserRole`
|
|
313
323
|
userRoles UserRole[]
|
|
314
|
-
walletId String?
|
|
315
|
-
wallet Wallet?
|
|
316
|
-
createdAt DateTime
|
|
317
|
-
updatedAt DateTime
|
|
324
|
+
walletId String? @unique
|
|
325
|
+
wallet Wallet? @relation(fields: [walletId], references: [id])
|
|
326
|
+
createdAt DateTime @default(now())
|
|
327
|
+
updatedAt DateTime @updatedAt
|
|
318
328
|
emailVerifiedAt DateTime?
|
|
319
329
|
emailVerificationToken String?
|
|
320
330
|
lastLoginAt DateTime?
|
|
@@ -368,29 +378,51 @@ model User {
|
|
|
368
378
|
}
|
|
369
379
|
|
|
370
380
|
model Role {
|
|
371
|
-
id String
|
|
372
|
-
name String
|
|
381
|
+
id String @id @default(cuid())
|
|
382
|
+
name String
|
|
373
383
|
description String?
|
|
384
|
+
// Tenant-scoping: NULL = global template, set = tenant-specific role
|
|
385
|
+
tenantId String?
|
|
386
|
+
tenant Tenant? @relation(fields: [tenantId], references: [id], onDelete: Cascade)
|
|
387
|
+
// System roles cannot be deleted (admin, user, etc.)
|
|
388
|
+
isSystem Boolean @default(false)
|
|
389
|
+
isActive Boolean @default(true)
|
|
390
|
+
// Legacy: UserRole for backward compatibility
|
|
374
391
|
userRoles UserRole[]
|
|
392
|
+
// New: TenantMembership for federated users
|
|
393
|
+
memberships TenantMembership[]
|
|
375
394
|
permissions RolePermission[]
|
|
376
|
-
createdAt DateTime
|
|
377
|
-
updatedAt DateTime
|
|
395
|
+
createdAt DateTime @default(now())
|
|
396
|
+
updatedAt DateTime @updatedAt
|
|
378
397
|
|
|
398
|
+
@@unique([name, tenantId]) // Unique name per tenant (null tenantId = global)
|
|
399
|
+
@@index([tenantId])
|
|
379
400
|
@@map("roles")
|
|
380
401
|
}
|
|
381
402
|
|
|
403
|
+
/// Permission defines a path pattern + HTTP methods + effect
|
|
404
|
+
/// Supports path-based authorization matching the authorizer's policy structure
|
|
382
405
|
model Permission {
|
|
383
406
|
id String @id @default(cuid())
|
|
384
|
-
name String
|
|
407
|
+
name String // Descriptive name: "Read Users", "Manage Properties"
|
|
385
408
|
description String?
|
|
386
|
-
|
|
387
|
-
|
|
409
|
+
// Path pattern: /users, /users/:id, /properties/*, etc.
|
|
410
|
+
path String
|
|
411
|
+
// HTTP methods: ["GET"], ["GET", "POST"], ["*"] - stored as JSON
|
|
412
|
+
methods Json @default("[]")
|
|
413
|
+
// Allow or Deny this path/methods
|
|
414
|
+
effect PermissionEffect @default(ALLOW)
|
|
415
|
+
// Tenant-scoping: NULL = global template, set = tenant-specific
|
|
416
|
+
tenantId String?
|
|
417
|
+
tenant Tenant? @relation(fields: [tenantId], references: [id], onDelete: Cascade)
|
|
418
|
+
// System permissions cannot be deleted
|
|
419
|
+
isSystem Boolean @default(false)
|
|
388
420
|
roles RolePermission[]
|
|
389
421
|
createdAt DateTime @default(now())
|
|
390
422
|
updatedAt DateTime @updatedAt
|
|
391
423
|
|
|
392
|
-
@@unique([
|
|
393
|
-
@@index([
|
|
424
|
+
@@unique([path, tenantId]) // Unique path per tenant
|
|
425
|
+
@@index([tenantId])
|
|
394
426
|
@@map("permissions")
|
|
395
427
|
}
|
|
396
428
|
|
|
@@ -405,6 +437,8 @@ model RolePermission {
|
|
|
405
437
|
@@map("role_permissions")
|
|
406
438
|
}
|
|
407
439
|
|
|
440
|
+
/// Legacy: Direct user-role assignment (global, not tenant-scoped)
|
|
441
|
+
/// @deprecated Use TenantMembership for tenant-scoped role assignments
|
|
408
442
|
model UserRole {
|
|
409
443
|
userId String
|
|
410
444
|
roleId String
|
|
@@ -416,6 +450,30 @@ model UserRole {
|
|
|
416
450
|
@@map("user_roles")
|
|
417
451
|
}
|
|
418
452
|
|
|
453
|
+
/// Tenant Membership: Links users to tenants with specific roles
|
|
454
|
+
/// Enables federated users across multiple tenants with different roles per tenant
|
|
455
|
+
model TenantMembership {
|
|
456
|
+
id String @id @default(cuid())
|
|
457
|
+
userId String
|
|
458
|
+
tenantId String
|
|
459
|
+
roleId String
|
|
460
|
+
// Whether this membership is active
|
|
461
|
+
isActive Boolean @default(true)
|
|
462
|
+
// Whether this is the user's default tenant (for login without specifying tenant)
|
|
463
|
+
isDefault Boolean @default(false)
|
|
464
|
+
createdAt DateTime @default(now())
|
|
465
|
+
updatedAt DateTime @updatedAt
|
|
466
|
+
|
|
467
|
+
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
468
|
+
tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)
|
|
469
|
+
role Role @relation(fields: [roleId], references: [id], onDelete: Restrict)
|
|
470
|
+
|
|
471
|
+
@@unique([userId, tenantId]) // User can only have one membership per tenant
|
|
472
|
+
@@index([tenantId])
|
|
473
|
+
@@index([userId])
|
|
474
|
+
@@map("tenant_memberships")
|
|
475
|
+
}
|
|
476
|
+
|
|
419
477
|
model Tenant {
|
|
420
478
|
id String @id @default(cuid())
|
|
421
479
|
name String
|
|
@@ -431,6 +489,12 @@ model Tenant {
|
|
|
431
489
|
paymentMethods PropertyPaymentMethod[]
|
|
432
490
|
contracts Contract[]
|
|
433
491
|
|
|
492
|
+
// RBAC: Tenant-scoped roles and permissions
|
|
493
|
+
roles Role[]
|
|
494
|
+
permissions Permission[]
|
|
495
|
+
// Federated user memberships
|
|
496
|
+
memberships TenantMembership[]
|
|
497
|
+
|
|
434
498
|
// Payment method changes
|
|
435
499
|
paymentMethodChangeRequests PaymentMethodChangeRequest[]
|
|
436
500
|
documentRequirementRules DocumentRequirementRule[]
|