@venturialstd/organization 0.0.2 โ 0.0.4
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 +910 -910
- package/dist/constants/repository-provider.constant.d.ts +6 -0
- package/dist/constants/repository-provider.constant.d.ts.map +1 -0
- package/dist/constants/repository-provider.constant.js +10 -0
- package/dist/constants/repository-provider.constant.js.map +1 -0
- package/dist/controllers/organization-repository.controller.d.ts +16 -0
- package/dist/controllers/organization-repository.controller.d.ts.map +1 -0
- package/dist/controllers/organization-repository.controller.js +84 -0
- package/dist/controllers/organization-repository.controller.js.map +1 -0
- package/dist/dtos/repository-auth.dto.d.ts +6 -0
- package/dist/dtos/repository-auth.dto.d.ts.map +1 -0
- package/dist/dtos/repository-auth.dto.js +28 -0
- package/dist/dtos/repository-auth.dto.js.map +1 -0
- package/dist/organization.module.d.ts.map +1 -1
- package/dist/organization.module.js +4 -1
- package/dist/organization.module.js.map +1 -1
- package/package.json +42 -42
package/README.md
CHANGED
|
@@ -1,910 +1,910 @@
|
|
|
1
|
-
# @venturialstd/organization
|
|
2
|
-
|
|
3
|
-
A comprehensive NestJS module for managing organizations with multi-tenant capabilities, role-based access control, and isolated per-organization settings.
|
|
4
|
-
|
|
5
|
-
## ๐ Table of Contents
|
|
6
|
-
|
|
7
|
-
- [Features](#features)
|
|
8
|
-
- [Installation](#installation)
|
|
9
|
-
- [Quick Start](#quick-start)
|
|
10
|
-
- [Core Concepts](#core-concepts)
|
|
11
|
-
- [Architecture](#architecture)
|
|
12
|
-
- [API Reference](#api-reference)
|
|
13
|
-
- [Organization Settings System](#organization-settings-system)
|
|
14
|
-
- [Test Server](#test-server)
|
|
15
|
-
- [Migration Guide](#migration-guide)
|
|
16
|
-
- [Examples](#examples)
|
|
17
|
-
- [Best Practices](#best-practices)
|
|
18
|
-
|
|
19
|
-
---
|
|
20
|
-
|
|
21
|
-
## โจ Features
|
|
22
|
-
|
|
23
|
-
- ๐ข **Organization Management**: Complete CRUD operations for organizations
|
|
24
|
-
- ๐ฅ **Member Management**: Add, invite, and manage organization members
|
|
25
|
-
- ๐ **Role-Based Access Control**: Owner, Admin, Member, and Viewer roles
|
|
26
|
-
- โ๏ธ **Isolated Settings System**: Per-organization configuration for modules
|
|
27
|
-
- ๐ **Complete Isolation**: No fallback between organizations
|
|
28
|
-
- ๐ก๏ธ **Guards & Decorators**: Request-level access control
|
|
29
|
-
- ๐ **TypeORM Integration**: Full database support with migrations
|
|
30
|
-
- ๐ฆ **Fully Typed**: Complete TypeScript support
|
|
31
|
-
- ๐งช **Test Infrastructure**: Standalone test server
|
|
32
|
-
|
|
33
|
-
---
|
|
34
|
-
|
|
35
|
-
## ๐ฆ Installation
|
|
36
|
-
|
|
37
|
-
\`\`\`bash
|
|
38
|
-
npm install @venturialstd/organization
|
|
39
|
-
\`\`\`
|
|
40
|
-
|
|
41
|
-
### Peer Dependencies
|
|
42
|
-
|
|
43
|
-
\`\`\`json
|
|
44
|
-
{
|
|
45
|
-
"@nestjs/common": "^11.0.11",
|
|
46
|
-
"@nestjs/core": "^11.0.5",
|
|
47
|
-
"@nestjs/typeorm": "^10.0.0",
|
|
48
|
-
"@venturialstd/core": "^1.0.16",
|
|
49
|
-
"class-transformer": "^0.5.1",
|
|
50
|
-
"class-validator": "^0.14.1",
|
|
51
|
-
"typeorm": "^0.3.20"
|
|
52
|
-
}
|
|
53
|
-
\`\`\`
|
|
54
|
-
|
|
55
|
-
---
|
|
56
|
-
|
|
57
|
-
## ๐ Quick Start
|
|
58
|
-
|
|
59
|
-
### 1. Import the Module
|
|
60
|
-
|
|
61
|
-
\`\`\`typescript
|
|
62
|
-
import { Module } from '@nestjs/common';
|
|
63
|
-
import { OrganizationModule } from '@venturialstd/organization';
|
|
64
|
-
|
|
65
|
-
@Module({
|
|
66
|
-
imports: [OrganizationModule],
|
|
67
|
-
})
|
|
68
|
-
export class AppModule {}
|
|
69
|
-
\`\`\`
|
|
70
|
-
|
|
71
|
-
### 2. Run Migrations
|
|
72
|
-
|
|
73
|
-
\`\`\`bash
|
|
74
|
-
# Create and run migrations
|
|
75
|
-
npm run migration:generate -- CreateOrganizations
|
|
76
|
-
npm run migration:run
|
|
77
|
-
\`\`\`
|
|
78
|
-
|
|
79
|
-
### 3. Use the Services
|
|
80
|
-
|
|
81
|
-
\`\`\`typescript
|
|
82
|
-
import { Injectable } from '@nestjs/common';
|
|
83
|
-
import {
|
|
84
|
-
OrganizationService,
|
|
85
|
-
OrganizationUserService,
|
|
86
|
-
ORGANIZATION_USER_ROLE
|
|
87
|
-
} from '@venturialstd/organization';
|
|
88
|
-
|
|
89
|
-
@Injectable()
|
|
90
|
-
export class YourService {
|
|
91
|
-
constructor(
|
|
92
|
-
private readonly organizationService: OrganizationService,
|
|
93
|
-
private readonly organizationUserService: OrganizationUserService,
|
|
94
|
-
) {}
|
|
95
|
-
|
|
96
|
-
async createOrganization(userId: string) {
|
|
97
|
-
return this.organizationService.createOrganization(
|
|
98
|
-
'My Organization',
|
|
99
|
-
'my-org',
|
|
100
|
-
'example.com',
|
|
101
|
-
'Description',
|
|
102
|
-
userId
|
|
103
|
-
);
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
async addMember(organizationId: string, userId: string) {
|
|
107
|
-
return this.organizationUserService.addUserToOrganization(
|
|
108
|
-
organizationId,
|
|
109
|
-
userId,
|
|
110
|
-
ORGANIZATION_USER_ROLE.MEMBER
|
|
111
|
-
);
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
\`\`\`
|
|
115
|
-
|
|
116
|
-
---
|
|
117
|
-
|
|
118
|
-
## ๐ก Core Concepts
|
|
119
|
-
|
|
120
|
-
### Organizations
|
|
121
|
-
|
|
122
|
-
An organization represents a tenant in your multi-tenant application. Each organization:
|
|
123
|
-
- Has a unique name and slug
|
|
124
|
-
- Can have a custom domain
|
|
125
|
-
- Has its own settings and configuration
|
|
126
|
-
- Contains multiple members with different roles
|
|
127
|
-
- Is completely isolated from other organizations
|
|
128
|
-
|
|
129
|
-
### Organization Members
|
|
130
|
-
|
|
131
|
-
Users can belong to multiple organizations with different roles:
|
|
132
|
-
- **Owner**: Full control, can delete organization
|
|
133
|
-
- **Admin**: Can manage members and settings
|
|
134
|
-
- **Member**: Standard access to organization resources
|
|
135
|
-
- **Viewer**: Read-only access
|
|
136
|
-
|
|
137
|
-
### Organization Settings
|
|
138
|
-
|
|
139
|
-
Each organization can have isolated settings for any module in your application:
|
|
140
|
-
- **Complete Isolation**: No sharing between organizations
|
|
141
|
-
- **No Fallback**: Each organization must configure its own settings
|
|
142
|
-
- **Type-Safe**: Fully typed with TypeScript
|
|
143
|
-
- **Module-Agnostic**: Works with any module (GitLab, Jira, Slack, etc.)
|
|
144
|
-
|
|
145
|
-
---
|
|
146
|
-
|
|
147
|
-
## ๐๏ธ Architecture
|
|
148
|
-
|
|
149
|
-
### Module Structure
|
|
150
|
-
|
|
151
|
-
\`\`\`
|
|
152
|
-
OrganizationModule
|
|
153
|
-
โโโ Entities
|
|
154
|
-
โ โโโ Organization # Organization entity
|
|
155
|
-
โ โโโ OrganizationUser # User-Organization relationship
|
|
156
|
-
โ โโโ OrganizationSettings # Per-organization settings
|
|
157
|
-
โโโ Services
|
|
158
|
-
โ โโโ OrganizationService # CRUD operations
|
|
159
|
-
โ โโโ OrganizationUserService # Member management
|
|
160
|
-
โ โโโ OrganizationSettingsService # Settings management
|
|
161
|
-
โโโ Guards
|
|
162
|
-
โ โโโ OrganizationGuard # Check user belongs to org
|
|
163
|
-
โ โโโ OrganizationRoleGuard # Check user role
|
|
164
|
-
โโโ Decorators
|
|
165
|
-
โโโ @OrganizationId() # Extract org ID from request
|
|
166
|
-
โโโ @UserId() # Extract user ID from request
|
|
167
|
-
โโโ @Roles() # Define required roles
|
|
168
|
-
โโโ @OrganizationContext() # Extract full context
|
|
169
|
-
\`\`\`
|
|
170
|
-
|
|
171
|
-
### Settings Isolation
|
|
172
|
-
|
|
173
|
-
\`\`\`
|
|
174
|
-
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
175
|
-
โ Application โ
|
|
176
|
-
โ โ
|
|
177
|
-
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
|
|
178
|
-
โ โ Organization A โ โ
|
|
179
|
-
โ โ โข GitLab: gitlab-a.com + token-a โ โ
|
|
180
|
-
โ โ โข Jira: jira-a.com + credentials-a โ โ
|
|
181
|
-
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
|
|
182
|
-
โ โ
|
|
183
|
-
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
|
|
184
|
-
โ โ Organization B โ โ
|
|
185
|
-
โ โ โข GitLab: gitlab.com + token-b โ โ
|
|
186
|
-
โ โ โข Jira: [Not configured] โ โ
|
|
187
|
-
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
|
|
188
|
-
โ โ
|
|
189
|
-
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
190
|
-
\`\`\`
|
|
191
|
-
|
|
192
|
-
---
|
|
193
|
-
|
|
194
|
-
## ๐ API Reference
|
|
195
|
-
|
|
196
|
-
### Services
|
|
197
|
-
|
|
198
|
-
#### OrganizationService
|
|
199
|
-
|
|
200
|
-
\`\`\`typescript
|
|
201
|
-
class OrganizationService {
|
|
202
|
-
// Create a new organization
|
|
203
|
-
createOrganization(
|
|
204
|
-
name: string,
|
|
205
|
-
slug: string,
|
|
206
|
-
domain: string,
|
|
207
|
-
description: string,
|
|
208
|
-
ownerId: string
|
|
209
|
-
): Promise<Organization>
|
|
210
|
-
|
|
211
|
-
// Get organization by ID
|
|
212
|
-
getOrganizationById(id: string): Promise<Organization>
|
|
213
|
-
|
|
214
|
-
// Update organization
|
|
215
|
-
updateOrganization(id: string, data: Partial<Organization>): Promise<Organization>
|
|
216
|
-
|
|
217
|
-
// Delete organization
|
|
218
|
-
deleteOrganization(id: string): Promise<void>
|
|
219
|
-
|
|
220
|
-
// Get user's organizations
|
|
221
|
-
getUserOrganizations(userId: string): Promise<Organization[]>
|
|
222
|
-
}
|
|
223
|
-
\`\`\`
|
|
224
|
-
|
|
225
|
-
#### OrganizationUserService
|
|
226
|
-
|
|
227
|
-
\`\`\`typescript
|
|
228
|
-
class OrganizationUserService {
|
|
229
|
-
// Add user to organization
|
|
230
|
-
addUserToOrganization(
|
|
231
|
-
organizationId: string,
|
|
232
|
-
userId: string,
|
|
233
|
-
role: ORGANIZATION_USER_ROLE
|
|
234
|
-
): Promise<OrganizationUser>
|
|
235
|
-
|
|
236
|
-
// Remove user from organization
|
|
237
|
-
removeUserFromOrganization(
|
|
238
|
-
organizationId: string,
|
|
239
|
-
userId: string
|
|
240
|
-
): Promise<void>
|
|
241
|
-
|
|
242
|
-
// Update user role
|
|
243
|
-
updateUserRole(
|
|
244
|
-
organizationId: string,
|
|
245
|
-
userId: string,
|
|
246
|
-
role: ORGANIZATION_USER_ROLE
|
|
247
|
-
): Promise<OrganizationUser>
|
|
248
|
-
|
|
249
|
-
// Get organization members
|
|
250
|
-
getOrganizationMembers(organizationId: string): Promise<OrganizationUser[]>
|
|
251
|
-
|
|
252
|
-
// Check user role
|
|
253
|
-
getUserRole(
|
|
254
|
-
organizationId: string,
|
|
255
|
-
userId: string
|
|
256
|
-
): Promise<ORGANIZATION_USER_ROLE>
|
|
257
|
-
}
|
|
258
|
-
\`\`\`
|
|
259
|
-
|
|
260
|
-
#### OrganizationSettingsService
|
|
261
|
-
|
|
262
|
-
\`\`\`typescript
|
|
263
|
-
class OrganizationSettingsService {
|
|
264
|
-
// Get a single setting
|
|
265
|
-
get(organizationId: string, key: string): Promise<string | undefined>
|
|
266
|
-
|
|
267
|
-
// Get multiple settings
|
|
268
|
-
getManySettings(
|
|
269
|
-
organizationId: string,
|
|
270
|
-
keys: string[]
|
|
271
|
-
): Promise<Record<string, string>>
|
|
272
|
-
|
|
273
|
-
// Get all settings for organization
|
|
274
|
-
getAllForOrganization(
|
|
275
|
-
organizationId: string,
|
|
276
|
-
module?: string
|
|
277
|
-
): Promise<OrganizationSettings[]>
|
|
278
|
-
|
|
279
|
-
// Set or update a setting
|
|
280
|
-
setOrganizationSetting(
|
|
281
|
-
organizationId: string,
|
|
282
|
-
dto: Partial<OrganizationSettings>
|
|
283
|
-
): Promise<OrganizationSettings>
|
|
284
|
-
|
|
285
|
-
// Delete a setting
|
|
286
|
-
deleteOrganizationSetting(
|
|
287
|
-
organizationId: string,
|
|
288
|
-
module: string,
|
|
289
|
-
section: string,
|
|
290
|
-
key: string
|
|
291
|
-
): Promise<void>
|
|
292
|
-
|
|
293
|
-
// Reset all settings for a module
|
|
294
|
-
resetModuleSettings(
|
|
295
|
-
organizationId: string,
|
|
296
|
-
module: string
|
|
297
|
-
): Promise<void>
|
|
298
|
-
|
|
299
|
-
// Bulk update settings
|
|
300
|
-
bulkUpdateSettings(
|
|
301
|
-
organizationId: string,
|
|
302
|
-
settings: Partial<OrganizationSettings>[]
|
|
303
|
-
): Promise<OrganizationSettings[]>
|
|
304
|
-
}
|
|
305
|
-
\`\`\`
|
|
306
|
-
|
|
307
|
-
### Entities
|
|
308
|
-
|
|
309
|
-
#### Organization
|
|
310
|
-
|
|
311
|
-
\`\`\`typescript
|
|
312
|
-
class Organization {
|
|
313
|
-
id: string;
|
|
314
|
-
name: string;
|
|
315
|
-
slug: string;
|
|
316
|
-
domain: string;
|
|
317
|
-
description: string;
|
|
318
|
-
isActive: boolean;
|
|
319
|
-
settings: Record<string, unknown>;
|
|
320
|
-
ownerId: string;
|
|
321
|
-
createdAt: Date;
|
|
322
|
-
updatedAt: Date;
|
|
323
|
-
}
|
|
324
|
-
\`\`\`
|
|
325
|
-
|
|
326
|
-
#### OrganizationUser
|
|
327
|
-
|
|
328
|
-
\`\`\`typescript
|
|
329
|
-
class OrganizationUser {
|
|
330
|
-
id: string;
|
|
331
|
-
organizationId: string;
|
|
332
|
-
userId: string;
|
|
333
|
-
role: ORGANIZATION_USER_ROLE;
|
|
334
|
-
isActive: boolean;
|
|
335
|
-
createdAt: Date;
|
|
336
|
-
updatedAt: Date;
|
|
337
|
-
}
|
|
338
|
-
\`\`\`
|
|
339
|
-
|
|
340
|
-
#### OrganizationSettings
|
|
341
|
-
|
|
342
|
-
\`\`\`typescript
|
|
343
|
-
class OrganizationSettings {
|
|
344
|
-
id: string;
|
|
345
|
-
organizationId: string;
|
|
346
|
-
module: string; // e.g., 'GITLAB', 'JIRA'
|
|
347
|
-
section: string; // e.g., 'CREDENTIALS', 'OAUTH'
|
|
348
|
-
key: string; // e.g., 'TOKEN', 'API_KEY'
|
|
349
|
-
value: string;
|
|
350
|
-
type: SETTINGS_TYPE;
|
|
351
|
-
label: string;
|
|
352
|
-
description: string;
|
|
353
|
-
options: object | null;
|
|
354
|
-
sortOrder: number;
|
|
355
|
-
createdAt: Date;
|
|
356
|
-
updatedAt: Date;
|
|
357
|
-
}
|
|
358
|
-
\`\`\`
|
|
359
|
-
|
|
360
|
-
### Decorators
|
|
361
|
-
|
|
362
|
-
\`\`\`typescript
|
|
363
|
-
// Extract organization ID from request
|
|
364
|
-
@Get()
|
|
365
|
-
getData(@OrganizationId() organizationId: string) { }
|
|
366
|
-
|
|
367
|
-
// Extract user ID from request
|
|
368
|
-
@Get()
|
|
369
|
-
getData(@UserId() userId: string) { }
|
|
370
|
-
|
|
371
|
-
// Extract full context
|
|
372
|
-
@Get()
|
|
373
|
-
getData(@OrganizationContext() context: OrganizationContextType) { }
|
|
374
|
-
|
|
375
|
-
// Define required roles
|
|
376
|
-
@Roles(ORGANIZATION_USER_ROLE.ADMIN, ORGANIZATION_USER_ROLE.OWNER)
|
|
377
|
-
@UseGuards(OrganizationRoleGuard)
|
|
378
|
-
adminEndpoint() { }
|
|
379
|
-
\`\`\`
|
|
380
|
-
|
|
381
|
-
### Guards
|
|
382
|
-
|
|
383
|
-
\`\`\`typescript
|
|
384
|
-
// Check user belongs to organization
|
|
385
|
-
@UseGuards(OrganizationGuard)
|
|
386
|
-
@Get('organizations/:organizationId/data')
|
|
387
|
-
getData() { }
|
|
388
|
-
|
|
389
|
-
// Check user has required role
|
|
390
|
-
@Roles(ORGANIZATION_USER_ROLE.ADMIN)
|
|
391
|
-
@UseGuards(OrganizationRoleGuard)
|
|
392
|
-
adminEndpoint() { }
|
|
393
|
-
\`\`\`
|
|
394
|
-
|
|
395
|
-
### Constants
|
|
396
|
-
|
|
397
|
-
\`\`\`typescript
|
|
398
|
-
// User roles
|
|
399
|
-
enum ORGANIZATION_USER_ROLE {
|
|
400
|
-
OWNER = 'owner',
|
|
401
|
-
ADMIN = 'admin',
|
|
402
|
-
MEMBER = 'member',
|
|
403
|
-
VIEWER = 'viewer',
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
// Settings types
|
|
407
|
-
enum SETTINGS_TYPE {
|
|
408
|
-
TEXT = 'text',
|
|
409
|
-
TEXTAREA = 'textarea',
|
|
410
|
-
NUMBER = 'number',
|
|
411
|
-
BOOLEAN = 'boolean',
|
|
412
|
-
SELECT = 'select',
|
|
413
|
-
MULTISELECT = 'multiselect',
|
|
414
|
-
JSON = 'json',
|
|
415
|
-
DATE = 'date',
|
|
416
|
-
SECRET = 'secret',
|
|
417
|
-
}
|
|
418
|
-
\`\`\`
|
|
419
|
-
|
|
420
|
-
---
|
|
421
|
-
|
|
422
|
-
## โ๏ธ Organization Settings System
|
|
423
|
-
|
|
424
|
-
### Complete Isolation
|
|
425
|
-
|
|
426
|
-
Organization settings are **completely isolated** with no fallback mechanism:
|
|
427
|
-
|
|
428
|
-
\`\`\`typescript
|
|
429
|
-
// Organization A
|
|
430
|
-
const tokenA = await orgSettings.get('org-a-id', 'GITLAB:CREDENTIALS:TOKEN');
|
|
431
|
-
// Returns: 'token-a'
|
|
432
|
-
|
|
433
|
-
// Organization B (not configured)
|
|
434
|
-
const tokenB = await orgSettings.get('org-b-id', 'GITLAB:CREDENTIALS:TOKEN');
|
|
435
|
-
// Returns: undefined (NO FALLBACK)
|
|
436
|
-
\`\`\`
|
|
437
|
-
|
|
438
|
-
### Settings Types
|
|
439
|
-
|
|
440
|
-
Use \`SETTINGS_TYPE\` enum for type-safe settings:
|
|
441
|
-
|
|
442
|
-
\`\`\`typescript
|
|
443
|
-
import { SETTINGS_TYPE } from '@venturialstd/organization';
|
|
444
|
-
|
|
445
|
-
const settings = [
|
|
446
|
-
{
|
|
447
|
-
module: 'GITLAB',
|
|
448
|
-
section: 'CREDENTIALS',
|
|
449
|
-
key: 'TOKEN',
|
|
450
|
-
type: SETTINGS_TYPE.SECRET, // Hides value in UI
|
|
451
|
-
// ...
|
|
452
|
-
},
|
|
453
|
-
{
|
|
454
|
-
module: 'GITLAB',
|
|
455
|
-
section: 'PREFERENCES',
|
|
456
|
-
key: 'AUTO_MERGE',
|
|
457
|
-
type: SETTINGS_TYPE.BOOLEAN, // true/false toggle
|
|
458
|
-
// ...
|
|
459
|
-
},
|
|
460
|
-
];
|
|
461
|
-
\`\`\`
|
|
462
|
-
|
|
463
|
-
### Using Settings in Your Module
|
|
464
|
-
|
|
465
|
-
#### 1. Define Settings for Your Module
|
|
466
|
-
|
|
467
|
-
\`\`\`typescript
|
|
468
|
-
// src/your-module/settings/your-module.settings.ts
|
|
469
|
-
import { SETTINGS_TYPE } from '@venturialstd/organization';
|
|
470
|
-
|
|
471
|
-
export const SETTINGS = [
|
|
472
|
-
{
|
|
473
|
-
scope: 'ORGANIZATION',
|
|
474
|
-
module: 'YOUR_MODULE',
|
|
475
|
-
section: 'CREDENTIALS',
|
|
476
|
-
key: 'API_KEY',
|
|
477
|
-
label: 'API Key',
|
|
478
|
-
description: 'Your module API key',
|
|
479
|
-
type: SETTINGS_TYPE.SECRET,
|
|
480
|
-
value: '',
|
|
481
|
-
options: {
|
|
482
|
-
placeholder: 'Enter your API key',
|
|
483
|
-
help: 'Get your API key from your module dashboard',
|
|
484
|
-
},
|
|
485
|
-
sortOrder: 10,
|
|
486
|
-
},
|
|
487
|
-
{
|
|
488
|
-
scope: 'ORGANIZATION',
|
|
489
|
-
module: 'YOUR_MODULE',
|
|
490
|
-
section: 'CREDENTIALS',
|
|
491
|
-
key: 'API_SECRET',
|
|
492
|
-
label: 'API Secret',
|
|
493
|
-
description: 'Your module API secret',
|
|
494
|
-
type: SETTINGS_TYPE.SECRET,
|
|
495
|
-
value: '',
|
|
496
|
-
options: null,
|
|
497
|
-
sortOrder: 11,
|
|
498
|
-
},
|
|
499
|
-
];
|
|
500
|
-
\`\`\`
|
|
501
|
-
|
|
502
|
-
#### 2. Use Settings in Your Service
|
|
503
|
-
|
|
504
|
-
\`\`\`typescript
|
|
505
|
-
import { Injectable } from '@nestjs/common';
|
|
506
|
-
import { OrganizationSettingsService } from '@venturialstd/organization';
|
|
507
|
-
|
|
508
|
-
@Injectable()
|
|
509
|
-
export class YourModuleService {
|
|
510
|
-
constructor(
|
|
511
|
-
private readonly orgSettings: OrganizationSettingsService
|
|
512
|
-
) {}
|
|
513
|
-
|
|
514
|
-
async connect(organizationId: string) {
|
|
515
|
-
// Get organization-specific credentials
|
|
516
|
-
const apiKey = await this.orgSettings.get(
|
|
517
|
-
organizationId,
|
|
518
|
-
'YOUR_MODULE:CREDENTIALS:API_KEY'
|
|
519
|
-
);
|
|
520
|
-
|
|
521
|
-
const apiSecret = await this.orgSettings.get(
|
|
522
|
-
organizationId,
|
|
523
|
-
'YOUR_MODULE:CREDENTIALS:API_SECRET'
|
|
524
|
-
);
|
|
525
|
-
|
|
526
|
-
// Check if configured
|
|
527
|
-
if (!apiKey || !apiSecret) {
|
|
528
|
-
throw new Error('Module not configured for this organization');
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
// Use organization-specific credentials
|
|
532
|
-
return this.client.connect({ apiKey, apiSecret });
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
async getMultipleSettings(organizationId: string) {
|
|
536
|
-
// Get multiple settings at once
|
|
537
|
-
const settings = await this.orgSettings.getManySettings(
|
|
538
|
-
organizationId,
|
|
539
|
-
[
|
|
540
|
-
'YOUR_MODULE:CREDENTIALS:API_KEY',
|
|
541
|
-
'YOUR_MODULE:CREDENTIALS:API_SECRET',
|
|
542
|
-
'YOUR_MODULE:PREFERENCES:TIMEOUT',
|
|
543
|
-
]
|
|
544
|
-
);
|
|
545
|
-
|
|
546
|
-
return settings;
|
|
547
|
-
}
|
|
548
|
-
}
|
|
549
|
-
\`\`\`
|
|
550
|
-
|
|
551
|
-
#### 3. Import OrganizationModule
|
|
552
|
-
|
|
553
|
-
\`\`\`typescript
|
|
554
|
-
import { Module } from '@nestjs/common';
|
|
555
|
-
import { OrganizationModule } from '@venturialstd/organization';
|
|
556
|
-
|
|
557
|
-
@Module({
|
|
558
|
-
imports: [OrganizationModule],
|
|
559
|
-
providers: [YourModuleService],
|
|
560
|
-
})
|
|
561
|
-
export class YourModule {}
|
|
562
|
-
\`\`\`
|
|
563
|
-
|
|
564
|
-
### Database Schema
|
|
565
|
-
|
|
566
|
-
\`\`\`sql
|
|
567
|
-
CREATE TABLE organization_settings (
|
|
568
|
-
id UUID PRIMARY KEY,
|
|
569
|
-
organizationId UUID REFERENCES organization(id) ON DELETE CASCADE,
|
|
570
|
-
module VARCHAR NOT NULL, -- 'GITLAB', 'JIRA', 'YOUR_MODULE'
|
|
571
|
-
section VARCHAR NOT NULL, -- 'CREDENTIALS', 'OAUTH', 'PREFERENCES'
|
|
572
|
-
key VARCHAR NOT NULL, -- 'TOKEN', 'API_KEY', 'HOST'
|
|
573
|
-
value VARCHAR NOT NULL, -- Actual value
|
|
574
|
-
type settings_type_enum NOT NULL,
|
|
575
|
-
label VARCHAR NOT NULL,
|
|
576
|
-
description VARCHAR,
|
|
577
|
-
options JSONB,
|
|
578
|
-
sortOrder INTEGER DEFAULT 0,
|
|
579
|
-
createdAt TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
580
|
-
updatedAt TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
581
|
-
UNIQUE(organizationId, module, section, key)
|
|
582
|
-
);
|
|
583
|
-
\`\`\`
|
|
584
|
-
|
|
585
|
-
---
|
|
586
|
-
|
|
587
|
-
## ๐งช Test Server
|
|
588
|
-
|
|
589
|
-
The module includes a standalone test server for development and testing.
|
|
590
|
-
|
|
591
|
-
### Start the Test Server
|
|
592
|
-
|
|
593
|
-
\`\`\`bash
|
|
594
|
-
cd src/organization
|
|
595
|
-
npm install
|
|
596
|
-
npm run start:dev
|
|
597
|
-
\`\`\`
|
|
598
|
-
|
|
599
|
-
The server runs on **http://localhost:3002**
|
|
600
|
-
|
|
601
|
-
### Test Endpoints
|
|
602
|
-
|
|
603
|
-
\`\`\`bash
|
|
604
|
-
# Get all organizations
|
|
605
|
-
GET http://localhost:3002/test/organizations
|
|
606
|
-
|
|
607
|
-
# Get organization by ID
|
|
608
|
-
GET http://localhost:3002/test/organizations/:id
|
|
609
|
-
|
|
610
|
-
# Get organization settings
|
|
611
|
-
GET http://localhost:3002/test/organizations/:id/settings
|
|
612
|
-
|
|
613
|
-
# Get settings for specific module
|
|
614
|
-
GET http://localhost:3002/test/organizations/:id/settings?module=GITLAB
|
|
615
|
-
|
|
616
|
-
# Create/update setting
|
|
617
|
-
PUT http://localhost:3002/test/organizations/:id/settings
|
|
618
|
-
Content-Type: application/json
|
|
619
|
-
|
|
620
|
-
{
|
|
621
|
-
"module": "GITLAB",
|
|
622
|
-
"section": "CREDENTIALS",
|
|
623
|
-
"key": "TOKEN",
|
|
624
|
-
"value": "your-token",
|
|
625
|
-
"label": "GitLab Token",
|
|
626
|
-
"type": "secret"
|
|
627
|
-
}
|
|
628
|
-
\`\`\`
|
|
629
|
-
|
|
630
|
-
### Test Controller
|
|
631
|
-
|
|
632
|
-
The test controller is for **reference and development only**. In production, implement your own controllers with:
|
|
633
|
-
- Custom authentication
|
|
634
|
-
- Custom authorization
|
|
635
|
-
- Custom validation
|
|
636
|
-
- Custom error handling
|
|
637
|
-
|
|
638
|
-
**Location:** \`test/controllers/organization-settings-test.controller.ts\`
|
|
639
|
-
|
|
640
|
-
---
|
|
641
|
-
|
|
642
|
-
## ๐ Migration Guide
|
|
643
|
-
|
|
644
|
-
### Running Migrations
|
|
645
|
-
|
|
646
|
-
\`\`\`bash
|
|
647
|
-
# Generate migration
|
|
648
|
-
npm run migration:generate -- YourMigrationName
|
|
649
|
-
|
|
650
|
-
# Run migrations
|
|
651
|
-
npm run migration:run
|
|
652
|
-
|
|
653
|
-
# Revert migration
|
|
654
|
-
npm run migration:revert
|
|
655
|
-
\`\`\`
|
|
656
|
-
|
|
657
|
-
### Required Migrations
|
|
658
|
-
|
|
659
|
-
The module requires these migrations:
|
|
660
|
-
1. Create organizations table
|
|
661
|
-
2. Create organization_users table
|
|
662
|
-
3. Create organization_settings table
|
|
663
|
-
|
|
664
|
-
Migrations are located in: \`database/migrations/\`
|
|
665
|
-
|
|
666
|
-
---
|
|
667
|
-
|
|
668
|
-
## ๐ป Examples
|
|
669
|
-
|
|
670
|
-
### Example 1: Create Organization with Owner
|
|
671
|
-
|
|
672
|
-
\`\`\`typescript
|
|
673
|
-
@Injectable()
|
|
674
|
-
export class OnboardingService {
|
|
675
|
-
constructor(
|
|
676
|
-
private readonly orgService: OrganizationService,
|
|
677
|
-
private readonly orgUserService: OrganizationUserService,
|
|
678
|
-
) {}
|
|
679
|
-
|
|
680
|
-
async onboardUser(userId: string, companyName: string) {
|
|
681
|
-
// Create organization
|
|
682
|
-
const org = await this.orgService.createOrganization(
|
|
683
|
-
companyName,
|
|
684
|
-
this.generateSlug(companyName),
|
|
685
|
-
\`\${this.generateSlug(companyName)}.example.com\`,
|
|
686
|
-
\`\${companyName} organization\`,
|
|
687
|
-
userId
|
|
688
|
-
);
|
|
689
|
-
|
|
690
|
-
// Owner is automatically added by createOrganization
|
|
691
|
-
return org;
|
|
692
|
-
}
|
|
693
|
-
|
|
694
|
-
private generateSlug(name: string): string {
|
|
695
|
-
return name.toLowerCase().replace(/\\s+/g, '-');
|
|
696
|
-
}
|
|
697
|
-
}
|
|
698
|
-
\`\`\`
|
|
699
|
-
|
|
700
|
-
### Example 2: Check User Permission
|
|
701
|
-
|
|
702
|
-
\`\`\`typescript
|
|
703
|
-
@Controller('organizations/:organizationId/data')
|
|
704
|
-
export class DataController {
|
|
705
|
-
constructor(
|
|
706
|
-
private readonly orgUserService: OrganizationUserService,
|
|
707
|
-
) {}
|
|
708
|
-
|
|
709
|
-
@Get()
|
|
710
|
-
@UseGuards(OrganizationGuard)
|
|
711
|
-
async getData(
|
|
712
|
-
@OrganizationId() organizationId: string,
|
|
713
|
-
@UserId() userId: string,
|
|
714
|
-
) {
|
|
715
|
-
const role = await this.orgUserService.getUserRole(
|
|
716
|
-
organizationId,
|
|
717
|
-
userId
|
|
718
|
-
);
|
|
719
|
-
|
|
720
|
-
// Check if user has required role
|
|
721
|
-
if (![ORGANIZATION_USER_ROLE.ADMIN, ORGANIZATION_USER_ROLE.OWNER].includes(role)) {
|
|
722
|
-
throw new ForbiddenException('Insufficient permissions');
|
|
723
|
-
}
|
|
724
|
-
|
|
725
|
-
// Return data
|
|
726
|
-
}
|
|
727
|
-
}
|
|
728
|
-
\`\`\`
|
|
729
|
-
|
|
730
|
-
### Example 3: Module with Organization Settings
|
|
731
|
-
|
|
732
|
-
\`\`\`typescript
|
|
733
|
-
// GitLab Service
|
|
734
|
-
@Injectable()
|
|
735
|
-
export class GitLabService {
|
|
736
|
-
constructor(
|
|
737
|
-
private readonly orgSettings: OrganizationSettingsService
|
|
738
|
-
) {}
|
|
739
|
-
|
|
740
|
-
async getClient(organizationId: string) {
|
|
741
|
-
// Get org-specific GitLab credentials
|
|
742
|
-
const [host, token] = await Promise.all([
|
|
743
|
-
this.orgSettings.get(organizationId, 'GITLAB:CREDENTIALS:HOST'),
|
|
744
|
-
this.orgSettings.get(organizationId, 'GITLAB:CREDENTIALS:TOKEN'),
|
|
745
|
-
]);
|
|
746
|
-
|
|
747
|
-
// Validate configuration
|
|
748
|
-
if (!host || !token) {
|
|
749
|
-
throw new Error(
|
|
750
|
-
'GitLab not configured for this organization. ' +
|
|
751
|
-
'Please configure it in organization settings.'
|
|
752
|
-
);
|
|
753
|
-
}
|
|
754
|
-
|
|
755
|
-
// Return organization-specific client
|
|
756
|
-
return new GitLab({ host, token });
|
|
757
|
-
}
|
|
758
|
-
|
|
759
|
-
async getRepositories(organizationId: string) {
|
|
760
|
-
const client = await this.getClient(organizationId);
|
|
761
|
-
return client.Projects.all();
|
|
762
|
-
}
|
|
763
|
-
}
|
|
764
|
-
\`\`\`
|
|
765
|
-
|
|
766
|
-
### Example 4: Bulk Configure Organization
|
|
767
|
-
|
|
768
|
-
\`\`\`typescript
|
|
769
|
-
@Injectable()
|
|
770
|
-
export class OrganizationSetupService {
|
|
771
|
-
constructor(
|
|
772
|
-
private readonly orgSettings: OrganizationSettingsService,
|
|
773
|
-
) {}
|
|
774
|
-
|
|
775
|
-
async configureGitLab(
|
|
776
|
-
organizationId: string,
|
|
777
|
-
config: { host: string; token: string }
|
|
778
|
-
) {
|
|
779
|
-
return this.orgSettings.bulkUpdateSettings(organizationId, [
|
|
780
|
-
{
|
|
781
|
-
module: 'GITLAB',
|
|
782
|
-
section: 'CREDENTIALS',
|
|
783
|
-
key: 'HOST',
|
|
784
|
-
value: config.host,
|
|
785
|
-
label: 'GitLab Host',
|
|
786
|
-
type: SETTINGS_TYPE.TEXT,
|
|
787
|
-
},
|
|
788
|
-
{
|
|
789
|
-
module: 'GITLAB',
|
|
790
|
-
section: 'CREDENTIALS',
|
|
791
|
-
key: 'TOKEN',
|
|
792
|
-
value: config.token,
|
|
793
|
-
label: 'GitLab Token',
|
|
794
|
-
type: SETTINGS_TYPE.SECRET,
|
|
795
|
-
},
|
|
796
|
-
]);
|
|
797
|
-
}
|
|
798
|
-
}
|
|
799
|
-
\`\`\`
|
|
800
|
-
|
|
801
|
-
### Example 5: Settings Resolution Pattern
|
|
802
|
-
|
|
803
|
-
\`\`\`typescript
|
|
804
|
-
@Injectable()
|
|
805
|
-
export class IntegrationService {
|
|
806
|
-
constructor(
|
|
807
|
-
private readonly orgSettings: OrganizationSettingsService,
|
|
808
|
-
) {}
|
|
809
|
-
|
|
810
|
-
async getIntegrationConfig(organizationId: string, integration: string) {
|
|
811
|
-
// Get all settings for this integration
|
|
812
|
-
const settings = await this.orgSettings.getAllForOrganization(
|
|
813
|
-
organizationId,
|
|
814
|
-
integration
|
|
815
|
-
);
|
|
816
|
-
|
|
817
|
-
// Check if configured
|
|
818
|
-
if (settings.length === 0) {
|
|
819
|
-
return {
|
|
820
|
-
configured: false,
|
|
821
|
-
message: \`\${integration} is not configured for this organization\`,
|
|
822
|
-
};
|
|
823
|
-
}
|
|
824
|
-
|
|
825
|
-
// Convert to key-value object
|
|
826
|
-
const config = settings.reduce((acc, setting) => {
|
|
827
|
-
acc[\`\${setting.section}:\${setting.key}\`] = setting.value;
|
|
828
|
-
return acc;
|
|
829
|
-
}, {});
|
|
830
|
-
|
|
831
|
-
return {
|
|
832
|
-
configured: true,
|
|
833
|
-
config,
|
|
834
|
-
};
|
|
835
|
-
}
|
|
836
|
-
}
|
|
837
|
-
\`\`\`
|
|
838
|
-
|
|
839
|
-
---
|
|
840
|
-
|
|
841
|
-
## ๐ Best Practices
|
|
842
|
-
|
|
843
|
-
### 1. Always Check Configuration
|
|
844
|
-
|
|
845
|
-
\`\`\`typescript
|
|
846
|
-
// โ Don't assume settings exist
|
|
847
|
-
const token = await orgSettings.get(orgId, 'MODULE:CREDENTIALS:TOKEN');
|
|
848
|
-
await client.connect(token); // Could be undefined!
|
|
849
|
-
|
|
850
|
-
// โ
Always validate
|
|
851
|
-
const token = await orgSettings.get(orgId, 'MODULE:CREDENTIALS:TOKEN');
|
|
852
|
-
if (!token) {
|
|
853
|
-
throw new Error('Module not configured');
|
|
854
|
-
}
|
|
855
|
-
await client.connect(token);
|
|
856
|
-
\`\`\`
|
|
857
|
-
|
|
858
|
-
### 2. Use Bulk Operations
|
|
859
|
-
|
|
860
|
-
\`\`\`typescript
|
|
861
|
-
// โ Multiple single calls
|
|
862
|
-
const host = await orgSettings.get(orgId, 'MODULE:CREDENTIALS:HOST');
|
|
863
|
-
const token = await orgSettings.get(orgId, 'MODULE:CREDENTIALS:TOKEN');
|
|
864
|
-
const secret = await orgSettings.get(orgId, 'MODULE:CREDENTIALS:SECRET');
|
|
865
|
-
|
|
866
|
-
// โ
Single bulk call
|
|
867
|
-
const settings = await orgSettings.getManySettings(orgId, [
|
|
868
|
-
'MODULE:CREDENTIALS:HOST',
|
|
869
|
-
'MODULE:CREDENTIALS:TOKEN',
|
|
870
|
-
'MODULE:CREDENTIALS:SECRET',
|
|
871
|
-
]);
|
|
872
|
-
\`\`\`
|
|
873
|
-
|
|
874
|
-
### 3. Implement Your Own Controllers
|
|
875
|
-
|
|
876
|
-
\`\`\`typescript
|
|
877
|
-
// โ Don't use test controller in production
|
|
878
|
-
import { OrganizationSettingsTestController } from '@venturialstd/organization';
|
|
879
|
-
|
|
880
|
-
// โ
Implement your own
|
|
881
|
-
@Controller('api/organizations/:organizationId/settings')
|
|
882
|
-
@UseGuards(JwtAuthGuard, OrganizationGuard)
|
|
883
|
-
export class OrganizationSettingsController {
|
|
884
|
-
// Your implementation with proper auth
|
|
885
|
-
}
|
|
886
|
-
\`\`\`
|
|
887
|
-
|
|
888
|
-
### 4. Use Type-Safe Settings Types
|
|
889
|
-
|
|
890
|
-
\`\`\`typescript
|
|
891
|
-
// โ
Import and use the enum
|
|
892
|
-
import { SETTINGS_TYPE } from '@venturialstd/organization';
|
|
893
|
-
|
|
894
|
-
const setting = {
|
|
895
|
-
type: SETTINGS_TYPE.SECRET, // Type-safe
|
|
896
|
-
// ...
|
|
897
|
-
};
|
|
898
|
-
\`\`\`
|
|
899
|
-
|
|
900
|
-
---
|
|
901
|
-
|
|
902
|
-
## ๐ License
|
|
903
|
-
|
|
904
|
-
MIT
|
|
905
|
-
|
|
906
|
-
---
|
|
907
|
-
|
|
908
|
-
## ๐ค Contributing
|
|
909
|
-
|
|
910
|
-
Contributions welcome! Please read the contributing guidelines before submitting PRs.
|
|
1
|
+
# @venturialstd/organization
|
|
2
|
+
|
|
3
|
+
A comprehensive NestJS module for managing organizations with multi-tenant capabilities, role-based access control, and isolated per-organization settings.
|
|
4
|
+
|
|
5
|
+
## ๐ Table of Contents
|
|
6
|
+
|
|
7
|
+
- [Features](#features)
|
|
8
|
+
- [Installation](#installation)
|
|
9
|
+
- [Quick Start](#quick-start)
|
|
10
|
+
- [Core Concepts](#core-concepts)
|
|
11
|
+
- [Architecture](#architecture)
|
|
12
|
+
- [API Reference](#api-reference)
|
|
13
|
+
- [Organization Settings System](#organization-settings-system)
|
|
14
|
+
- [Test Server](#test-server)
|
|
15
|
+
- [Migration Guide](#migration-guide)
|
|
16
|
+
- [Examples](#examples)
|
|
17
|
+
- [Best Practices](#best-practices)
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## โจ Features
|
|
22
|
+
|
|
23
|
+
- ๐ข **Organization Management**: Complete CRUD operations for organizations
|
|
24
|
+
- ๐ฅ **Member Management**: Add, invite, and manage organization members
|
|
25
|
+
- ๐ **Role-Based Access Control**: Owner, Admin, Member, and Viewer roles
|
|
26
|
+
- โ๏ธ **Isolated Settings System**: Per-organization configuration for modules
|
|
27
|
+
- ๐ **Complete Isolation**: No fallback between organizations
|
|
28
|
+
- ๐ก๏ธ **Guards & Decorators**: Request-level access control
|
|
29
|
+
- ๐ **TypeORM Integration**: Full database support with migrations
|
|
30
|
+
- ๐ฆ **Fully Typed**: Complete TypeScript support
|
|
31
|
+
- ๐งช **Test Infrastructure**: Standalone test server
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## ๐ฆ Installation
|
|
36
|
+
|
|
37
|
+
\`\`\`bash
|
|
38
|
+
npm install @venturialstd/organization
|
|
39
|
+
\`\`\`
|
|
40
|
+
|
|
41
|
+
### Peer Dependencies
|
|
42
|
+
|
|
43
|
+
\`\`\`json
|
|
44
|
+
{
|
|
45
|
+
"@nestjs/common": "^11.0.11",
|
|
46
|
+
"@nestjs/core": "^11.0.5",
|
|
47
|
+
"@nestjs/typeorm": "^10.0.0",
|
|
48
|
+
"@venturialstd/core": "^1.0.16",
|
|
49
|
+
"class-transformer": "^0.5.1",
|
|
50
|
+
"class-validator": "^0.14.1",
|
|
51
|
+
"typeorm": "^0.3.20"
|
|
52
|
+
}
|
|
53
|
+
\`\`\`
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## ๐ Quick Start
|
|
58
|
+
|
|
59
|
+
### 1. Import the Module
|
|
60
|
+
|
|
61
|
+
\`\`\`typescript
|
|
62
|
+
import { Module } from '@nestjs/common';
|
|
63
|
+
import { OrganizationModule } from '@venturialstd/organization';
|
|
64
|
+
|
|
65
|
+
@Module({
|
|
66
|
+
imports: [OrganizationModule],
|
|
67
|
+
})
|
|
68
|
+
export class AppModule {}
|
|
69
|
+
\`\`\`
|
|
70
|
+
|
|
71
|
+
### 2. Run Migrations
|
|
72
|
+
|
|
73
|
+
\`\`\`bash
|
|
74
|
+
# Create and run migrations
|
|
75
|
+
npm run migration:generate -- CreateOrganizations
|
|
76
|
+
npm run migration:run
|
|
77
|
+
\`\`\`
|
|
78
|
+
|
|
79
|
+
### 3. Use the Services
|
|
80
|
+
|
|
81
|
+
\`\`\`typescript
|
|
82
|
+
import { Injectable } from '@nestjs/common';
|
|
83
|
+
import {
|
|
84
|
+
OrganizationService,
|
|
85
|
+
OrganizationUserService,
|
|
86
|
+
ORGANIZATION_USER_ROLE
|
|
87
|
+
} from '@venturialstd/organization';
|
|
88
|
+
|
|
89
|
+
@Injectable()
|
|
90
|
+
export class YourService {
|
|
91
|
+
constructor(
|
|
92
|
+
private readonly organizationService: OrganizationService,
|
|
93
|
+
private readonly organizationUserService: OrganizationUserService,
|
|
94
|
+
) {}
|
|
95
|
+
|
|
96
|
+
async createOrganization(userId: string) {
|
|
97
|
+
return this.organizationService.createOrganization(
|
|
98
|
+
'My Organization',
|
|
99
|
+
'my-org',
|
|
100
|
+
'example.com',
|
|
101
|
+
'Description',
|
|
102
|
+
userId
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async addMember(organizationId: string, userId: string) {
|
|
107
|
+
return this.organizationUserService.addUserToOrganization(
|
|
108
|
+
organizationId,
|
|
109
|
+
userId,
|
|
110
|
+
ORGANIZATION_USER_ROLE.MEMBER
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
\`\`\`
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
## ๐ก Core Concepts
|
|
119
|
+
|
|
120
|
+
### Organizations
|
|
121
|
+
|
|
122
|
+
An organization represents a tenant in your multi-tenant application. Each organization:
|
|
123
|
+
- Has a unique name and slug
|
|
124
|
+
- Can have a custom domain
|
|
125
|
+
- Has its own settings and configuration
|
|
126
|
+
- Contains multiple members with different roles
|
|
127
|
+
- Is completely isolated from other organizations
|
|
128
|
+
|
|
129
|
+
### Organization Members
|
|
130
|
+
|
|
131
|
+
Users can belong to multiple organizations with different roles:
|
|
132
|
+
- **Owner**: Full control, can delete organization
|
|
133
|
+
- **Admin**: Can manage members and settings
|
|
134
|
+
- **Member**: Standard access to organization resources
|
|
135
|
+
- **Viewer**: Read-only access
|
|
136
|
+
|
|
137
|
+
### Organization Settings
|
|
138
|
+
|
|
139
|
+
Each organization can have isolated settings for any module in your application:
|
|
140
|
+
- **Complete Isolation**: No sharing between organizations
|
|
141
|
+
- **No Fallback**: Each organization must configure its own settings
|
|
142
|
+
- **Type-Safe**: Fully typed with TypeScript
|
|
143
|
+
- **Module-Agnostic**: Works with any module (GitLab, Jira, Slack, etc.)
|
|
144
|
+
|
|
145
|
+
---
|
|
146
|
+
|
|
147
|
+
## ๐๏ธ Architecture
|
|
148
|
+
|
|
149
|
+
### Module Structure
|
|
150
|
+
|
|
151
|
+
\`\`\`
|
|
152
|
+
OrganizationModule
|
|
153
|
+
โโโ Entities
|
|
154
|
+
โ โโโ Organization # Organization entity
|
|
155
|
+
โ โโโ OrganizationUser # User-Organization relationship
|
|
156
|
+
โ โโโ OrganizationSettings # Per-organization settings
|
|
157
|
+
โโโ Services
|
|
158
|
+
โ โโโ OrganizationService # CRUD operations
|
|
159
|
+
โ โโโ OrganizationUserService # Member management
|
|
160
|
+
โ โโโ OrganizationSettingsService # Settings management
|
|
161
|
+
โโโ Guards
|
|
162
|
+
โ โโโ OrganizationGuard # Check user belongs to org
|
|
163
|
+
โ โโโ OrganizationRoleGuard # Check user role
|
|
164
|
+
โโโ Decorators
|
|
165
|
+
โโโ @OrganizationId() # Extract org ID from request
|
|
166
|
+
โโโ @UserId() # Extract user ID from request
|
|
167
|
+
โโโ @Roles() # Define required roles
|
|
168
|
+
โโโ @OrganizationContext() # Extract full context
|
|
169
|
+
\`\`\`
|
|
170
|
+
|
|
171
|
+
### Settings Isolation
|
|
172
|
+
|
|
173
|
+
\`\`\`
|
|
174
|
+
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
175
|
+
โ Application โ
|
|
176
|
+
โ โ
|
|
177
|
+
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
|
|
178
|
+
โ โ Organization A โ โ
|
|
179
|
+
โ โ โข GitLab: gitlab-a.com + token-a โ โ
|
|
180
|
+
โ โ โข Jira: jira-a.com + credentials-a โ โ
|
|
181
|
+
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
|
|
182
|
+
โ โ
|
|
183
|
+
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
|
|
184
|
+
โ โ Organization B โ โ
|
|
185
|
+
โ โ โข GitLab: gitlab.com + token-b โ โ
|
|
186
|
+
โ โ โข Jira: [Not configured] โ โ
|
|
187
|
+
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
|
|
188
|
+
โ โ
|
|
189
|
+
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
190
|
+
\`\`\`
|
|
191
|
+
|
|
192
|
+
---
|
|
193
|
+
|
|
194
|
+
## ๐ API Reference
|
|
195
|
+
|
|
196
|
+
### Services
|
|
197
|
+
|
|
198
|
+
#### OrganizationService
|
|
199
|
+
|
|
200
|
+
\`\`\`typescript
|
|
201
|
+
class OrganizationService {
|
|
202
|
+
// Create a new organization
|
|
203
|
+
createOrganization(
|
|
204
|
+
name: string,
|
|
205
|
+
slug: string,
|
|
206
|
+
domain: string,
|
|
207
|
+
description: string,
|
|
208
|
+
ownerId: string
|
|
209
|
+
): Promise<Organization>
|
|
210
|
+
|
|
211
|
+
// Get organization by ID
|
|
212
|
+
getOrganizationById(id: string): Promise<Organization>
|
|
213
|
+
|
|
214
|
+
// Update organization
|
|
215
|
+
updateOrganization(id: string, data: Partial<Organization>): Promise<Organization>
|
|
216
|
+
|
|
217
|
+
// Delete organization
|
|
218
|
+
deleteOrganization(id: string): Promise<void>
|
|
219
|
+
|
|
220
|
+
// Get user's organizations
|
|
221
|
+
getUserOrganizations(userId: string): Promise<Organization[]>
|
|
222
|
+
}
|
|
223
|
+
\`\`\`
|
|
224
|
+
|
|
225
|
+
#### OrganizationUserService
|
|
226
|
+
|
|
227
|
+
\`\`\`typescript
|
|
228
|
+
class OrganizationUserService {
|
|
229
|
+
// Add user to organization
|
|
230
|
+
addUserToOrganization(
|
|
231
|
+
organizationId: string,
|
|
232
|
+
userId: string,
|
|
233
|
+
role: ORGANIZATION_USER_ROLE
|
|
234
|
+
): Promise<OrganizationUser>
|
|
235
|
+
|
|
236
|
+
// Remove user from organization
|
|
237
|
+
removeUserFromOrganization(
|
|
238
|
+
organizationId: string,
|
|
239
|
+
userId: string
|
|
240
|
+
): Promise<void>
|
|
241
|
+
|
|
242
|
+
// Update user role
|
|
243
|
+
updateUserRole(
|
|
244
|
+
organizationId: string,
|
|
245
|
+
userId: string,
|
|
246
|
+
role: ORGANIZATION_USER_ROLE
|
|
247
|
+
): Promise<OrganizationUser>
|
|
248
|
+
|
|
249
|
+
// Get organization members
|
|
250
|
+
getOrganizationMembers(organizationId: string): Promise<OrganizationUser[]>
|
|
251
|
+
|
|
252
|
+
// Check user role
|
|
253
|
+
getUserRole(
|
|
254
|
+
organizationId: string,
|
|
255
|
+
userId: string
|
|
256
|
+
): Promise<ORGANIZATION_USER_ROLE>
|
|
257
|
+
}
|
|
258
|
+
\`\`\`
|
|
259
|
+
|
|
260
|
+
#### OrganizationSettingsService
|
|
261
|
+
|
|
262
|
+
\`\`\`typescript
|
|
263
|
+
class OrganizationSettingsService {
|
|
264
|
+
// Get a single setting
|
|
265
|
+
get(organizationId: string, key: string): Promise<string | undefined>
|
|
266
|
+
|
|
267
|
+
// Get multiple settings
|
|
268
|
+
getManySettings(
|
|
269
|
+
organizationId: string,
|
|
270
|
+
keys: string[]
|
|
271
|
+
): Promise<Record<string, string>>
|
|
272
|
+
|
|
273
|
+
// Get all settings for organization
|
|
274
|
+
getAllForOrganization(
|
|
275
|
+
organizationId: string,
|
|
276
|
+
module?: string
|
|
277
|
+
): Promise<OrganizationSettings[]>
|
|
278
|
+
|
|
279
|
+
// Set or update a setting
|
|
280
|
+
setOrganizationSetting(
|
|
281
|
+
organizationId: string,
|
|
282
|
+
dto: Partial<OrganizationSettings>
|
|
283
|
+
): Promise<OrganizationSettings>
|
|
284
|
+
|
|
285
|
+
// Delete a setting
|
|
286
|
+
deleteOrganizationSetting(
|
|
287
|
+
organizationId: string,
|
|
288
|
+
module: string,
|
|
289
|
+
section: string,
|
|
290
|
+
key: string
|
|
291
|
+
): Promise<void>
|
|
292
|
+
|
|
293
|
+
// Reset all settings for a module
|
|
294
|
+
resetModuleSettings(
|
|
295
|
+
organizationId: string,
|
|
296
|
+
module: string
|
|
297
|
+
): Promise<void>
|
|
298
|
+
|
|
299
|
+
// Bulk update settings
|
|
300
|
+
bulkUpdateSettings(
|
|
301
|
+
organizationId: string,
|
|
302
|
+
settings: Partial<OrganizationSettings>[]
|
|
303
|
+
): Promise<OrganizationSettings[]>
|
|
304
|
+
}
|
|
305
|
+
\`\`\`
|
|
306
|
+
|
|
307
|
+
### Entities
|
|
308
|
+
|
|
309
|
+
#### Organization
|
|
310
|
+
|
|
311
|
+
\`\`\`typescript
|
|
312
|
+
class Organization {
|
|
313
|
+
id: string;
|
|
314
|
+
name: string;
|
|
315
|
+
slug: string;
|
|
316
|
+
domain: string;
|
|
317
|
+
description: string;
|
|
318
|
+
isActive: boolean;
|
|
319
|
+
settings: Record<string, unknown>;
|
|
320
|
+
ownerId: string;
|
|
321
|
+
createdAt: Date;
|
|
322
|
+
updatedAt: Date;
|
|
323
|
+
}
|
|
324
|
+
\`\`\`
|
|
325
|
+
|
|
326
|
+
#### OrganizationUser
|
|
327
|
+
|
|
328
|
+
\`\`\`typescript
|
|
329
|
+
class OrganizationUser {
|
|
330
|
+
id: string;
|
|
331
|
+
organizationId: string;
|
|
332
|
+
userId: string;
|
|
333
|
+
role: ORGANIZATION_USER_ROLE;
|
|
334
|
+
isActive: boolean;
|
|
335
|
+
createdAt: Date;
|
|
336
|
+
updatedAt: Date;
|
|
337
|
+
}
|
|
338
|
+
\`\`\`
|
|
339
|
+
|
|
340
|
+
#### OrganizationSettings
|
|
341
|
+
|
|
342
|
+
\`\`\`typescript
|
|
343
|
+
class OrganizationSettings {
|
|
344
|
+
id: string;
|
|
345
|
+
organizationId: string;
|
|
346
|
+
module: string; // e.g., 'GITLAB', 'JIRA'
|
|
347
|
+
section: string; // e.g., 'CREDENTIALS', 'OAUTH'
|
|
348
|
+
key: string; // e.g., 'TOKEN', 'API_KEY'
|
|
349
|
+
value: string;
|
|
350
|
+
type: SETTINGS_TYPE;
|
|
351
|
+
label: string;
|
|
352
|
+
description: string;
|
|
353
|
+
options: object | null;
|
|
354
|
+
sortOrder: number;
|
|
355
|
+
createdAt: Date;
|
|
356
|
+
updatedAt: Date;
|
|
357
|
+
}
|
|
358
|
+
\`\`\`
|
|
359
|
+
|
|
360
|
+
### Decorators
|
|
361
|
+
|
|
362
|
+
\`\`\`typescript
|
|
363
|
+
// Extract organization ID from request
|
|
364
|
+
@Get()
|
|
365
|
+
getData(@OrganizationId() organizationId: string) { }
|
|
366
|
+
|
|
367
|
+
// Extract user ID from request
|
|
368
|
+
@Get()
|
|
369
|
+
getData(@UserId() userId: string) { }
|
|
370
|
+
|
|
371
|
+
// Extract full context
|
|
372
|
+
@Get()
|
|
373
|
+
getData(@OrganizationContext() context: OrganizationContextType) { }
|
|
374
|
+
|
|
375
|
+
// Define required roles
|
|
376
|
+
@Roles(ORGANIZATION_USER_ROLE.ADMIN, ORGANIZATION_USER_ROLE.OWNER)
|
|
377
|
+
@UseGuards(OrganizationRoleGuard)
|
|
378
|
+
adminEndpoint() { }
|
|
379
|
+
\`\`\`
|
|
380
|
+
|
|
381
|
+
### Guards
|
|
382
|
+
|
|
383
|
+
\`\`\`typescript
|
|
384
|
+
// Check user belongs to organization
|
|
385
|
+
@UseGuards(OrganizationGuard)
|
|
386
|
+
@Get('organizations/:organizationId/data')
|
|
387
|
+
getData() { }
|
|
388
|
+
|
|
389
|
+
// Check user has required role
|
|
390
|
+
@Roles(ORGANIZATION_USER_ROLE.ADMIN)
|
|
391
|
+
@UseGuards(OrganizationRoleGuard)
|
|
392
|
+
adminEndpoint() { }
|
|
393
|
+
\`\`\`
|
|
394
|
+
|
|
395
|
+
### Constants
|
|
396
|
+
|
|
397
|
+
\`\`\`typescript
|
|
398
|
+
// User roles
|
|
399
|
+
enum ORGANIZATION_USER_ROLE {
|
|
400
|
+
OWNER = 'owner',
|
|
401
|
+
ADMIN = 'admin',
|
|
402
|
+
MEMBER = 'member',
|
|
403
|
+
VIEWER = 'viewer',
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// Settings types
|
|
407
|
+
enum SETTINGS_TYPE {
|
|
408
|
+
TEXT = 'text',
|
|
409
|
+
TEXTAREA = 'textarea',
|
|
410
|
+
NUMBER = 'number',
|
|
411
|
+
BOOLEAN = 'boolean',
|
|
412
|
+
SELECT = 'select',
|
|
413
|
+
MULTISELECT = 'multiselect',
|
|
414
|
+
JSON = 'json',
|
|
415
|
+
DATE = 'date',
|
|
416
|
+
SECRET = 'secret',
|
|
417
|
+
}
|
|
418
|
+
\`\`\`
|
|
419
|
+
|
|
420
|
+
---
|
|
421
|
+
|
|
422
|
+
## โ๏ธ Organization Settings System
|
|
423
|
+
|
|
424
|
+
### Complete Isolation
|
|
425
|
+
|
|
426
|
+
Organization settings are **completely isolated** with no fallback mechanism:
|
|
427
|
+
|
|
428
|
+
\`\`\`typescript
|
|
429
|
+
// Organization A
|
|
430
|
+
const tokenA = await orgSettings.get('org-a-id', 'GITLAB:CREDENTIALS:TOKEN');
|
|
431
|
+
// Returns: 'token-a'
|
|
432
|
+
|
|
433
|
+
// Organization B (not configured)
|
|
434
|
+
const tokenB = await orgSettings.get('org-b-id', 'GITLAB:CREDENTIALS:TOKEN');
|
|
435
|
+
// Returns: undefined (NO FALLBACK)
|
|
436
|
+
\`\`\`
|
|
437
|
+
|
|
438
|
+
### Settings Types
|
|
439
|
+
|
|
440
|
+
Use \`SETTINGS_TYPE\` enum for type-safe settings:
|
|
441
|
+
|
|
442
|
+
\`\`\`typescript
|
|
443
|
+
import { SETTINGS_TYPE } from '@venturialstd/organization';
|
|
444
|
+
|
|
445
|
+
const settings = [
|
|
446
|
+
{
|
|
447
|
+
module: 'GITLAB',
|
|
448
|
+
section: 'CREDENTIALS',
|
|
449
|
+
key: 'TOKEN',
|
|
450
|
+
type: SETTINGS_TYPE.SECRET, // Hides value in UI
|
|
451
|
+
// ...
|
|
452
|
+
},
|
|
453
|
+
{
|
|
454
|
+
module: 'GITLAB',
|
|
455
|
+
section: 'PREFERENCES',
|
|
456
|
+
key: 'AUTO_MERGE',
|
|
457
|
+
type: SETTINGS_TYPE.BOOLEAN, // true/false toggle
|
|
458
|
+
// ...
|
|
459
|
+
},
|
|
460
|
+
];
|
|
461
|
+
\`\`\`
|
|
462
|
+
|
|
463
|
+
### Using Settings in Your Module
|
|
464
|
+
|
|
465
|
+
#### 1. Define Settings for Your Module
|
|
466
|
+
|
|
467
|
+
\`\`\`typescript
|
|
468
|
+
// src/your-module/settings/your-module.settings.ts
|
|
469
|
+
import { SETTINGS_TYPE } from '@venturialstd/organization';
|
|
470
|
+
|
|
471
|
+
export const SETTINGS = [
|
|
472
|
+
{
|
|
473
|
+
scope: 'ORGANIZATION',
|
|
474
|
+
module: 'YOUR_MODULE',
|
|
475
|
+
section: 'CREDENTIALS',
|
|
476
|
+
key: 'API_KEY',
|
|
477
|
+
label: 'API Key',
|
|
478
|
+
description: 'Your module API key',
|
|
479
|
+
type: SETTINGS_TYPE.SECRET,
|
|
480
|
+
value: '',
|
|
481
|
+
options: {
|
|
482
|
+
placeholder: 'Enter your API key',
|
|
483
|
+
help: 'Get your API key from your module dashboard',
|
|
484
|
+
},
|
|
485
|
+
sortOrder: 10,
|
|
486
|
+
},
|
|
487
|
+
{
|
|
488
|
+
scope: 'ORGANIZATION',
|
|
489
|
+
module: 'YOUR_MODULE',
|
|
490
|
+
section: 'CREDENTIALS',
|
|
491
|
+
key: 'API_SECRET',
|
|
492
|
+
label: 'API Secret',
|
|
493
|
+
description: 'Your module API secret',
|
|
494
|
+
type: SETTINGS_TYPE.SECRET,
|
|
495
|
+
value: '',
|
|
496
|
+
options: null,
|
|
497
|
+
sortOrder: 11,
|
|
498
|
+
},
|
|
499
|
+
];
|
|
500
|
+
\`\`\`
|
|
501
|
+
|
|
502
|
+
#### 2. Use Settings in Your Service
|
|
503
|
+
|
|
504
|
+
\`\`\`typescript
|
|
505
|
+
import { Injectable } from '@nestjs/common';
|
|
506
|
+
import { OrganizationSettingsService } from '@venturialstd/organization';
|
|
507
|
+
|
|
508
|
+
@Injectable()
|
|
509
|
+
export class YourModuleService {
|
|
510
|
+
constructor(
|
|
511
|
+
private readonly orgSettings: OrganizationSettingsService
|
|
512
|
+
) {}
|
|
513
|
+
|
|
514
|
+
async connect(organizationId: string) {
|
|
515
|
+
// Get organization-specific credentials
|
|
516
|
+
const apiKey = await this.orgSettings.get(
|
|
517
|
+
organizationId,
|
|
518
|
+
'YOUR_MODULE:CREDENTIALS:API_KEY'
|
|
519
|
+
);
|
|
520
|
+
|
|
521
|
+
const apiSecret = await this.orgSettings.get(
|
|
522
|
+
organizationId,
|
|
523
|
+
'YOUR_MODULE:CREDENTIALS:API_SECRET'
|
|
524
|
+
);
|
|
525
|
+
|
|
526
|
+
// Check if configured
|
|
527
|
+
if (!apiKey || !apiSecret) {
|
|
528
|
+
throw new Error('Module not configured for this organization');
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
// Use organization-specific credentials
|
|
532
|
+
return this.client.connect({ apiKey, apiSecret });
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
async getMultipleSettings(organizationId: string) {
|
|
536
|
+
// Get multiple settings at once
|
|
537
|
+
const settings = await this.orgSettings.getManySettings(
|
|
538
|
+
organizationId,
|
|
539
|
+
[
|
|
540
|
+
'YOUR_MODULE:CREDENTIALS:API_KEY',
|
|
541
|
+
'YOUR_MODULE:CREDENTIALS:API_SECRET',
|
|
542
|
+
'YOUR_MODULE:PREFERENCES:TIMEOUT',
|
|
543
|
+
]
|
|
544
|
+
);
|
|
545
|
+
|
|
546
|
+
return settings;
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
\`\`\`
|
|
550
|
+
|
|
551
|
+
#### 3. Import OrganizationModule
|
|
552
|
+
|
|
553
|
+
\`\`\`typescript
|
|
554
|
+
import { Module } from '@nestjs/common';
|
|
555
|
+
import { OrganizationModule } from '@venturialstd/organization';
|
|
556
|
+
|
|
557
|
+
@Module({
|
|
558
|
+
imports: [OrganizationModule],
|
|
559
|
+
providers: [YourModuleService],
|
|
560
|
+
})
|
|
561
|
+
export class YourModule {}
|
|
562
|
+
\`\`\`
|
|
563
|
+
|
|
564
|
+
### Database Schema
|
|
565
|
+
|
|
566
|
+
\`\`\`sql
|
|
567
|
+
CREATE TABLE organization_settings (
|
|
568
|
+
id UUID PRIMARY KEY,
|
|
569
|
+
organizationId UUID REFERENCES organization(id) ON DELETE CASCADE,
|
|
570
|
+
module VARCHAR NOT NULL, -- 'GITLAB', 'JIRA', 'YOUR_MODULE'
|
|
571
|
+
section VARCHAR NOT NULL, -- 'CREDENTIALS', 'OAUTH', 'PREFERENCES'
|
|
572
|
+
key VARCHAR NOT NULL, -- 'TOKEN', 'API_KEY', 'HOST'
|
|
573
|
+
value VARCHAR NOT NULL, -- Actual value
|
|
574
|
+
type settings_type_enum NOT NULL,
|
|
575
|
+
label VARCHAR NOT NULL,
|
|
576
|
+
description VARCHAR,
|
|
577
|
+
options JSONB,
|
|
578
|
+
sortOrder INTEGER DEFAULT 0,
|
|
579
|
+
createdAt TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
580
|
+
updatedAt TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
581
|
+
UNIQUE(organizationId, module, section, key)
|
|
582
|
+
);
|
|
583
|
+
\`\`\`
|
|
584
|
+
|
|
585
|
+
---
|
|
586
|
+
|
|
587
|
+
## ๐งช Test Server
|
|
588
|
+
|
|
589
|
+
The module includes a standalone test server for development and testing.
|
|
590
|
+
|
|
591
|
+
### Start the Test Server
|
|
592
|
+
|
|
593
|
+
\`\`\`bash
|
|
594
|
+
cd src/organization
|
|
595
|
+
npm install
|
|
596
|
+
npm run start:dev
|
|
597
|
+
\`\`\`
|
|
598
|
+
|
|
599
|
+
The server runs on **http://localhost:3002**
|
|
600
|
+
|
|
601
|
+
### Test Endpoints
|
|
602
|
+
|
|
603
|
+
\`\`\`bash
|
|
604
|
+
# Get all organizations
|
|
605
|
+
GET http://localhost:3002/test/organizations
|
|
606
|
+
|
|
607
|
+
# Get organization by ID
|
|
608
|
+
GET http://localhost:3002/test/organizations/:id
|
|
609
|
+
|
|
610
|
+
# Get organization settings
|
|
611
|
+
GET http://localhost:3002/test/organizations/:id/settings
|
|
612
|
+
|
|
613
|
+
# Get settings for specific module
|
|
614
|
+
GET http://localhost:3002/test/organizations/:id/settings?module=GITLAB
|
|
615
|
+
|
|
616
|
+
# Create/update setting
|
|
617
|
+
PUT http://localhost:3002/test/organizations/:id/settings
|
|
618
|
+
Content-Type: application/json
|
|
619
|
+
|
|
620
|
+
{
|
|
621
|
+
"module": "GITLAB",
|
|
622
|
+
"section": "CREDENTIALS",
|
|
623
|
+
"key": "TOKEN",
|
|
624
|
+
"value": "your-token",
|
|
625
|
+
"label": "GitLab Token",
|
|
626
|
+
"type": "secret"
|
|
627
|
+
}
|
|
628
|
+
\`\`\`
|
|
629
|
+
|
|
630
|
+
### Test Controller
|
|
631
|
+
|
|
632
|
+
The test controller is for **reference and development only**. In production, implement your own controllers with:
|
|
633
|
+
- Custom authentication
|
|
634
|
+
- Custom authorization
|
|
635
|
+
- Custom validation
|
|
636
|
+
- Custom error handling
|
|
637
|
+
|
|
638
|
+
**Location:** \`test/controllers/organization-settings-test.controller.ts\`
|
|
639
|
+
|
|
640
|
+
---
|
|
641
|
+
|
|
642
|
+
## ๐ Migration Guide
|
|
643
|
+
|
|
644
|
+
### Running Migrations
|
|
645
|
+
|
|
646
|
+
\`\`\`bash
|
|
647
|
+
# Generate migration
|
|
648
|
+
npm run migration:generate -- YourMigrationName
|
|
649
|
+
|
|
650
|
+
# Run migrations
|
|
651
|
+
npm run migration:run
|
|
652
|
+
|
|
653
|
+
# Revert migration
|
|
654
|
+
npm run migration:revert
|
|
655
|
+
\`\`\`
|
|
656
|
+
|
|
657
|
+
### Required Migrations
|
|
658
|
+
|
|
659
|
+
The module requires these migrations:
|
|
660
|
+
1. Create organizations table
|
|
661
|
+
2. Create organization_users table
|
|
662
|
+
3. Create organization_settings table
|
|
663
|
+
|
|
664
|
+
Migrations are located in: \`database/migrations/\`
|
|
665
|
+
|
|
666
|
+
---
|
|
667
|
+
|
|
668
|
+
## ๐ป Examples
|
|
669
|
+
|
|
670
|
+
### Example 1: Create Organization with Owner
|
|
671
|
+
|
|
672
|
+
\`\`\`typescript
|
|
673
|
+
@Injectable()
|
|
674
|
+
export class OnboardingService {
|
|
675
|
+
constructor(
|
|
676
|
+
private readonly orgService: OrganizationService,
|
|
677
|
+
private readonly orgUserService: OrganizationUserService,
|
|
678
|
+
) {}
|
|
679
|
+
|
|
680
|
+
async onboardUser(userId: string, companyName: string) {
|
|
681
|
+
// Create organization
|
|
682
|
+
const org = await this.orgService.createOrganization(
|
|
683
|
+
companyName,
|
|
684
|
+
this.generateSlug(companyName),
|
|
685
|
+
\`\${this.generateSlug(companyName)}.example.com\`,
|
|
686
|
+
\`\${companyName} organization\`,
|
|
687
|
+
userId
|
|
688
|
+
);
|
|
689
|
+
|
|
690
|
+
// Owner is automatically added by createOrganization
|
|
691
|
+
return org;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
private generateSlug(name: string): string {
|
|
695
|
+
return name.toLowerCase().replace(/\\s+/g, '-');
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
\`\`\`
|
|
699
|
+
|
|
700
|
+
### Example 2: Check User Permission
|
|
701
|
+
|
|
702
|
+
\`\`\`typescript
|
|
703
|
+
@Controller('organizations/:organizationId/data')
|
|
704
|
+
export class DataController {
|
|
705
|
+
constructor(
|
|
706
|
+
private readonly orgUserService: OrganizationUserService,
|
|
707
|
+
) {}
|
|
708
|
+
|
|
709
|
+
@Get()
|
|
710
|
+
@UseGuards(OrganizationGuard)
|
|
711
|
+
async getData(
|
|
712
|
+
@OrganizationId() organizationId: string,
|
|
713
|
+
@UserId() userId: string,
|
|
714
|
+
) {
|
|
715
|
+
const role = await this.orgUserService.getUserRole(
|
|
716
|
+
organizationId,
|
|
717
|
+
userId
|
|
718
|
+
);
|
|
719
|
+
|
|
720
|
+
// Check if user has required role
|
|
721
|
+
if (![ORGANIZATION_USER_ROLE.ADMIN, ORGANIZATION_USER_ROLE.OWNER].includes(role)) {
|
|
722
|
+
throw new ForbiddenException('Insufficient permissions');
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
// Return data
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
\`\`\`
|
|
729
|
+
|
|
730
|
+
### Example 3: Module with Organization Settings
|
|
731
|
+
|
|
732
|
+
\`\`\`typescript
|
|
733
|
+
// GitLab Service
|
|
734
|
+
@Injectable()
|
|
735
|
+
export class GitLabService {
|
|
736
|
+
constructor(
|
|
737
|
+
private readonly orgSettings: OrganizationSettingsService
|
|
738
|
+
) {}
|
|
739
|
+
|
|
740
|
+
async getClient(organizationId: string) {
|
|
741
|
+
// Get org-specific GitLab credentials
|
|
742
|
+
const [host, token] = await Promise.all([
|
|
743
|
+
this.orgSettings.get(organizationId, 'GITLAB:CREDENTIALS:HOST'),
|
|
744
|
+
this.orgSettings.get(organizationId, 'GITLAB:CREDENTIALS:TOKEN'),
|
|
745
|
+
]);
|
|
746
|
+
|
|
747
|
+
// Validate configuration
|
|
748
|
+
if (!host || !token) {
|
|
749
|
+
throw new Error(
|
|
750
|
+
'GitLab not configured for this organization. ' +
|
|
751
|
+
'Please configure it in organization settings.'
|
|
752
|
+
);
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
// Return organization-specific client
|
|
756
|
+
return new GitLab({ host, token });
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
async getRepositories(organizationId: string) {
|
|
760
|
+
const client = await this.getClient(organizationId);
|
|
761
|
+
return client.Projects.all();
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
\`\`\`
|
|
765
|
+
|
|
766
|
+
### Example 4: Bulk Configure Organization
|
|
767
|
+
|
|
768
|
+
\`\`\`typescript
|
|
769
|
+
@Injectable()
|
|
770
|
+
export class OrganizationSetupService {
|
|
771
|
+
constructor(
|
|
772
|
+
private readonly orgSettings: OrganizationSettingsService,
|
|
773
|
+
) {}
|
|
774
|
+
|
|
775
|
+
async configureGitLab(
|
|
776
|
+
organizationId: string,
|
|
777
|
+
config: { host: string; token: string }
|
|
778
|
+
) {
|
|
779
|
+
return this.orgSettings.bulkUpdateSettings(organizationId, [
|
|
780
|
+
{
|
|
781
|
+
module: 'GITLAB',
|
|
782
|
+
section: 'CREDENTIALS',
|
|
783
|
+
key: 'HOST',
|
|
784
|
+
value: config.host,
|
|
785
|
+
label: 'GitLab Host',
|
|
786
|
+
type: SETTINGS_TYPE.TEXT,
|
|
787
|
+
},
|
|
788
|
+
{
|
|
789
|
+
module: 'GITLAB',
|
|
790
|
+
section: 'CREDENTIALS',
|
|
791
|
+
key: 'TOKEN',
|
|
792
|
+
value: config.token,
|
|
793
|
+
label: 'GitLab Token',
|
|
794
|
+
type: SETTINGS_TYPE.SECRET,
|
|
795
|
+
},
|
|
796
|
+
]);
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
\`\`\`
|
|
800
|
+
|
|
801
|
+
### Example 5: Settings Resolution Pattern
|
|
802
|
+
|
|
803
|
+
\`\`\`typescript
|
|
804
|
+
@Injectable()
|
|
805
|
+
export class IntegrationService {
|
|
806
|
+
constructor(
|
|
807
|
+
private readonly orgSettings: OrganizationSettingsService,
|
|
808
|
+
) {}
|
|
809
|
+
|
|
810
|
+
async getIntegrationConfig(organizationId: string, integration: string) {
|
|
811
|
+
// Get all settings for this integration
|
|
812
|
+
const settings = await this.orgSettings.getAllForOrganization(
|
|
813
|
+
organizationId,
|
|
814
|
+
integration
|
|
815
|
+
);
|
|
816
|
+
|
|
817
|
+
// Check if configured
|
|
818
|
+
if (settings.length === 0) {
|
|
819
|
+
return {
|
|
820
|
+
configured: false,
|
|
821
|
+
message: \`\${integration} is not configured for this organization\`,
|
|
822
|
+
};
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
// Convert to key-value object
|
|
826
|
+
const config = settings.reduce((acc, setting) => {
|
|
827
|
+
acc[\`\${setting.section}:\${setting.key}\`] = setting.value;
|
|
828
|
+
return acc;
|
|
829
|
+
}, {});
|
|
830
|
+
|
|
831
|
+
return {
|
|
832
|
+
configured: true,
|
|
833
|
+
config,
|
|
834
|
+
};
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
\`\`\`
|
|
838
|
+
|
|
839
|
+
---
|
|
840
|
+
|
|
841
|
+
## ๐ Best Practices
|
|
842
|
+
|
|
843
|
+
### 1. Always Check Configuration
|
|
844
|
+
|
|
845
|
+
\`\`\`typescript
|
|
846
|
+
// โ Don't assume settings exist
|
|
847
|
+
const token = await orgSettings.get(orgId, 'MODULE:CREDENTIALS:TOKEN');
|
|
848
|
+
await client.connect(token); // Could be undefined!
|
|
849
|
+
|
|
850
|
+
// โ
Always validate
|
|
851
|
+
const token = await orgSettings.get(orgId, 'MODULE:CREDENTIALS:TOKEN');
|
|
852
|
+
if (!token) {
|
|
853
|
+
throw new Error('Module not configured');
|
|
854
|
+
}
|
|
855
|
+
await client.connect(token);
|
|
856
|
+
\`\`\`
|
|
857
|
+
|
|
858
|
+
### 2. Use Bulk Operations
|
|
859
|
+
|
|
860
|
+
\`\`\`typescript
|
|
861
|
+
// โ Multiple single calls
|
|
862
|
+
const host = await orgSettings.get(orgId, 'MODULE:CREDENTIALS:HOST');
|
|
863
|
+
const token = await orgSettings.get(orgId, 'MODULE:CREDENTIALS:TOKEN');
|
|
864
|
+
const secret = await orgSettings.get(orgId, 'MODULE:CREDENTIALS:SECRET');
|
|
865
|
+
|
|
866
|
+
// โ
Single bulk call
|
|
867
|
+
const settings = await orgSettings.getManySettings(orgId, [
|
|
868
|
+
'MODULE:CREDENTIALS:HOST',
|
|
869
|
+
'MODULE:CREDENTIALS:TOKEN',
|
|
870
|
+
'MODULE:CREDENTIALS:SECRET',
|
|
871
|
+
]);
|
|
872
|
+
\`\`\`
|
|
873
|
+
|
|
874
|
+
### 3. Implement Your Own Controllers
|
|
875
|
+
|
|
876
|
+
\`\`\`typescript
|
|
877
|
+
// โ Don't use test controller in production
|
|
878
|
+
import { OrganizationSettingsTestController } from '@venturialstd/organization';
|
|
879
|
+
|
|
880
|
+
// โ
Implement your own
|
|
881
|
+
@Controller('api/organizations/:organizationId/settings')
|
|
882
|
+
@UseGuards(JwtAuthGuard, OrganizationGuard)
|
|
883
|
+
export class OrganizationSettingsController {
|
|
884
|
+
// Your implementation with proper auth
|
|
885
|
+
}
|
|
886
|
+
\`\`\`
|
|
887
|
+
|
|
888
|
+
### 4. Use Type-Safe Settings Types
|
|
889
|
+
|
|
890
|
+
\`\`\`typescript
|
|
891
|
+
// โ
Import and use the enum
|
|
892
|
+
import { SETTINGS_TYPE } from '@venturialstd/organization';
|
|
893
|
+
|
|
894
|
+
const setting = {
|
|
895
|
+
type: SETTINGS_TYPE.SECRET, // Type-safe
|
|
896
|
+
// ...
|
|
897
|
+
};
|
|
898
|
+
\`\`\`
|
|
899
|
+
|
|
900
|
+
---
|
|
901
|
+
|
|
902
|
+
## ๐ License
|
|
903
|
+
|
|
904
|
+
MIT
|
|
905
|
+
|
|
906
|
+
---
|
|
907
|
+
|
|
908
|
+
## ๐ค Contributing
|
|
909
|
+
|
|
910
|
+
Contributions welcome! Please read the contributing guidelines before submitting PRs.
|