launchbase 1.1.2 → 1.1.3
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/bin/launchbase.js +2 -119
- package/package.json +1 -1
- package/template/frontend/src/App.tsx +6 -0
- package/template/frontend/src/components/Layout.tsx +6 -0
- package/template/frontend/src/lib/api.ts +0 -2
- package/template/frontend/src/lib/sdk.ts +1 -3
- package/template/frontend/src/pages/Deployments.tsx +332 -0
- package/template/frontend/src/pages/EdgeFunctions.tsx +478 -0
- package/template/prisma/migrations/0_init/migration.sql +262 -75
- package/template/prisma/schema.prisma +211 -147
- package/template/sdk/README.md +1 -2
- package/template/sdk/index.ts +1 -3
- package/template/src/modules/audit/audit.interceptor.ts +1 -1
- package/template/src/modules/auth/auth.service.ts +12 -7
- package/template/src/modules/billing/billing.service.ts +1 -1
- package/template/src/modules/common/filters/all-exceptions.filter.ts +15 -5
- package/template/src/modules/common/project.guard.ts +52 -0
- package/template/src/modules/common/tenant.guard.ts +3 -3
- package/template/src/modules/orgs/orgs.service.ts +18 -15
- package/template/src/modules/projects/dto/create-project.dto.ts +1 -5
- package/template/types/src/index.ts +0 -2
|
@@ -13,29 +13,179 @@ enum Role {
|
|
|
13
13
|
MEMBER
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
+
// ============================================
|
|
17
|
+
// Core Multi-Tenant Models
|
|
18
|
+
// ============================================
|
|
19
|
+
|
|
16
20
|
model User {
|
|
17
|
-
id
|
|
18
|
-
email
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
emailVerified
|
|
21
|
+
id String @id @default(cuid())
|
|
22
|
+
email String @unique
|
|
23
|
+
name String?
|
|
24
|
+
image String?
|
|
25
|
+
passwordHash String?
|
|
26
|
+
emailVerified DateTime?
|
|
23
27
|
twoFactorSecret String?
|
|
24
|
-
twoFactorEnabled Boolean
|
|
25
|
-
oauthProvider
|
|
26
|
-
oauthId
|
|
27
|
-
createdAt
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
28
|
+
twoFactorEnabled Boolean @default(false)
|
|
29
|
+
oauthProvider String? // google, github, etc.
|
|
30
|
+
oauthId String? // OAuth provider user ID
|
|
31
|
+
createdAt DateTime @default(now())
|
|
32
|
+
updatedAt DateTime @updatedAt
|
|
33
|
+
|
|
34
|
+
// Relations
|
|
35
|
+
ownedOrganizations Organization[]
|
|
36
|
+
memberships OrganizationMember[]
|
|
37
|
+
refreshTokens RefreshToken[]
|
|
38
|
+
verificationTokens VerificationToken[]
|
|
31
39
|
passwordResetTokens PasswordResetToken[]
|
|
32
|
-
oauthAccounts
|
|
33
|
-
files
|
|
40
|
+
oauthAccounts OAuthAccount[]
|
|
41
|
+
files File[]
|
|
34
42
|
|
|
35
43
|
@@index([email])
|
|
36
44
|
@@index([oauthProvider, oauthId])
|
|
37
45
|
}
|
|
38
46
|
|
|
47
|
+
model Organization {
|
|
48
|
+
id String @id @default(cuid())
|
|
49
|
+
name String
|
|
50
|
+
slug String @unique
|
|
51
|
+
ownerId String
|
|
52
|
+
plan String @default("free")
|
|
53
|
+
stripeCustomerId String?
|
|
54
|
+
stripeSubscriptionId String?
|
|
55
|
+
createdAt DateTime @default(now())
|
|
56
|
+
updatedAt DateTime @updatedAt
|
|
57
|
+
|
|
58
|
+
// Relations
|
|
59
|
+
owner User @relation(fields: [ownerId], references: [id], onDelete: Cascade)
|
|
60
|
+
members OrganizationMember[]
|
|
61
|
+
projects Project[]
|
|
62
|
+
invitations Invitation[]
|
|
63
|
+
|
|
64
|
+
@@index([slug])
|
|
65
|
+
@@index([ownerId])
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
model OrganizationMember {
|
|
69
|
+
id String @id @default(cuid())
|
|
70
|
+
organizationId String
|
|
71
|
+
userId String
|
|
72
|
+
role Role
|
|
73
|
+
createdAt DateTime @default(now())
|
|
74
|
+
|
|
75
|
+
// Relations
|
|
76
|
+
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
|
|
77
|
+
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
78
|
+
|
|
79
|
+
@@unique([organizationId, userId])
|
|
80
|
+
@@index([organizationId])
|
|
81
|
+
@@index([userId])
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
model Project {
|
|
85
|
+
id String @id @default(cuid())
|
|
86
|
+
organizationId String
|
|
87
|
+
name String
|
|
88
|
+
slug String
|
|
89
|
+
createdAt DateTime @default(now())
|
|
90
|
+
updatedAt DateTime @updatedAt
|
|
91
|
+
|
|
92
|
+
// Relations
|
|
93
|
+
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
|
|
94
|
+
edgeFunctions EdgeFunction[]
|
|
95
|
+
vectorCollections VectorCollection[]
|
|
96
|
+
deployments Deployment[]
|
|
97
|
+
dbCollections DBCollection[]
|
|
98
|
+
|
|
99
|
+
@@unique([organizationId, slug])
|
|
100
|
+
@@index([organizationId])
|
|
101
|
+
@@index([slug])
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// ============================================
|
|
105
|
+
// Project-Scoped Resources
|
|
106
|
+
// ============================================
|
|
107
|
+
|
|
108
|
+
model EdgeFunction {
|
|
109
|
+
id String @id @default(cuid())
|
|
110
|
+
projectId String
|
|
111
|
+
name String
|
|
112
|
+
slug String
|
|
113
|
+
runtime String @default("nodejs18")
|
|
114
|
+
sourceCode String
|
|
115
|
+
environment Json?
|
|
116
|
+
triggers Json?
|
|
117
|
+
status String @default("draft")
|
|
118
|
+
deployedAt DateTime?
|
|
119
|
+
deploymentUrl String?
|
|
120
|
+
createdAt DateTime @default(now())
|
|
121
|
+
updatedAt DateTime @updatedAt
|
|
122
|
+
logs EdgeFunctionLog[]
|
|
123
|
+
|
|
124
|
+
// Relations
|
|
125
|
+
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
|
126
|
+
|
|
127
|
+
@@unique([projectId, slug])
|
|
128
|
+
@@index([projectId])
|
|
129
|
+
@@index([status])
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
model EdgeFunctionLog {
|
|
133
|
+
id String @id @default(cuid())
|
|
134
|
+
functionId String
|
|
135
|
+
status String
|
|
136
|
+
duration Int
|
|
137
|
+
input Json?
|
|
138
|
+
output Json?
|
|
139
|
+
error String?
|
|
140
|
+
triggeredBy String?
|
|
141
|
+
createdAt DateTime @default(now())
|
|
142
|
+
|
|
143
|
+
// Relations
|
|
144
|
+
function EdgeFunction @relation(fields: [functionId], references: [id], onDelete: Cascade)
|
|
145
|
+
|
|
146
|
+
@@index([functionId])
|
|
147
|
+
@@index([createdAt])
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
model VectorCollection {
|
|
151
|
+
id String @id @default(cuid())
|
|
152
|
+
projectId String
|
|
153
|
+
name String
|
|
154
|
+
provider String @default("local")
|
|
155
|
+
dimension Int @default(1536)
|
|
156
|
+
embeddingModel String @default("text-embedding-3-small")
|
|
157
|
+
createdAt DateTime @default(now())
|
|
158
|
+
updatedAt DateTime @updatedAt
|
|
159
|
+
|
|
160
|
+
// Relations
|
|
161
|
+
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
|
162
|
+
|
|
163
|
+
@@unique([projectId, name])
|
|
164
|
+
@@index([projectId])
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
model Deployment {
|
|
168
|
+
id String @id @default(cuid())
|
|
169
|
+
projectId String
|
|
170
|
+
status String @default("pending")
|
|
171
|
+
version String?
|
|
172
|
+
deployedAt DateTime?
|
|
173
|
+
platform String?
|
|
174
|
+
url String?
|
|
175
|
+
logs Json?
|
|
176
|
+
createdAt DateTime @default(now())
|
|
177
|
+
|
|
178
|
+
// Relations
|
|
179
|
+
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
|
180
|
+
|
|
181
|
+
@@index([projectId])
|
|
182
|
+
@@index([status])
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// ============================================
|
|
186
|
+
// Auth & Session Models
|
|
187
|
+
// ============================================
|
|
188
|
+
|
|
39
189
|
model VerificationToken {
|
|
40
190
|
id String @id @default(cuid())
|
|
41
191
|
userId String
|
|
@@ -60,76 +210,59 @@ model PasswordResetToken {
|
|
|
60
210
|
@@index([token])
|
|
61
211
|
}
|
|
62
212
|
|
|
63
|
-
model
|
|
213
|
+
model RefreshToken {
|
|
64
214
|
id String @id @default(cuid())
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
token String @unique
|
|
215
|
+
userId String
|
|
216
|
+
tokenHash String
|
|
217
|
+
revokedAt DateTime?
|
|
69
218
|
expiresAt DateTime
|
|
70
219
|
createdAt DateTime @default(now())
|
|
71
|
-
acceptedAt DateTime?
|
|
72
|
-
|
|
73
|
-
org Organization @relation(fields: [orgId], references: [id], onDelete: Cascade)
|
|
74
|
-
|
|
75
|
-
@@unique([email, orgId])
|
|
76
|
-
@@index([token])
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
model Organization {
|
|
80
|
-
id String @id @default(cuid())
|
|
81
|
-
name String
|
|
82
|
-
plan String @default("free")
|
|
83
|
-
stripeCustomerId String?
|
|
84
|
-
stripeSubscriptionId String?
|
|
85
|
-
createdAt DateTime @default(now())
|
|
86
|
-
memberships Membership[]
|
|
87
|
-
projects Project[]
|
|
88
|
-
invitations Invitation[]
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
model Membership {
|
|
92
|
-
id String @id @default(cuid())
|
|
93
|
-
userId String
|
|
94
|
-
orgId String
|
|
95
|
-
role Role
|
|
96
|
-
createdAt DateTime @default(now())
|
|
97
220
|
|
|
98
221
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
99
|
-
org Organization @relation(fields: [orgId], references: [id], onDelete: Cascade)
|
|
100
222
|
|
|
101
|
-
@@
|
|
102
|
-
@@index([orgId])
|
|
223
|
+
@@index([userId])
|
|
103
224
|
}
|
|
104
225
|
|
|
105
|
-
model
|
|
106
|
-
id
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
226
|
+
model Invitation {
|
|
227
|
+
id String @id @default(cuid())
|
|
228
|
+
email String
|
|
229
|
+
orgId String
|
|
230
|
+
role Role
|
|
231
|
+
token String @unique
|
|
232
|
+
expiresAt DateTime
|
|
233
|
+
createdAt DateTime @default(now())
|
|
234
|
+
acceptedAt DateTime?
|
|
110
235
|
|
|
111
236
|
org Organization @relation(fields: [orgId], references: [id], onDelete: Cascade)
|
|
112
237
|
|
|
113
|
-
@@
|
|
238
|
+
@@unique([email, orgId])
|
|
239
|
+
@@index([token])
|
|
114
240
|
}
|
|
115
241
|
|
|
116
|
-
model
|
|
117
|
-
id
|
|
118
|
-
userId
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
242
|
+
model OAuthAccount {
|
|
243
|
+
id String @id @default(cuid())
|
|
244
|
+
userId String
|
|
245
|
+
provider String
|
|
246
|
+
providerId String
|
|
247
|
+
accessToken String?
|
|
248
|
+
refreshToken String?
|
|
249
|
+
createdAt DateTime @default(now())
|
|
123
250
|
|
|
124
251
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
125
252
|
|
|
253
|
+
@@unique([provider, providerId])
|
|
126
254
|
@@index([userId])
|
|
127
255
|
}
|
|
128
256
|
|
|
257
|
+
// ============================================
|
|
258
|
+
// Other Models
|
|
259
|
+
// ============================================
|
|
260
|
+
|
|
129
261
|
model AuditLog {
|
|
130
262
|
id String @id @default(cuid())
|
|
131
263
|
orgId String?
|
|
132
264
|
userId String?
|
|
265
|
+
projectId String?
|
|
133
266
|
action String
|
|
134
267
|
entity String?
|
|
135
268
|
entityId String?
|
|
@@ -140,101 +273,31 @@ model AuditLog {
|
|
|
140
273
|
|
|
141
274
|
@@index([orgId])
|
|
142
275
|
@@index([userId])
|
|
276
|
+
@@index([projectId])
|
|
143
277
|
@@index([action])
|
|
144
278
|
@@index([createdAt])
|
|
145
279
|
}
|
|
146
280
|
|
|
147
|
-
model
|
|
281
|
+
model File {
|
|
148
282
|
id String @id @default(cuid())
|
|
149
283
|
userId String
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
refreshToken String?
|
|
154
|
-
createdAt DateTime @default(now())
|
|
155
|
-
|
|
156
|
-
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
157
|
-
|
|
158
|
-
@@unique([provider, providerId])
|
|
159
|
-
@@index([userId])
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
model File {
|
|
163
|
-
id String @id @default(cuid())
|
|
164
|
-
userId String
|
|
165
|
-
orgId String?
|
|
166
|
-
filename String
|
|
284
|
+
orgId String?
|
|
285
|
+
projectId String?
|
|
286
|
+
filename String
|
|
167
287
|
originalName String
|
|
168
|
-
mimeType
|
|
169
|
-
size
|
|
170
|
-
url
|
|
171
|
-
provider
|
|
172
|
-
createdAt
|
|
288
|
+
mimeType String
|
|
289
|
+
size Int
|
|
290
|
+
url String
|
|
291
|
+
provider String @default("local")
|
|
292
|
+
createdAt DateTime @default(now())
|
|
173
293
|
|
|
174
294
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
175
295
|
|
|
176
296
|
@@index([userId])
|
|
177
297
|
@@index([orgId])
|
|
298
|
+
@@index([projectId])
|
|
178
299
|
}
|
|
179
300
|
|
|
180
|
-
// Edge Functions
|
|
181
|
-
model EdgeFunction {
|
|
182
|
-
id String @id @default(cuid())
|
|
183
|
-
name String
|
|
184
|
-
description String?
|
|
185
|
-
code String
|
|
186
|
-
runtime String @default("nodejs18")
|
|
187
|
-
timeout Int @default(30000)
|
|
188
|
-
memory Int @default(128)
|
|
189
|
-
environment Json?
|
|
190
|
-
triggers Json?
|
|
191
|
-
status String @default("draft")
|
|
192
|
-
deployedAt DateTime?
|
|
193
|
-
deploymentUrl String?
|
|
194
|
-
deploymentId String?
|
|
195
|
-
organizationId String
|
|
196
|
-
createdBy String
|
|
197
|
-
createdAt DateTime @default(now())
|
|
198
|
-
updatedAt DateTime @updatedAt
|
|
199
|
-
logs EdgeFunctionLog[]
|
|
200
|
-
|
|
201
|
-
@@unique([organizationId, name])
|
|
202
|
-
@@index([organizationId])
|
|
203
|
-
@@index([status])
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
model EdgeFunctionLog {
|
|
207
|
-
id String @id @default(cuid())
|
|
208
|
-
functionId String
|
|
209
|
-
status String
|
|
210
|
-
duration Int
|
|
211
|
-
input Json?
|
|
212
|
-
output Json?
|
|
213
|
-
error String?
|
|
214
|
-
triggeredBy String?
|
|
215
|
-
createdAt DateTime @default(now())
|
|
216
|
-
|
|
217
|
-
function EdgeFunction @relation(fields: [functionId], references: [id], onDelete: Cascade)
|
|
218
|
-
|
|
219
|
-
@@index([functionId])
|
|
220
|
-
@@index([createdAt])
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
// Vector Embeddings
|
|
224
|
-
model VectorCollection {
|
|
225
|
-
id String @id @default(cuid())
|
|
226
|
-
name String
|
|
227
|
-
description String?
|
|
228
|
-
dimension Int @default(1536)
|
|
229
|
-
embeddingModel String @default("text-embedding-3-small")
|
|
230
|
-
organizationId String
|
|
231
|
-
createdAt DateTime @default(now())
|
|
232
|
-
|
|
233
|
-
@@unique([organizationId, name])
|
|
234
|
-
@@index([organizationId])
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
// Frontend Scanner Results
|
|
238
301
|
model ScannedProject {
|
|
239
302
|
id String @id @default(cuid())
|
|
240
303
|
userId String
|
|
@@ -253,18 +316,19 @@ model ScannedProject {
|
|
|
253
316
|
@@index([status])
|
|
254
317
|
}
|
|
255
318
|
|
|
256
|
-
// NoSQL Database Collections
|
|
257
319
|
model DBCollection {
|
|
258
|
-
id String
|
|
320
|
+
id String @id @default(cuid())
|
|
259
321
|
projectId String
|
|
260
322
|
name String
|
|
261
323
|
description String?
|
|
262
324
|
schema Json?
|
|
263
325
|
indexes Json?
|
|
264
|
-
createdAt DateTime
|
|
265
|
-
updatedAt DateTime
|
|
326
|
+
createdAt DateTime @default(now())
|
|
327
|
+
updatedAt DateTime @updatedAt
|
|
266
328
|
documents DBDocument[]
|
|
267
329
|
|
|
330
|
+
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
|
331
|
+
|
|
268
332
|
@@unique([projectId, name])
|
|
269
333
|
@@index([projectId])
|
|
270
334
|
}
|
package/template/sdk/README.md
CHANGED
package/template/sdk/index.ts
CHANGED
|
@@ -18,7 +18,6 @@ export interface User {
|
|
|
18
18
|
export interface Organization {
|
|
19
19
|
id: string;
|
|
20
20
|
name: string;
|
|
21
|
-
slug: string;
|
|
22
21
|
plan: string;
|
|
23
22
|
stripeCustomerId?: string;
|
|
24
23
|
createdAt: Date;
|
|
@@ -27,7 +26,6 @@ export interface Organization {
|
|
|
27
26
|
export interface Project {
|
|
28
27
|
id: string;
|
|
29
28
|
name: string;
|
|
30
|
-
description?: string;
|
|
31
29
|
orgId: string;
|
|
32
30
|
createdAt: Date;
|
|
33
31
|
}
|
|
@@ -229,7 +227,7 @@ export class LaunchBaseClient {
|
|
|
229
227
|
return res.data;
|
|
230
228
|
}
|
|
231
229
|
|
|
232
|
-
async createProject(data: { name: string
|
|
230
|
+
async createProject(data: { name: string }): Promise<Project> {
|
|
233
231
|
const res = await this.client.post('/api/projects', data);
|
|
234
232
|
return res.data;
|
|
235
233
|
}
|
|
@@ -34,7 +34,7 @@ export class AuditInterceptor implements NestInterceptor {
|
|
|
34
34
|
return next.handle().pipe(
|
|
35
35
|
tap({
|
|
36
36
|
next: (result) => {
|
|
37
|
-
const entityId = result?.id ?? result?.project?.id ?? result?.org?.id ?? result?.
|
|
37
|
+
const entityId = result?.id ?? result?.project?.id ?? result?.org?.id ?? result?.organizationMember?.id;
|
|
38
38
|
|
|
39
39
|
this.audit.log({
|
|
40
40
|
orgId: tenant?.orgId,
|
|
@@ -65,6 +65,9 @@ export class AuthService {
|
|
|
65
65
|
|
|
66
66
|
const passwordHash = await this.hashPassword(dto.password);
|
|
67
67
|
|
|
68
|
+
// Generate slug from org name
|
|
69
|
+
const slug = dto.orgName.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
|
|
70
|
+
|
|
68
71
|
const user = await this.prisma.user.create({
|
|
69
72
|
data: {
|
|
70
73
|
email: dto.email,
|
|
@@ -75,14 +78,16 @@ export class AuthService {
|
|
|
75
78
|
// Create first org for the user
|
|
76
79
|
const org = await this.prisma.organization.create({
|
|
77
80
|
data: {
|
|
78
|
-
name: dto.orgName
|
|
81
|
+
name: dto.orgName,
|
|
82
|
+
slug,
|
|
83
|
+
ownerId: user.id
|
|
79
84
|
}
|
|
80
85
|
});
|
|
81
86
|
|
|
82
|
-
await this.prisma.
|
|
87
|
+
await this.prisma.organizationMember.create({
|
|
83
88
|
data: {
|
|
84
89
|
userId: user.id,
|
|
85
|
-
|
|
90
|
+
organizationId: org.id,
|
|
86
91
|
role: Role.OWNER
|
|
87
92
|
}
|
|
88
93
|
});
|
|
@@ -91,7 +96,7 @@ export class AuthService {
|
|
|
91
96
|
|
|
92
97
|
return {
|
|
93
98
|
user: { id: user.id, email: user.email },
|
|
94
|
-
org: { id: org.id, name: org.name },
|
|
99
|
+
org: { id: org.id, name: org.name, slug: org.slug },
|
|
95
100
|
...tokens
|
|
96
101
|
};
|
|
97
102
|
}
|
|
@@ -99,7 +104,7 @@ export class AuthService {
|
|
|
99
104
|
async login(dto: LoginDto) {
|
|
100
105
|
const user = await this.prisma.user.findUnique({
|
|
101
106
|
where: { email: dto.email },
|
|
102
|
-
include: {
|
|
107
|
+
include: { organizationMemberships: { include: { organization: true } } }
|
|
103
108
|
});
|
|
104
109
|
if (!user || !user.passwordHash) throw new UnauthorizedException('Invalid credentials');
|
|
105
110
|
|
|
@@ -109,11 +114,11 @@ export class AuthService {
|
|
|
109
114
|
const tokens = await this.issueTokens({ id: user.id, email: user.email });
|
|
110
115
|
|
|
111
116
|
// Get first org for the user
|
|
112
|
-
const firstMembership = user.
|
|
117
|
+
const firstMembership = user.organizationMemberships[0];
|
|
113
118
|
|
|
114
119
|
return {
|
|
115
120
|
user: { id: user.id, email: user.email },
|
|
116
|
-
org: firstMembership ? { id: firstMembership.
|
|
121
|
+
org: firstMembership ? { id: firstMembership.organization.id, name: firstMembership.organization.name, slug: firstMembership.organization.slug } : null,
|
|
117
122
|
...tokens
|
|
118
123
|
};
|
|
119
124
|
}
|
|
@@ -196,7 +196,7 @@ export class BillingService {
|
|
|
196
196
|
}
|
|
197
197
|
|
|
198
198
|
if (type === 'members') {
|
|
199
|
-
const count = await this.prisma.
|
|
199
|
+
const count = await this.prisma.organizationMember.count({ where: { organizationId: orgId } });
|
|
200
200
|
return count < limits.members;
|
|
201
201
|
}
|
|
202
202
|
|
|
@@ -22,17 +22,27 @@ export class AllExceptionsFilter implements ExceptionFilter {
|
|
|
22
22
|
? exception.getStatus()
|
|
23
23
|
: HttpStatus.INTERNAL_SERVER_ERROR;
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
25
|
+
let errorMessage: string;
|
|
26
|
+
if (exception instanceof HttpException) {
|
|
27
|
+
const response = exception.getResponse();
|
|
28
|
+
if (typeof response === 'string') {
|
|
29
|
+
errorMessage = response;
|
|
30
|
+
} else if (typeof response === 'object' && response !== null && 'message' in response) {
|
|
31
|
+
const msg = (response as any).message;
|
|
32
|
+
errorMessage = Array.isArray(msg) ? msg[0] : String(msg);
|
|
33
|
+
} else {
|
|
34
|
+
errorMessage = 'An error occurred';
|
|
35
|
+
}
|
|
36
|
+
} else {
|
|
37
|
+
errorMessage = 'Internal server error';
|
|
38
|
+
}
|
|
29
39
|
|
|
30
40
|
const errorResponse = {
|
|
31
41
|
statusCode: status,
|
|
32
42
|
timestamp: new Date().toISOString(),
|
|
33
43
|
path: request.url,
|
|
34
44
|
method: request.method,
|
|
35
|
-
message:
|
|
45
|
+
message: errorMessage,
|
|
36
46
|
};
|
|
37
47
|
|
|
38
48
|
// Log the error
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { CanActivate, ExecutionContext, ForbiddenException, Injectable } from '@nestjs/common';
|
|
2
|
+
import { PrismaService } from '../prisma/prisma.service';
|
|
3
|
+
|
|
4
|
+
@Injectable()
|
|
5
|
+
export class ProjectGuard implements CanActivate {
|
|
6
|
+
constructor(private readonly prisma: PrismaService) {}
|
|
7
|
+
|
|
8
|
+
async canActivate(context: ExecutionContext): Promise<boolean> {
|
|
9
|
+
const req = context.switchToHttp().getRequest();
|
|
10
|
+
const user = req.user as { userId?: string } | undefined;
|
|
11
|
+
const tenant = req.tenant as { orgId?: string; role?: string } | undefined;
|
|
12
|
+
|
|
13
|
+
// Get project identifier from params or headers
|
|
14
|
+
const projectSlug = req.params?.projectSlug || req.headers['x-project-slug'] as string | undefined;
|
|
15
|
+
const projectId = req.params?.projectId || req.headers['x-project-id'] as string | undefined;
|
|
16
|
+
|
|
17
|
+
if (!user?.userId) throw new ForbiddenException('Missing auth context');
|
|
18
|
+
if (!tenant?.orgId) throw new ForbiddenException('Missing tenant context - ensure TenantGuard runs first');
|
|
19
|
+
|
|
20
|
+
// Find project by slug or ID
|
|
21
|
+
let project;
|
|
22
|
+
if (projectId) {
|
|
23
|
+
project = await this.prisma.project.findUnique({
|
|
24
|
+
where: { id: projectId }
|
|
25
|
+
});
|
|
26
|
+
} else if (projectSlug) {
|
|
27
|
+
project = await this.prisma.project.findFirst({
|
|
28
|
+
where: {
|
|
29
|
+
slug: projectSlug,
|
|
30
|
+
organizationId: tenant.orgId
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (!project) throw new ForbiddenException('Project not found');
|
|
36
|
+
|
|
37
|
+
// Verify project belongs to the organization
|
|
38
|
+
if (project.organizationId !== tenant.orgId) {
|
|
39
|
+
throw new ForbiddenException('Project does not belong to this organization');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Attach project to request for use in controllers
|
|
43
|
+
req.project = {
|
|
44
|
+
id: project.id,
|
|
45
|
+
slug: project.slug,
|
|
46
|
+
name: project.name,
|
|
47
|
+
organizationId: project.organizationId
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -13,11 +13,11 @@ export class TenantGuard implements CanActivate {
|
|
|
13
13
|
if (!user?.userId) throw new ForbiddenException('Missing auth context');
|
|
14
14
|
if (!orgId) throw new ForbiddenException('Missing x-org-id header');
|
|
15
15
|
|
|
16
|
-
const membership = await this.prisma.
|
|
16
|
+
const membership = await this.prisma.organizationMember.findUnique({
|
|
17
17
|
where: {
|
|
18
|
-
|
|
18
|
+
organizationId_userId: {
|
|
19
19
|
userId: user.userId,
|
|
20
|
-
orgId
|
|
20
|
+
organizationId: orgId
|
|
21
21
|
}
|
|
22
22
|
}
|
|
23
23
|
});
|