@venturialstd/organization 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 +736 -504
- package/dist/constants/settings-type.constant.d.ts +12 -0
- package/dist/constants/settings-type.constant.d.ts.map +1 -0
- package/dist/constants/settings-type.constant.js +16 -0
- package/dist/constants/settings-type.constant.js.map +1 -0
- package/dist/decorators/organization.decorator.d.ts.map +1 -1
- package/dist/decorators/organization.decorator.js +6 -2
- package/dist/decorators/organization.decorator.js.map +1 -1
- package/dist/entities/organization-settings.entity.d.ts +19 -0
- package/dist/entities/organization-settings.entity.d.ts.map +1 -0
- package/dist/entities/organization-settings.entity.js +124 -0
- package/dist/entities/organization-settings.entity.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/organization.module.d.ts.map +1 -1
- package/dist/organization.module.js +8 -3
- package/dist/organization.module.js.map +1 -1
- package/dist/services/organization-settings.service.d.ts +15 -0
- package/dist/services/organization-settings.service.d.ts.map +1 -0
- package/dist/services/organization-settings.service.js +114 -0
- package/dist/services/organization-settings.service.js.map +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,46 +1,46 @@
|
|
|
1
1
|
# @venturialstd/organization
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A comprehensive NestJS module for managing organizations with multi-tenant capabilities, role-based access control, and isolated per-organization settings.
|
|
4
4
|
|
|
5
5
|
## ๐ Table of Contents
|
|
6
6
|
|
|
7
7
|
- [Features](#features)
|
|
8
8
|
- [Installation](#installation)
|
|
9
|
-
- [Module Structure](#module-structure)
|
|
10
9
|
- [Quick Start](#quick-start)
|
|
11
|
-
- [
|
|
12
|
-
- [
|
|
10
|
+
- [Core Concepts](#core-concepts)
|
|
11
|
+
- [Architecture](#architecture)
|
|
13
12
|
- [API Reference](#api-reference)
|
|
14
|
-
- [
|
|
15
|
-
- [Guards & Decorators](#guards--decorators)
|
|
13
|
+
- [Organization Settings System](#organization-settings-system)
|
|
16
14
|
- [Test Server](#test-server)
|
|
17
|
-
- [
|
|
18
|
-
- [
|
|
15
|
+
- [Migration Guide](#migration-guide)
|
|
16
|
+
- [Examples](#examples)
|
|
17
|
+
- [Best Practices](#best-practices)
|
|
19
18
|
|
|
20
19
|
---
|
|
21
20
|
|
|
22
21
|
## โจ Features
|
|
23
22
|
|
|
24
|
-
- ๐ข **Organization Management**: Complete CRUD operations
|
|
25
|
-
- ๐ฅ **Member Management**: Add, invite,
|
|
26
|
-
- ๐ **Role-Based Access**: Owner, Admin, Member, Viewer roles
|
|
27
|
-
- โ๏ธ **Settings System**:
|
|
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
28
|
- ๐ก๏ธ **Guards & Decorators**: Request-level access control
|
|
29
29
|
- ๐ **TypeORM Integration**: Full database support with migrations
|
|
30
30
|
- ๐ฆ **Fully Typed**: Complete TypeScript support
|
|
31
|
-
- ๐งช **Test Infrastructure**: Standalone test server
|
|
31
|
+
- ๐งช **Test Infrastructure**: Standalone test server
|
|
32
32
|
|
|
33
33
|
---
|
|
34
34
|
|
|
35
35
|
## ๐ฆ Installation
|
|
36
36
|
|
|
37
|
-
|
|
37
|
+
\`\`\`bash
|
|
38
38
|
npm install @venturialstd/organization
|
|
39
|
-
|
|
39
|
+
\`\`\`
|
|
40
40
|
|
|
41
41
|
### Peer Dependencies
|
|
42
42
|
|
|
43
|
-
|
|
43
|
+
\`\`\`json
|
|
44
44
|
{
|
|
45
45
|
"@nestjs/common": "^11.0.11",
|
|
46
46
|
"@nestjs/core": "^11.0.5",
|
|
@@ -50,47 +50,7 @@ npm install @venturialstd/organization
|
|
|
50
50
|
"class-validator": "^0.14.1",
|
|
51
51
|
"typeorm": "^0.3.20"
|
|
52
52
|
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
---
|
|
56
|
-
|
|
57
|
-
## ๐ Module Structure
|
|
58
|
-
|
|
59
|
-
```
|
|
60
|
-
src/organization/
|
|
61
|
-
โโโ package.json # Module configuration
|
|
62
|
-
โโโ tsconfig.json # TypeScript configuration
|
|
63
|
-
โโโ README.md # This file
|
|
64
|
-
โโโ .npmignore # NPM publish exclusions
|
|
65
|
-
โโโ src/ # Source code
|
|
66
|
-
โ โโโ constants/
|
|
67
|
-
โ โ โโโ organization.constant.ts # Enums (roles, status)
|
|
68
|
-
โ โ โโโ organization.settings.constant.ts # Settings keys
|
|
69
|
-
โ โโโ decorators/
|
|
70
|
-
โ โ โโโ organization.decorator.ts # @OrganizationId(), @UserId(), etc.
|
|
71
|
-
โ โ โโโ roles.decorator.ts # @Roles() decorator
|
|
72
|
-
โ โโโ entities/
|
|
73
|
-
โ โ โโโ organization.entity.ts # Organization entity
|
|
74
|
-
โ โ โโโ organization-user.entity.ts # OrganizationUser entity
|
|
75
|
-
โ โโโ guards/
|
|
76
|
-
โ โ โโโ organization.guard.ts # OrganizationGuard, OrganizationRoleGuard
|
|
77
|
-
โ โโโ services/
|
|
78
|
-
โ โ โโโ organization.service.ts # Organization CRUD service
|
|
79
|
-
โ โ โโโ organization-user.service.ts # User-Organization service
|
|
80
|
-
โ โโโ settings/
|
|
81
|
-
โ โ โโโ organization.settings.ts # Module settings definition
|
|
82
|
-
โ โโโ organization.module.ts # NestJS module
|
|
83
|
-
โ โโโ index.ts # Public exports
|
|
84
|
-
โโโ test/ # Test infrastructure
|
|
85
|
-
โโโ controllers/
|
|
86
|
-
โ โโโ organization-test.controller.ts
|
|
87
|
-
โ โโโ organization-user-test.controller.ts
|
|
88
|
-
โโโ migrations/ # Database migrations
|
|
89
|
-
โโโ .env.example # Environment template
|
|
90
|
-
โโโ data-source.ts # TypeORM DataSource
|
|
91
|
-
โโโ test-app.module.ts # Test app module
|
|
92
|
-
โโโ main.ts # Test server bootstrap
|
|
93
|
-
```
|
|
53
|
+
\`\`\`
|
|
94
54
|
|
|
95
55
|
---
|
|
96
56
|
|
|
@@ -98,7 +58,7 @@ src/organization/
|
|
|
98
58
|
|
|
99
59
|
### 1. Import the Module
|
|
100
60
|
|
|
101
|
-
|
|
61
|
+
\`\`\`typescript
|
|
102
62
|
import { Module } from '@nestjs/common';
|
|
103
63
|
import { OrganizationModule } from '@venturialstd/organization';
|
|
104
64
|
|
|
@@ -106,13 +66,25 @@ import { OrganizationModule } from '@venturialstd/organization';
|
|
|
106
66
|
imports: [OrganizationModule],
|
|
107
67
|
})
|
|
108
68
|
export class AppModule {}
|
|
109
|
-
|
|
69
|
+
\`\`\`
|
|
110
70
|
|
|
111
|
-
### 2.
|
|
71
|
+
### 2. Run Migrations
|
|
72
|
+
|
|
73
|
+
\`\`\`bash
|
|
74
|
+
# Create and run migrations
|
|
75
|
+
npm run migration:generate -- CreateOrganizations
|
|
76
|
+
npm run migration:run
|
|
77
|
+
\`\`\`
|
|
112
78
|
|
|
113
|
-
|
|
79
|
+
### 3. Use the Services
|
|
80
|
+
|
|
81
|
+
\`\`\`typescript
|
|
114
82
|
import { Injectable } from '@nestjs/common';
|
|
115
|
-
import {
|
|
83
|
+
import {
|
|
84
|
+
OrganizationService,
|
|
85
|
+
OrganizationUserService,
|
|
86
|
+
ORGANIZATION_USER_ROLE
|
|
87
|
+
} from '@venturialstd/organization';
|
|
116
88
|
|
|
117
89
|
@Injectable()
|
|
118
90
|
export class YourService {
|
|
@@ -121,558 +93,818 @@ export class YourService {
|
|
|
121
93
|
private readonly organizationUserService: OrganizationUserService,
|
|
122
94
|
) {}
|
|
123
95
|
|
|
124
|
-
async createOrganization() {
|
|
125
|
-
|
|
96
|
+
async createOrganization(userId: string) {
|
|
97
|
+
return this.organizationService.createOrganization(
|
|
126
98
|
'My Organization',
|
|
127
99
|
'my-org',
|
|
128
100
|
'example.com',
|
|
129
|
-
'
|
|
130
|
-
|
|
101
|
+
'Description',
|
|
102
|
+
userId
|
|
131
103
|
);
|
|
132
|
-
|
|
133
|
-
return organization;
|
|
134
104
|
}
|
|
135
105
|
|
|
136
|
-
async addMember() {
|
|
106
|
+
async addMember(organizationId: string, userId: string) {
|
|
137
107
|
return this.organizationUserService.addUserToOrganization(
|
|
138
|
-
|
|
139
|
-
|
|
108
|
+
organizationId,
|
|
109
|
+
userId,
|
|
140
110
|
ORGANIZATION_USER_ROLE.MEMBER
|
|
141
111
|
);
|
|
142
112
|
}
|
|
143
113
|
}
|
|
144
|
-
|
|
114
|
+
\`\`\`
|
|
145
115
|
|
|
146
116
|
---
|
|
147
117
|
|
|
148
|
-
##
|
|
118
|
+
## ๐ก Core Concepts
|
|
149
119
|
|
|
150
|
-
###
|
|
120
|
+
### Organizations
|
|
151
121
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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
|
|
155
128
|
|
|
156
|
-
|
|
157
|
-
npm install --legacy-peer-deps
|
|
129
|
+
### Organization Members
|
|
158
130
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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
|
|
162
136
|
|
|
163
|
-
|
|
164
|
-
npm run migration:generate --name=InitialSchema
|
|
137
|
+
### Organization Settings
|
|
165
138
|
|
|
166
|
-
|
|
167
|
-
|
|
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.)
|
|
168
144
|
|
|
169
|
-
|
|
170
|
-
npm run test:dev
|
|
171
|
-
```
|
|
172
|
-
|
|
173
|
-
### Environment Variables (test/.env)
|
|
145
|
+
---
|
|
174
146
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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
|
+
\`\`\`
|
|
183
191
|
|
|
184
192
|
---
|
|
185
193
|
|
|
186
|
-
##
|
|
187
|
-
|
|
188
|
-
### Organization
|
|
189
|
-
|
|
190
|
-
```typescript
|
|
191
|
-
@Entity('organization')
|
|
192
|
-
export class Organization {
|
|
193
|
-
id: string; // UUID primary key
|
|
194
|
-
name: string; // Unique organization name
|
|
195
|
-
slug: string; // Unique URL-friendly slug
|
|
196
|
-
domain?: string; // Optional custom domain
|
|
197
|
-
description?: string; // Organization description
|
|
198
|
-
isActive: boolean; // Active/inactive status
|
|
199
|
-
settings: Record<string, unknown>; // JSONB settings
|
|
200
|
-
ownerId?: string; // Owner user ID
|
|
201
|
-
createdAt: Date; // Creation timestamp
|
|
202
|
-
updatedAt: Date; // Update timestamp
|
|
203
|
-
}
|
|
204
|
-
```
|
|
205
|
-
|
|
206
|
-
**Key Features:**
|
|
207
|
-
- Unique `name` and `slug` constraints
|
|
208
|
-
- JSONB `settings` for flexible configuration
|
|
209
|
-
- Soft activation with `isActive` flag
|
|
210
|
-
- Owner tracking via `ownerId`
|
|
211
|
-
|
|
212
|
-
### OrganizationUser
|
|
213
|
-
|
|
214
|
-
```typescript
|
|
215
|
-
@Entity('organization_user')
|
|
216
|
-
export class OrganizationUser {
|
|
217
|
-
id: string; // UUID primary key
|
|
218
|
-
organizationId: string; // Organization reference (indexed)
|
|
219
|
-
userId: string; // User reference (indexed)
|
|
220
|
-
role: ORGANIZATION_USER_ROLE; // User role in organization
|
|
221
|
-
status: ORGANIZATION_USER_STATUS; // Member status
|
|
222
|
-
isPrimary: boolean; // Primary owner flag
|
|
223
|
-
invitedBy?: string; // Inviter user ID
|
|
224
|
-
invitedAt?: Date; // Invitation timestamp
|
|
225
|
-
joinedAt?: Date; // Join timestamp
|
|
226
|
-
permissions: Record<string, unknown>; // JSONB custom permissions
|
|
227
|
-
createdAt: Date; // Creation timestamp
|
|
228
|
-
updatedAt: Date; // Update timestamp
|
|
229
|
-
}
|
|
230
|
-
```
|
|
194
|
+
## ๐ API Reference
|
|
231
195
|
|
|
232
|
-
|
|
233
|
-
- Unique index on `(organizationId, userId)`
|
|
234
|
-
- Separate indexes on `organizationId` and `userId`
|
|
235
|
-
- Invitation system support
|
|
236
|
-
- Primary owner designation
|
|
237
|
-
- Custom permissions via JSONB
|
|
196
|
+
### Services
|
|
238
197
|
|
|
239
|
-
|
|
198
|
+
#### OrganizationService
|
|
240
199
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
description?: string,
|
|
252
|
-
ownerId?: string
|
|
253
|
-
): Promise<Organization>
|
|
254
|
-
```
|
|
255
|
-
|
|
256
|
-
#### Find Methods
|
|
257
|
-
```typescript
|
|
258
|
-
getOrganizationBySlug(slug: string): Promise<Organization>
|
|
259
|
-
getOrganizationByDomain(domain: string): Promise<Organization>
|
|
260
|
-
getActiveOrganizations(): Promise<Organization[]>
|
|
261
|
-
getOrganizationsByOwner(ownerId: string): Promise<Organization[]>
|
|
262
|
-
```
|
|
263
|
-
|
|
264
|
-
#### Update Methods
|
|
265
|
-
```typescript
|
|
266
|
-
updateOrganizationSettings(
|
|
267
|
-
organizationId: string,
|
|
268
|
-
settings: Record<string, unknown>
|
|
269
|
-
): Promise<Organization>
|
|
270
|
-
|
|
271
|
-
setOrganizationStatus(
|
|
272
|
-
organizationId: string,
|
|
273
|
-
isActive: boolean
|
|
274
|
-
): Promise<Organization>
|
|
275
|
-
```
|
|
276
|
-
|
|
277
|
-
### OrganizationUserService
|
|
278
|
-
|
|
279
|
-
#### Member Management
|
|
280
|
-
```typescript
|
|
281
|
-
addUserToOrganization(
|
|
282
|
-
organizationId: string,
|
|
283
|
-
userId: string,
|
|
284
|
-
role?: ORGANIZATION_USER_ROLE,
|
|
285
|
-
invitedBy?: string
|
|
286
|
-
): Promise<OrganizationUser>
|
|
287
|
-
|
|
288
|
-
inviteUserToOrganization(
|
|
289
|
-
organizationId: string,
|
|
290
|
-
userId: string,
|
|
291
|
-
role: ORGANIZATION_USER_ROLE,
|
|
292
|
-
invitedBy: string
|
|
293
|
-
): Promise<OrganizationUser>
|
|
294
|
-
|
|
295
|
-
acceptInvitation(
|
|
296
|
-
organizationId: string,
|
|
297
|
-
userId: string
|
|
298
|
-
): Promise<OrganizationUser>
|
|
299
|
-
|
|
300
|
-
removeUserFromOrganization(
|
|
301
|
-
organizationId: string,
|
|
302
|
-
userId: string
|
|
303
|
-
): Promise<void>
|
|
304
|
-
```
|
|
305
|
-
|
|
306
|
-
#### Role Management
|
|
307
|
-
```typescript
|
|
308
|
-
updateUserRole(
|
|
309
|
-
organizationId: string,
|
|
310
|
-
userId: string,
|
|
311
|
-
newRole: ORGANIZATION_USER_ROLE
|
|
312
|
-
): Promise<OrganizationUser>
|
|
313
|
-
|
|
314
|
-
setPrimaryOwner(
|
|
315
|
-
organizationId: string,
|
|
316
|
-
userId: string
|
|
317
|
-
): Promise<OrganizationUser>
|
|
318
|
-
|
|
319
|
-
transferOwnership(
|
|
320
|
-
organizationId: string,
|
|
321
|
-
fromUserId: string,
|
|
322
|
-
toUserId: string
|
|
323
|
-
): Promise<void>
|
|
324
|
-
```
|
|
325
|
-
|
|
326
|
-
#### Query Methods
|
|
327
|
-
```typescript
|
|
328
|
-
getUserOrganizations(userId: string): Promise<OrganizationUser[]>
|
|
329
|
-
getOrganizationUsers(organizationId: string): Promise<OrganizationUser[]>
|
|
330
|
-
getActiveOrganizationUsers(organizationId: string): Promise<OrganizationUser[]>
|
|
331
|
-
getUserInvitations(userId: string): Promise<OrganizationUser[]>
|
|
332
|
-
hasAccess(organizationId: string, userId: string): Promise<boolean>
|
|
333
|
-
getUserRole(organizationId: string, userId: string): Promise<ORGANIZATION_USER_ROLE | null>
|
|
334
|
-
```
|
|
335
|
-
|
|
336
|
-
#### Status Management
|
|
337
|
-
```typescript
|
|
338
|
-
suspendUser(organizationId: string, userId: string): Promise<OrganizationUser>
|
|
339
|
-
reactivateUser(organizationId: string, userId: string): Promise<OrganizationUser>
|
|
340
|
-
```
|
|
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>
|
|
341
210
|
|
|
342
|
-
|
|
211
|
+
// Get organization by ID
|
|
212
|
+
getOrganizationById(id: string): Promise<Organization>
|
|
343
213
|
|
|
344
|
-
|
|
214
|
+
// Update organization
|
|
215
|
+
updateOrganization(id: string, data: Partial<Organization>): Promise<Organization>
|
|
345
216
|
|
|
346
|
-
|
|
217
|
+
// Delete organization
|
|
218
|
+
deleteOrganization(id: string): Promise<void>
|
|
347
219
|
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
OWNER = 'owner', // Full control over organization
|
|
351
|
-
ADMIN = 'admin', // Administrative access
|
|
352
|
-
MEMBER = 'member', // Standard member access
|
|
353
|
-
VIEWER = 'viewer' // Read-only access
|
|
220
|
+
// Get user's organizations
|
|
221
|
+
getUserOrganizations(userId: string): Promise<Organization[]>
|
|
354
222
|
}
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
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>
|
|
364
257
|
}
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
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[]>
|
|
379
304
|
}
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
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
|
+
\`\`\`
|
|
385
359
|
|
|
386
360
|
### Decorators
|
|
387
361
|
|
|
388
|
-
|
|
389
|
-
Extract organization ID from request
|
|
390
|
-
```typescript
|
|
362
|
+
\`\`\`typescript
|
|
363
|
+
// Extract organization ID from request
|
|
391
364
|
@Get()
|
|
392
|
-
|
|
393
|
-
return this.service.getData(organizationId);
|
|
394
|
-
}
|
|
395
|
-
```
|
|
365
|
+
getData(@OrganizationId() organizationId: string) { }
|
|
396
366
|
|
|
397
|
-
|
|
398
|
-
Extract user ID from request:
|
|
399
|
-
```typescript
|
|
367
|
+
// Extract user ID from request
|
|
400
368
|
@Get()
|
|
401
|
-
|
|
402
|
-
return this.service.getUserData(userId);
|
|
403
|
-
}
|
|
404
|
-
```
|
|
369
|
+
getData(@UserId() userId: string) { }
|
|
405
370
|
|
|
406
|
-
|
|
407
|
-
Extract full context:
|
|
408
|
-
```typescript
|
|
371
|
+
// Extract full context
|
|
409
372
|
@Get()
|
|
410
|
-
|
|
411
|
-
// context = { organizationId, userId, role, user }
|
|
412
|
-
return this.service.getData(context);
|
|
413
|
-
}
|
|
414
|
-
```
|
|
373
|
+
getData(@OrganizationContext() context: OrganizationContextType) { }
|
|
415
374
|
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
@Delete(':id')
|
|
422
|
-
async delete() { ... }
|
|
423
|
-
```
|
|
375
|
+
// Define required roles
|
|
376
|
+
@Roles(ORGANIZATION_USER_ROLE.ADMIN, ORGANIZATION_USER_ROLE.OWNER)
|
|
377
|
+
@UseGuards(OrganizationRoleGuard)
|
|
378
|
+
adminEndpoint() { }
|
|
379
|
+
\`\`\`
|
|
424
380
|
|
|
425
381
|
### Guards
|
|
426
382
|
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
```typescript
|
|
383
|
+
\`\`\`typescript
|
|
384
|
+
// Check user belongs to organization
|
|
430
385
|
@UseGuards(OrganizationGuard)
|
|
431
|
-
@Get()
|
|
432
|
-
|
|
433
|
-
|
|
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',
|
|
434
404
|
}
|
|
435
|
-
```
|
|
436
405
|
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
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
|
+
\`\`\`
|
|
445
419
|
|
|
446
420
|
---
|
|
447
421
|
|
|
448
|
-
##
|
|
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';
|
|
449
507
|
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
### Available Endpoints
|
|
457
|
-
|
|
458
|
-
#### Organization Management
|
|
459
|
-
```
|
|
460
|
-
POST /organizations - Create organization
|
|
461
|
-
GET /organizations - Get all organizations
|
|
462
|
-
GET /organizations/active - Get active organizations
|
|
463
|
-
GET /organizations/owner/:ownerId - Get by owner
|
|
464
|
-
GET /organizations/slug/:slug - Get by slug
|
|
465
|
-
GET /organizations/domain/:domain - Get by domain
|
|
466
|
-
GET /organizations/:id - Get by ID
|
|
467
|
-
PUT /organizations/:id/settings - Update settings
|
|
468
|
-
PUT /organizations/:id/status - Update status
|
|
469
|
-
DELETE /organizations/:id - Delete organization
|
|
470
|
-
```
|
|
471
|
-
|
|
472
|
-
#### User-Organization Management
|
|
473
|
-
```
|
|
474
|
-
POST /organization-users/add - Add user
|
|
475
|
-
POST /organization-users/invite - Invite user
|
|
476
|
-
POST /organization-users/accept-invitation - Accept invite
|
|
477
|
-
GET /organization-users/organization/:organizationId - Get members
|
|
478
|
-
GET /organization-users/organization/:organizationId/active - Get active members
|
|
479
|
-
GET /organization-users/user/:userId - Get user's organizations
|
|
480
|
-
GET /organization-users/user/:userId/invitations - Get invitations
|
|
481
|
-
GET /organization-users/check-access/:orgId/:userId - Check access
|
|
482
|
-
GET /organization-users/role/:orgId/:userId - Get user role
|
|
483
|
-
PUT /organization-users/role - Update role
|
|
484
|
-
PUT /organization-users/primary-owner - Set primary owner
|
|
485
|
-
POST /organization-users/transfer-ownership - Transfer ownership
|
|
486
|
-
PUT /organization-users/suspend - Suspend user
|
|
487
|
-
PUT /organization-users/reactivate - Reactivate user
|
|
488
|
-
DELETE /organization-users/remove - Remove user
|
|
489
|
-
```
|
|
508
|
+
@Injectable()
|
|
509
|
+
export class YourModuleService {
|
|
510
|
+
constructor(
|
|
511
|
+
private readonly orgSettings: OrganizationSettingsService
|
|
512
|
+
) {}
|
|
490
513
|
|
|
491
|
-
|
|
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
|
+
);
|
|
492
520
|
|
|
493
|
-
|
|
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
|
+
}
|
|
494
534
|
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
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
|
+
\`\`\`
|
|
499
550
|
|
|
500
|
-
|
|
501
|
-
npm run release:patch # Build, bump version, publish
|
|
551
|
+
#### 3. Import OrganizationModule
|
|
502
552
|
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
553
|
+
\`\`\`typescript
|
|
554
|
+
import { Module } from '@nestjs/common';
|
|
555
|
+
import { OrganizationModule } from '@venturialstd/organization';
|
|
506
556
|
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
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
|
+
\`\`\`
|
|
512
584
|
|
|
513
585
|
---
|
|
514
586
|
|
|
515
|
-
##
|
|
587
|
+
## ๐งช Test Server
|
|
516
588
|
|
|
517
|
-
|
|
589
|
+
The module includes a standalone test server for development and testing.
|
|
518
590
|
|
|
519
|
-
|
|
520
|
-
import { OrganizationService } from '@venturialstd/organization';
|
|
591
|
+
### Start the Test Server
|
|
521
592
|
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
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"
|
|
535
627
|
}
|
|
536
|
-
|
|
628
|
+
\`\`\`
|
|
537
629
|
|
|
538
|
-
###
|
|
630
|
+
### Test Controller
|
|
539
631
|
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
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/\`
|
|
545
665
|
|
|
666
|
+
---
|
|
667
|
+
|
|
668
|
+
## ๐ป Examples
|
|
669
|
+
|
|
670
|
+
### Example 1: Create Organization with Owner
|
|
671
|
+
|
|
672
|
+
\`\`\`typescript
|
|
546
673
|
@Injectable()
|
|
547
|
-
export class
|
|
548
|
-
constructor(
|
|
674
|
+
export class OnboardingService {
|
|
675
|
+
constructor(
|
|
676
|
+
private readonly orgService: OrganizationService,
|
|
677
|
+
private readonly orgUserService: OrganizationUserService,
|
|
678
|
+
) {}
|
|
549
679
|
|
|
550
|
-
async
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
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
|
|
556
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, '-');
|
|
557
696
|
}
|
|
558
697
|
}
|
|
559
|
-
|
|
698
|
+
\`\`\`
|
|
560
699
|
|
|
561
|
-
###
|
|
700
|
+
### Example 2: Check User Permission
|
|
562
701
|
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
}
|
|
569
|
-
import {
|
|
570
|
-
OrganizationGuard,
|
|
571
|
-
OrganizationRoleGuard,
|
|
572
|
-
OrganizationId,
|
|
573
|
-
Roles,
|
|
574
|
-
ORGANIZATION_USER_ROLE
|
|
575
|
-
} from '@venturialstd/organization';
|
|
702
|
+
\`\`\`typescript
|
|
703
|
+
@Controller('organizations/:organizationId/data')
|
|
704
|
+
export class DataController {
|
|
705
|
+
constructor(
|
|
706
|
+
private readonly orgUserService: OrganizationUserService,
|
|
707
|
+
) {}
|
|
576
708
|
|
|
577
|
-
@Controller('projects')
|
|
578
|
-
@UseGuards(OrganizationGuard)
|
|
579
|
-
export class ProjectController {
|
|
580
|
-
|
|
581
|
-
// Any member can view
|
|
582
709
|
@Get()
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
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
|
+
);
|
|
586
719
|
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
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
|
|
593
726
|
}
|
|
594
727
|
}
|
|
595
|
-
|
|
728
|
+
\`\`\`
|
|
596
729
|
|
|
597
|
-
###
|
|
730
|
+
### Example 3: Module with Organization Settings
|
|
598
731
|
|
|
599
|
-
|
|
732
|
+
\`\`\`typescript
|
|
733
|
+
// GitLab Service
|
|
600
734
|
@Injectable()
|
|
601
|
-
export class
|
|
602
|
-
constructor(
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
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
|
+
}
|
|
615
754
|
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
return this.orgUserService.getUserInvitations(userId);
|
|
755
|
+
// Return organization-specific client
|
|
756
|
+
return new GitLab({ host, token });
|
|
619
757
|
}
|
|
620
758
|
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
return
|
|
759
|
+
async getRepositories(organizationId: string) {
|
|
760
|
+
const client = await this.getClient(organizationId);
|
|
761
|
+
return client.Projects.all();
|
|
624
762
|
}
|
|
625
763
|
}
|
|
626
|
-
|
|
764
|
+
\`\`\`
|
|
627
765
|
|
|
628
|
-
###
|
|
766
|
+
### Example 4: Bulk Configure Organization
|
|
629
767
|
|
|
630
|
-
|
|
768
|
+
\`\`\`typescript
|
|
631
769
|
@Injectable()
|
|
632
|
-
export class
|
|
633
|
-
constructor(
|
|
770
|
+
export class OrganizationSetupService {
|
|
771
|
+
constructor(
|
|
772
|
+
private readonly orgSettings: OrganizationSettingsService,
|
|
773
|
+
) {}
|
|
634
774
|
|
|
635
|
-
async
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
newOwnerId: string
|
|
775
|
+
async configureGitLab(
|
|
776
|
+
organizationId: string,
|
|
777
|
+
config: { host: string; token: string }
|
|
639
778
|
) {
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
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
|
+
\`\`\`
|
|
647
800
|
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
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
|
|
653
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
|
+
};
|
|
654
835
|
}
|
|
655
836
|
}
|
|
656
|
-
|
|
837
|
+
\`\`\`
|
|
657
838
|
|
|
658
839
|
---
|
|
659
840
|
|
|
660
|
-
##
|
|
841
|
+
## ๐ Best Practices
|
|
661
842
|
|
|
662
|
-
|
|
843
|
+
### 1. Always Check Configuration
|
|
663
844
|
|
|
664
|
-
|
|
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!
|
|
665
849
|
|
|
666
|
-
|
|
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';
|
|
667
893
|
|
|
668
|
-
|
|
894
|
+
const setting = {
|
|
895
|
+
type: SETTINGS_TYPE.SECRET, // Type-safe
|
|
896
|
+
// ...
|
|
897
|
+
};
|
|
898
|
+
\`\`\`
|
|
669
899
|
|
|
670
900
|
---
|
|
671
901
|
|
|
672
|
-
##
|
|
902
|
+
## ๐ License
|
|
673
903
|
|
|
674
|
-
|
|
904
|
+
MIT
|
|
675
905
|
|
|
676
906
|
---
|
|
677
907
|
|
|
678
|
-
|
|
908
|
+
## ๐ค Contributing
|
|
909
|
+
|
|
910
|
+
Contributions welcome! Please read the contributing guidelines before submitting PRs.
|