@valentine-efagene/qshelter-common 2.0.87 → 2.0.89

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.
Files changed (32) hide show
  1. package/dist/generated/client/browser.d.ts +10 -2
  2. package/dist/generated/client/client.d.ts +10 -2
  3. package/dist/generated/client/commonInputTypes.d.ts +30 -0
  4. package/dist/generated/client/enums.d.ts +5 -0
  5. package/dist/generated/client/enums.js +4 -0
  6. package/dist/generated/client/internal/class.d.ts +11 -0
  7. package/dist/generated/client/internal/class.js +2 -2
  8. package/dist/generated/client/internal/prismaNamespace.d.ts +113 -16
  9. package/dist/generated/client/internal/prismaNamespace.js +38 -14
  10. package/dist/generated/client/internal/prismaNamespaceBrowser.d.ts +41 -15
  11. package/dist/generated/client/internal/prismaNamespaceBrowser.js +38 -14
  12. package/dist/generated/client/models/Permission.d.ts +333 -68
  13. package/dist/generated/client/models/Role.d.ts +403 -3
  14. package/dist/generated/client/models/Tenant.d.ts +761 -4
  15. package/dist/generated/client/models/TenantMembership.d.ts +1395 -0
  16. package/dist/generated/client/models/TenantMembership.js +1 -0
  17. package/dist/generated/client/models/User.d.ts +375 -0
  18. package/dist/generated/client/models/UserRole.d.ts +2 -1
  19. package/dist/generated/client/models/index.d.ts +5 -0
  20. package/dist/generated/client/models/index.js +5 -0
  21. package/dist/generated/client/models.d.ts +1 -0
  22. package/dist/src/events/index.d.ts +2 -0
  23. package/dist/src/events/index.js +3 -0
  24. package/dist/src/events/policies/index.d.ts +2 -0
  25. package/dist/src/events/policies/index.js +2 -0
  26. package/dist/src/events/policies/policy-event.d.ts +89 -0
  27. package/dist/src/events/policies/policy-event.js +22 -0
  28. package/dist/src/events/policies/policy-publisher.d.ts +60 -0
  29. package/dist/src/events/policies/policy-publisher.js +128 -0
  30. package/dist/src/prisma/tenant.js +13 -5
  31. package/package.json +1 -1
  32. 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 = {
@@ -12,6 +12,7 @@ export * from './ContractTermination';
12
12
  export * from './DeviceEndpoint';
13
13
  export * from './DocumentRequirementRule';
14
14
  export * from './DocumentTemplate';
15
+ export * from './DocumentationPhase';
15
16
  export * from './DocumentationStep';
16
17
  export * from './DocumentationStepApproval';
17
18
  export * from './DocumentationStepDocument';
@@ -26,7 +27,9 @@ export * from './OfferLetter';
26
27
  export * from './PasswordReset';
27
28
  export * from './PaymentMethodChangeRequest';
28
29
  export * from './PaymentMethodPhaseDocument';
30
+ export * from './PaymentMethodPhaseField';
29
31
  export * from './PaymentMethodPhaseStep';
32
+ export * from './PaymentPhase';
30
33
  export * from './PaymentPlan';
31
34
  export * from './Permission';
32
35
  export * from './Property';
@@ -41,6 +44,8 @@ export * from './PropertyUnit';
41
44
  export * from './PropertyVariant';
42
45
  export * from './PropertyVariantAmenity';
43
46
  export * from './PropertyVariantMedia';
47
+ export * from './QuestionnaireField';
48
+ export * from './QuestionnairePhase';
44
49
  export * from './RefreshToken';
45
50
  export * from './Role';
46
51
  export * from './RolePermission';
@@ -12,6 +12,7 @@ export * from './ContractTermination';
12
12
  export * from './DeviceEndpoint';
13
13
  export * from './DocumentRequirementRule';
14
14
  export * from './DocumentTemplate';
15
+ export * from './DocumentationPhase';
15
16
  export * from './DocumentationStep';
16
17
  export * from './DocumentationStepApproval';
17
18
  export * from './DocumentationStepDocument';
@@ -26,7 +27,9 @@ export * from './OfferLetter';
26
27
  export * from './PasswordReset';
27
28
  export * from './PaymentMethodChangeRequest';
28
29
  export * from './PaymentMethodPhaseDocument';
30
+ export * from './PaymentMethodPhaseField';
29
31
  export * from './PaymentMethodPhaseStep';
32
+ export * from './PaymentPhase';
30
33
  export * from './PaymentPlan';
31
34
  export * from './Permission';
32
35
  export * from './Property';
@@ -41,6 +44,8 @@ export * from './PropertyUnit';
41
44
  export * from './PropertyVariant';
42
45
  export * from './PropertyVariantAmenity';
43
46
  export * from './PropertyVariantMedia';
47
+ export * from './QuestionnaireField';
48
+ export * from './QuestionnairePhase';
44
49
  export * from './RefreshToken';
45
50
  export * from './Role';
46
51
  export * from './RolePermission';
@@ -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';
@@ -3,5 +3,7 @@ export * from './notifications/notification-event';
3
3
  export * from './notifications/event-publisher';
4
4
  export * from './payments/payment-event';
5
5
  export * from './payments/payment-publisher';
6
+ export * from './policies/policy-event';
7
+ export * from './policies/policy-publisher';
6
8
  export * from './bus/event-bus.types';
7
9
  export * from './bus/event-bus.service';
@@ -5,6 +5,9 @@ export * from './notifications/event-publisher';
5
5
  // Payment events and publisher (SNS-based)
6
6
  export * from './payments/payment-event';
7
7
  export * from './payments/payment-publisher';
8
+ // Policy sync events and publisher (SNS-based)
9
+ export * from './policies/policy-event';
10
+ export * from './policies/policy-publisher';
8
11
  // Event bus (multi-transport delivery)
9
12
  export * from './bus/event-bus.types';
10
13
  export * from './bus/event-bus.service';
@@ -0,0 +1,2 @@
1
+ export * from './policy-event';
2
+ export * from './policy-publisher';
@@ -0,0 +1,2 @@
1
+ export * from './policy-event';
2
+ export * from './policy-publisher';
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Policy Sync Event Types
3
+ *
4
+ * These events are published when role/permission data changes in RDS,
5
+ * triggering synchronization to DynamoDB for the authorizer.
6
+ */
7
+ export declare enum PolicyEventType {
8
+ ROLE_CREATED = "POLICY.ROLE_CREATED",
9
+ ROLE_UPDATED = "POLICY.ROLE_UPDATED",
10
+ ROLE_DELETED = "POLICY.ROLE_DELETED",
11
+ PERMISSION_CREATED = "POLICY.PERMISSION_CREATED",
12
+ PERMISSION_UPDATED = "POLICY.PERMISSION_UPDATED",
13
+ PERMISSION_DELETED = "POLICY.PERMISSION_DELETED",
14
+ ROLE_PERMISSION_ASSIGNED = "POLICY.ROLE_PERMISSION_ASSIGNED",
15
+ ROLE_PERMISSION_REVOKED = "POLICY.ROLE_PERMISSION_REVOKED",
16
+ FULL_SYNC_REQUESTED = "POLICY.FULL_SYNC_REQUESTED"
17
+ }
18
+ export interface RoleData {
19
+ id: string;
20
+ name: string;
21
+ description?: string | null;
22
+ }
23
+ export interface PermissionData {
24
+ id: string;
25
+ name: string;
26
+ description?: string | null;
27
+ resource: string;
28
+ action: string;
29
+ }
30
+ export interface RolePermissionData {
31
+ roleId: string;
32
+ roleName: string;
33
+ permissions: Array<{
34
+ id: string;
35
+ resource: string;
36
+ action: string;
37
+ }>;
38
+ }
39
+ export interface PolicyEventMeta {
40
+ source: string;
41
+ timestamp: string;
42
+ correlationId?: string;
43
+ userId?: string;
44
+ tenantId?: string;
45
+ }
46
+ export interface PolicyEvent<T = unknown> {
47
+ eventType: PolicyEventType;
48
+ eventId: string;
49
+ timestamp: string;
50
+ tenantId?: string;
51
+ source: string;
52
+ data: T;
53
+ metadata?: Record<string, unknown>;
54
+ }
55
+ export interface RoleCreatedEvent extends PolicyEvent<RoleData> {
56
+ eventType: PolicyEventType.ROLE_CREATED;
57
+ }
58
+ export interface RoleUpdatedEvent extends PolicyEvent<RoleData> {
59
+ eventType: PolicyEventType.ROLE_UPDATED;
60
+ }
61
+ export interface RoleDeletedEvent extends PolicyEvent<{
62
+ roleId: string;
63
+ roleName: string;
64
+ }> {
65
+ eventType: PolicyEventType.ROLE_DELETED;
66
+ }
67
+ export interface PermissionCreatedEvent extends PolicyEvent<PermissionData> {
68
+ eventType: PolicyEventType.PERMISSION_CREATED;
69
+ }
70
+ export interface PermissionUpdatedEvent extends PolicyEvent<PermissionData> {
71
+ eventType: PolicyEventType.PERMISSION_UPDATED;
72
+ }
73
+ export interface PermissionDeletedEvent extends PolicyEvent<{
74
+ permissionId: string;
75
+ }> {
76
+ eventType: PolicyEventType.PERMISSION_DELETED;
77
+ }
78
+ export interface RolePermissionAssignedEvent extends PolicyEvent<RolePermissionData> {
79
+ eventType: PolicyEventType.ROLE_PERMISSION_ASSIGNED;
80
+ }
81
+ export interface RolePermissionRevokedEvent extends PolicyEvent<RolePermissionData> {
82
+ eventType: PolicyEventType.ROLE_PERMISSION_REVOKED;
83
+ }
84
+ export interface FullSyncRequestedEvent extends PolicyEvent<{
85
+ requestedBy: string;
86
+ }> {
87
+ eventType: PolicyEventType.FULL_SYNC_REQUESTED;
88
+ }
89
+ export type AnyPolicyEvent = RoleCreatedEvent | RoleUpdatedEvent | RoleDeletedEvent | PermissionCreatedEvent | PermissionUpdatedEvent | PermissionDeletedEvent | RolePermissionAssignedEvent | RolePermissionRevokedEvent | FullSyncRequestedEvent;
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Policy Sync Event Types
3
+ *
4
+ * These events are published when role/permission data changes in RDS,
5
+ * triggering synchronization to DynamoDB for the authorizer.
6
+ */
7
+ export var PolicyEventType;
8
+ (function (PolicyEventType) {
9
+ // Role events
10
+ PolicyEventType["ROLE_CREATED"] = "POLICY.ROLE_CREATED";
11
+ PolicyEventType["ROLE_UPDATED"] = "POLICY.ROLE_UPDATED";
12
+ PolicyEventType["ROLE_DELETED"] = "POLICY.ROLE_DELETED";
13
+ // Permission events
14
+ PolicyEventType["PERMISSION_CREATED"] = "POLICY.PERMISSION_CREATED";
15
+ PolicyEventType["PERMISSION_UPDATED"] = "POLICY.PERMISSION_UPDATED";
16
+ PolicyEventType["PERMISSION_DELETED"] = "POLICY.PERMISSION_DELETED";
17
+ // Role-Permission association events
18
+ PolicyEventType["ROLE_PERMISSION_ASSIGNED"] = "POLICY.ROLE_PERMISSION_ASSIGNED";
19
+ PolicyEventType["ROLE_PERMISSION_REVOKED"] = "POLICY.ROLE_PERMISSION_REVOKED";
20
+ // Bulk sync events
21
+ PolicyEventType["FULL_SYNC_REQUESTED"] = "POLICY.FULL_SYNC_REQUESTED";
22
+ })(PolicyEventType || (PolicyEventType = {}));
@@ -0,0 +1,60 @@
1
+ import { PolicyEventType, PolicyEventMeta, RoleData, PermissionData, RolePermissionData } from './policy-event';
2
+ /**
3
+ * Configuration for the policy event publisher
4
+ */
5
+ interface PolicyPublisherConfig {
6
+ region?: string;
7
+ endpoint?: string;
8
+ topicArn?: string;
9
+ }
10
+ /**
11
+ * Policy Event Publisher for sending policy sync events to SNS
12
+ * Used by user-service to notify policy-sync-service of changes
13
+ */
14
+ export declare class PolicyEventPublisher {
15
+ private readonly snsClient;
16
+ private readonly topicArn;
17
+ private readonly serviceName;
18
+ constructor(serviceName: string, config?: PolicyPublisherConfig);
19
+ /**
20
+ * Publish a policy event to SNS
21
+ */
22
+ publish<T>(eventType: PolicyEventType, data: T, meta?: Partial<PolicyEventMeta>): Promise<string>;
23
+ /**
24
+ * Publish role created event
25
+ */
26
+ publishRoleCreated(role: RoleData, meta?: Partial<PolicyEventMeta>): Promise<string>;
27
+ /**
28
+ * Publish role updated event
29
+ */
30
+ publishRoleUpdated(role: RoleData, meta?: Partial<PolicyEventMeta>): Promise<string>;
31
+ /**
32
+ * Publish role deleted event
33
+ */
34
+ publishRoleDeleted(roleId: string, roleName: string, meta?: Partial<PolicyEventMeta>): Promise<string>;
35
+ /**
36
+ * Publish permission created event
37
+ */
38
+ publishPermissionCreated(permission: PermissionData, meta?: Partial<PolicyEventMeta>): Promise<string>;
39
+ /**
40
+ * Publish permission updated event
41
+ */
42
+ publishPermissionUpdated(permission: PermissionData, meta?: Partial<PolicyEventMeta>): Promise<string>;
43
+ /**
44
+ * Publish permission deleted event
45
+ */
46
+ publishPermissionDeleted(permissionId: string, meta?: Partial<PolicyEventMeta>): Promise<string>;
47
+ /**
48
+ * Publish role permission assigned event
49
+ */
50
+ publishRolePermissionAssigned(data: RolePermissionData, meta?: Partial<PolicyEventMeta>): Promise<string>;
51
+ /**
52
+ * Publish role permission revoked event
53
+ */
54
+ publishRolePermissionRevoked(data: RolePermissionData, meta?: Partial<PolicyEventMeta>): Promise<string>;
55
+ /**
56
+ * Publish full sync requested event
57
+ */
58
+ publishFullSyncRequested(requestedBy: string, meta?: Partial<PolicyEventMeta>): Promise<string>;
59
+ }
60
+ export {};
@@ -0,0 +1,128 @@
1
+ import { SNSClient, PublishCommand } from '@aws-sdk/client-sns';
2
+ import { PolicyEventType, } from './policy-event';
3
+ /**
4
+ * Get SNS client configured for LocalStack or AWS
5
+ */
6
+ function createSNSClient(config) {
7
+ const endpoint = config.endpoint || process.env.LOCALSTACK_ENDPOINT;
8
+ const region = config.region || process.env.AWS_REGION || 'us-east-1';
9
+ const clientConfig = { region };
10
+ // For LocalStack, set custom endpoint
11
+ if (endpoint) {
12
+ clientConfig.endpoint = endpoint;
13
+ }
14
+ return new SNSClient(clientConfig);
15
+ }
16
+ /**
17
+ * Policy Event Publisher for sending policy sync events to SNS
18
+ * Used by user-service to notify policy-sync-service of changes
19
+ */
20
+ export class PolicyEventPublisher {
21
+ snsClient;
22
+ topicArn;
23
+ serviceName;
24
+ constructor(serviceName, config) {
25
+ this.serviceName = serviceName;
26
+ this.snsClient = createSNSClient(config || {});
27
+ // Topic ARN can be passed directly or constructed from env vars
28
+ const stage = process.env.STAGE || process.env.NODE_ENV || 'test';
29
+ const region = config?.region || process.env.AWS_REGION || 'us-east-1';
30
+ const accountId = process.env.AWS_ACCOUNT_ID || '000000000000';
31
+ this.topicArn = config?.topicArn ||
32
+ process.env.POLICY_SYNC_TOPIC_ARN ||
33
+ `arn:aws:sns:${region}:${accountId}:qshelter-${stage}-policy-sync`;
34
+ }
35
+ /**
36
+ * Publish a policy event to SNS
37
+ */
38
+ async publish(eventType, data, meta) {
39
+ const eventId = crypto.randomUUID();
40
+ const event = {
41
+ eventType,
42
+ eventId,
43
+ timestamp: new Date().toISOString(),
44
+ source: this.serviceName,
45
+ tenantId: meta?.tenantId,
46
+ data,
47
+ metadata: {
48
+ correlationId: meta?.correlationId || eventId,
49
+ userId: meta?.userId,
50
+ },
51
+ };
52
+ const command = new PublishCommand({
53
+ TopicArn: this.topicArn,
54
+ Message: JSON.stringify(event),
55
+ MessageAttributes: {
56
+ eventType: {
57
+ DataType: 'String',
58
+ StringValue: eventType,
59
+ },
60
+ source: {
61
+ DataType: 'String',
62
+ StringValue: this.serviceName,
63
+ },
64
+ },
65
+ });
66
+ const result = await this.snsClient.send(command);
67
+ console.log(`[PolicyEventPublisher] Published ${eventType} event to SNS`, {
68
+ topicArn: this.topicArn,
69
+ messageId: result.MessageId,
70
+ eventId,
71
+ });
72
+ return result.MessageId || '';
73
+ }
74
+ /**
75
+ * Publish role created event
76
+ */
77
+ async publishRoleCreated(role, meta) {
78
+ return this.publish(PolicyEventType.ROLE_CREATED, role, meta);
79
+ }
80
+ /**
81
+ * Publish role updated event
82
+ */
83
+ async publishRoleUpdated(role, meta) {
84
+ return this.publish(PolicyEventType.ROLE_UPDATED, role, meta);
85
+ }
86
+ /**
87
+ * Publish role deleted event
88
+ */
89
+ async publishRoleDeleted(roleId, roleName, meta) {
90
+ return this.publish(PolicyEventType.ROLE_DELETED, { roleId, roleName }, meta);
91
+ }
92
+ /**
93
+ * Publish permission created event
94
+ */
95
+ async publishPermissionCreated(permission, meta) {
96
+ return this.publish(PolicyEventType.PERMISSION_CREATED, permission, meta);
97
+ }
98
+ /**
99
+ * Publish permission updated event
100
+ */
101
+ async publishPermissionUpdated(permission, meta) {
102
+ return this.publish(PolicyEventType.PERMISSION_UPDATED, permission, meta);
103
+ }
104
+ /**
105
+ * Publish permission deleted event
106
+ */
107
+ async publishPermissionDeleted(permissionId, meta) {
108
+ return this.publish(PolicyEventType.PERMISSION_DELETED, { permissionId }, meta);
109
+ }
110
+ /**
111
+ * Publish role permission assigned event
112
+ */
113
+ async publishRolePermissionAssigned(data, meta) {
114
+ return this.publish(PolicyEventType.ROLE_PERMISSION_ASSIGNED, data, meta);
115
+ }
116
+ /**
117
+ * Publish role permission revoked event
118
+ */
119
+ async publishRolePermissionRevoked(data, meta) {
120
+ return this.publish(PolicyEventType.ROLE_PERMISSION_REVOKED, data, meta);
121
+ }
122
+ /**
123
+ * Publish full sync requested event
124
+ */
125
+ async publishFullSyncRequested(requestedBy, meta) {
126
+ return this.publish(PolicyEventType.FULL_SYNC_REQUESTED, { requestedBy }, meta);
127
+ }
128
+ }
@@ -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
- "role",
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 = ["paymentPlan"];
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@valentine-efagene/qshelter-common",
3
- "version": "2.0.87",
3
+ "version": "2.0.89",
4
4
  "description": "Shared database schemas and utilities for QShelter services",
5
5
  "main": "dist/src/index.js",
6
6
  "types": "dist/src/index.d.ts",
@@ -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 @id @default(cuid())
301
- email String @unique
306
+ id String @id @default(cuid())
307
+ email String @unique
302
308
  password String?
303
- phone String? @unique
309
+ phone String? @unique
304
310
  firstName String?
305
311
  lastName String?
306
- isActive Boolean @default(true)
307
- isEmailVerified Boolean @default(false)
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? @relation(fields: [tenantId], references: [id], onDelete: SetNull)
312
- // Support multiple roles via explicit join table `UserRole`
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? @unique
315
- wallet Wallet? @relation(fields: [walletId], references: [id])
316
- createdAt DateTime @default(now())
317
- updatedAt DateTime @updatedAt
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 @id @default(cuid())
372
- name String @unique
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 @default(now())
377
- updatedAt DateTime @updatedAt
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 @unique
407
+ name String // Descriptive name: "Read Users", "Manage Properties"
385
408
  description String?
386
- resource String
387
- action String
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([resource, action])
393
- @@index([resource])
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[]