@nestledjs/api 2.7.0 → 2.9.0
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/package.json +1 -1
- package/src/app/files/src/main.ts__tmpl__ +1 -0
- package/src/app/files/webpack.config.js__tmpl__ +18 -18
- package/src/config/files/src/lib/config.service.ts__tmpl__ +37 -0
- package/src/config/files/src/lib/configuration.ts__tmpl__ +36 -9
- package/src/config/files/src/lib/validation.ts__tmpl__ +19 -17
- package/src/core/files/data-access/src/lib/api-core-data-access.service.ts__tmpl__ +18 -7
- package/src/generate-crud/CLAUDE.md +13 -0
- package/src/prisma/CLAUDE.md +13 -0
- package/src/prisma/files/config/CLAUDE.md +12 -0
- package/src/prisma/files/src/lib/schemas/CLAUDE.md +12 -0
- package/src/prisma/files/src/lib/seed/CLAUDE.md +13 -0
- package/src/prisma/files/src/lib/seed/seed.ts__tmpl__ +17 -1
- package/src/prisma/generator.js +2 -1
- package/src/prisma/generator.js.map +1 -1
- package/src/setup/CLAUDE.md +12 -0
- package/src/setup/generator.js +1 -0
- package/src/setup/generator.js.map +1 -1
- package/src/utils/files/src/index.ts__tmpl__ +9 -1
- package/src/utils/files/src/lib/decorators/ctx-organization.decorator.ts__tmpl__ +41 -0
- package/src/utils/files/src/lib/decorators/ctx-user.decorator.ts__tmpl__ +4 -4
- package/src/utils/files/src/lib/guards/gql-auth-admin.guard.ts__tmpl__ +4 -5
- package/src/utils/files/src/lib/guards/gql-organization-scoped.guard.ts__tmpl__ +43 -0
- package/src/utils/files/src/lib/guards/permissions.guard.ts__tmpl__ +101 -0
- package/src/utils/files/src/lib/guards/subscription.guard.ts__tmpl__ +79 -0
- package/src/utils/files/src/lib/services/auth-cache.service.ts__tmpl__ +232 -0
- package/src/utils/files/src/lib/services/auth-loader.service.ts__tmpl__ +176 -0
- package/src/utils/files/src/lib/types/nest-context-type.ts__tmpl__ +15 -1
package/package.json
CHANGED
|
@@ -29,27 +29,27 @@ module.exports = {
|
|
|
29
29
|
],
|
|
30
30
|
},
|
|
31
31
|
externals: [
|
|
32
|
+
// Custom externals function to handle Prisma npm packages
|
|
33
|
+
// IMPORTANT: This must come before nodeExternals to catch Prisma packages first
|
|
34
|
+
function ({ request }, callback) {
|
|
35
|
+
// Externalize actual Prisma npm packages (required for native binaries)
|
|
36
|
+
// But NOT @{npmScope}/api/prisma which is a local workspace module
|
|
37
|
+
if (request && (
|
|
38
|
+
request.includes('@prisma/client') ||
|
|
39
|
+
request.includes('.prisma/client') ||
|
|
40
|
+
request.includes('@prisma/adapter-pg') ||
|
|
41
|
+
request.includes('@prisma/internals') ||
|
|
42
|
+
request.includes('@prisma/extension-optimize')
|
|
43
|
+
)) {
|
|
44
|
+
return callback(null, `commonjs ${request}`)
|
|
45
|
+
}
|
|
46
|
+
callback()
|
|
47
|
+
},
|
|
32
48
|
nodeExternals({
|
|
33
49
|
allowlist: [
|
|
34
|
-
|
|
50
|
+
// Allow all @{npmScope}/api libs to be bundled (including api/prisma)
|
|
51
|
+
/^@<%= npmScope %>\/api/,
|
|
35
52
|
],
|
|
36
53
|
}),
|
|
37
|
-
// Externalize Prisma packages for v7 compatibility
|
|
38
|
-
function ({ request }, callback) {
|
|
39
|
-
if (request && request.includes('@prisma/client')) {
|
|
40
|
-
return callback(null, 'commonjs ' + request)
|
|
41
|
-
}
|
|
42
|
-
if (request && request.includes('.prisma/client')) {
|
|
43
|
-
return callback(null, 'commonjs ' + request)
|
|
44
|
-
}
|
|
45
|
-
// Externalize Prisma adapter for v7 compatibility
|
|
46
|
-
if (request && (request === '@prisma/adapter-pg' || request.startsWith('@prisma/adapter-pg/'))) {
|
|
47
|
-
return callback(null, 'commonjs ' + request)
|
|
48
|
-
}
|
|
49
|
-
if (request && request === '@<%= npmScope %>/api/prisma') {
|
|
50
|
-
return callback(null, 'commonjs ' + request)
|
|
51
|
-
}
|
|
52
|
-
callback()
|
|
53
|
-
}
|
|
54
54
|
]
|
|
55
55
|
};
|
|
@@ -6,6 +6,10 @@ import { CookieOptions } from 'express'
|
|
|
6
6
|
export class ConfigService {
|
|
7
7
|
constructor(public readonly config: NestConfigService) {}
|
|
8
8
|
|
|
9
|
+
get environment(): string {
|
|
10
|
+
return this.config.getOrThrow<string>('environment')
|
|
11
|
+
}
|
|
12
|
+
|
|
9
13
|
get apiUrl(): string {
|
|
10
14
|
return this.config.getOrThrow<string>('apiUrl')
|
|
11
15
|
}
|
|
@@ -52,10 +56,26 @@ export class ConfigService {
|
|
|
52
56
|
return this.config.getOrThrow<string>('siteUrl')
|
|
53
57
|
}
|
|
54
58
|
|
|
59
|
+
get emailProvider(): string {
|
|
60
|
+
const provider = this.config.get<string>('email.provider')
|
|
61
|
+
if (provider) {
|
|
62
|
+
return provider
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Auto-detect: if SMTP is configured, use it; otherwise use mock
|
|
66
|
+
const smtpHost = this.config.get<string>('smtp.host')
|
|
67
|
+
return smtpHost ? 'smtp' : 'mock'
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
get frontendUrl(): string {
|
|
71
|
+
return this.config.getOrThrow<string>('frontend.url')
|
|
72
|
+
}
|
|
73
|
+
|
|
55
74
|
get mailerConfig() {
|
|
56
75
|
return {
|
|
57
76
|
host: this.config.getOrThrow<string>('smtp.host'),
|
|
58
77
|
port: this.config.getOrThrow<string>('smtp.port'),
|
|
78
|
+
secure: this.config.get<boolean>('smtp.secure') || false,
|
|
59
79
|
auth: {
|
|
60
80
|
user: this.config.getOrThrow<string>('smtp.user'),
|
|
61
81
|
pass: this.config.getOrThrow<string>('smtp.pass'),
|
|
@@ -63,6 +83,23 @@ export class ConfigService {
|
|
|
63
83
|
}
|
|
64
84
|
}
|
|
65
85
|
|
|
86
|
+
get twilio() {
|
|
87
|
+
return {
|
|
88
|
+
accountSid: this.config.get('twilio.accountSid'),
|
|
89
|
+
authToken: this.config.get('twilio.authToken'),
|
|
90
|
+
fromNumber: this.config.get('twilio.fromNumber'),
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
get stripe() {
|
|
95
|
+
return {
|
|
96
|
+
secretKey: this.config.get<string>('STRIPE_SECRET_KEY') || '',
|
|
97
|
+
publishableKey: this.config.get<string>('STRIPE_PUBLISHABLE_KEY') || '',
|
|
98
|
+
webhookSecret: this.config.get<string>('STRIPE_WEBHOOK_SECRET') || '',
|
|
99
|
+
currency: this.config.get<string>('STRIPE_CURRENCY') || 'usd',
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
66
103
|
get prismaOptimizeEnabled(): boolean {
|
|
67
104
|
return this.config.get<boolean>('prisma.optimize.enabled') ?? false
|
|
68
105
|
}
|
|
@@ -1,39 +1,66 @@
|
|
|
1
1
|
export const configuration = () => ({
|
|
2
2
|
prefix: 'api',
|
|
3
3
|
environment: process.env['NODE_ENV'],
|
|
4
|
-
host: process.env['HOST'],
|
|
4
|
+
host: process.env['HOST'] ?? '0.0.0.0',
|
|
5
5
|
port: parseInt(process.env['PORT'] ?? '3000', 10),
|
|
6
6
|
apiUrl: process.env['API_URL'],
|
|
7
7
|
api: {
|
|
8
8
|
cookie: {
|
|
9
9
|
name: process.env['API_COOKIE_NAME'],
|
|
10
|
-
secret: process.env['API_COOKIE_SECRET']
|
|
10
|
+
secret: process.env['API_COOKIE_SECRET'] ?? 'secret',
|
|
11
11
|
options: {
|
|
12
|
-
domain
|
|
12
|
+
// Only set cookie domain if it is a valid registrable domain. Avoid 'localhost' or IPs.
|
|
13
|
+
...(() => {
|
|
14
|
+
const dom = (process.env['API_COOKIE_DOMAIN'] ?? '').trim()
|
|
15
|
+
if (!dom || dom === 'localhost' || dom === '127.0.0.1' || dom === '[::1]') {
|
|
16
|
+
return {}
|
|
17
|
+
}
|
|
18
|
+
return { domain: dom }
|
|
19
|
+
})(),
|
|
13
20
|
httpOnly: true,
|
|
21
|
+
secure: process.env['NODE_ENV'] === 'production',
|
|
22
|
+
sameSite: 'lax',
|
|
23
|
+
path: '/',
|
|
14
24
|
},
|
|
15
25
|
},
|
|
16
26
|
cors: {
|
|
17
|
-
origin:
|
|
27
|
+
origin: (process.env['ALLOWED_ORIGINS'] ?? '').split(',').map(o => o.trim()).filter(o => o.length > 0),
|
|
18
28
|
},
|
|
19
29
|
},
|
|
20
|
-
siteUrl: process.env['SITE_URL']
|
|
30
|
+
siteUrl: process.env['SITE_URL'] ?? process.env['API_URL']?.replace('/api', ''),
|
|
21
31
|
app: {
|
|
22
32
|
email: process.env['APP_EMAIL'],
|
|
23
33
|
supportEmail: process.env['APP_SUPPORT_EMAIL'],
|
|
24
34
|
adminEmails: process.env['APP_ADMIN_EMAILS'],
|
|
25
35
|
name: process.env['APP_NAME'],
|
|
26
36
|
},
|
|
37
|
+
email: {
|
|
38
|
+
provider: process.env['EMAIL_PROVIDER'] ?? 'smtp',
|
|
39
|
+
},
|
|
27
40
|
smtp: {
|
|
28
41
|
host: process.env['SMTP_HOST'],
|
|
29
42
|
port: process.env['SMTP_PORT'],
|
|
30
43
|
user: process.env['SMTP_USER'],
|
|
31
44
|
pass: process.env['SMTP_PASS'],
|
|
32
45
|
},
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
46
|
+
twoFactor: {
|
|
47
|
+
// Issuer name shown in authenticator apps
|
|
48
|
+
issuer: process.env['TWO_FACTOR_ISSUER'] ?? process.env['APP_NAME'] ?? 'MyApp',
|
|
49
|
+
// Window for time drift (in 30-second increments, 2 = 60 seconds tolerance)
|
|
50
|
+
window: parseInt(process.env['TWO_FACTOR_WINDOW'] ?? '2', 10),
|
|
51
|
+
// Encryption key for storing secrets (should be 32 characters)
|
|
52
|
+
encryptionKey: process.env['TWO_FACTOR_ENCRYPTION_KEY'] ?? process.env['JWT_SECRET'],
|
|
53
|
+
},
|
|
54
|
+
oauth: {
|
|
55
|
+
google: {
|
|
56
|
+
clientId: process.env['GOOGLE_OAUTH_CLIENT_ID'],
|
|
57
|
+
clientSecret: process.env['GOOGLE_OAUTH_CLIENT_SECRET'],
|
|
58
|
+
enabled: !!(process.env['GOOGLE_OAUTH_CLIENT_ID'] && process.env['GOOGLE_OAUTH_CLIENT_SECRET']),
|
|
59
|
+
},
|
|
60
|
+
github: {
|
|
61
|
+
clientId: process.env['GITHUB_OAUTH_CLIENT_ID'],
|
|
62
|
+
clientSecret: process.env['GITHUB_OAUTH_CLIENT_SECRET'],
|
|
63
|
+
enabled: !!(process.env['GITHUB_OAUTH_CLIENT_ID'] && process.env['GITHUB_OAUTH_CLIENT_SECRET']),
|
|
37
64
|
},
|
|
38
65
|
},
|
|
39
66
|
})
|
|
@@ -2,25 +2,27 @@ import * as Joi from 'joi'
|
|
|
2
2
|
|
|
3
3
|
export const validationSchema = Joi.object({
|
|
4
4
|
NODE_ENV: Joi.string().valid('development', 'production', 'test'),
|
|
5
|
-
HOST: Joi.alternatives()
|
|
6
|
-
.
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
)
|
|
10
|
-
.default('localhost'),
|
|
5
|
+
HOST: Joi.alternatives().try(
|
|
6
|
+
Joi.string().ip(), // Allow IP addresses like 0.0.0.0, 127.0.0.1
|
|
7
|
+
Joi.string().hostname(), // Allow hostnames like localhost, example.com
|
|
8
|
+
).default('localhost'),
|
|
11
9
|
PORT: Joi.number().default(3000),
|
|
12
10
|
WEB_PORT: Joi.number().default(4200),
|
|
13
|
-
WEB_URL: Joi.string().default(
|
|
11
|
+
WEB_URL: Joi.string().default(
|
|
12
|
+
`http://${process.env['HOST'] || 'localhost'}:${process.env['WEB_PORT']}`,
|
|
13
|
+
),
|
|
14
14
|
API_COOKIE_DOMAIN: Joi.string().default('localhost'),
|
|
15
15
|
API_COOKIE_NAME: Joi.string().default('__session'),
|
|
16
|
-
API_URL: Joi.string().default(
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
16
|
+
API_URL: Joi.string().default(
|
|
17
|
+
`http://${process.env['HOST'] || 'localhost'}:${process.env['PORT']}/api`,
|
|
18
|
+
),
|
|
19
|
+
APP_NAME: Joi.string().default('BizToBiz'), // Made optional with default
|
|
20
|
+
APP_EMAIL: Joi.string().email().default('admin@example.com'), // Made optional with default
|
|
21
|
+
APP_SUPPORT_EMAIL: Joi.string().email().default('support@example.com'), // Made optional with default
|
|
22
|
+
APP_ADMIN_EMAILS: Joi.string().default('admin@example.com'), // Made optional with default
|
|
23
|
+
SITE_URL: Joi.string().uri().default('http://localhost:4200'), // Made optional with default
|
|
24
|
+
SMTP_HOST: Joi.string().default('localhost'), // Made optional with default
|
|
25
|
+
SMTP_PORT: Joi.string().default('587'), // Made optional with default
|
|
26
|
+
SMTP_USER: Joi.string().default('user'), // Made optional with default
|
|
27
|
+
SMTP_PASS: Joi.string().default('pass'), // Made optional with default
|
|
26
28
|
})
|
|
@@ -4,14 +4,25 @@ import { CorePagingInput } from './dto/core-paging.input'
|
|
|
4
4
|
import { PrismaPg } from '@prisma/adapter-pg'
|
|
5
5
|
|
|
6
6
|
function createAdapter() {
|
|
7
|
-
const
|
|
8
|
-
|
|
7
|
+
const connectionString = process.env['DATABASE_URL'] || ''
|
|
8
|
+
|
|
9
|
+
// Auto-detect SSL for cloud databases (Heroku, Railway, AWS RDS)
|
|
10
|
+
const requireSsl =
|
|
11
|
+
connectionString.includes('amazonaws.com') ||
|
|
12
|
+
connectionString.includes('.railway.app') ||
|
|
13
|
+
connectionString.includes('heroku') ||
|
|
14
|
+
process.env['DATABASE_SSL'] === 'true'
|
|
15
|
+
|
|
9
16
|
const usePgBouncer = process.env['PGBOUNCER_ENABLED'] === 'true'
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
17
|
+
|
|
18
|
+
// Pass PoolConfig to PrismaPg - it manages its own Pool internally
|
|
19
|
+
// This avoids connection management conflicts that can cause ECONNREFUSED errors
|
|
20
|
+
return new PrismaPg({
|
|
21
|
+
connectionString,
|
|
22
|
+
ssl: requireSsl ? { rejectUnauthorized: false } : undefined,
|
|
23
|
+
max: usePgBouncer ? 5 : 30,
|
|
24
|
+
idleTimeoutMillis: usePgBouncer ? 10000 : 30000,
|
|
25
|
+
})
|
|
15
26
|
}
|
|
16
27
|
|
|
17
28
|
@Injectable()
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<claude-mem-context>
|
|
2
|
+
# Recent Activity
|
|
3
|
+
|
|
4
|
+
<!-- This section is auto-generated by claude-mem. Edit content outside the tags. -->
|
|
5
|
+
|
|
6
|
+
### Feb 4, 2026
|
|
7
|
+
|
|
8
|
+
| ID | Time | T | Title | Read |
|
|
9
|
+
|----|------|---|-------|------|
|
|
10
|
+
| #150 | 3:20 PM | ⚖️ | Prisma v7 Migration Strategy for Nestled Generator Framework | ~584 |
|
|
11
|
+
| #108 | 2:41 PM | 🔵 | CRUD Generator Output File Patterns Identified | ~343 |
|
|
12
|
+
| #77 | 2:26 PM | 🔵 | CRUD Generator Uses Prisma Internals DMMF Parser | ~383 |
|
|
13
|
+
</claude-mem-context>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<claude-mem-context>
|
|
2
|
+
# Recent Activity
|
|
3
|
+
|
|
4
|
+
<!-- This section is auto-generated by claude-mem. Edit content outside the tags. -->
|
|
5
|
+
|
|
6
|
+
### Feb 4, 2026
|
|
7
|
+
|
|
8
|
+
| ID | Time | T | Title | Read |
|
|
9
|
+
|----|------|---|-------|------|
|
|
10
|
+
| #150 | 3:20 PM | ⚖️ | Prisma v7 Migration Strategy for Nestled Generator Framework | ~584 |
|
|
11
|
+
| #110 | 2:41 PM | 🔵 | DB-Update Workflow Command Chain | ~350 |
|
|
12
|
+
| #78 | 2:26 PM | 🔵 | Prisma Generator Creates Configuration and Scripts | ~384 |
|
|
13
|
+
</claude-mem-context>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<claude-mem-context>
|
|
2
|
+
# Recent Activity
|
|
3
|
+
|
|
4
|
+
<!-- This section is auto-generated by claude-mem. Edit content outside the tags. -->
|
|
5
|
+
|
|
6
|
+
### Feb 4, 2026
|
|
7
|
+
|
|
8
|
+
| ID | Time | T | Title | Read |
|
|
9
|
+
|----|------|---|-------|------|
|
|
10
|
+
| #150 | 3:20 PM | ⚖️ | Prisma v7 Migration Strategy for Nestled Generator Framework | ~584 |
|
|
11
|
+
| #91 | 2:34 PM | 🔵 | Prisma Config Template Already V7-Compatible | ~331 |
|
|
12
|
+
</claude-mem-context>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<claude-mem-context>
|
|
2
|
+
# Recent Activity
|
|
3
|
+
|
|
4
|
+
<!-- This section is auto-generated by claude-mem. Edit content outside the tags. -->
|
|
5
|
+
|
|
6
|
+
### Feb 4, 2026
|
|
7
|
+
|
|
8
|
+
| ID | Time | T | Title | Read |
|
|
9
|
+
|----|------|---|-------|------|
|
|
10
|
+
| #150 | 3:20 PM | ⚖️ | Prisma v7 Migration Strategy for Nestled Generator Framework | ~584 |
|
|
11
|
+
| #92 | 2:34 PM | 🔵 | Schema Template Requires V7 Migration Updates | ~392 |
|
|
12
|
+
</claude-mem-context>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<claude-mem-context>
|
|
2
|
+
# Recent Activity
|
|
3
|
+
|
|
4
|
+
<!-- This section is auto-generated by claude-mem. Edit content outside the tags. -->
|
|
5
|
+
|
|
6
|
+
### Feb 4, 2026
|
|
7
|
+
|
|
8
|
+
| ID | Time | T | Title | Read |
|
|
9
|
+
|----|------|---|-------|------|
|
|
10
|
+
| #208 | 4:17 PM | 🔴 | Seed Script Template Updated with Prisma v7 Adapter Pattern | ~384 |
|
|
11
|
+
| #197 | 3:57 PM | ✅ | Seed Template Updated for Prisma v7 Client Import Path | ~336 |
|
|
12
|
+
| #171 | 3:26 PM | 🔵 | Seed Script Template Uses Direct PrismaClient Instantiation | ~331 |
|
|
13
|
+
</claude-mem-context>
|
|
@@ -5,7 +5,23 @@ import { countries } from './seed-data/iso-3166-countries'
|
|
|
5
5
|
import { seedUsers } from './seed-data/seed-users'
|
|
6
6
|
import { hashSync } from 'bcryptjs'
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
function createAdapter() {
|
|
9
|
+
const connectionString = process.env.DATABASE_URL || ''
|
|
10
|
+
|
|
11
|
+
// Auto-detect SSL for cloud databases (Heroku, Railway, AWS RDS)
|
|
12
|
+
const requireSsl =
|
|
13
|
+
connectionString.includes('amazonaws.com') ||
|
|
14
|
+
connectionString.includes('.railway.app') ||
|
|
15
|
+
connectionString.includes('heroku') ||
|
|
16
|
+
process.env.DATABASE_SSL === 'true'
|
|
17
|
+
|
|
18
|
+
return new PrismaPg({
|
|
19
|
+
connectionString,
|
|
20
|
+
ssl: requireSsl ? { rejectUnauthorized: false } : undefined,
|
|
21
|
+
})
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const adapter = createAdapter()
|
|
9
25
|
const prisma = new PrismaClient({ adapter })
|
|
10
26
|
|
|
11
27
|
async function main() {
|
package/src/prisma/generator.js
CHANGED
|
@@ -6,6 +6,7 @@ const devkit_1 = require("@nx/devkit");
|
|
|
6
6
|
const utils_1 = require("@nestledjs/utils");
|
|
7
7
|
function generateLibraries(tree_1) {
|
|
8
8
|
return tslib_1.__awaiter(this, arguments, void 0, function* (tree, options = {}) {
|
|
9
|
+
const npmScope = (0, utils_1.getNpmScope)(tree);
|
|
9
10
|
const templateRootPath = (0, devkit_1.joinPathFragments)(__dirname, './files');
|
|
10
11
|
const overwrite = options.overwrite === true;
|
|
11
12
|
// Create prisma.config.ts at workspace root via template (idempotent)
|
|
@@ -68,7 +69,7 @@ function generateLibraries(tree_1) {
|
|
|
68
69
|
// Add db-update convenience script to regenerate CRUD, models, custom, and SDK
|
|
69
70
|
if (!json.scripts['db-update']) {
|
|
70
71
|
json.scripts['db-update'] =
|
|
71
|
-
|
|
72
|
+
`pnpm prisma:generate && nx g @${npmScope}/api:generate-crud && pnpm generate:models && nx g @${npmScope}/api:custom && nx g @${npmScope}/shared:sdk`;
|
|
72
73
|
}
|
|
73
74
|
return json;
|
|
74
75
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"generator.js","sourceRoot":"","sources":["../../../../../generators/api/src/prisma/generator.ts"],"names":[],"mappings":";;AAIA,
|
|
1
|
+
{"version":3,"file":"generator.js","sourceRoot":"","sources":["../../../../../generators/api/src/prisma/generator.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<claude-mem-context>
|
|
2
|
+
# Recent Activity
|
|
3
|
+
|
|
4
|
+
<!-- This section is auto-generated by claude-mem. Edit content outside the tags. -->
|
|
5
|
+
|
|
6
|
+
### Feb 4, 2026
|
|
7
|
+
|
|
8
|
+
| ID | Time | T | Title | Read |
|
|
9
|
+
|----|------|---|-------|------|
|
|
10
|
+
| #150 | 3:20 PM | ⚖️ | Prisma v7 Migration Strategy for Nestled Generator Framework | ~584 |
|
|
11
|
+
| #102 | 2:37 PM | 🔵 | Setup Generator Dependencies for Prisma v6 | ~333 |
|
|
12
|
+
</claude-mem-context>
|
package/src/setup/generator.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"generator.js","sourceRoot":"","sources":["../../../../../generators/api/src/setup/generator.ts"],"names":[],"mappings":";;AASA,
|
|
1
|
+
{"version":3,"file":"generator.js","sourceRoot":"","sources":["../../../../../generators/api/src/setup/generator.ts"],"names":[],"mappings":";;AASA,8CAoGC;;AA7GD,uCAAkF;AAClF,4CAAiF;AAEjF,SAAS,WAAW,CAAC,IAAU;IAC7B,MAAM,SAAS,GAAG,QAAQ,CAAA;IAC1B,MAAM,OAAO,GAAG,oCAAoC,CAAA;IACpD,IAAI,CAAC,KAAK,CAAC,SAAS,EAAE,OAAO,CAAC,CAAA;AAChC,CAAC;AAED,SAAsB,iBAAiB,CAAC,IAAU;;QAChD,mBAAmB;QACnB,IAAA,qCAA4B,EAC1B,IAAI,EACJ;YACE,gBAAgB,EAAE,SAAS;YAC3B,eAAe,EAAE,QAAQ;YACzB,gBAAgB,EAAE,SAAS;YAC3B,gBAAgB,EAAE,QAAQ;YAC1B,cAAc,EAAE,SAAS;YACzB,iBAAiB,EAAE,SAAS;YAC5B,2BAA2B,EAAE,QAAQ;YACrC,aAAa,EAAE,SAAS;YACxB,kBAAkB,EAAE,SAAS;YAC7B,0BAA0B,EAAE,SAAS;YACrC,KAAK,EAAE,QAAQ;YACf,QAAQ,EAAE,QAAQ;YAClB,OAAO,EAAE,QAAQ;YACjB,OAAO,EAAE,UAAU;YACnB,0BAA0B,EAAE,OAAO;YACnC,iBAAiB,EAAE,SAAS;YAC5B,uBAAuB,EAAE,QAAQ;YACjC,YAAY,EAAE,QAAQ;YACtB,GAAG,EAAE,UAAU;YACf,UAAU,EAAE,QAAQ;YACpB,kBAAkB,EAAE,QAAQ;YAC5B,IAAI,EAAE,QAAQ;YACd,cAAc,EAAE,aAAa;YAC7B,gBAAgB,EAAE,QAAQ;YAC1B,iBAAiB,EAAE,SAAS;YAC5B,mBAAmB,EAAE,QAAQ;YAC7B,6BAA6B,EAAE,QAAQ;YACvC,OAAO,EAAE,QAAQ;YACjB,eAAe,EAAE,QAAQ;YACzB,MAAM,EAAE,SAAS;YACjB,MAAM,EAAE,QAAQ;YAChB,cAAc,EAAE,QAAQ;SACzB,EACD;YACE,EAAE,EAAE,QAAQ;YACZ,QAAQ,EAAE,QAAQ;YAClB,UAAU,EAAE,QAAQ;YACpB,UAAU,EAAE,QAAQ;YACpB,aAAa,EAAE,QAAQ;YACvB,oBAAoB,EAAE,SAAS;YAC/B,iBAAiB,EAAE,SAAS;YAC5B,4BAA4B,EAAE,OAAO;YACrC,oBAAoB,EAAE,UAAU;YAChC,UAAU,EAAE,QAAQ;YACpB,WAAW,EAAE,SAAS;YACtB,sBAAsB,EAAE,QAAQ;YAChC,gBAAgB,EAAE,QAAQ;YAC1B,mBAAmB,EAAE,QAAQ;YAC7B,qBAAqB,EAAE,QAAQ;YAC/B,aAAa,EAAE,SAAS;YACxB,oBAAoB,EAAE,QAAQ;YAC9B,gBAAgB,EAAE,QAAQ;YAC1B,mBAAmB,EAAE,QAAQ;YAC7B,YAAY,EAAE,SAAS;YACvB,IAAI,EAAE,SAAS;YACf,uBAAuB,EAAE,SAAS;YAClC,EAAE,EAAE,QAAQ;YACZ,MAAM,EAAE,QAAQ;YAChB,SAAS,EAAE,QAAQ;YACnB,IAAI,EAAE,QAAQ;YACd,WAAW,EAAE,QAAQ;YACrB,aAAa,EAAE,QAAQ;YACvB,wBAAwB,EAAE,QAAQ;YAClC,+BAA+B,EAAE,QAAQ;YACzC,6BAA6B,EAAE,QAAQ;YACvC,mBAAmB,EAAE,QAAQ;SAC9B,CACF,CAAA;QAED,qDAAqD;QACrD,MAAM,eAAe,GAAG;YACtB,oBAAoB;YACpB,iBAAiB;YACjB,oBAAoB;YACpB,gBAAgB;YAChB,4BAA4B;YAC5B,iBAAiB;YACjB,SAAS;YACT,IAAI;YACJ,QAAQ;YACR,SAAS;YACT,6BAA6B;YAC7B,cAAc;YACd,cAAc;YACd,SAAS;YACT,eAAe;YACf,WAAW;SACZ,CAAA;QACD,IAAA,iCAAyB,EAAC,IAAI,EAAE,EAAE,qBAAqB,EAAE,eAAe,EAAE,CAAC,CAAA;QAE3E,iDAAiD;QACjD,WAAW,CAAC,IAAI,CAAC,CAAA;QAEjB,gEAAgE;QAChE,OAAO,IAAA,2BAAmB,GAAE,CAAA;IAC9B,CAAC;CAAA;AAED,kBAAe,iBAAiB,CAAA"}
|
|
@@ -1,4 +1,12 @@
|
|
|
1
1
|
export * from './lib/guards/gql-auth.guard'
|
|
2
2
|
export * from './lib/guards/gql-auth-admin.guard'
|
|
3
|
+
export * from './lib/guards/gql-organization-scoped.guard'
|
|
4
|
+
export * from './lib/guards/permissions.guard'
|
|
5
|
+
export * from './lib/guards/subscription.guard'
|
|
3
6
|
export * from './lib/decorators/ctx-user.decorator'
|
|
4
|
-
export * from './lib/
|
|
7
|
+
export * from './lib/decorators/ctx-organization.decorator'
|
|
8
|
+
export * from './lib/services/auth-cache.service'
|
|
9
|
+
export * from './lib/services/auth-loader.service'
|
|
10
|
+
|
|
11
|
+
// Explicitly export types for webpack compatibility
|
|
12
|
+
export type { OrganizationContext, NestContextType } from './lib/types/nest-context-type'
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { createParamDecorator, ExecutionContext, UnauthorizedException } from '@nestjs/common'
|
|
2
|
+
import { GqlExecutionContext } from '@nestjs/graphql'
|
|
3
|
+
import { OrganizationContext } from '../types/nest-context-type'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Extract organization context from GraphQL request
|
|
7
|
+
* Throws UnauthorizedException if no organization context is found
|
|
8
|
+
*/
|
|
9
|
+
export const CtxOrganization = createParamDecorator(
|
|
10
|
+
(data: unknown, ctx: ExecutionContext): OrganizationContext => {
|
|
11
|
+
const gqlContext = GqlExecutionContext.create(ctx).getContext()
|
|
12
|
+
const organizationContext = gqlContext.req.organizationContext
|
|
13
|
+
|
|
14
|
+
if (!organizationContext) {
|
|
15
|
+
throw new UnauthorizedException(
|
|
16
|
+
'Organization context required. Please set X-Organization-ID header or ensure user has an active organization.'
|
|
17
|
+
)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return organizationContext
|
|
21
|
+
}
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Extract organization ID from context
|
|
26
|
+
* Throws UnauthorizedException if no organization context is found
|
|
27
|
+
*/
|
|
28
|
+
export const CtxOrganizationId = createParamDecorator(
|
|
29
|
+
(data: unknown, ctx: ExecutionContext): string => {
|
|
30
|
+
const gqlContext = GqlExecutionContext.create(ctx).getContext()
|
|
31
|
+
const organizationContext = gqlContext.req.organizationContext
|
|
32
|
+
|
|
33
|
+
if (!organizationContext) {
|
|
34
|
+
throw new UnauthorizedException(
|
|
35
|
+
'Organization context required. Please set X-Organization-ID header or ensure user has an active organization.'
|
|
36
|
+
)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return organizationContext.organizationId
|
|
40
|
+
}
|
|
41
|
+
)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { createParamDecorator } from '@nestjs/common'
|
|
1
|
+
import { createParamDecorator, ExecutionContext } from '@nestjs/common'
|
|
2
2
|
import { GqlExecutionContext } from '@nestjs/graphql'
|
|
3
3
|
|
|
4
|
-
export const CtxUser = createParamDecorator(
|
|
5
|
-
|
|
6
|
-
)
|
|
4
|
+
export const CtxUser = createParamDecorator((data: unknown, ctx: ExecutionContext) => {
|
|
5
|
+
return GqlExecutionContext.create(ctx).getContext().req.user
|
|
6
|
+
})
|
|
@@ -5,8 +5,6 @@ import { User } from '@<%= npmScope %>/api/core/models'
|
|
|
5
5
|
|
|
6
6
|
@Injectable()
|
|
7
7
|
export class GqlAuthAdminGuard extends AuthGuard('jwt') {
|
|
8
|
-
private readonly _roles: string[] = ['Admin']
|
|
9
|
-
|
|
10
8
|
override getRequest(context: ExecutionContext) {
|
|
11
9
|
const ctx = GqlExecutionContext.create(context)
|
|
12
10
|
|
|
@@ -22,18 +20,19 @@ export class GqlAuthAdminGuard extends AuthGuard('jwt') {
|
|
|
22
20
|
const ctx = GqlExecutionContext.create(context)
|
|
23
21
|
const req = ctx.getContext().req
|
|
24
22
|
|
|
25
|
-
if (!req
|
|
23
|
+
if (!req?.user) {
|
|
26
24
|
return false
|
|
27
25
|
}
|
|
28
26
|
const hasAccess = this.hasAccess(req.user)
|
|
29
27
|
|
|
30
28
|
if (!hasAccess) {
|
|
31
|
-
throw new ForbiddenException(`You need to have Admin access`)
|
|
29
|
+
throw new ForbiddenException(`You need to have Super Admin access`)
|
|
32
30
|
}
|
|
33
31
|
return req && req.user && this.hasAccess(req.user)
|
|
34
32
|
}
|
|
35
33
|
|
|
36
34
|
private hasAccess(user: User): boolean {
|
|
37
|
-
|
|
35
|
+
// Only super admins can access admin-protected routes
|
|
36
|
+
return !!user.isSuperAdmin
|
|
38
37
|
}
|
|
39
38
|
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { ExecutionContext, Injectable, ForbiddenException } from '@nestjs/common'
|
|
2
|
+
import { GqlExecutionContext } from '@nestjs/graphql'
|
|
3
|
+
import { AuthGuard } from '@nestjs/passport'
|
|
4
|
+
import { OrganizationContext } from '../types/nest-context-type'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Guard that requires both authentication AND organization context.
|
|
8
|
+
* Use this guard for resolvers that operate on organization-scoped data.
|
|
9
|
+
*/
|
|
10
|
+
@Injectable()
|
|
11
|
+
export class GqlOrganizationScopedGuard extends AuthGuard('jwt') {
|
|
12
|
+
override getRequest(context: ExecutionContext): any {
|
|
13
|
+
const ctx = GqlExecutionContext.create(context)
|
|
14
|
+
return ctx.getContext().req
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
override async canActivate(context: ExecutionContext): Promise<boolean> {
|
|
18
|
+
// First, run the standard JWT authentication
|
|
19
|
+
const canActivate = await super.canActivate(context)
|
|
20
|
+
if (!canActivate) {
|
|
21
|
+
return false
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Then check for organization context
|
|
25
|
+
const req = this.getRequest(context)
|
|
26
|
+
const organizationContext: OrganizationContext | undefined = req.organizationContext
|
|
27
|
+
|
|
28
|
+
if (!organizationContext) {
|
|
29
|
+
throw new ForbiddenException(
|
|
30
|
+
'Organization context is required for this operation. Please set an active organization.'
|
|
31
|
+
)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Check if organization context is complete
|
|
35
|
+
if (!organizationContext.organizationId || !organizationContext.roleId) {
|
|
36
|
+
throw new ForbiddenException(
|
|
37
|
+
'Invalid organization context. Please ensure you have a valid membership in the organization.'
|
|
38
|
+
)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return true
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { Injectable, CanActivate, ExecutionContext, ForbiddenException, SetMetadata } from '@nestjs/common'
|
|
2
|
+
import { Reflector } from '@nestjs/core'
|
|
3
|
+
import { GqlExecutionContext } from '@nestjs/graphql'
|
|
4
|
+
import { OrganizationContext } from '../types/nest-context-type'
|
|
5
|
+
|
|
6
|
+
export const PERMISSIONS_KEY = 'permissions'
|
|
7
|
+
|
|
8
|
+
export interface PermissionRequirement {
|
|
9
|
+
subject: string
|
|
10
|
+
action: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Decorator to require specific permissions for a resolver/mutation
|
|
15
|
+
* Usage: @RequirePermissions({ subject: 'organization', action: 'update' })
|
|
16
|
+
*/
|
|
17
|
+
export const RequirePermissions = (...permissions: PermissionRequirement[]) =>
|
|
18
|
+
SetMetadata(PERMISSIONS_KEY, permissions)
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Guard that checks if the user has required permissions in their current organization
|
|
22
|
+
*/
|
|
23
|
+
@Injectable()
|
|
24
|
+
export class PermissionsGuard implements CanActivate {
|
|
25
|
+
constructor(private reflector: Reflector) {}
|
|
26
|
+
|
|
27
|
+
canActivate(context: ExecutionContext): boolean {
|
|
28
|
+
const requiredPermissions = this.reflector.getAllAndOverride<PermissionRequirement[]>(
|
|
29
|
+
PERMISSIONS_KEY,
|
|
30
|
+
[context.getHandler(), context.getClass()]
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
if (!requiredPermissions || requiredPermissions.length === 0) {
|
|
34
|
+
return true
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const gqlContext = GqlExecutionContext.create(context).getContext()
|
|
38
|
+
const organizationContext: OrganizationContext | undefined = gqlContext.req.organizationContext
|
|
39
|
+
|
|
40
|
+
if (!organizationContext) {
|
|
41
|
+
throw new ForbiddenException('Organization context required for this operation')
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const hasAllPermissions = requiredPermissions.every(required =>
|
|
45
|
+
organizationContext.permissions.some(
|
|
46
|
+
p => p.subject === required.subject && p.action === required.action
|
|
47
|
+
)
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
if (!hasAllPermissions) {
|
|
51
|
+
const missingPermissions = requiredPermissions
|
|
52
|
+
.filter(
|
|
53
|
+
required =>
|
|
54
|
+
!organizationContext.permissions.some(
|
|
55
|
+
p => p.subject === required.subject && p.action === required.action
|
|
56
|
+
)
|
|
57
|
+
)
|
|
58
|
+
.map(p => `${p.subject}:${p.action}`)
|
|
59
|
+
|
|
60
|
+
throw new ForbiddenException(
|
|
61
|
+
`Missing required permissions: ${missingPermissions.join(', ')}. Current role: ${organizationContext.roleName}`
|
|
62
|
+
)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return true
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Helper function to check permissions programmatically in services
|
|
71
|
+
*/
|
|
72
|
+
export function hasPermission(
|
|
73
|
+
organizationContext: OrganizationContext | undefined,
|
|
74
|
+
subject: string,
|
|
75
|
+
action: string
|
|
76
|
+
): boolean {
|
|
77
|
+
if (!organizationContext) {
|
|
78
|
+
return false
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return organizationContext.permissions.some(
|
|
82
|
+
p => p.subject === subject && p.action === action
|
|
83
|
+
)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Helper function to require permission or throw
|
|
88
|
+
*/
|
|
89
|
+
export function requirePermission(
|
|
90
|
+
organizationContext: OrganizationContext | undefined,
|
|
91
|
+
subject: string,
|
|
92
|
+
action: string
|
|
93
|
+
): void {
|
|
94
|
+
if (!hasPermission(organizationContext, subject, action)) {
|
|
95
|
+
throw new ForbiddenException(
|
|
96
|
+
`Missing required permission: ${subject}:${action}${
|
|
97
|
+
organizationContext ? `. Current role: ${organizationContext.roleName}` : ''
|
|
98
|
+
}`
|
|
99
|
+
)
|
|
100
|
+
}
|
|
101
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { Injectable, CanActivate, ExecutionContext, HttpException, HttpStatus } from '@nestjs/common'
|
|
2
|
+
import { GqlExecutionContext } from '@nestjs/graphql'
|
|
3
|
+
import { SubscriptionStatus } from '@<%= npmScope %>/api/prisma'
|
|
4
|
+
import { ApiCoreDataAccessService } from '@<%= npmScope %>/api/core/data-access'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Subscription Guard - Protects resolvers by requiring an active subscription.
|
|
8
|
+
*/
|
|
9
|
+
@Injectable()
|
|
10
|
+
export class SubscriptionGuard implements CanActivate {
|
|
11
|
+
constructor(private readonly prisma: ApiCoreDataAccessService) {}
|
|
12
|
+
|
|
13
|
+
async canActivate(context: ExecutionContext): Promise<boolean> {
|
|
14
|
+
const ctx = GqlExecutionContext.create(context)
|
|
15
|
+
const { req } = ctx.getContext()
|
|
16
|
+
|
|
17
|
+
const user = req.user
|
|
18
|
+
if (!user) {
|
|
19
|
+
throw new HttpException('Authentication required', HttpStatus.UNAUTHORIZED)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const activeOrganizationId = user.activeOrganizationId
|
|
23
|
+
if (!activeOrganizationId) {
|
|
24
|
+
throw new HttpException(
|
|
25
|
+
'No active organization. Please select an organization.',
|
|
26
|
+
HttpStatus.FORBIDDEN,
|
|
27
|
+
)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const subscription = await this.prisma.subscription.findUnique({
|
|
31
|
+
where: { organizationId: activeOrganizationId },
|
|
32
|
+
include: { plan: true },
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
if (!subscription) {
|
|
36
|
+
throw new HttpException(
|
|
37
|
+
'No subscription found. Please subscribe to a plan to access this feature.',
|
|
38
|
+
HttpStatus.PAYMENT_REQUIRED,
|
|
39
|
+
)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const validStatuses: SubscriptionStatus[] = [
|
|
43
|
+
SubscriptionStatus.ACTIVE,
|
|
44
|
+
SubscriptionStatus.TRIALING,
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
if (!validStatuses.includes(subscription.status)) {
|
|
48
|
+
if (subscription.status === SubscriptionStatus.PAST_DUE) {
|
|
49
|
+
const gracePeriodDays = 3
|
|
50
|
+
const currentPeriodEnd = subscription.stripeCurrentPeriodEnd
|
|
51
|
+
if (currentPeriodEnd) {
|
|
52
|
+
const gracePeriodEnd = new Date(currentPeriodEnd)
|
|
53
|
+
gracePeriodEnd.setDate(gracePeriodEnd.getDate() + gracePeriodDays)
|
|
54
|
+
|
|
55
|
+
if (new Date() <= gracePeriodEnd) {
|
|
56
|
+
return true
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
throw new HttpException(
|
|
62
|
+
`Subscription is ${subscription.status.toLowerCase()}. Please update your payment method.`,
|
|
63
|
+
HttpStatus.PAYMENT_REQUIRED,
|
|
64
|
+
)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (subscription.cancelAtPeriodEnd && subscription.stripeCurrentPeriodEnd) {
|
|
68
|
+
const periodEnd = new Date(subscription.stripeCurrentPeriodEnd)
|
|
69
|
+
if (new Date() > periodEnd) {
|
|
70
|
+
throw new HttpException(
|
|
71
|
+
'Subscription has expired. Please renew your subscription.',
|
|
72
|
+
HttpStatus.PAYMENT_REQUIRED,
|
|
73
|
+
)
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return true
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import { Injectable, Logger, OnModuleInit } from '@nestjs/common'
|
|
2
|
+
import { Redis } from 'ioredis'
|
|
3
|
+
import { OrganizationContext } from '../types/nest-context-type'
|
|
4
|
+
|
|
5
|
+
const SESSION_TTL = 15 * 60 // 15 minutes
|
|
6
|
+
const MEMBERSHIP_TTL = 10 * 60 // 10 minutes
|
|
7
|
+
const USER_ORG_TTL = 10 * 60 // 10 minutes
|
|
8
|
+
|
|
9
|
+
export interface CachedSession {
|
|
10
|
+
userId: string
|
|
11
|
+
email: string
|
|
12
|
+
isSuperAdmin: boolean
|
|
13
|
+
activeOrganizationId?: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface CachedMembership {
|
|
17
|
+
organizationId: string
|
|
18
|
+
userId: string
|
|
19
|
+
roleId: string
|
|
20
|
+
roleName: string
|
|
21
|
+
permissions: Array<{ subject: string; action: string }>
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
@Injectable()
|
|
25
|
+
export class AuthCacheService implements OnModuleInit {
|
|
26
|
+
private readonly logger = new Logger(AuthCacheService.name)
|
|
27
|
+
private redis: Redis | null = null
|
|
28
|
+
private isConnected = false
|
|
29
|
+
|
|
30
|
+
async onModuleInit() {
|
|
31
|
+
const redisUrl = process.env['REDIS_URL']
|
|
32
|
+
if (!redisUrl) {
|
|
33
|
+
this.logger.warn('REDIS_URL not configured - auth caching disabled')
|
|
34
|
+
return
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
this.redis = new Redis(redisUrl, {
|
|
39
|
+
maxRetriesPerRequest: 3,
|
|
40
|
+
retryDelayOnFailover: 100,
|
|
41
|
+
lazyConnect: true,
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
this.redis.on('connect', () => {
|
|
45
|
+
this.isConnected = true
|
|
46
|
+
this.logger.log('Redis connected for auth caching')
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
this.redis.on('error', (err) => {
|
|
50
|
+
this.isConnected = false
|
|
51
|
+
this.logger.warn(`Redis error: ${err.message}`)
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
this.redis.on('close', () => {
|
|
55
|
+
this.isConnected = false
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
await this.redis.connect()
|
|
59
|
+
} catch (error) {
|
|
60
|
+
this.logger.warn(`Failed to connect to Redis: ${error}`)
|
|
61
|
+
this.redis = null
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
private get available(): boolean {
|
|
66
|
+
return this.redis !== null && this.isConnected
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Session caching
|
|
70
|
+
private sessionKey(userId: string): string {
|
|
71
|
+
return `auth:session:${userId}`
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async getSession(userId: string): Promise<CachedSession | null> {
|
|
75
|
+
if (!this.available) return null
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
const data = await this.redis!.get(this.sessionKey(userId))
|
|
79
|
+
return data ? JSON.parse(data) : null
|
|
80
|
+
} catch (error) {
|
|
81
|
+
this.logger.debug(`Cache miss for session ${userId}: ${error}`)
|
|
82
|
+
return null
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async setSession(userId: string, session: CachedSession): Promise<void> {
|
|
87
|
+
if (!this.available) return
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
await this.redis!.setex(
|
|
91
|
+
this.sessionKey(userId),
|
|
92
|
+
SESSION_TTL,
|
|
93
|
+
JSON.stringify(session)
|
|
94
|
+
)
|
|
95
|
+
} catch (error) {
|
|
96
|
+
this.logger.debug(`Failed to cache session: ${error}`)
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async invalidateSession(userId: string): Promise<void> {
|
|
101
|
+
if (!this.available) return
|
|
102
|
+
|
|
103
|
+
try {
|
|
104
|
+
await this.redis!.del(this.sessionKey(userId))
|
|
105
|
+
} catch (error) {
|
|
106
|
+
this.logger.debug(`Failed to invalidate session: ${error}`)
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Membership context caching
|
|
111
|
+
private membershipKey(userId: string, organizationId: string): string {
|
|
112
|
+
return `auth:membership:${userId}:${organizationId}`
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async getMembershipContext(
|
|
116
|
+
userId: string,
|
|
117
|
+
organizationId: string
|
|
118
|
+
): Promise<OrganizationContext | null> {
|
|
119
|
+
if (!this.available) return null
|
|
120
|
+
|
|
121
|
+
try {
|
|
122
|
+
const data = await this.redis!.get(this.membershipKey(userId, organizationId))
|
|
123
|
+
return data ? JSON.parse(data) : null
|
|
124
|
+
} catch (error) {
|
|
125
|
+
this.logger.debug(`Cache miss for membership ${userId}:${organizationId}: ${error}`)
|
|
126
|
+
return null
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async setMembershipContext(
|
|
131
|
+
userId: string,
|
|
132
|
+
organizationId: string,
|
|
133
|
+
context: OrganizationContext
|
|
134
|
+
): Promise<void> {
|
|
135
|
+
if (!this.available) return
|
|
136
|
+
|
|
137
|
+
try {
|
|
138
|
+
await this.redis!.setex(
|
|
139
|
+
this.membershipKey(userId, organizationId),
|
|
140
|
+
MEMBERSHIP_TTL,
|
|
141
|
+
JSON.stringify(context)
|
|
142
|
+
)
|
|
143
|
+
} catch (error) {
|
|
144
|
+
this.logger.debug(`Failed to cache membership context: ${error}`)
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async invalidateMembership(userId: string, organizationId: string): Promise<void> {
|
|
149
|
+
if (!this.available) return
|
|
150
|
+
|
|
151
|
+
try {
|
|
152
|
+
await this.redis!.del(this.membershipKey(userId, organizationId))
|
|
153
|
+
} catch (error) {
|
|
154
|
+
this.logger.debug(`Failed to invalidate membership: ${error}`)
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async invalidateAllMembershipsForUser(userId: string): Promise<void> {
|
|
159
|
+
if (!this.available) return
|
|
160
|
+
|
|
161
|
+
try {
|
|
162
|
+
const pattern = `auth:membership:${userId}:*`
|
|
163
|
+
const keys = await this.redis!.keys(pattern)
|
|
164
|
+
if (keys.length > 0) {
|
|
165
|
+
await this.redis!.del(...keys)
|
|
166
|
+
}
|
|
167
|
+
} catch (error) {
|
|
168
|
+
this.logger.debug(`Failed to invalidate all memberships for user: ${error}`)
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async invalidateAllMembershipsForOrganization(organizationId: string): Promise<void> {
|
|
173
|
+
if (!this.available) return
|
|
174
|
+
|
|
175
|
+
try {
|
|
176
|
+
const pattern = `auth:membership:*:${organizationId}`
|
|
177
|
+
const keys = await this.redis!.keys(pattern)
|
|
178
|
+
if (keys.length > 0) {
|
|
179
|
+
await this.redis!.del(...keys)
|
|
180
|
+
}
|
|
181
|
+
} catch (error) {
|
|
182
|
+
this.logger.debug(`Failed to invalidate all memberships for org: ${error}`)
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// User active organization caching
|
|
187
|
+
private userOrgKey(userId: string): string {
|
|
188
|
+
return `auth:user-org:${userId}`
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
async getUserActiveOrganization(userId: string): Promise<string | null> {
|
|
192
|
+
if (!this.available) return null
|
|
193
|
+
|
|
194
|
+
try {
|
|
195
|
+
return await this.redis!.get(this.userOrgKey(userId))
|
|
196
|
+
} catch (error) {
|
|
197
|
+
this.logger.debug(`Cache miss for user org ${userId}: ${error}`)
|
|
198
|
+
return null
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async setUserActiveOrganization(userId: string, organizationId: string): Promise<void> {
|
|
203
|
+
if (!this.available) return
|
|
204
|
+
|
|
205
|
+
try {
|
|
206
|
+
await this.redis!.setex(this.userOrgKey(userId), USER_ORG_TTL, organizationId)
|
|
207
|
+
} catch (error) {
|
|
208
|
+
this.logger.debug(`Failed to cache user org: ${error}`)
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
async invalidateUserActiveOrganization(userId: string): Promise<void> {
|
|
213
|
+
if (!this.available) return
|
|
214
|
+
|
|
215
|
+
try {
|
|
216
|
+
await this.redis!.del(this.userOrgKey(userId))
|
|
217
|
+
} catch (error) {
|
|
218
|
+
this.logger.debug(`Failed to invalidate user org: ${error}`)
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Role change invalidation
|
|
223
|
+
async onRolePermissionsChanged(roleId: string, organizationId: string): Promise<void> {
|
|
224
|
+
// When role permissions change, invalidate all memberships for that organization
|
|
225
|
+
await this.invalidateAllMembershipsForOrganization(organizationId)
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
async onUserRoleChanged(userId: string, organizationId: string): Promise<void> {
|
|
229
|
+
// When a user's role changes, invalidate their membership context
|
|
230
|
+
await this.invalidateMembership(userId, organizationId)
|
|
231
|
+
}
|
|
232
|
+
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { Injectable, Scope } from '@nestjs/common'
|
|
2
|
+
import DataLoader from 'dataloader'
|
|
3
|
+
import { ApiCoreDataAccessService } from '@<%= npmScope %>/api/core/data-access'
|
|
4
|
+
import { AuthCacheService } from './auth-cache.service'
|
|
5
|
+
import { OrganizationContext } from '../types/nest-context-type'
|
|
6
|
+
|
|
7
|
+
interface MembershipKey {
|
|
8
|
+
userId: string
|
|
9
|
+
organizationId: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface MembershipWithRole {
|
|
13
|
+
id: string
|
|
14
|
+
userId: string
|
|
15
|
+
organizationId: string
|
|
16
|
+
role: {
|
|
17
|
+
id: string
|
|
18
|
+
name: string
|
|
19
|
+
permissions: Array<{
|
|
20
|
+
permission: {
|
|
21
|
+
subject: string
|
|
22
|
+
action: string
|
|
23
|
+
}
|
|
24
|
+
}>
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Request-scoped DataLoader service for batching auth lookups.
|
|
30
|
+
* Uses three-tier caching: request → Redis → database
|
|
31
|
+
*/
|
|
32
|
+
@Injectable({ scope: Scope.REQUEST })
|
|
33
|
+
export class AuthLoaderService {
|
|
34
|
+
private membershipLoader: DataLoader<string, MembershipWithRole | null>
|
|
35
|
+
|
|
36
|
+
constructor(
|
|
37
|
+
private readonly prisma: ApiCoreDataAccessService,
|
|
38
|
+
private readonly authCache: AuthCacheService
|
|
39
|
+
) {
|
|
40
|
+
this.membershipLoader = new DataLoader<string, MembershipWithRole | null>(
|
|
41
|
+
async (keys) => this.batchLoadMemberships(keys as string[]),
|
|
42
|
+
{
|
|
43
|
+
cacheKeyFn: (key) => key,
|
|
44
|
+
}
|
|
45
|
+
)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
private membershipKeyToString(key: MembershipKey): string {
|
|
49
|
+
return `${key.userId}:${key.organizationId}`
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
private stringToMembershipKey(str: string): MembershipKey {
|
|
53
|
+
const [userId, organizationId] = str.split(':')
|
|
54
|
+
return { userId, organizationId }
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
private async batchLoadMemberships(
|
|
58
|
+
keys: string[]
|
|
59
|
+
): Promise<(MembershipWithRole | null)[]> {
|
|
60
|
+
const membershipKeys = keys.map((k) => this.stringToMembershipKey(k))
|
|
61
|
+
|
|
62
|
+
// Build WHERE conditions for batch query
|
|
63
|
+
const conditions = membershipKeys.map((k) => ({
|
|
64
|
+
userId: k.userId,
|
|
65
|
+
organizationId: k.organizationId,
|
|
66
|
+
}))
|
|
67
|
+
|
|
68
|
+
// Batch fetch all memberships with their roles and permissions
|
|
69
|
+
const memberships = await this.prisma.organizationMembership.findMany({
|
|
70
|
+
where: {
|
|
71
|
+
OR: conditions,
|
|
72
|
+
},
|
|
73
|
+
include: {
|
|
74
|
+
role: {
|
|
75
|
+
include: {
|
|
76
|
+
permissions: {
|
|
77
|
+
include: {
|
|
78
|
+
permission: true,
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
// Map results back to original key order
|
|
87
|
+
const membershipMap = new Map<string, MembershipWithRole>()
|
|
88
|
+
for (const membership of memberships) {
|
|
89
|
+
const key = this.membershipKeyToString({
|
|
90
|
+
userId: membership.userId,
|
|
91
|
+
organizationId: membership.organizationId,
|
|
92
|
+
})
|
|
93
|
+
membershipMap.set(key, membership as MembershipWithRole)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return keys.map((key) => membershipMap.get(key) || null)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Load membership context with three-tier caching
|
|
101
|
+
*/
|
|
102
|
+
async loadMembershipContext(
|
|
103
|
+
userId: string,
|
|
104
|
+
organizationId: string
|
|
105
|
+
): Promise<OrganizationContext | null> {
|
|
106
|
+
// Tier 1: Check Redis cache
|
|
107
|
+
const cached = await this.authCache.getMembershipContext(userId, organizationId)
|
|
108
|
+
if (cached) {
|
|
109
|
+
return cached
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Tier 2: Use DataLoader (request-scoped batch caching)
|
|
113
|
+
const key = this.membershipKeyToString({ userId, organizationId })
|
|
114
|
+
const membership = await this.membershipLoader.load(key)
|
|
115
|
+
|
|
116
|
+
if (!membership) {
|
|
117
|
+
return null
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Build organization context from membership
|
|
121
|
+
const context: OrganizationContext = {
|
|
122
|
+
organizationId: membership.organizationId,
|
|
123
|
+
userId: membership.userId,
|
|
124
|
+
roleId: membership.role.id,
|
|
125
|
+
roleName: membership.role.name,
|
|
126
|
+
permissions: membership.role.permissions.map((rp) => ({
|
|
127
|
+
subject: rp.permission.subject,
|
|
128
|
+
action: rp.permission.action,
|
|
129
|
+
})),
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Tier 3: Store in Redis for future requests
|
|
133
|
+
await this.authCache.setMembershipContext(userId, organizationId, context)
|
|
134
|
+
|
|
135
|
+
return context
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Preload multiple membership contexts (useful for batch operations)
|
|
140
|
+
*/
|
|
141
|
+
async preloadMembershipContexts(
|
|
142
|
+
keys: MembershipKey[]
|
|
143
|
+
): Promise<Map<string, OrganizationContext | null>> {
|
|
144
|
+
const results = new Map<string, OrganizationContext | null>()
|
|
145
|
+
|
|
146
|
+
await Promise.all(
|
|
147
|
+
keys.map(async (key) => {
|
|
148
|
+
const context = await this.loadMembershipContext(key.userId, key.organizationId)
|
|
149
|
+
results.set(this.membershipKeyToString(key), context)
|
|
150
|
+
})
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
return results
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Clear the request-scoped DataLoader cache
|
|
158
|
+
*/
|
|
159
|
+
clearCache(): void {
|
|
160
|
+
this.membershipLoader.clearAll()
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Factory function for creating AuthLoaderService
|
|
166
|
+
* Use this when you need to inject the service with its dependencies
|
|
167
|
+
*/
|
|
168
|
+
export const AUTH_LOADER_SERVICE = 'AUTH_LOADER_SERVICE'
|
|
169
|
+
|
|
170
|
+
export const authLoaderServiceFactory = {
|
|
171
|
+
provide: AUTH_LOADER_SERVICE,
|
|
172
|
+
useFactory: (prisma: ApiCoreDataAccessService, authCache: AuthCacheService) => {
|
|
173
|
+
return new AuthLoaderService(prisma, authCache)
|
|
174
|
+
},
|
|
175
|
+
inject: [ApiCoreDataAccessService, AuthCacheService],
|
|
176
|
+
}
|
|
@@ -1,4 +1,18 @@
|
|
|
1
|
+
import { Request, Response } from 'express'
|
|
2
|
+
import { User } from '@<%= npmScope %>/api/core/models'
|
|
3
|
+
|
|
4
|
+
export interface OrganizationContext {
|
|
5
|
+
organizationId: string
|
|
6
|
+
userId: string
|
|
7
|
+
roleId: string
|
|
8
|
+
roleName: string
|
|
9
|
+
permissions: Array<{ subject: string; action: string }>
|
|
10
|
+
}
|
|
11
|
+
|
|
1
12
|
export interface NestContextType {
|
|
2
|
-
req: Request
|
|
13
|
+
req: Request & {
|
|
14
|
+
user?: User
|
|
15
|
+
organizationContext?: OrganizationContext
|
|
16
|
+
}
|
|
3
17
|
res: Response
|
|
4
18
|
}
|