@venturialstd/tenant 0.0.1 → 0.0.2
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/README.md +974 -974
- package/dist/tenant.module.js +1 -1
- package/dist/tenant.module.js.map +1 -1
- package/package.json +42 -42
package/README.md
CHANGED
|
@@ -1,974 +1,974 @@
|
|
|
1
|
-
# @venturialstd/tenant
|
|
2
|
-
|
|
3
|
-
A comprehensive **Multi-Tenant SaaS Platform Package**, developed by **Venturial**, that provides complete tenant management capabilities with user isolation for building scalable SaaS applications. This package follows the Venturial architecture pattern and integrates seamlessly with TypeORM and NestJS.
|
|
4
|
-
|
|
5
|
-
---
|
|
6
|
-
|
|
7
|
-
## Features
|
|
8
|
-
|
|
9
|
-
- **Multi-Tenant Management**: Create and manage multiple tenants with isolated data
|
|
10
|
-
- **User-Tenant Relationships**: Complete user isolation per tenant with role-based access control
|
|
11
|
-
- **Role-Based Access Control (RBAC)**: Owner, Admin, Member, and Viewer roles
|
|
12
|
-
- **Subscription Plans**: Support for Free, Starter, Professional, and Enterprise plans
|
|
13
|
-
- **Trial Period**: Configurable trial periods for new tenants
|
|
14
|
-
- **Custom Domains**: Optional custom domain support for tenants
|
|
15
|
-
- **Invitation System**: Invite users to tenants with pending invitation management
|
|
16
|
-
- **Ownership Transfer**: Transfer tenant ownership between users
|
|
17
|
-
- **User Suspension**: Suspend and reactivate user access to tenants
|
|
18
|
-
- **Guards & Decorators**: Built-in guards and decorators for easy tenant isolation
|
|
19
|
-
- **CRUD Operations**: Full TypeORM CRUD service integration
|
|
20
|
-
- **Settings Management**: Dynamic tenant-level settings with `@venturialstd/core`
|
|
21
|
-
|
|
22
|
-
---
|
|
23
|
-
|
|
24
|
-
## Installation
|
|
25
|
-
|
|
26
|
-
```bash
|
|
27
|
-
npm install @venturialstd/tenant
|
|
28
|
-
# or
|
|
29
|
-
yarn add @venturialstd/tenant
|
|
30
|
-
```
|
|
31
|
-
|
|
32
|
-
---
|
|
33
|
-
|
|
34
|
-
## Basic Usage
|
|
35
|
-
|
|
36
|
-
### 1. Import the TenantModule in your application
|
|
37
|
-
|
|
38
|
-
```ts
|
|
39
|
-
import { Module } from '@nestjs/common';
|
|
40
|
-
import { TenantModule } from '@venturialstd/tenant';
|
|
41
|
-
|
|
42
|
-
@Module({
|
|
43
|
-
imports: [
|
|
44
|
-
TenantModule,
|
|
45
|
-
// ... other modules
|
|
46
|
-
],
|
|
47
|
-
})
|
|
48
|
-
export class AppModule {}
|
|
49
|
-
```
|
|
50
|
-
|
|
51
|
-
### 2. Use the services in your application
|
|
52
|
-
|
|
53
|
-
```ts
|
|
54
|
-
import { Injectable } from '@nestjs/common';
|
|
55
|
-
import { TenantService, TenantUserService, TenantUserRole } from '@venturialstd/tenant';
|
|
56
|
-
|
|
57
|
-
@Injectable()
|
|
58
|
-
export class YourService {
|
|
59
|
-
constructor(
|
|
60
|
-
private readonly tenantService: TenantService,
|
|
61
|
-
private readonly tenantUserService: TenantUserService,
|
|
62
|
-
) {}
|
|
63
|
-
|
|
64
|
-
async createNewTenant(userId: string) {
|
|
65
|
-
// Create tenant
|
|
66
|
-
const tenant = await this.tenantService.createTenant(
|
|
67
|
-
'Acme Corp',
|
|
68
|
-
'acme-corp',
|
|
69
|
-
'acme.example.com',
|
|
70
|
-
'A leading software company',
|
|
71
|
-
userId
|
|
72
|
-
);
|
|
73
|
-
|
|
74
|
-
// Add user as primary owner
|
|
75
|
-
await this.tenantUserService.addUserToTenant(
|
|
76
|
-
tenant.id,
|
|
77
|
-
userId,
|
|
78
|
-
TenantUserRole.OWNER
|
|
79
|
-
);
|
|
80
|
-
|
|
81
|
-
await this.tenantUserService.setPrimaryOwner(tenant.id, userId);
|
|
82
|
-
|
|
83
|
-
return tenant;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
async inviteUserToTenant(tenantId: string, userId: string, invitedBy: string) {
|
|
87
|
-
return this.tenantUserService.inviteUserToTenant(
|
|
88
|
-
tenantId,
|
|
89
|
-
userId,
|
|
90
|
-
TenantUserRole.MEMBER,
|
|
91
|
-
invitedBy
|
|
92
|
-
);
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
```
|
|
96
|
-
|
|
97
|
-
### 3. Use Guards and Decorators for tenant isolation
|
|
98
|
-
|
|
99
|
-
```ts
|
|
100
|
-
import { Controller, Get, UseGuards } from '@nestjs/common';
|
|
101
|
-
import {
|
|
102
|
-
TenantGuard,
|
|
103
|
-
TenantRoleGuard,
|
|
104
|
-
TenantId,
|
|
105
|
-
UserId,
|
|
106
|
-
Roles,
|
|
107
|
-
TenantUserRole
|
|
108
|
-
} from '@venturialstd/tenant';
|
|
109
|
-
|
|
110
|
-
@Controller('data')
|
|
111
|
-
@UseGuards(TenantGuard) // Ensure user has access to tenant
|
|
112
|
-
export class DataController {
|
|
113
|
-
constructor(private readonly dataService: DataService) {}
|
|
114
|
-
|
|
115
|
-
@Get()
|
|
116
|
-
async getData(@TenantId() tenantId: string, @UserId() userId: string) {
|
|
117
|
-
// tenantId and userId are automatically extracted and validated
|
|
118
|
-
return this.dataService.getTenantData(tenantId, userId);
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
@Delete(':id')
|
|
122
|
-
@Roles(TenantUserRole.OWNER, TenantUserRole.ADMIN) // Only owners and admins
|
|
123
|
-
@UseGuards(TenantRoleGuard) // Enforce role-based access
|
|
124
|
-
async deleteData(@TenantId() tenantId: string, @Param('id') id: string) {
|
|
125
|
-
return this.dataService.deleteData(tenantId, id);
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
```
|
|
129
|
-
|
|
130
|
-
---
|
|
131
|
-
|
|
132
|
-
## Entity Structures
|
|
133
|
-
|
|
134
|
-
### Tenant Entity
|
|
135
|
-
|
|
136
|
-
The `Tenant` entity includes the following fields:
|
|
137
|
-
|
|
138
|
-
- **id**: UUID primary key
|
|
139
|
-
- **name**: Tenant name (required, unique)
|
|
140
|
-
- **slug**: URL-friendly identifier (required, unique)
|
|
141
|
-
- **domain**: Custom domain (optional)
|
|
142
|
-
- **description**: Tenant description (optional)
|
|
143
|
-
- **isActive**: Active/inactive status (default: true)
|
|
144
|
-
- **settings**: JSON field for tenant-specific settings
|
|
145
|
-
- **ownerId**: Reference to the owner user
|
|
146
|
-
- **plan**: Subscription plan (free, starter, professional, enterprise)
|
|
147
|
-
- **trialEndsAt**: Trial period expiration date
|
|
148
|
-
- **subscriptionEndsAt**: Subscription expiration date
|
|
149
|
-
- **createdAt**: Creation timestamp
|
|
150
|
-
- **updatedAt**: Last update timestamp
|
|
151
|
-
|
|
152
|
-
### TenantUser Entity
|
|
153
|
-
|
|
154
|
-
The `TenantUser` entity manages user-tenant relationships:
|
|
155
|
-
|
|
156
|
-
- **id**: UUID primary key
|
|
157
|
-
- **tenantId**: Reference to tenant (indexed)
|
|
158
|
-
- **userId**: Reference to user (indexed)
|
|
159
|
-
- **role**: User role (OWNER, ADMIN, MEMBER, VIEWER)
|
|
160
|
-
- **status**: User status (ACTIVE, INVITED, SUSPENDED)
|
|
161
|
-
- **isPrimary**: Whether user is the primary owner
|
|
162
|
-
- **invitedBy**: User who sent the invitation
|
|
163
|
-
- **invitedAt**: Invitation timestamp
|
|
164
|
-
- **joinedAt**: Join timestamp
|
|
165
|
-
- **permissions**: JSON field for custom permissions
|
|
166
|
-
- **createdAt**: Creation timestamp
|
|
167
|
-
- **updatedAt**: Last update timestamp
|
|
168
|
-
|
|
169
|
-
**Unique constraint**: (tenantId, userId) - A user can only be added once per tenant
|
|
170
|
-
|
|
171
|
-
---
|
|
172
|
-
|
|
173
|
-
## Available Methods
|
|
174
|
-
|
|
175
|
-
### TenantService Methods
|
|
176
|
-
|
|
177
|
-
#### `createTenant(name, slug, domain?, description?, ownerId?)`
|
|
178
|
-
Creates a new tenant with validation and default settings.
|
|
179
|
-
|
|
180
|
-
```ts
|
|
181
|
-
const tenant = await tenantService.createTenant(
|
|
182
|
-
'My Company',
|
|
183
|
-
'my-company',
|
|
184
|
-
'mycompany.com',
|
|
185
|
-
'Company description',
|
|
186
|
-
'owner-uuid'
|
|
187
|
-
);
|
|
188
|
-
```
|
|
189
|
-
|
|
190
|
-
#### `getTenantBySlug(slug)`
|
|
191
|
-
Retrieve a tenant by its slug.
|
|
192
|
-
|
|
193
|
-
```ts
|
|
194
|
-
const tenant = await tenantService.getTenantBySlug('my-company');
|
|
195
|
-
```
|
|
196
|
-
|
|
197
|
-
#### `getTenantByDomain(domain)`
|
|
198
|
-
Retrieve a tenant by its custom domain.
|
|
199
|
-
|
|
200
|
-
```ts
|
|
201
|
-
const tenant = await tenantService.getTenantByDomain('mycompany.com');
|
|
202
|
-
```
|
|
203
|
-
|
|
204
|
-
#### `updateTenantSettings(tenantId, settings)`
|
|
205
|
-
Update tenant-specific settings.
|
|
206
|
-
|
|
207
|
-
```ts
|
|
208
|
-
await tenantService.updateTenantSettings('tenant-uuid', {
|
|
209
|
-
theme: 'dark',
|
|
210
|
-
language: 'en',
|
|
211
|
-
features: { api: true }
|
|
212
|
-
});
|
|
213
|
-
```
|
|
214
|
-
|
|
215
|
-
#### `setTenantStatus(tenantId, isActive)`
|
|
216
|
-
Activate or deactivate a tenant.
|
|
217
|
-
|
|
218
|
-
```ts
|
|
219
|
-
await tenantService.setTenantStatus('tenant-uuid', false); // Deactivate
|
|
220
|
-
await tenantService.setTenantStatus('tenant-uuid', true); // Activate
|
|
221
|
-
```
|
|
222
|
-
|
|
223
|
-
#### `updateTenantPlan(tenantId, plan, subscriptionEndsAt?)`
|
|
224
|
-
Update the tenant's subscription plan.
|
|
225
|
-
|
|
226
|
-
```ts
|
|
227
|
-
const endDate = new Date();
|
|
228
|
-
endDate.setMonth(endDate.getMonth() + 1);
|
|
229
|
-
|
|
230
|
-
await tenantService.updateTenantPlan(
|
|
231
|
-
'tenant-uuid',
|
|
232
|
-
'professional',
|
|
233
|
-
endDate
|
|
234
|
-
);
|
|
235
|
-
```
|
|
236
|
-
|
|
237
|
-
#### `getActiveTenants()`
|
|
238
|
-
Get all active tenants.
|
|
239
|
-
|
|
240
|
-
```ts
|
|
241
|
-
const activeTenants = await tenantService.getActiveTenants();
|
|
242
|
-
```
|
|
243
|
-
|
|
244
|
-
#### `getTenantsByOwner(ownerId)`
|
|
245
|
-
Get all tenants owned by a specific user.
|
|
246
|
-
|
|
247
|
-
```ts
|
|
248
|
-
const tenants = await tenantService.getTenantsByOwner('owner-uuid');
|
|
249
|
-
```
|
|
250
|
-
|
|
251
|
-
#### `isInTrialPeriod(tenantId)`
|
|
252
|
-
Check if a tenant is currently in trial period.
|
|
253
|
-
|
|
254
|
-
```ts
|
|
255
|
-
const inTrial = await tenantService.isInTrialPeriod('tenant-uuid');
|
|
256
|
-
```
|
|
257
|
-
|
|
258
|
-
#### `hasActiveSubscription(tenantId)`
|
|
259
|
-
Check if a tenant has an active subscription.
|
|
260
|
-
|
|
261
|
-
```ts
|
|
262
|
-
const isActive = await tenantService.hasActiveSubscription('tenant-uuid');
|
|
263
|
-
```
|
|
264
|
-
|
|
265
|
-
### TenantUserService Methods
|
|
266
|
-
|
|
267
|
-
#### `addUserToTenant(tenantId, userId, role, invitedBy?)`
|
|
268
|
-
Add a user to a tenant with a specific role.
|
|
269
|
-
|
|
270
|
-
```ts
|
|
271
|
-
await tenantUserService.addUserToTenant(
|
|
272
|
-
'tenant-uuid',
|
|
273
|
-
'user-uuid',
|
|
274
|
-
TenantUserRole.MEMBER,
|
|
275
|
-
'inviter-uuid'
|
|
276
|
-
);
|
|
277
|
-
```
|
|
278
|
-
|
|
279
|
-
#### `inviteUserToTenant(tenantId, userId, role, invitedBy)`
|
|
280
|
-
Invite a user to a tenant (creates pending invitation).
|
|
281
|
-
|
|
282
|
-
```ts
|
|
283
|
-
await tenantUserService.inviteUserToTenant(
|
|
284
|
-
'tenant-uuid',
|
|
285
|
-
'user-uuid',
|
|
286
|
-
TenantUserRole.ADMIN,
|
|
287
|
-
'inviter-uuid'
|
|
288
|
-
);
|
|
289
|
-
```
|
|
290
|
-
|
|
291
|
-
#### `acceptInvitation(tenantId, userId)`
|
|
292
|
-
Accept a pending tenant invitation.
|
|
293
|
-
|
|
294
|
-
```ts
|
|
295
|
-
await tenantUserService.acceptInvitation('tenant-uuid', 'user-uuid');
|
|
296
|
-
```
|
|
297
|
-
|
|
298
|
-
#### `removeUserFromTenant(tenantId, userId)`
|
|
299
|
-
Remove a user from a tenant (cannot remove primary owner).
|
|
300
|
-
|
|
301
|
-
```ts
|
|
302
|
-
await tenantUserService.removeUserFromTenant('tenant-uuid', 'user-uuid');
|
|
303
|
-
```
|
|
304
|
-
|
|
305
|
-
#### `updateUserRole(tenantId, userId, newRole)`
|
|
306
|
-
Update a user's role in a tenant.
|
|
307
|
-
|
|
308
|
-
```ts
|
|
309
|
-
await tenantUserService.updateUserRole(
|
|
310
|
-
'tenant-uuid',
|
|
311
|
-
'user-uuid',
|
|
312
|
-
TenantUserRole.ADMIN
|
|
313
|
-
);
|
|
314
|
-
```
|
|
315
|
-
|
|
316
|
-
#### `getUserTenants(userId)`
|
|
317
|
-
Get all tenants a user belongs to.
|
|
318
|
-
|
|
319
|
-
```ts
|
|
320
|
-
const userTenants = await tenantUserService.getUserTenants('user-uuid');
|
|
321
|
-
```
|
|
322
|
-
|
|
323
|
-
#### `getTenantUsers(tenantId)`
|
|
324
|
-
Get all users in a tenant (including invited).
|
|
325
|
-
|
|
326
|
-
```ts
|
|
327
|
-
const users = await tenantUserService.getTenantUsers('tenant-uuid');
|
|
328
|
-
```
|
|
329
|
-
|
|
330
|
-
#### `getActiveTenantUsers(tenantId)`
|
|
331
|
-
Get all active users in a tenant.
|
|
332
|
-
|
|
333
|
-
```ts
|
|
334
|
-
const activeUsers = await tenantUserService.getActiveTenantUsers('tenant-uuid');
|
|
335
|
-
```
|
|
336
|
-
|
|
337
|
-
#### `hasAccess(tenantId, userId)`
|
|
338
|
-
Check if a user has access to a tenant.
|
|
339
|
-
|
|
340
|
-
```ts
|
|
341
|
-
const hasAccess = await tenantUserService.hasAccess('tenant-uuid', 'user-uuid');
|
|
342
|
-
```
|
|
343
|
-
|
|
344
|
-
#### `hasRole(tenantId, userId, role)`
|
|
345
|
-
Check if a user has a specific role in a tenant.
|
|
346
|
-
|
|
347
|
-
```ts
|
|
348
|
-
const isAdmin = await tenantUserService.hasRole(
|
|
349
|
-
'tenant-uuid',
|
|
350
|
-
'user-uuid',
|
|
351
|
-
TenantUserRole.ADMIN
|
|
352
|
-
);
|
|
353
|
-
```
|
|
354
|
-
|
|
355
|
-
#### `isAdminOrOwner(tenantId, userId)`
|
|
356
|
-
Check if a user is an admin or owner in a tenant.
|
|
357
|
-
|
|
358
|
-
```ts
|
|
359
|
-
const isAdminOrOwner = await tenantUserService.isAdminOrOwner('tenant-uuid', 'user-uuid');
|
|
360
|
-
```
|
|
361
|
-
|
|
362
|
-
#### `getUserRole(tenantId, userId)`
|
|
363
|
-
Get a user's role in a tenant.
|
|
364
|
-
|
|
365
|
-
```ts
|
|
366
|
-
const role = await tenantUserService.getUserRole('tenant-uuid', 'user-uuid');
|
|
367
|
-
```
|
|
368
|
-
|
|
369
|
-
#### `setPrimaryOwner(tenantId, userId)`
|
|
370
|
-
Set a user as the primary owner of a tenant.
|
|
371
|
-
|
|
372
|
-
```ts
|
|
373
|
-
await tenantUserService.setPrimaryOwner('tenant-uuid', 'user-uuid');
|
|
374
|
-
```
|
|
375
|
-
|
|
376
|
-
#### `transferOwnership(tenantId, fromUserId, toUserId)`
|
|
377
|
-
Transfer tenant ownership between users.
|
|
378
|
-
|
|
379
|
-
```ts
|
|
380
|
-
await tenantUserService.transferOwnership(
|
|
381
|
-
'tenant-uuid',
|
|
382
|
-
'current-owner-uuid',
|
|
383
|
-
'new-owner-uuid'
|
|
384
|
-
);
|
|
385
|
-
```
|
|
386
|
-
|
|
387
|
-
#### `getUserInvitations(userId)`
|
|
388
|
-
Get all pending invitations for a user.
|
|
389
|
-
|
|
390
|
-
```ts
|
|
391
|
-
const invitations = await tenantUserService.getUserInvitations('user-uuid');
|
|
392
|
-
```
|
|
393
|
-
|
|
394
|
-
#### `suspendUser(tenantId, userId)`
|
|
395
|
-
Suspend a user's access to a tenant.
|
|
396
|
-
|
|
397
|
-
```ts
|
|
398
|
-
await tenantUserService.suspendUser('tenant-uuid', 'user-uuid');
|
|
399
|
-
```
|
|
400
|
-
|
|
401
|
-
#### `reactivateUser(tenantId, userId)`
|
|
402
|
-
Reactivate a suspended user.
|
|
403
|
-
|
|
404
|
-
```ts
|
|
405
|
-
await tenantUserService.reactivateUser('tenant-uuid', 'user-uuid');
|
|
406
|
-
```
|
|
407
|
-
|
|
408
|
-
---
|
|
409
|
-
|
|
410
|
-
## Guards and Decorators
|
|
411
|
-
|
|
412
|
-
### TenantGuard
|
|
413
|
-
|
|
414
|
-
Enforces tenant access control by validating that the authenticated user has access to the requested tenant.
|
|
415
|
-
|
|
416
|
-
```ts
|
|
417
|
-
@Controller('api')
|
|
418
|
-
@UseGuards(TenantGuard)
|
|
419
|
-
export class ApiController {
|
|
420
|
-
@Get('data')
|
|
421
|
-
async getData(@TenantId() tenantId: string) {
|
|
422
|
-
// User access to tenant is already validated
|
|
423
|
-
return this.service.getData(tenantId);
|
|
424
|
-
}
|
|
425
|
-
}
|
|
426
|
-
```
|
|
427
|
-
|
|
428
|
-
### TenantRoleGuard
|
|
429
|
-
|
|
430
|
-
Enforces role-based access control within a tenant. Use with `@Roles()` decorator.
|
|
431
|
-
|
|
432
|
-
```ts
|
|
433
|
-
@Controller('admin')
|
|
434
|
-
@UseGuards(TenantGuard, TenantRoleGuard)
|
|
435
|
-
export class AdminController {
|
|
436
|
-
@Delete('user/:userId')
|
|
437
|
-
@Roles(TenantUserRole.OWNER, TenantUserRole.ADMIN)
|
|
438
|
-
async removeUser(@TenantId() tenantId: string, @Param('userId') userId: string) {
|
|
439
|
-
// Only owners and admins can access this endpoint
|
|
440
|
-
return this.service.removeUser(tenantId, userId);
|
|
441
|
-
}
|
|
442
|
-
}
|
|
443
|
-
```
|
|
444
|
-
|
|
445
|
-
### @TenantId()
|
|
446
|
-
|
|
447
|
-
Extracts the tenant ID from the request. Checks in order:
|
|
448
|
-
1. `request.tenantId`
|
|
449
|
-
2. `request.headers['x-tenant-id']`
|
|
450
|
-
3. `request.params.tenantId`
|
|
451
|
-
4. `request.query.tenantId`
|
|
452
|
-
|
|
453
|
-
```ts
|
|
454
|
-
@Get()
|
|
455
|
-
async getData(@TenantId() tenantId: string) {
|
|
456
|
-
return this.service.getData(tenantId);
|
|
457
|
-
}
|
|
458
|
-
```
|
|
459
|
-
|
|
460
|
-
### @UserId()
|
|
461
|
-
|
|
462
|
-
Extracts the user ID from the authenticated request.
|
|
463
|
-
|
|
464
|
-
```ts
|
|
465
|
-
@Get('profile')
|
|
466
|
-
async getProfile(@UserId() userId: string, @TenantId() tenantId: string) {
|
|
467
|
-
return this.service.getUserProfile(tenantId, userId);
|
|
468
|
-
}
|
|
469
|
-
```
|
|
470
|
-
|
|
471
|
-
### @TenantContext()
|
|
472
|
-
|
|
473
|
-
Extracts the complete tenant context including tenantId, userId, role, and user object.
|
|
474
|
-
|
|
475
|
-
```ts
|
|
476
|
-
@Get()
|
|
477
|
-
async getData(@TenantContext() context: { tenantId: string, userId: string, role: TenantUserRole, user: any }) {
|
|
478
|
-
return this.service.getData(context);
|
|
479
|
-
}
|
|
480
|
-
```
|
|
481
|
-
|
|
482
|
-
### @Roles(...roles)
|
|
483
|
-
|
|
484
|
-
Specifies required roles for accessing an endpoint. Must be used with `TenantRoleGuard`.
|
|
485
|
-
|
|
486
|
-
```ts
|
|
487
|
-
@Post('invite')
|
|
488
|
-
@Roles(TenantUserRole.OWNER, TenantUserRole.ADMIN)
|
|
489
|
-
@UseGuards(TenantGuard, TenantRoleGuard)
|
|
490
|
-
async inviteUser(@TenantId() tenantId: string, @Body() dto: InviteUserDto) {
|
|
491
|
-
return this.service.inviteUser(tenantId, dto);
|
|
492
|
-
}
|
|
493
|
-
```
|
|
494
|
-
|
|
495
|
-
---
|
|
496
|
-
|
|
497
|
-
## Configuration Settings
|
|
498
|
-
|
|
499
|
-
The module provides the following configurable settings through `SettingsService`:
|
|
500
|
-
|
|
501
|
-
### General Settings
|
|
502
|
-
- **ENABLED**: Enable/disable the tenant module (boolean)
|
|
503
|
-
- **MAX_TENANTS**: Maximum number of tenants allowed (0 = unlimited)
|
|
504
|
-
- **DEFAULT_PLAN**: Default subscription plan for new tenants
|
|
505
|
-
- **TRIAL_DAYS**: Number of trial days for new tenants (default: 14)
|
|
506
|
-
|
|
507
|
-
### Feature Settings
|
|
508
|
-
- **CUSTOM_DOMAIN**: Allow custom domains for tenants
|
|
509
|
-
- **API_ACCESS**: Allow API access for tenants
|
|
510
|
-
|
|
511
|
-
---
|
|
512
|
-
|
|
513
|
-
## Enums and Types
|
|
514
|
-
|
|
515
|
-
### TenantUserRole
|
|
516
|
-
```ts
|
|
517
|
-
enum TenantUserRole {
|
|
518
|
-
OWNER = 'owner',
|
|
519
|
-
ADMIN = 'admin',
|
|
520
|
-
MEMBER = 'member',
|
|
521
|
-
VIEWER = 'viewer',
|
|
522
|
-
}
|
|
523
|
-
```
|
|
524
|
-
|
|
525
|
-
### TenantUserStatus
|
|
526
|
-
```ts
|
|
527
|
-
enum TenantUserStatus {
|
|
528
|
-
ACTIVE = 'active',
|
|
529
|
-
INVITED = 'invited',
|
|
530
|
-
SUSPENDED = 'suspended',
|
|
531
|
-
}
|
|
532
|
-
```
|
|
533
|
-
|
|
534
|
-
### TENANT_PLAN
|
|
535
|
-
```ts
|
|
536
|
-
enum TENANT_PLAN {
|
|
537
|
-
FREE = 'free',
|
|
538
|
-
STARTER = 'starter',
|
|
539
|
-
PROFESSIONAL = 'professional',
|
|
540
|
-
ENTERPRISE = 'enterprise',
|
|
541
|
-
}
|
|
542
|
-
```
|
|
543
|
-
|
|
544
|
-
### TENANT_STATUS
|
|
545
|
-
```ts
|
|
546
|
-
enum TENANT_STATUS {
|
|
547
|
-
ACTIVE = 'active',
|
|
548
|
-
INACTIVE = 'inactive',
|
|
549
|
-
SUSPENDED = 'suspended',
|
|
550
|
-
TRIAL = 'trial',
|
|
551
|
-
}
|
|
552
|
-
```
|
|
553
|
-
|
|
554
|
-
---
|
|
555
|
-
|
|
556
|
-
## Integration with @venturialstd/core
|
|
557
|
-
|
|
558
|
-
This package uses `@venturialstd/core` for:
|
|
559
|
-
- **SharedModule**: Core functionality and configuration
|
|
560
|
-
- **SettingsService**: Dynamic settings management
|
|
561
|
-
- **AppLogger**: Structured logging
|
|
562
|
-
|
|
563
|
-
---
|
|
564
|
-
|
|
565
|
-
## TypeORM Integration
|
|
566
|
-
|
|
567
|
-
The package extends `TypeOrmCrudService` from `@dataui/crud-typeorm`, providing:
|
|
568
|
-
- Standard CRUD operations
|
|
569
|
-
- Query builders
|
|
570
|
-
- Pagination support
|
|
571
|
-
- Filtering and sorting capabilities
|
|
572
|
-
|
|
573
|
-
---
|
|
574
|
-
|
|
575
|
-
## Example: Complete SaaS Implementation with User Isolation
|
|
576
|
-
|
|
577
|
-
```ts
|
|
578
|
-
import { Module } from '@nestjs/common';
|
|
579
|
-
import { TypeOrmModule } from '@nestjs/typeorm';
|
|
580
|
-
import { TenantModule, TenantService, TenantUserService } from '@venturialstd/tenant';
|
|
581
|
-
|
|
582
|
-
@Module({
|
|
583
|
-
imports: [
|
|
584
|
-
TypeOrmModule.forRoot({
|
|
585
|
-
// Your database configuration
|
|
586
|
-
}),
|
|
587
|
-
TenantModule,
|
|
588
|
-
],
|
|
589
|
-
})
|
|
590
|
-
export class AppModule {}
|
|
591
|
-
|
|
592
|
-
// Service for onboarding new tenants
|
|
593
|
-
@Injectable()
|
|
594
|
-
export class SaasService {
|
|
595
|
-
constructor(
|
|
596
|
-
private readonly tenantService: TenantService,
|
|
597
|
-
private readonly tenantUserService: TenantUserService,
|
|
598
|
-
) {}
|
|
599
|
-
|
|
600
|
-
async onboardNewTenant(data: any, ownerId: string) {
|
|
601
|
-
// Create tenant
|
|
602
|
-
const tenant = await this.tenantService.createTenant(
|
|
603
|
-
data.companyName,
|
|
604
|
-
data.slug,
|
|
605
|
-
data.domain,
|
|
606
|
-
data.description,
|
|
607
|
-
ownerId
|
|
608
|
-
);
|
|
609
|
-
|
|
610
|
-
// Add owner to tenant
|
|
611
|
-
await this.tenantUserService.addUserToTenant(
|
|
612
|
-
tenant.id,
|
|
613
|
-
ownerId,
|
|
614
|
-
TenantUserRole.OWNER
|
|
615
|
-
);
|
|
616
|
-
|
|
617
|
-
// Set as primary owner
|
|
618
|
-
await this.tenantUserService.setPrimaryOwner(tenant.id, ownerId);
|
|
619
|
-
|
|
620
|
-
// Configure tenant settings
|
|
621
|
-
await this.tenantService.updateTenantSettings(tenant.id, {
|
|
622
|
-
theme: data.preferences?.theme || 'light',
|
|
623
|
-
locale: data.preferences?.locale || 'en',
|
|
624
|
-
notifications: true,
|
|
625
|
-
});
|
|
626
|
-
|
|
627
|
-
return tenant;
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
async inviteTeamMember(
|
|
631
|
-
tenantId: string,
|
|
632
|
-
userId: string,
|
|
633
|
-
invitedBy: string,
|
|
634
|
-
role: TenantUserRole = TenantUserRole.MEMBER
|
|
635
|
-
) {
|
|
636
|
-
// Verify inviter is admin or owner
|
|
637
|
-
const isAdmin = await this.tenantUserService.isAdminOrOwner(tenantId, invitedBy);
|
|
638
|
-
if (!isAdmin) {
|
|
639
|
-
throw new ForbiddenException('Only admins can invite team members');
|
|
640
|
-
}
|
|
641
|
-
|
|
642
|
-
// Invite user
|
|
643
|
-
await this.tenantUserService.inviteUserToTenant(
|
|
644
|
-
tenantId,
|
|
645
|
-
userId,
|
|
646
|
-
role,
|
|
647
|
-
invitedBy
|
|
648
|
-
);
|
|
649
|
-
|
|
650
|
-
// Send invitation email (implement your email service)
|
|
651
|
-
// await this.emailService.sendInvitation(userId, tenantId);
|
|
652
|
-
}
|
|
653
|
-
|
|
654
|
-
async checkTenantAccess(tenantId: string, userId: string) {
|
|
655
|
-
const tenant = await this.tenantService.repo.findOne({
|
|
656
|
-
where: { id: tenantId }
|
|
657
|
-
});
|
|
658
|
-
|
|
659
|
-
if (!tenant.isActive) {
|
|
660
|
-
throw new UnauthorizedException('Tenant is inactive');
|
|
661
|
-
}
|
|
662
|
-
|
|
663
|
-
const hasAccess = await this.tenantUserService.hasAccess(tenantId, userId);
|
|
664
|
-
if (!hasAccess) {
|
|
665
|
-
throw new UnauthorizedException('No access to this tenant');
|
|
666
|
-
}
|
|
667
|
-
|
|
668
|
-
const hasSubscription = await this.tenantService.hasActiveSubscription(tenantId);
|
|
669
|
-
if (!hasSubscription) {
|
|
670
|
-
throw new UnauthorizedException('Subscription expired');
|
|
671
|
-
}
|
|
672
|
-
|
|
673
|
-
return true;
|
|
674
|
-
}
|
|
675
|
-
}
|
|
676
|
-
|
|
677
|
-
// Controller with tenant isolation
|
|
678
|
-
@Controller('projects')
|
|
679
|
-
@UseGuards(TenantGuard)
|
|
680
|
-
export class ProjectsController {
|
|
681
|
-
constructor(private readonly projectsService: ProjectsService) {}
|
|
682
|
-
|
|
683
|
-
@Get()
|
|
684
|
-
async list(@TenantId() tenantId: string, @UserId() userId: string) {
|
|
685
|
-
// Automatically scoped to tenant
|
|
686
|
-
return this.projectsService.findByTenant(tenantId);
|
|
687
|
-
}
|
|
688
|
-
|
|
689
|
-
@Post()
|
|
690
|
-
async create(
|
|
691
|
-
@TenantId() tenantId: string,
|
|
692
|
-
@UserId() userId: string,
|
|
693
|
-
@Body() dto: CreateProjectDto
|
|
694
|
-
) {
|
|
695
|
-
return this.projectsService.create(tenantId, userId, dto);
|
|
696
|
-
}
|
|
697
|
-
|
|
698
|
-
@Delete(':id')
|
|
699
|
-
@Roles(TenantUserRole.OWNER, TenantUserRole.ADMIN)
|
|
700
|
-
@UseGuards(TenantRoleGuard)
|
|
701
|
-
async delete(@TenantId() tenantId: string, @Param('id') id: string) {
|
|
702
|
-
// Only owners and admins can delete
|
|
703
|
-
return this.projectsService.delete(tenantId, id);
|
|
704
|
-
}
|
|
705
|
-
}
|
|
706
|
-
```
|
|
707
|
-
|
|
708
|
-
---
|
|
709
|
-
|
|
710
|
-
## Database Migrations
|
|
711
|
-
|
|
712
|
-
The package requires two database tables: `tenant` and `tenant_user`. Create migrations for both:
|
|
713
|
-
|
|
714
|
-
### Tenant Table Migration
|
|
715
|
-
|
|
716
|
-
```bash
|
|
717
|
-
npm run typeorm migration:create -- -n CreateTenant
|
|
718
|
-
```
|
|
719
|
-
|
|
720
|
-
```ts
|
|
721
|
-
import { MigrationInterface, QueryRunner, Table, TableIndex } from 'typeorm';
|
|
722
|
-
|
|
723
|
-
export class CreateTenant1234567890123 implements MigrationInterface {
|
|
724
|
-
public async up(queryRunner: QueryRunner): Promise<void> {
|
|
725
|
-
await queryRunner.createTable(
|
|
726
|
-
new Table({
|
|
727
|
-
name: 'tenant',
|
|
728
|
-
columns: [
|
|
729
|
-
{ name: 'id', type: 'uuid', isPrimary: true, generationStrategy: 'uuid', default: 'uuid_generate_v4()' },
|
|
730
|
-
{ name: 'name', type: 'varchar', isUnique: true },
|
|
731
|
-
{ name: 'slug', type: 'varchar', isUnique: true },
|
|
732
|
-
{ name: 'domain', type: 'varchar', isNullable: true },
|
|
733
|
-
{ name: 'description', type: 'text', isNullable: true },
|
|
734
|
-
{ name: 'isActive', type: 'boolean', default: true },
|
|
735
|
-
{ name: 'settings', type: 'jsonb', isNullable: true },
|
|
736
|
-
{ name: 'ownerId', type: 'varchar', isNullable: true },
|
|
737
|
-
{ name: 'plan', type: 'varchar', isNullable: true },
|
|
738
|
-
{ name: 'trialEndsAt', type: 'timestamptz', isNullable: true },
|
|
739
|
-
{ name: 'subscriptionEndsAt', type: 'timestamptz', isNullable: true },
|
|
740
|
-
{ name: 'createdAt', type: 'timestamptz', default: 'CURRENT_TIMESTAMP' },
|
|
741
|
-
{ name: 'updatedAt', type: 'timestamptz', default: 'CURRENT_TIMESTAMP' },
|
|
742
|
-
],
|
|
743
|
-
}),
|
|
744
|
-
true,
|
|
745
|
-
);
|
|
746
|
-
|
|
747
|
-
await queryRunner.createIndex('tenant', new TableIndex({ name: 'IDX_tenant_slug', columnNames: ['slug'] }));
|
|
748
|
-
await queryRunner.createIndex('tenant', new TableIndex({ name: 'IDX_tenant_domain', columnNames: ['domain'] }));
|
|
749
|
-
}
|
|
750
|
-
|
|
751
|
-
public async down(queryRunner: QueryRunner): Promise<void> {
|
|
752
|
-
await queryRunner.dropTable('tenant');
|
|
753
|
-
}
|
|
754
|
-
}
|
|
755
|
-
```
|
|
756
|
-
|
|
757
|
-
### TenantUser Table Migration
|
|
758
|
-
|
|
759
|
-
```bash
|
|
760
|
-
npm run typeorm migration:create -- -n CreateTenantUser
|
|
761
|
-
```
|
|
762
|
-
|
|
763
|
-
```ts
|
|
764
|
-
import { MigrationInterface, QueryRunner, Table, TableIndex } from 'typeorm';
|
|
765
|
-
|
|
766
|
-
export class CreateTenantUser1234567890124 implements MigrationInterface {
|
|
767
|
-
public async up(queryRunner: QueryRunner): Promise<void> {
|
|
768
|
-
await queryRunner.createTable(
|
|
769
|
-
new Table({
|
|
770
|
-
name: 'tenant_user',
|
|
771
|
-
columns: [
|
|
772
|
-
{ name: 'id', type: 'uuid', isPrimary: true, generationStrategy: 'uuid', default: 'uuid_generate_v4()' },
|
|
773
|
-
{ name: 'tenantId', type: 'uuid' },
|
|
774
|
-
{ name: 'userId', type: 'uuid' },
|
|
775
|
-
{ name: 'role', type: 'enum', enum: ['owner', 'admin', 'member', 'viewer'], default: "'member'" },
|
|
776
|
-
{ name: 'status', type: 'enum', enum: ['active', 'invited', 'suspended'], default: "'active'" },
|
|
777
|
-
{ name: 'isPrimary', type: 'boolean', default: false },
|
|
778
|
-
{ name: 'invitedBy', type: 'uuid', isNullable: true },
|
|
779
|
-
{ name: 'invitedAt', type: 'timestamptz', isNullable: true },
|
|
780
|
-
{ name: 'joinedAt', type: 'timestamptz', isNullable: true },
|
|
781
|
-
{ name: 'permissions', type: 'jsonb', isNullable: true },
|
|
782
|
-
{ name: 'createdAt', type: 'timestamptz', default: 'CURRENT_TIMESTAMP' },
|
|
783
|
-
{ name: 'updatedAt', type: 'timestamptz', default: 'CURRENT_TIMESTAMP' },
|
|
784
|
-
],
|
|
785
|
-
}),
|
|
786
|
-
true,
|
|
787
|
-
);
|
|
788
|
-
|
|
789
|
-
await queryRunner.createIndex('tenant_user', new TableIndex({ name: 'IDX_tenant_user_tenantId', columnNames: ['tenantId'] }));
|
|
790
|
-
await queryRunner.createIndex('tenant_user', new TableIndex({ name: 'IDX_tenant_user_userId', columnNames: ['userId'] }));
|
|
791
|
-
await queryRunner.createIndex('tenant_user', new TableIndex({ name: 'IDX_tenant_user_tenant_user', columnNames: ['tenantId', 'userId'], isUnique: true }));
|
|
792
|
-
}
|
|
793
|
-
|
|
794
|
-
public async down(queryRunner: QueryRunner): Promise<void> {
|
|
795
|
-
await queryRunner.dropTable('tenant_user');
|
|
796
|
-
}
|
|
797
|
-
}
|
|
798
|
-
```
|
|
799
|
-
|
|
800
|
-
Run migrations:
|
|
801
|
-
|
|
802
|
-
```bash
|
|
803
|
-
npm run typeorm migration:run
|
|
804
|
-
```
|
|
805
|
-
|
|
806
|
-
---
|
|
807
|
-
|
|
808
|
-
## Testing
|
|
809
|
-
|
|
810
|
-
The module includes an isolated NestJS test environment for testing all functionality without integrating it into your main application.
|
|
811
|
-
|
|
812
|
-
### Setup Test Environment
|
|
813
|
-
|
|
814
|
-
1. **Copy environment configuration**:
|
|
815
|
-
```bash
|
|
816
|
-
cd test
|
|
817
|
-
cp .env.example .env
|
|
818
|
-
```
|
|
819
|
-
|
|
820
|
-
2. **Configure your database** in `test/.env`:
|
|
821
|
-
```env
|
|
822
|
-
DB_HOST=localhost
|
|
823
|
-
DB_PORT=5432
|
|
824
|
-
DB_USERNAME=postgres
|
|
825
|
-
DB_PASSWORD=postgres
|
|
826
|
-
DB_DATABASE=tenant_test
|
|
827
|
-
TEST_PORT=3001
|
|
828
|
-
```
|
|
829
|
-
|
|
830
|
-
3. **Install dependencies** (from the module root):
|
|
831
|
-
```bash
|
|
832
|
-
npm install
|
|
833
|
-
```
|
|
834
|
-
|
|
835
|
-
4. **Create test database**:
|
|
836
|
-
```bash
|
|
837
|
-
createdb tenant_test
|
|
838
|
-
# or using psql:
|
|
839
|
-
psql -U postgres -c "CREATE DATABASE tenant_test;"
|
|
840
|
-
```
|
|
841
|
-
|
|
842
|
-
### Run Test Server
|
|
843
|
-
|
|
844
|
-
Start the test server (runs on port 3001 by default):
|
|
845
|
-
|
|
846
|
-
```bash
|
|
847
|
-
npm run test:dev
|
|
848
|
-
```
|
|
849
|
-
|
|
850
|
-
Or with auto-reload on file changes:
|
|
851
|
-
|
|
852
|
-
```bash
|
|
853
|
-
npm run test:watch
|
|
854
|
-
```
|
|
855
|
-
|
|
856
|
-
The server will display all available endpoints:
|
|
857
|
-
|
|
858
|
-
```
|
|
859
|
-
🚀 Tenant Module Test Server running on: http://localhost:3001
|
|
860
|
-
|
|
861
|
-
📋 Available endpoints:
|
|
862
|
-
Tenant Management:
|
|
863
|
-
POST /tenants - Create tenant
|
|
864
|
-
GET /tenants - Get all tenants
|
|
865
|
-
GET /tenants/active - Get active tenants
|
|
866
|
-
GET /tenants/owner/:ownerId - Get tenants by owner
|
|
867
|
-
GET /tenants/slug/:slug - Get tenant by slug
|
|
868
|
-
GET /tenants/domain/:domain - Get tenant by domain
|
|
869
|
-
GET /tenants/:id - Get tenant by ID
|
|
870
|
-
PUT /tenants/:id/settings - Update tenant settings
|
|
871
|
-
PUT /tenants/:id/status - Update tenant status
|
|
872
|
-
PUT /tenants/:id/plan - Update tenant plan
|
|
873
|
-
GET /tenants/:id/trial - Check if trial is active
|
|
874
|
-
GET /tenants/:id/subscription - Check if subscription is active
|
|
875
|
-
DELETE /tenants/:id - Delete tenant
|
|
876
|
-
|
|
877
|
-
User-Tenant Management:
|
|
878
|
-
POST /tenant-users/add - Add user to tenant
|
|
879
|
-
POST /tenant-users/invite - Invite user to tenant
|
|
880
|
-
GET /tenant-users/tenant/:tenantId - Get all users in tenant
|
|
881
|
-
GET /tenant-users/user/:userId - Get user's tenants
|
|
882
|
-
PUT /tenant-users/update-role - Update user role
|
|
883
|
-
POST /tenant-users/transfer-ownership - Transfer ownership
|
|
884
|
-
... and more
|
|
885
|
-
```
|
|
886
|
-
|
|
887
|
-
### Testing with HTTP Requests
|
|
888
|
-
|
|
889
|
-
Use the provided `test/requests.http` file with REST Client (VS Code extension) or Postman:
|
|
890
|
-
|
|
891
|
-
1. **Create a tenant**:
|
|
892
|
-
```http
|
|
893
|
-
POST http://localhost:3001/tenants
|
|
894
|
-
Content-Type: application/json
|
|
895
|
-
|
|
896
|
-
{
|
|
897
|
-
"name": "Acme Corporation",
|
|
898
|
-
"slug": "acme-corp",
|
|
899
|
-
"ownerId": "user-123",
|
|
900
|
-
"domain": "acme.example.com"
|
|
901
|
-
}
|
|
902
|
-
```
|
|
903
|
-
|
|
904
|
-
2. **Add users to tenant**:
|
|
905
|
-
```http
|
|
906
|
-
POST http://localhost:3001/tenant-users/add
|
|
907
|
-
Content-Type: application/json
|
|
908
|
-
|
|
909
|
-
{
|
|
910
|
-
"tenantId": "<tenant-id>",
|
|
911
|
-
"userId": "user-456",
|
|
912
|
-
"role": "member"
|
|
913
|
-
}
|
|
914
|
-
```
|
|
915
|
-
|
|
916
|
-
3. **Test role-based access**:
|
|
917
|
-
```http
|
|
918
|
-
GET http://localhost:3001/tenant-users/role/<tenant-id>/user-456
|
|
919
|
-
```
|
|
920
|
-
|
|
921
|
-
See `test/requests.http` for a complete set of example requests.
|
|
922
|
-
|
|
923
|
-
### Test Structure
|
|
924
|
-
|
|
925
|
-
```
|
|
926
|
-
test/
|
|
927
|
-
├── .env.example # Environment configuration template
|
|
928
|
-
├── main.ts # Test server bootstrap
|
|
929
|
-
├── test-app.module.ts # Test NestJS application module
|
|
930
|
-
├── requests.http # HTTP request examples
|
|
931
|
-
└── controllers/
|
|
932
|
-
├── tenant-test.controller.ts # Tenant CRUD endpoints
|
|
933
|
-
└── tenant-user-test.controller.ts # User-tenant management endpoints
|
|
934
|
-
```
|
|
935
|
-
|
|
936
|
-
---
|
|
937
|
-
|
|
938
|
-
## Development
|
|
939
|
-
|
|
940
|
-
### Build the package
|
|
941
|
-
|
|
942
|
-
```bash
|
|
943
|
-
npm run build
|
|
944
|
-
```
|
|
945
|
-
|
|
946
|
-
### Publish the package
|
|
947
|
-
|
|
948
|
-
```bash
|
|
949
|
-
npm run release:patch
|
|
950
|
-
```
|
|
951
|
-
|
|
952
|
-
---
|
|
953
|
-
|
|
954
|
-
## Dependencies
|
|
955
|
-
|
|
956
|
-
- `@nestjs/common` ^11.0.11
|
|
957
|
-
- `@nestjs/typeorm` ^10.0.0
|
|
958
|
-
- `@venturialstd/core` ^1.0.16
|
|
959
|
-
- `@dataui/crud-typeorm` (for CRUD operations)
|
|
960
|
-
- `typeorm` ^0.3.20
|
|
961
|
-
- `class-validator` ^0.14.1
|
|
962
|
-
- `class-transformer` ^0.5.1
|
|
963
|
-
|
|
964
|
-
---
|
|
965
|
-
|
|
966
|
-
## License
|
|
967
|
-
|
|
968
|
-
This package is part of the Venturial ecosystem and follows the organization's licensing.
|
|
969
|
-
|
|
970
|
-
---
|
|
971
|
-
|
|
972
|
-
## Support
|
|
973
|
-
|
|
974
|
-
For issues, questions, or contributions, please contact the Venturial development team.
|
|
1
|
+
# @venturialstd/tenant
|
|
2
|
+
|
|
3
|
+
A comprehensive **Multi-Tenant SaaS Platform Package**, developed by **Venturial**, that provides complete tenant management capabilities with user isolation for building scalable SaaS applications. This package follows the Venturial architecture pattern and integrates seamlessly with TypeORM and NestJS.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- **Multi-Tenant Management**: Create and manage multiple tenants with isolated data
|
|
10
|
+
- **User-Tenant Relationships**: Complete user isolation per tenant with role-based access control
|
|
11
|
+
- **Role-Based Access Control (RBAC)**: Owner, Admin, Member, and Viewer roles
|
|
12
|
+
- **Subscription Plans**: Support for Free, Starter, Professional, and Enterprise plans
|
|
13
|
+
- **Trial Period**: Configurable trial periods for new tenants
|
|
14
|
+
- **Custom Domains**: Optional custom domain support for tenants
|
|
15
|
+
- **Invitation System**: Invite users to tenants with pending invitation management
|
|
16
|
+
- **Ownership Transfer**: Transfer tenant ownership between users
|
|
17
|
+
- **User Suspension**: Suspend and reactivate user access to tenants
|
|
18
|
+
- **Guards & Decorators**: Built-in guards and decorators for easy tenant isolation
|
|
19
|
+
- **CRUD Operations**: Full TypeORM CRUD service integration
|
|
20
|
+
- **Settings Management**: Dynamic tenant-level settings with `@venturialstd/core`
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Installation
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
npm install @venturialstd/tenant
|
|
28
|
+
# or
|
|
29
|
+
yarn add @venturialstd/tenant
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## Basic Usage
|
|
35
|
+
|
|
36
|
+
### 1. Import the TenantModule in your application
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
import { Module } from '@nestjs/common';
|
|
40
|
+
import { TenantModule } from '@venturialstd/tenant';
|
|
41
|
+
|
|
42
|
+
@Module({
|
|
43
|
+
imports: [
|
|
44
|
+
TenantModule,
|
|
45
|
+
// ... other modules
|
|
46
|
+
],
|
|
47
|
+
})
|
|
48
|
+
export class AppModule {}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### 2. Use the services in your application
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
import { Injectable } from '@nestjs/common';
|
|
55
|
+
import { TenantService, TenantUserService, TenantUserRole } from '@venturialstd/tenant';
|
|
56
|
+
|
|
57
|
+
@Injectable()
|
|
58
|
+
export class YourService {
|
|
59
|
+
constructor(
|
|
60
|
+
private readonly tenantService: TenantService,
|
|
61
|
+
private readonly tenantUserService: TenantUserService,
|
|
62
|
+
) {}
|
|
63
|
+
|
|
64
|
+
async createNewTenant(userId: string) {
|
|
65
|
+
// Create tenant
|
|
66
|
+
const tenant = await this.tenantService.createTenant(
|
|
67
|
+
'Acme Corp',
|
|
68
|
+
'acme-corp',
|
|
69
|
+
'acme.example.com',
|
|
70
|
+
'A leading software company',
|
|
71
|
+
userId
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
// Add user as primary owner
|
|
75
|
+
await this.tenantUserService.addUserToTenant(
|
|
76
|
+
tenant.id,
|
|
77
|
+
userId,
|
|
78
|
+
TenantUserRole.OWNER
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
await this.tenantUserService.setPrimaryOwner(tenant.id, userId);
|
|
82
|
+
|
|
83
|
+
return tenant;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async inviteUserToTenant(tenantId: string, userId: string, invitedBy: string) {
|
|
87
|
+
return this.tenantUserService.inviteUserToTenant(
|
|
88
|
+
tenantId,
|
|
89
|
+
userId,
|
|
90
|
+
TenantUserRole.MEMBER,
|
|
91
|
+
invitedBy
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### 3. Use Guards and Decorators for tenant isolation
|
|
98
|
+
|
|
99
|
+
```ts
|
|
100
|
+
import { Controller, Get, UseGuards } from '@nestjs/common';
|
|
101
|
+
import {
|
|
102
|
+
TenantGuard,
|
|
103
|
+
TenantRoleGuard,
|
|
104
|
+
TenantId,
|
|
105
|
+
UserId,
|
|
106
|
+
Roles,
|
|
107
|
+
TenantUserRole
|
|
108
|
+
} from '@venturialstd/tenant';
|
|
109
|
+
|
|
110
|
+
@Controller('data')
|
|
111
|
+
@UseGuards(TenantGuard) // Ensure user has access to tenant
|
|
112
|
+
export class DataController {
|
|
113
|
+
constructor(private readonly dataService: DataService) {}
|
|
114
|
+
|
|
115
|
+
@Get()
|
|
116
|
+
async getData(@TenantId() tenantId: string, @UserId() userId: string) {
|
|
117
|
+
// tenantId and userId are automatically extracted and validated
|
|
118
|
+
return this.dataService.getTenantData(tenantId, userId);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
@Delete(':id')
|
|
122
|
+
@Roles(TenantUserRole.OWNER, TenantUserRole.ADMIN) // Only owners and admins
|
|
123
|
+
@UseGuards(TenantRoleGuard) // Enforce role-based access
|
|
124
|
+
async deleteData(@TenantId() tenantId: string, @Param('id') id: string) {
|
|
125
|
+
return this.dataService.deleteData(tenantId, id);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
|
|
132
|
+
## Entity Structures
|
|
133
|
+
|
|
134
|
+
### Tenant Entity
|
|
135
|
+
|
|
136
|
+
The `Tenant` entity includes the following fields:
|
|
137
|
+
|
|
138
|
+
- **id**: UUID primary key
|
|
139
|
+
- **name**: Tenant name (required, unique)
|
|
140
|
+
- **slug**: URL-friendly identifier (required, unique)
|
|
141
|
+
- **domain**: Custom domain (optional)
|
|
142
|
+
- **description**: Tenant description (optional)
|
|
143
|
+
- **isActive**: Active/inactive status (default: true)
|
|
144
|
+
- **settings**: JSON field for tenant-specific settings
|
|
145
|
+
- **ownerId**: Reference to the owner user
|
|
146
|
+
- **plan**: Subscription plan (free, starter, professional, enterprise)
|
|
147
|
+
- **trialEndsAt**: Trial period expiration date
|
|
148
|
+
- **subscriptionEndsAt**: Subscription expiration date
|
|
149
|
+
- **createdAt**: Creation timestamp
|
|
150
|
+
- **updatedAt**: Last update timestamp
|
|
151
|
+
|
|
152
|
+
### TenantUser Entity
|
|
153
|
+
|
|
154
|
+
The `TenantUser` entity manages user-tenant relationships:
|
|
155
|
+
|
|
156
|
+
- **id**: UUID primary key
|
|
157
|
+
- **tenantId**: Reference to tenant (indexed)
|
|
158
|
+
- **userId**: Reference to user (indexed)
|
|
159
|
+
- **role**: User role (OWNER, ADMIN, MEMBER, VIEWER)
|
|
160
|
+
- **status**: User status (ACTIVE, INVITED, SUSPENDED)
|
|
161
|
+
- **isPrimary**: Whether user is the primary owner
|
|
162
|
+
- **invitedBy**: User who sent the invitation
|
|
163
|
+
- **invitedAt**: Invitation timestamp
|
|
164
|
+
- **joinedAt**: Join timestamp
|
|
165
|
+
- **permissions**: JSON field for custom permissions
|
|
166
|
+
- **createdAt**: Creation timestamp
|
|
167
|
+
- **updatedAt**: Last update timestamp
|
|
168
|
+
|
|
169
|
+
**Unique constraint**: (tenantId, userId) - A user can only be added once per tenant
|
|
170
|
+
|
|
171
|
+
---
|
|
172
|
+
|
|
173
|
+
## Available Methods
|
|
174
|
+
|
|
175
|
+
### TenantService Methods
|
|
176
|
+
|
|
177
|
+
#### `createTenant(name, slug, domain?, description?, ownerId?)`
|
|
178
|
+
Creates a new tenant with validation and default settings.
|
|
179
|
+
|
|
180
|
+
```ts
|
|
181
|
+
const tenant = await tenantService.createTenant(
|
|
182
|
+
'My Company',
|
|
183
|
+
'my-company',
|
|
184
|
+
'mycompany.com',
|
|
185
|
+
'Company description',
|
|
186
|
+
'owner-uuid'
|
|
187
|
+
);
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
#### `getTenantBySlug(slug)`
|
|
191
|
+
Retrieve a tenant by its slug.
|
|
192
|
+
|
|
193
|
+
```ts
|
|
194
|
+
const tenant = await tenantService.getTenantBySlug('my-company');
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
#### `getTenantByDomain(domain)`
|
|
198
|
+
Retrieve a tenant by its custom domain.
|
|
199
|
+
|
|
200
|
+
```ts
|
|
201
|
+
const tenant = await tenantService.getTenantByDomain('mycompany.com');
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
#### `updateTenantSettings(tenantId, settings)`
|
|
205
|
+
Update tenant-specific settings.
|
|
206
|
+
|
|
207
|
+
```ts
|
|
208
|
+
await tenantService.updateTenantSettings('tenant-uuid', {
|
|
209
|
+
theme: 'dark',
|
|
210
|
+
language: 'en',
|
|
211
|
+
features: { api: true }
|
|
212
|
+
});
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
#### `setTenantStatus(tenantId, isActive)`
|
|
216
|
+
Activate or deactivate a tenant.
|
|
217
|
+
|
|
218
|
+
```ts
|
|
219
|
+
await tenantService.setTenantStatus('tenant-uuid', false); // Deactivate
|
|
220
|
+
await tenantService.setTenantStatus('tenant-uuid', true); // Activate
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
#### `updateTenantPlan(tenantId, plan, subscriptionEndsAt?)`
|
|
224
|
+
Update the tenant's subscription plan.
|
|
225
|
+
|
|
226
|
+
```ts
|
|
227
|
+
const endDate = new Date();
|
|
228
|
+
endDate.setMonth(endDate.getMonth() + 1);
|
|
229
|
+
|
|
230
|
+
await tenantService.updateTenantPlan(
|
|
231
|
+
'tenant-uuid',
|
|
232
|
+
'professional',
|
|
233
|
+
endDate
|
|
234
|
+
);
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
#### `getActiveTenants()`
|
|
238
|
+
Get all active tenants.
|
|
239
|
+
|
|
240
|
+
```ts
|
|
241
|
+
const activeTenants = await tenantService.getActiveTenants();
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
#### `getTenantsByOwner(ownerId)`
|
|
245
|
+
Get all tenants owned by a specific user.
|
|
246
|
+
|
|
247
|
+
```ts
|
|
248
|
+
const tenants = await tenantService.getTenantsByOwner('owner-uuid');
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
#### `isInTrialPeriod(tenantId)`
|
|
252
|
+
Check if a tenant is currently in trial period.
|
|
253
|
+
|
|
254
|
+
```ts
|
|
255
|
+
const inTrial = await tenantService.isInTrialPeriod('tenant-uuid');
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
#### `hasActiveSubscription(tenantId)`
|
|
259
|
+
Check if a tenant has an active subscription.
|
|
260
|
+
|
|
261
|
+
```ts
|
|
262
|
+
const isActive = await tenantService.hasActiveSubscription('tenant-uuid');
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
### TenantUserService Methods
|
|
266
|
+
|
|
267
|
+
#### `addUserToTenant(tenantId, userId, role, invitedBy?)`
|
|
268
|
+
Add a user to a tenant with a specific role.
|
|
269
|
+
|
|
270
|
+
```ts
|
|
271
|
+
await tenantUserService.addUserToTenant(
|
|
272
|
+
'tenant-uuid',
|
|
273
|
+
'user-uuid',
|
|
274
|
+
TenantUserRole.MEMBER,
|
|
275
|
+
'inviter-uuid'
|
|
276
|
+
);
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
#### `inviteUserToTenant(tenantId, userId, role, invitedBy)`
|
|
280
|
+
Invite a user to a tenant (creates pending invitation).
|
|
281
|
+
|
|
282
|
+
```ts
|
|
283
|
+
await tenantUserService.inviteUserToTenant(
|
|
284
|
+
'tenant-uuid',
|
|
285
|
+
'user-uuid',
|
|
286
|
+
TenantUserRole.ADMIN,
|
|
287
|
+
'inviter-uuid'
|
|
288
|
+
);
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
#### `acceptInvitation(tenantId, userId)`
|
|
292
|
+
Accept a pending tenant invitation.
|
|
293
|
+
|
|
294
|
+
```ts
|
|
295
|
+
await tenantUserService.acceptInvitation('tenant-uuid', 'user-uuid');
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
#### `removeUserFromTenant(tenantId, userId)`
|
|
299
|
+
Remove a user from a tenant (cannot remove primary owner).
|
|
300
|
+
|
|
301
|
+
```ts
|
|
302
|
+
await tenantUserService.removeUserFromTenant('tenant-uuid', 'user-uuid');
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
#### `updateUserRole(tenantId, userId, newRole)`
|
|
306
|
+
Update a user's role in a tenant.
|
|
307
|
+
|
|
308
|
+
```ts
|
|
309
|
+
await tenantUserService.updateUserRole(
|
|
310
|
+
'tenant-uuid',
|
|
311
|
+
'user-uuid',
|
|
312
|
+
TenantUserRole.ADMIN
|
|
313
|
+
);
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
#### `getUserTenants(userId)`
|
|
317
|
+
Get all tenants a user belongs to.
|
|
318
|
+
|
|
319
|
+
```ts
|
|
320
|
+
const userTenants = await tenantUserService.getUserTenants('user-uuid');
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
#### `getTenantUsers(tenantId)`
|
|
324
|
+
Get all users in a tenant (including invited).
|
|
325
|
+
|
|
326
|
+
```ts
|
|
327
|
+
const users = await tenantUserService.getTenantUsers('tenant-uuid');
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
#### `getActiveTenantUsers(tenantId)`
|
|
331
|
+
Get all active users in a tenant.
|
|
332
|
+
|
|
333
|
+
```ts
|
|
334
|
+
const activeUsers = await tenantUserService.getActiveTenantUsers('tenant-uuid');
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
#### `hasAccess(tenantId, userId)`
|
|
338
|
+
Check if a user has access to a tenant.
|
|
339
|
+
|
|
340
|
+
```ts
|
|
341
|
+
const hasAccess = await tenantUserService.hasAccess('tenant-uuid', 'user-uuid');
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
#### `hasRole(tenantId, userId, role)`
|
|
345
|
+
Check if a user has a specific role in a tenant.
|
|
346
|
+
|
|
347
|
+
```ts
|
|
348
|
+
const isAdmin = await tenantUserService.hasRole(
|
|
349
|
+
'tenant-uuid',
|
|
350
|
+
'user-uuid',
|
|
351
|
+
TenantUserRole.ADMIN
|
|
352
|
+
);
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
#### `isAdminOrOwner(tenantId, userId)`
|
|
356
|
+
Check if a user is an admin or owner in a tenant.
|
|
357
|
+
|
|
358
|
+
```ts
|
|
359
|
+
const isAdminOrOwner = await tenantUserService.isAdminOrOwner('tenant-uuid', 'user-uuid');
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
#### `getUserRole(tenantId, userId)`
|
|
363
|
+
Get a user's role in a tenant.
|
|
364
|
+
|
|
365
|
+
```ts
|
|
366
|
+
const role = await tenantUserService.getUserRole('tenant-uuid', 'user-uuid');
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
#### `setPrimaryOwner(tenantId, userId)`
|
|
370
|
+
Set a user as the primary owner of a tenant.
|
|
371
|
+
|
|
372
|
+
```ts
|
|
373
|
+
await tenantUserService.setPrimaryOwner('tenant-uuid', 'user-uuid');
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
#### `transferOwnership(tenantId, fromUserId, toUserId)`
|
|
377
|
+
Transfer tenant ownership between users.
|
|
378
|
+
|
|
379
|
+
```ts
|
|
380
|
+
await tenantUserService.transferOwnership(
|
|
381
|
+
'tenant-uuid',
|
|
382
|
+
'current-owner-uuid',
|
|
383
|
+
'new-owner-uuid'
|
|
384
|
+
);
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
#### `getUserInvitations(userId)`
|
|
388
|
+
Get all pending invitations for a user.
|
|
389
|
+
|
|
390
|
+
```ts
|
|
391
|
+
const invitations = await tenantUserService.getUserInvitations('user-uuid');
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
#### `suspendUser(tenantId, userId)`
|
|
395
|
+
Suspend a user's access to a tenant.
|
|
396
|
+
|
|
397
|
+
```ts
|
|
398
|
+
await tenantUserService.suspendUser('tenant-uuid', 'user-uuid');
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
#### `reactivateUser(tenantId, userId)`
|
|
402
|
+
Reactivate a suspended user.
|
|
403
|
+
|
|
404
|
+
```ts
|
|
405
|
+
await tenantUserService.reactivateUser('tenant-uuid', 'user-uuid');
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
---
|
|
409
|
+
|
|
410
|
+
## Guards and Decorators
|
|
411
|
+
|
|
412
|
+
### TenantGuard
|
|
413
|
+
|
|
414
|
+
Enforces tenant access control by validating that the authenticated user has access to the requested tenant.
|
|
415
|
+
|
|
416
|
+
```ts
|
|
417
|
+
@Controller('api')
|
|
418
|
+
@UseGuards(TenantGuard)
|
|
419
|
+
export class ApiController {
|
|
420
|
+
@Get('data')
|
|
421
|
+
async getData(@TenantId() tenantId: string) {
|
|
422
|
+
// User access to tenant is already validated
|
|
423
|
+
return this.service.getData(tenantId);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
### TenantRoleGuard
|
|
429
|
+
|
|
430
|
+
Enforces role-based access control within a tenant. Use with `@Roles()` decorator.
|
|
431
|
+
|
|
432
|
+
```ts
|
|
433
|
+
@Controller('admin')
|
|
434
|
+
@UseGuards(TenantGuard, TenantRoleGuard)
|
|
435
|
+
export class AdminController {
|
|
436
|
+
@Delete('user/:userId')
|
|
437
|
+
@Roles(TenantUserRole.OWNER, TenantUserRole.ADMIN)
|
|
438
|
+
async removeUser(@TenantId() tenantId: string, @Param('userId') userId: string) {
|
|
439
|
+
// Only owners and admins can access this endpoint
|
|
440
|
+
return this.service.removeUser(tenantId, userId);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
### @TenantId()
|
|
446
|
+
|
|
447
|
+
Extracts the tenant ID from the request. Checks in order:
|
|
448
|
+
1. `request.tenantId`
|
|
449
|
+
2. `request.headers['x-tenant-id']`
|
|
450
|
+
3. `request.params.tenantId`
|
|
451
|
+
4. `request.query.tenantId`
|
|
452
|
+
|
|
453
|
+
```ts
|
|
454
|
+
@Get()
|
|
455
|
+
async getData(@TenantId() tenantId: string) {
|
|
456
|
+
return this.service.getData(tenantId);
|
|
457
|
+
}
|
|
458
|
+
```
|
|
459
|
+
|
|
460
|
+
### @UserId()
|
|
461
|
+
|
|
462
|
+
Extracts the user ID from the authenticated request.
|
|
463
|
+
|
|
464
|
+
```ts
|
|
465
|
+
@Get('profile')
|
|
466
|
+
async getProfile(@UserId() userId: string, @TenantId() tenantId: string) {
|
|
467
|
+
return this.service.getUserProfile(tenantId, userId);
|
|
468
|
+
}
|
|
469
|
+
```
|
|
470
|
+
|
|
471
|
+
### @TenantContext()
|
|
472
|
+
|
|
473
|
+
Extracts the complete tenant context including tenantId, userId, role, and user object.
|
|
474
|
+
|
|
475
|
+
```ts
|
|
476
|
+
@Get()
|
|
477
|
+
async getData(@TenantContext() context: { tenantId: string, userId: string, role: TenantUserRole, user: any }) {
|
|
478
|
+
return this.service.getData(context);
|
|
479
|
+
}
|
|
480
|
+
```
|
|
481
|
+
|
|
482
|
+
### @Roles(...roles)
|
|
483
|
+
|
|
484
|
+
Specifies required roles for accessing an endpoint. Must be used with `TenantRoleGuard`.
|
|
485
|
+
|
|
486
|
+
```ts
|
|
487
|
+
@Post('invite')
|
|
488
|
+
@Roles(TenantUserRole.OWNER, TenantUserRole.ADMIN)
|
|
489
|
+
@UseGuards(TenantGuard, TenantRoleGuard)
|
|
490
|
+
async inviteUser(@TenantId() tenantId: string, @Body() dto: InviteUserDto) {
|
|
491
|
+
return this.service.inviteUser(tenantId, dto);
|
|
492
|
+
}
|
|
493
|
+
```
|
|
494
|
+
|
|
495
|
+
---
|
|
496
|
+
|
|
497
|
+
## Configuration Settings
|
|
498
|
+
|
|
499
|
+
The module provides the following configurable settings through `SettingsService`:
|
|
500
|
+
|
|
501
|
+
### General Settings
|
|
502
|
+
- **ENABLED**: Enable/disable the tenant module (boolean)
|
|
503
|
+
- **MAX_TENANTS**: Maximum number of tenants allowed (0 = unlimited)
|
|
504
|
+
- **DEFAULT_PLAN**: Default subscription plan for new tenants
|
|
505
|
+
- **TRIAL_DAYS**: Number of trial days for new tenants (default: 14)
|
|
506
|
+
|
|
507
|
+
### Feature Settings
|
|
508
|
+
- **CUSTOM_DOMAIN**: Allow custom domains for tenants
|
|
509
|
+
- **API_ACCESS**: Allow API access for tenants
|
|
510
|
+
|
|
511
|
+
---
|
|
512
|
+
|
|
513
|
+
## Enums and Types
|
|
514
|
+
|
|
515
|
+
### TenantUserRole
|
|
516
|
+
```ts
|
|
517
|
+
enum TenantUserRole {
|
|
518
|
+
OWNER = 'owner',
|
|
519
|
+
ADMIN = 'admin',
|
|
520
|
+
MEMBER = 'member',
|
|
521
|
+
VIEWER = 'viewer',
|
|
522
|
+
}
|
|
523
|
+
```
|
|
524
|
+
|
|
525
|
+
### TenantUserStatus
|
|
526
|
+
```ts
|
|
527
|
+
enum TenantUserStatus {
|
|
528
|
+
ACTIVE = 'active',
|
|
529
|
+
INVITED = 'invited',
|
|
530
|
+
SUSPENDED = 'suspended',
|
|
531
|
+
}
|
|
532
|
+
```
|
|
533
|
+
|
|
534
|
+
### TENANT_PLAN
|
|
535
|
+
```ts
|
|
536
|
+
enum TENANT_PLAN {
|
|
537
|
+
FREE = 'free',
|
|
538
|
+
STARTER = 'starter',
|
|
539
|
+
PROFESSIONAL = 'professional',
|
|
540
|
+
ENTERPRISE = 'enterprise',
|
|
541
|
+
}
|
|
542
|
+
```
|
|
543
|
+
|
|
544
|
+
### TENANT_STATUS
|
|
545
|
+
```ts
|
|
546
|
+
enum TENANT_STATUS {
|
|
547
|
+
ACTIVE = 'active',
|
|
548
|
+
INACTIVE = 'inactive',
|
|
549
|
+
SUSPENDED = 'suspended',
|
|
550
|
+
TRIAL = 'trial',
|
|
551
|
+
}
|
|
552
|
+
```
|
|
553
|
+
|
|
554
|
+
---
|
|
555
|
+
|
|
556
|
+
## Integration with @venturialstd/core
|
|
557
|
+
|
|
558
|
+
This package uses `@venturialstd/core` for:
|
|
559
|
+
- **SharedModule**: Core functionality and configuration
|
|
560
|
+
- **SettingsService**: Dynamic settings management
|
|
561
|
+
- **AppLogger**: Structured logging
|
|
562
|
+
|
|
563
|
+
---
|
|
564
|
+
|
|
565
|
+
## TypeORM Integration
|
|
566
|
+
|
|
567
|
+
The package extends `TypeOrmCrudService` from `@dataui/crud-typeorm`, providing:
|
|
568
|
+
- Standard CRUD operations
|
|
569
|
+
- Query builders
|
|
570
|
+
- Pagination support
|
|
571
|
+
- Filtering and sorting capabilities
|
|
572
|
+
|
|
573
|
+
---
|
|
574
|
+
|
|
575
|
+
## Example: Complete SaaS Implementation with User Isolation
|
|
576
|
+
|
|
577
|
+
```ts
|
|
578
|
+
import { Module } from '@nestjs/common';
|
|
579
|
+
import { TypeOrmModule } from '@nestjs/typeorm';
|
|
580
|
+
import { TenantModule, TenantService, TenantUserService } from '@venturialstd/tenant';
|
|
581
|
+
|
|
582
|
+
@Module({
|
|
583
|
+
imports: [
|
|
584
|
+
TypeOrmModule.forRoot({
|
|
585
|
+
// Your database configuration
|
|
586
|
+
}),
|
|
587
|
+
TenantModule,
|
|
588
|
+
],
|
|
589
|
+
})
|
|
590
|
+
export class AppModule {}
|
|
591
|
+
|
|
592
|
+
// Service for onboarding new tenants
|
|
593
|
+
@Injectable()
|
|
594
|
+
export class SaasService {
|
|
595
|
+
constructor(
|
|
596
|
+
private readonly tenantService: TenantService,
|
|
597
|
+
private readonly tenantUserService: TenantUserService,
|
|
598
|
+
) {}
|
|
599
|
+
|
|
600
|
+
async onboardNewTenant(data: any, ownerId: string) {
|
|
601
|
+
// Create tenant
|
|
602
|
+
const tenant = await this.tenantService.createTenant(
|
|
603
|
+
data.companyName,
|
|
604
|
+
data.slug,
|
|
605
|
+
data.domain,
|
|
606
|
+
data.description,
|
|
607
|
+
ownerId
|
|
608
|
+
);
|
|
609
|
+
|
|
610
|
+
// Add owner to tenant
|
|
611
|
+
await this.tenantUserService.addUserToTenant(
|
|
612
|
+
tenant.id,
|
|
613
|
+
ownerId,
|
|
614
|
+
TenantUserRole.OWNER
|
|
615
|
+
);
|
|
616
|
+
|
|
617
|
+
// Set as primary owner
|
|
618
|
+
await this.tenantUserService.setPrimaryOwner(tenant.id, ownerId);
|
|
619
|
+
|
|
620
|
+
// Configure tenant settings
|
|
621
|
+
await this.tenantService.updateTenantSettings(tenant.id, {
|
|
622
|
+
theme: data.preferences?.theme || 'light',
|
|
623
|
+
locale: data.preferences?.locale || 'en',
|
|
624
|
+
notifications: true,
|
|
625
|
+
});
|
|
626
|
+
|
|
627
|
+
return tenant;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
async inviteTeamMember(
|
|
631
|
+
tenantId: string,
|
|
632
|
+
userId: string,
|
|
633
|
+
invitedBy: string,
|
|
634
|
+
role: TenantUserRole = TenantUserRole.MEMBER
|
|
635
|
+
) {
|
|
636
|
+
// Verify inviter is admin or owner
|
|
637
|
+
const isAdmin = await this.tenantUserService.isAdminOrOwner(tenantId, invitedBy);
|
|
638
|
+
if (!isAdmin) {
|
|
639
|
+
throw new ForbiddenException('Only admins can invite team members');
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
// Invite user
|
|
643
|
+
await this.tenantUserService.inviteUserToTenant(
|
|
644
|
+
tenantId,
|
|
645
|
+
userId,
|
|
646
|
+
role,
|
|
647
|
+
invitedBy
|
|
648
|
+
);
|
|
649
|
+
|
|
650
|
+
// Send invitation email (implement your email service)
|
|
651
|
+
// await this.emailService.sendInvitation(userId, tenantId);
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
async checkTenantAccess(tenantId: string, userId: string) {
|
|
655
|
+
const tenant = await this.tenantService.repo.findOne({
|
|
656
|
+
where: { id: tenantId }
|
|
657
|
+
});
|
|
658
|
+
|
|
659
|
+
if (!tenant.isActive) {
|
|
660
|
+
throw new UnauthorizedException('Tenant is inactive');
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
const hasAccess = await this.tenantUserService.hasAccess(tenantId, userId);
|
|
664
|
+
if (!hasAccess) {
|
|
665
|
+
throw new UnauthorizedException('No access to this tenant');
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
const hasSubscription = await this.tenantService.hasActiveSubscription(tenantId);
|
|
669
|
+
if (!hasSubscription) {
|
|
670
|
+
throw new UnauthorizedException('Subscription expired');
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
return true;
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
// Controller with tenant isolation
|
|
678
|
+
@Controller('projects')
|
|
679
|
+
@UseGuards(TenantGuard)
|
|
680
|
+
export class ProjectsController {
|
|
681
|
+
constructor(private readonly projectsService: ProjectsService) {}
|
|
682
|
+
|
|
683
|
+
@Get()
|
|
684
|
+
async list(@TenantId() tenantId: string, @UserId() userId: string) {
|
|
685
|
+
// Automatically scoped to tenant
|
|
686
|
+
return this.projectsService.findByTenant(tenantId);
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
@Post()
|
|
690
|
+
async create(
|
|
691
|
+
@TenantId() tenantId: string,
|
|
692
|
+
@UserId() userId: string,
|
|
693
|
+
@Body() dto: CreateProjectDto
|
|
694
|
+
) {
|
|
695
|
+
return this.projectsService.create(tenantId, userId, dto);
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
@Delete(':id')
|
|
699
|
+
@Roles(TenantUserRole.OWNER, TenantUserRole.ADMIN)
|
|
700
|
+
@UseGuards(TenantRoleGuard)
|
|
701
|
+
async delete(@TenantId() tenantId: string, @Param('id') id: string) {
|
|
702
|
+
// Only owners and admins can delete
|
|
703
|
+
return this.projectsService.delete(tenantId, id);
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
```
|
|
707
|
+
|
|
708
|
+
---
|
|
709
|
+
|
|
710
|
+
## Database Migrations
|
|
711
|
+
|
|
712
|
+
The package requires two database tables: `tenant` and `tenant_user`. Create migrations for both:
|
|
713
|
+
|
|
714
|
+
### Tenant Table Migration
|
|
715
|
+
|
|
716
|
+
```bash
|
|
717
|
+
npm run typeorm migration:create -- -n CreateTenant
|
|
718
|
+
```
|
|
719
|
+
|
|
720
|
+
```ts
|
|
721
|
+
import { MigrationInterface, QueryRunner, Table, TableIndex } from 'typeorm';
|
|
722
|
+
|
|
723
|
+
export class CreateTenant1234567890123 implements MigrationInterface {
|
|
724
|
+
public async up(queryRunner: QueryRunner): Promise<void> {
|
|
725
|
+
await queryRunner.createTable(
|
|
726
|
+
new Table({
|
|
727
|
+
name: 'tenant',
|
|
728
|
+
columns: [
|
|
729
|
+
{ name: 'id', type: 'uuid', isPrimary: true, generationStrategy: 'uuid', default: 'uuid_generate_v4()' },
|
|
730
|
+
{ name: 'name', type: 'varchar', isUnique: true },
|
|
731
|
+
{ name: 'slug', type: 'varchar', isUnique: true },
|
|
732
|
+
{ name: 'domain', type: 'varchar', isNullable: true },
|
|
733
|
+
{ name: 'description', type: 'text', isNullable: true },
|
|
734
|
+
{ name: 'isActive', type: 'boolean', default: true },
|
|
735
|
+
{ name: 'settings', type: 'jsonb', isNullable: true },
|
|
736
|
+
{ name: 'ownerId', type: 'varchar', isNullable: true },
|
|
737
|
+
{ name: 'plan', type: 'varchar', isNullable: true },
|
|
738
|
+
{ name: 'trialEndsAt', type: 'timestamptz', isNullable: true },
|
|
739
|
+
{ name: 'subscriptionEndsAt', type: 'timestamptz', isNullable: true },
|
|
740
|
+
{ name: 'createdAt', type: 'timestamptz', default: 'CURRENT_TIMESTAMP' },
|
|
741
|
+
{ name: 'updatedAt', type: 'timestamptz', default: 'CURRENT_TIMESTAMP' },
|
|
742
|
+
],
|
|
743
|
+
}),
|
|
744
|
+
true,
|
|
745
|
+
);
|
|
746
|
+
|
|
747
|
+
await queryRunner.createIndex('tenant', new TableIndex({ name: 'IDX_tenant_slug', columnNames: ['slug'] }));
|
|
748
|
+
await queryRunner.createIndex('tenant', new TableIndex({ name: 'IDX_tenant_domain', columnNames: ['domain'] }));
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
public async down(queryRunner: QueryRunner): Promise<void> {
|
|
752
|
+
await queryRunner.dropTable('tenant');
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
```
|
|
756
|
+
|
|
757
|
+
### TenantUser Table Migration
|
|
758
|
+
|
|
759
|
+
```bash
|
|
760
|
+
npm run typeorm migration:create -- -n CreateTenantUser
|
|
761
|
+
```
|
|
762
|
+
|
|
763
|
+
```ts
|
|
764
|
+
import { MigrationInterface, QueryRunner, Table, TableIndex } from 'typeorm';
|
|
765
|
+
|
|
766
|
+
export class CreateTenantUser1234567890124 implements MigrationInterface {
|
|
767
|
+
public async up(queryRunner: QueryRunner): Promise<void> {
|
|
768
|
+
await queryRunner.createTable(
|
|
769
|
+
new Table({
|
|
770
|
+
name: 'tenant_user',
|
|
771
|
+
columns: [
|
|
772
|
+
{ name: 'id', type: 'uuid', isPrimary: true, generationStrategy: 'uuid', default: 'uuid_generate_v4()' },
|
|
773
|
+
{ name: 'tenantId', type: 'uuid' },
|
|
774
|
+
{ name: 'userId', type: 'uuid' },
|
|
775
|
+
{ name: 'role', type: 'enum', enum: ['owner', 'admin', 'member', 'viewer'], default: "'member'" },
|
|
776
|
+
{ name: 'status', type: 'enum', enum: ['active', 'invited', 'suspended'], default: "'active'" },
|
|
777
|
+
{ name: 'isPrimary', type: 'boolean', default: false },
|
|
778
|
+
{ name: 'invitedBy', type: 'uuid', isNullable: true },
|
|
779
|
+
{ name: 'invitedAt', type: 'timestamptz', isNullable: true },
|
|
780
|
+
{ name: 'joinedAt', type: 'timestamptz', isNullable: true },
|
|
781
|
+
{ name: 'permissions', type: 'jsonb', isNullable: true },
|
|
782
|
+
{ name: 'createdAt', type: 'timestamptz', default: 'CURRENT_TIMESTAMP' },
|
|
783
|
+
{ name: 'updatedAt', type: 'timestamptz', default: 'CURRENT_TIMESTAMP' },
|
|
784
|
+
],
|
|
785
|
+
}),
|
|
786
|
+
true,
|
|
787
|
+
);
|
|
788
|
+
|
|
789
|
+
await queryRunner.createIndex('tenant_user', new TableIndex({ name: 'IDX_tenant_user_tenantId', columnNames: ['tenantId'] }));
|
|
790
|
+
await queryRunner.createIndex('tenant_user', new TableIndex({ name: 'IDX_tenant_user_userId', columnNames: ['userId'] }));
|
|
791
|
+
await queryRunner.createIndex('tenant_user', new TableIndex({ name: 'IDX_tenant_user_tenant_user', columnNames: ['tenantId', 'userId'], isUnique: true }));
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
public async down(queryRunner: QueryRunner): Promise<void> {
|
|
795
|
+
await queryRunner.dropTable('tenant_user');
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
```
|
|
799
|
+
|
|
800
|
+
Run migrations:
|
|
801
|
+
|
|
802
|
+
```bash
|
|
803
|
+
npm run typeorm migration:run
|
|
804
|
+
```
|
|
805
|
+
|
|
806
|
+
---
|
|
807
|
+
|
|
808
|
+
## Testing
|
|
809
|
+
|
|
810
|
+
The module includes an isolated NestJS test environment for testing all functionality without integrating it into your main application.
|
|
811
|
+
|
|
812
|
+
### Setup Test Environment
|
|
813
|
+
|
|
814
|
+
1. **Copy environment configuration**:
|
|
815
|
+
```bash
|
|
816
|
+
cd test
|
|
817
|
+
cp .env.example .env
|
|
818
|
+
```
|
|
819
|
+
|
|
820
|
+
2. **Configure your database** in `test/.env`:
|
|
821
|
+
```env
|
|
822
|
+
DB_HOST=localhost
|
|
823
|
+
DB_PORT=5432
|
|
824
|
+
DB_USERNAME=postgres
|
|
825
|
+
DB_PASSWORD=postgres
|
|
826
|
+
DB_DATABASE=tenant_test
|
|
827
|
+
TEST_PORT=3001
|
|
828
|
+
```
|
|
829
|
+
|
|
830
|
+
3. **Install dependencies** (from the module root):
|
|
831
|
+
```bash
|
|
832
|
+
npm install
|
|
833
|
+
```
|
|
834
|
+
|
|
835
|
+
4. **Create test database**:
|
|
836
|
+
```bash
|
|
837
|
+
createdb tenant_test
|
|
838
|
+
# or using psql:
|
|
839
|
+
psql -U postgres -c "CREATE DATABASE tenant_test;"
|
|
840
|
+
```
|
|
841
|
+
|
|
842
|
+
### Run Test Server
|
|
843
|
+
|
|
844
|
+
Start the test server (runs on port 3001 by default):
|
|
845
|
+
|
|
846
|
+
```bash
|
|
847
|
+
npm run test:dev
|
|
848
|
+
```
|
|
849
|
+
|
|
850
|
+
Or with auto-reload on file changes:
|
|
851
|
+
|
|
852
|
+
```bash
|
|
853
|
+
npm run test:watch
|
|
854
|
+
```
|
|
855
|
+
|
|
856
|
+
The server will display all available endpoints:
|
|
857
|
+
|
|
858
|
+
```
|
|
859
|
+
🚀 Tenant Module Test Server running on: http://localhost:3001
|
|
860
|
+
|
|
861
|
+
📋 Available endpoints:
|
|
862
|
+
Tenant Management:
|
|
863
|
+
POST /tenants - Create tenant
|
|
864
|
+
GET /tenants - Get all tenants
|
|
865
|
+
GET /tenants/active - Get active tenants
|
|
866
|
+
GET /tenants/owner/:ownerId - Get tenants by owner
|
|
867
|
+
GET /tenants/slug/:slug - Get tenant by slug
|
|
868
|
+
GET /tenants/domain/:domain - Get tenant by domain
|
|
869
|
+
GET /tenants/:id - Get tenant by ID
|
|
870
|
+
PUT /tenants/:id/settings - Update tenant settings
|
|
871
|
+
PUT /tenants/:id/status - Update tenant status
|
|
872
|
+
PUT /tenants/:id/plan - Update tenant plan
|
|
873
|
+
GET /tenants/:id/trial - Check if trial is active
|
|
874
|
+
GET /tenants/:id/subscription - Check if subscription is active
|
|
875
|
+
DELETE /tenants/:id - Delete tenant
|
|
876
|
+
|
|
877
|
+
User-Tenant Management:
|
|
878
|
+
POST /tenant-users/add - Add user to tenant
|
|
879
|
+
POST /tenant-users/invite - Invite user to tenant
|
|
880
|
+
GET /tenant-users/tenant/:tenantId - Get all users in tenant
|
|
881
|
+
GET /tenant-users/user/:userId - Get user's tenants
|
|
882
|
+
PUT /tenant-users/update-role - Update user role
|
|
883
|
+
POST /tenant-users/transfer-ownership - Transfer ownership
|
|
884
|
+
... and more
|
|
885
|
+
```
|
|
886
|
+
|
|
887
|
+
### Testing with HTTP Requests
|
|
888
|
+
|
|
889
|
+
Use the provided `test/requests.http` file with REST Client (VS Code extension) or Postman:
|
|
890
|
+
|
|
891
|
+
1. **Create a tenant**:
|
|
892
|
+
```http
|
|
893
|
+
POST http://localhost:3001/tenants
|
|
894
|
+
Content-Type: application/json
|
|
895
|
+
|
|
896
|
+
{
|
|
897
|
+
"name": "Acme Corporation",
|
|
898
|
+
"slug": "acme-corp",
|
|
899
|
+
"ownerId": "user-123",
|
|
900
|
+
"domain": "acme.example.com"
|
|
901
|
+
}
|
|
902
|
+
```
|
|
903
|
+
|
|
904
|
+
2. **Add users to tenant**:
|
|
905
|
+
```http
|
|
906
|
+
POST http://localhost:3001/tenant-users/add
|
|
907
|
+
Content-Type: application/json
|
|
908
|
+
|
|
909
|
+
{
|
|
910
|
+
"tenantId": "<tenant-id>",
|
|
911
|
+
"userId": "user-456",
|
|
912
|
+
"role": "member"
|
|
913
|
+
}
|
|
914
|
+
```
|
|
915
|
+
|
|
916
|
+
3. **Test role-based access**:
|
|
917
|
+
```http
|
|
918
|
+
GET http://localhost:3001/tenant-users/role/<tenant-id>/user-456
|
|
919
|
+
```
|
|
920
|
+
|
|
921
|
+
See `test/requests.http` for a complete set of example requests.
|
|
922
|
+
|
|
923
|
+
### Test Structure
|
|
924
|
+
|
|
925
|
+
```
|
|
926
|
+
test/
|
|
927
|
+
├── .env.example # Environment configuration template
|
|
928
|
+
├── main.ts # Test server bootstrap
|
|
929
|
+
├── test-app.module.ts # Test NestJS application module
|
|
930
|
+
├── requests.http # HTTP request examples
|
|
931
|
+
└── controllers/
|
|
932
|
+
├── tenant-test.controller.ts # Tenant CRUD endpoints
|
|
933
|
+
└── tenant-user-test.controller.ts # User-tenant management endpoints
|
|
934
|
+
```
|
|
935
|
+
|
|
936
|
+
---
|
|
937
|
+
|
|
938
|
+
## Development
|
|
939
|
+
|
|
940
|
+
### Build the package
|
|
941
|
+
|
|
942
|
+
```bash
|
|
943
|
+
npm run build
|
|
944
|
+
```
|
|
945
|
+
|
|
946
|
+
### Publish the package
|
|
947
|
+
|
|
948
|
+
```bash
|
|
949
|
+
npm run release:patch
|
|
950
|
+
```
|
|
951
|
+
|
|
952
|
+
---
|
|
953
|
+
|
|
954
|
+
## Dependencies
|
|
955
|
+
|
|
956
|
+
- `@nestjs/common` ^11.0.11
|
|
957
|
+
- `@nestjs/typeorm` ^10.0.0
|
|
958
|
+
- `@venturialstd/core` ^1.0.16
|
|
959
|
+
- `@dataui/crud-typeorm` (for CRUD operations)
|
|
960
|
+
- `typeorm` ^0.3.20
|
|
961
|
+
- `class-validator` ^0.14.1
|
|
962
|
+
- `class-transformer` ^0.5.1
|
|
963
|
+
|
|
964
|
+
---
|
|
965
|
+
|
|
966
|
+
## License
|
|
967
|
+
|
|
968
|
+
This package is part of the Venturial ecosystem and follows the organization's licensing.
|
|
969
|
+
|
|
970
|
+
---
|
|
971
|
+
|
|
972
|
+
## Support
|
|
973
|
+
|
|
974
|
+
For issues, questions, or contributions, please contact the Venturial development team.
|