@apso/cli 0.44.0 → 0.45.1
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/dist/commands/generate.js +21 -1
- package/dist/commands/mcp/serve.js +30 -9
- package/dist/lib/generators/base.d.ts +1 -0
- package/dist/lib/generators/go.d.ts +2 -1
- package/dist/lib/generators/go.js +34 -4
- package/dist/lib/generators/python.d.ts +4 -2
- package/dist/lib/generators/python.js +28 -6
- package/dist/lib/generators/typescript.d.ts +2 -1
- package/dist/lib/generators/typescript.js +43 -6
- package/dist/lib/guards.d.ts +11 -1
- package/dist/lib/guards.js +20 -1
- package/dist/lib/templates/go/index-module.eta +6 -0
- package/dist/lib/templates/go/middleware/auth.eta +299 -59
- package/dist/lib/templates/guards/api-key-auth.guard.eta +146 -0
- package/dist/lib/templates/guards/auth.guard.eta +6 -0
- package/dist/lib/templates/guards/guards.module.eta +11 -19
- package/dist/lib/templates/guards/index.eta +2 -2
- package/dist/lib/templates/guards/jwt-auth.guard.eta +172 -0
- package/dist/lib/templates/guards/scope.guard.eta +20 -2
- package/dist/lib/templates/python/auth.eta +217 -0
- package/dist/lib/templates/python/index-module.eta +11 -0
- package/dist/lib/templates/python/models/model-col-enum.eta +1 -1
- package/dist/lib/templates/python/schemas/schema.eta +22 -1
- package/dist/lib/templates/rest/controller-rest.eta +16 -1
- package/dist/lib/templates/rest/index-module-rest.eta +6 -0
- package/dist/lib/types/auth.d.ts +41 -7
- package/dist/lib/types/auth.js +61 -2
- package/dist/lib/types/generator.d.ts +6 -0
- package/dist/lib/types/index.d.ts +1 -1
- package/dist/lib/types/index.js +3 -1
- package/dist/lib/utils/crud-deps.d.ts +13 -0
- package/dist/lib/utils/crud-deps.js +37 -1
- package/npm-shrinkwrap.json +2 -2
- package/oclif.manifest.json +1 -1
- package/package.json +1 -1
|
@@ -2,9 +2,11 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
const tslib_1 = require("tslib");
|
|
4
4
|
const core_1 = require("@oclif/core");
|
|
5
|
+
const fs = tslib_1.__importStar(require("fs"));
|
|
5
6
|
const path = tslib_1.__importStar(require("path"));
|
|
6
7
|
const inquirer_1 = tslib_1.__importDefault(require("inquirer"));
|
|
7
8
|
const lib_1 = require("../lib");
|
|
9
|
+
const types_1 = require("../lib/types");
|
|
8
10
|
const base_command_1 = tslib_1.__importDefault(require("../lib/base-command"));
|
|
9
11
|
const interactive_1 = require("../lib/utils/interactive");
|
|
10
12
|
const perf_hooks_1 = require("perf_hooks");
|
|
@@ -173,6 +175,7 @@ class Generate extends base_command_1.default {
|
|
|
173
175
|
allEntities: entities,
|
|
174
176
|
apiType: lowerCaseApiType,
|
|
175
177
|
relationshipMap,
|
|
178
|
+
auth,
|
|
176
179
|
}));
|
|
177
180
|
}
|
|
178
181
|
// eslint-disable-next-line no-await-in-loop
|
|
@@ -202,7 +205,7 @@ class Generate extends base_command_1.default {
|
|
|
202
205
|
await writeGenerated(file.path, file.content);
|
|
203
206
|
}
|
|
204
207
|
// Generate index module
|
|
205
|
-
const indexFiles = await generator.generateIndexModule(entities, lowerCaseApiType, { emitEvents, http });
|
|
208
|
+
const indexFiles = await generator.generateIndexModule(entities, lowerCaseApiType, { emitEvents, http, auth });
|
|
206
209
|
for (const file of indexFiles) {
|
|
207
210
|
// eslint-disable-next-line no-await-in-loop
|
|
208
211
|
await writeGenerated(file.path, file.content);
|
|
@@ -265,6 +268,23 @@ class Generate extends base_command_1.default {
|
|
|
265
268
|
console.log("[apso] Updated src/orm.config.ts so `apso migrate verify` and `apso test smoke` can run on a local PGlite database. Run npm install before building.");
|
|
266
269
|
}
|
|
267
270
|
}
|
|
271
|
+
// The generated auth code imports a JWT library; declare it.
|
|
272
|
+
if (auth && (0, types_1.isJwtAuth)(auth)) {
|
|
273
|
+
if (language === "typescript" &&
|
|
274
|
+
(0, crud_deps_1.addMissingDeps)(process.cwd(), crud_deps_1.JOSE_DEP).length > 0)
|
|
275
|
+
console.log("[apso] Added jose to package.json (the generated auth guard imports it). Run npm install before building.");
|
|
276
|
+
if (language === "python" &&
|
|
277
|
+
(0, crud_deps_1.addMissingPythonDep)(process.cwd(), crud_deps_1.PYJWT_DEP))
|
|
278
|
+
console.log(`[apso] Added ${crud_deps_1.PYJWT_DEP} to pyproject.toml (the generated auth module imports it). Reinstall the project's dependencies.`);
|
|
279
|
+
if (language === "go")
|
|
280
|
+
console.log("[apso] The generated auth middleware imports github.com/golang-jwt/jwt/v5 and github.com/MicahParks/keyfunc/v3. Run go mod tidy before building.");
|
|
281
|
+
}
|
|
282
|
+
if (language === "python") {
|
|
283
|
+
const main = path.join(process.cwd(), "app", "main.py");
|
|
284
|
+
if (fs.existsSync(main) &&
|
|
285
|
+
!fs.readFileSync(main, "utf8").includes("autogen_router"))
|
|
286
|
+
console.warn("[apso] app/main.py does not include autogen_router, so the generated routes are not served. Add: from app.autogen import autogen_router; app.include_router(autogen_router)");
|
|
287
|
+
}
|
|
268
288
|
const totalBuildTime = perf_hooks_1.performance.now() - totalBuildStart;
|
|
269
289
|
console.log(`[apso] Finished building all entities in ${totalBuildTime.toFixed(2)} ms`);
|
|
270
290
|
// Best-effort: install the commit co-author hook so commits that include
|
|
@@ -382,6 +382,7 @@ class McpServe extends base_command_1.default {
|
|
|
382
382
|
allEntities: entities,
|
|
383
383
|
apiType: lowerCaseApiType,
|
|
384
384
|
relationshipMap,
|
|
385
|
+
auth,
|
|
385
386
|
}),
|
|
386
387
|
generator.generateModule({
|
|
387
388
|
entity,
|
|
@@ -408,7 +409,7 @@ class McpServe extends base_command_1.default {
|
|
|
408
409
|
filesGenerated.push(file.path);
|
|
409
410
|
}
|
|
410
411
|
// Generate index module
|
|
411
|
-
const indexFiles = await generator.generateIndexModule(entities, lowerCaseApiType);
|
|
412
|
+
const indexFiles = await generator.generateIndexModule(entities, lowerCaseApiType, { auth });
|
|
412
413
|
for (const file of indexFiles) {
|
|
413
414
|
const fullPath = path.join(autogenPath, file.path);
|
|
414
415
|
// eslint-disable-next-line no-await-in-loop
|
|
@@ -450,6 +451,7 @@ class McpServe extends base_command_1.default {
|
|
|
450
451
|
"auth0",
|
|
451
452
|
"clerk",
|
|
452
453
|
"cognito",
|
|
454
|
+
"jwt",
|
|
453
455
|
"api-key",
|
|
454
456
|
"custom-db-session",
|
|
455
457
|
])
|
|
@@ -518,11 +520,12 @@ class McpServe extends base_command_1.default {
|
|
|
518
520
|
issuer: "https://YOUR_TENANT.auth0.com/",
|
|
519
521
|
audience: "https://api.yourapp.com",
|
|
520
522
|
},
|
|
523
|
+
// Auth0 custom claims must be namespaced URLs, added by a post-login Action.
|
|
521
524
|
claims: {
|
|
522
525
|
userId: "sub",
|
|
523
526
|
email: "email",
|
|
524
|
-
roles: "roles",
|
|
525
|
-
|
|
527
|
+
roles: "https://yourapp.com/roles",
|
|
528
|
+
workspaceId: "https://yourapp.com/org_id",
|
|
526
529
|
},
|
|
527
530
|
},
|
|
528
531
|
entities: [],
|
|
@@ -531,15 +534,14 @@ class McpServe extends base_command_1.default {
|
|
|
531
534
|
clerk: {
|
|
532
535
|
auth: {
|
|
533
536
|
provider: "clerk",
|
|
537
|
+
// Clerk session tokens carry no aud; the active organization is o.id / o.rol.
|
|
534
538
|
jwt: {
|
|
535
539
|
issuer: "https://YOUR_CLERK_FRONTEND_API",
|
|
536
|
-
audience: "",
|
|
537
540
|
},
|
|
538
541
|
claims: {
|
|
539
542
|
userId: "sub",
|
|
540
|
-
|
|
541
|
-
roles: "
|
|
542
|
-
organizationId: "org_id",
|
|
543
|
+
workspaceId: "o.id",
|
|
544
|
+
roles: "o.rol",
|
|
543
545
|
},
|
|
544
546
|
},
|
|
545
547
|
entities: [],
|
|
@@ -556,7 +558,26 @@ class McpServe extends base_command_1.default {
|
|
|
556
558
|
userId: "sub",
|
|
557
559
|
email: "email",
|
|
558
560
|
roles: "cognito:groups",
|
|
559
|
-
|
|
561
|
+
workspaceId: "custom:org_id",
|
|
562
|
+
},
|
|
563
|
+
},
|
|
564
|
+
entities: [],
|
|
565
|
+
relationships: [],
|
|
566
|
+
},
|
|
567
|
+
jwt: {
|
|
568
|
+
// Any issuer that publishes a JWKS (Supabase Auth, Firebase, WorkOS, Okta, Better Auth's JWT plugin).
|
|
569
|
+
auth: {
|
|
570
|
+
provider: "jwt",
|
|
571
|
+
jwt: {
|
|
572
|
+
issuer: "https://YOUR_ISSUER",
|
|
573
|
+
audience: "YOUR_AUDIENCE",
|
|
574
|
+
jwksUri: "https://YOUR_ISSUER/.well-known/jwks.json",
|
|
575
|
+
},
|
|
576
|
+
claims: {
|
|
577
|
+
userId: "sub",
|
|
578
|
+
email: "email",
|
|
579
|
+
workspaceId: "org_id",
|
|
580
|
+
roles: "roles",
|
|
560
581
|
},
|
|
561
582
|
},
|
|
562
583
|
entities: [],
|
|
@@ -601,7 +622,7 @@ class McpServe extends base_command_1.default {
|
|
|
601
622
|
content: [
|
|
602
623
|
{
|
|
603
624
|
type: "text",
|
|
604
|
-
text: `# Auth Setup: ${authProvider}\n\nMerge the following into your .apsorc file:\n\n\`\`\`json\n${JSON.stringify(config, null, 2)}\n\`\`\`\n\nAfter updating .apsorc:\n1. Run \`apso generate\` (or use the scaffold_api tool) to regenerate code\n2. Run \`npm install\` if new dependencies are needed\n3. Run \`apso dev\` to start the server and create auth tables\n\n${authProvider === "better-auth" ? "**Important:** BetterAuth stores passwords in the `account` table (not User). The `providerId` field must be set to \"credential\" for email/password login to work. Use `@apso/better-auth-adapter@latest` in your frontend." : "
|
|
625
|
+
text: `# Auth Setup: ${authProvider}\n\nMerge the following into your .apsorc file:\n\n\`\`\`json\n${JSON.stringify(config, null, 2)}\n\`\`\`\n\nAfter updating .apsorc:\n1. Run \`apso generate\` (or use the scaffold_api tool) to regenerate code\n2. Run \`npm install\` if new dependencies are needed\n3. Run \`apso dev\` to start the server and create auth tables\n\n${authProvider === "better-auth" ? "**Important:** BetterAuth stores passwords in the `account` table (not User). The `providerId` field must be set to \"credential\" for email/password login to work. Use `@apso/better-auth-adapter@latest` in your frontend." : authProvider === "api-key" ? "Store each key's hex SHA-256 in ApiKey.keyHash, never the key itself; callers send the key in the x-api-key header." : "Replace the issuer, audience and claim names with your " + authProvider + " tenant's values. The service checks each token's signature against the issuer's JWKS. See https://docs.apso.ai/develop/auth/byoa."}`,
|
|
605
626
|
},
|
|
606
627
|
],
|
|
607
628
|
};
|
|
@@ -55,6 +55,7 @@ export declare abstract class BaseGenerator implements LanguageGenerator {
|
|
|
55
55
|
abstract generateIndexModule(entities: Entity[], apiType: string, opts?: {
|
|
56
56
|
emitEvents?: boolean;
|
|
57
57
|
http?: boolean;
|
|
58
|
+
auth?: AuthConfig;
|
|
58
59
|
}): Promise<GeneratedFile[]>;
|
|
59
60
|
/**
|
|
60
61
|
* Generate guard files (auth, scope, etc.)
|
|
@@ -35,6 +35,7 @@ export declare class GoGenerator extends BaseGenerator {
|
|
|
35
35
|
generateIndexModule(entities: Entity[], _apiType: string, opts?: {
|
|
36
36
|
emitEvents?: boolean;
|
|
37
37
|
http?: boolean;
|
|
38
|
+
auth?: AuthConfig;
|
|
38
39
|
}): Promise<GeneratedFile[]>;
|
|
39
40
|
/**
|
|
40
41
|
* Emits a schema-derived manifest of the entities that opted in to
|
|
@@ -54,6 +55,6 @@ export declare class GoGenerator extends BaseGenerator {
|
|
|
54
55
|
generateDomainEvents(entities: Entity[], _apiType?: string, opts?: {
|
|
55
56
|
emitEvents?: boolean;
|
|
56
57
|
}): Promise<GeneratedFile[]>;
|
|
57
|
-
generateGuards(
|
|
58
|
+
generateGuards(entities: Entity[], auth?: AuthConfig): Promise<GeneratedFile[]>;
|
|
58
59
|
generateQueryUtils(_entities: Entity[], _apiType: string): Promise<GeneratedFile[]>;
|
|
59
60
|
}
|
|
@@ -5,6 +5,8 @@ const tslib_1 = require("tslib");
|
|
|
5
5
|
const path = tslib_1.__importStar(require("path"));
|
|
6
6
|
const Eta = tslib_1.__importStar(require("eta"));
|
|
7
7
|
const base_1 = require("./base");
|
|
8
|
+
const types_1 = require("../types");
|
|
9
|
+
const guards_1 = require("../guards");
|
|
8
10
|
const field_1 = require("../utils/field");
|
|
9
11
|
const relationships_1 = require("../utils/relationships");
|
|
10
12
|
const casing_1 = require("../utils/casing");
|
|
@@ -128,6 +130,8 @@ class GoGenerator extends base_1.BaseGenerator {
|
|
|
128
130
|
goType: this.mapFieldToGoType(field),
|
|
129
131
|
pascalName: (0, casing_1.pascalCase)(field.name),
|
|
130
132
|
snakeName: (0, casing_1.snakeCase)(field.name),
|
|
133
|
+
// The enum type the enums package declares (EntityFieldEnum).
|
|
134
|
+
enumType: field.type === "enum" ? field.dataType : undefined,
|
|
131
135
|
}));
|
|
132
136
|
}
|
|
133
137
|
async generateEntity(options) {
|
|
@@ -357,6 +361,7 @@ ${downStatements}
|
|
|
357
361
|
});
|
|
358
362
|
const content = await this.renderTemplate("./index-module", {
|
|
359
363
|
entities: entitiesData,
|
|
364
|
+
hasAuth: Boolean(opts === null || opts === void 0 ? void 0 : opts.auth),
|
|
360
365
|
});
|
|
361
366
|
// Generate model registry for AutoMigrate support
|
|
362
367
|
const modelRefs = entities
|
|
@@ -415,10 +420,35 @@ ${modelRefs}
|
|
|
415
420
|
},
|
|
416
421
|
];
|
|
417
422
|
}
|
|
418
|
-
async generateGuards(
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
423
|
+
async generateGuards(entities, auth) {
|
|
424
|
+
if (!auth)
|
|
425
|
+
return [];
|
|
426
|
+
if ((0, guards_1.hasScopedEntities)(entities))
|
|
427
|
+
console.warn("[apso] scopeBy is enforced in TypeScript services only; this Go service authenticates requests but does not filter rows by tenant");
|
|
428
|
+
let data;
|
|
429
|
+
if ((0, types_1.isJwtAuth)(auth)) {
|
|
430
|
+
data = { jwt: (0, types_1.resolveJwtAuth)(auth) };
|
|
431
|
+
}
|
|
432
|
+
else if ((0, types_1.isApiKeyAuth)(auth)) {
|
|
433
|
+
const apiKey = (0, guards_1.apiKeyTemplateData)((0, types_1.resolveApiKeyAuth)(auth), entities);
|
|
434
|
+
const entity = entities.find((e) => e.name === apiKey.apiKeyEntity);
|
|
435
|
+
// gorm's default naming: snake_case of the Go field name.
|
|
436
|
+
const column = (name) => (0, casing_1.snakeCase)(name);
|
|
437
|
+
data = {
|
|
438
|
+
apiKey: {
|
|
439
|
+
...apiKey,
|
|
440
|
+
goModel: (0, casing_1.pascalCase)(entity.name),
|
|
441
|
+
keyColumn: column(apiKey.keyField),
|
|
442
|
+
expiresColumn: column("expiresAt"),
|
|
443
|
+
workspaceColumn: column(apiKey.workspaceField),
|
|
444
|
+
roleColumn: apiKey.roleField ? column(apiKey.roleField) : undefined,
|
|
445
|
+
},
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
else {
|
|
449
|
+
throw new Error(`auth.provider "${auth.provider}" is generated for TypeScript services only. For Go, use "jwt" (Better Auth issues JWTs with its JWT plugin) or "api-key".`);
|
|
450
|
+
}
|
|
451
|
+
const content = await this.renderTemplate("./middleware/auth", data);
|
|
422
452
|
return [
|
|
423
453
|
{
|
|
424
454
|
path: "middleware/auth.go",
|
|
@@ -16,7 +16,9 @@ export declare class PythonGenerator extends BaseGenerator {
|
|
|
16
16
|
generateModule(_options: EntityGenerationOptions): Promise<GeneratedFile[]>;
|
|
17
17
|
generateMigration(options: MigrationGenerationOptions): Promise<GeneratedFile[]>;
|
|
18
18
|
generateEnums(entities: Entity[], _apiType: string): Promise<GeneratedFile[]>;
|
|
19
|
-
generateIndexModule(entities: Entity[], _apiType: string
|
|
19
|
+
generateIndexModule(entities: Entity[], _apiType: string, opts?: {
|
|
20
|
+
auth?: AuthConfig;
|
|
21
|
+
}): Promise<GeneratedFile[]>;
|
|
20
22
|
/**
|
|
21
23
|
* Emits a schema-derived manifest of the entities that opted in to
|
|
22
24
|
* domain-event emission (`.apsorc` `emitEvents`), mirroring the TypeScript
|
|
@@ -35,6 +37,6 @@ export declare class PythonGenerator extends BaseGenerator {
|
|
|
35
37
|
generateDomainEvents(entities: Entity[], _apiType?: string, opts?: {
|
|
36
38
|
emitEvents?: boolean;
|
|
37
39
|
}): Promise<GeneratedFile[]>;
|
|
38
|
-
generateGuards(
|
|
40
|
+
generateGuards(entities: Entity[], auth?: AuthConfig): Promise<GeneratedFile[]>;
|
|
39
41
|
generateQueryUtils(_entities: Entity[], _apiType: string): Promise<GeneratedFile[]>;
|
|
40
42
|
}
|
|
@@ -5,10 +5,12 @@ const tslib_1 = require("tslib");
|
|
|
5
5
|
const path = tslib_1.__importStar(require("path"));
|
|
6
6
|
const Eta = tslib_1.__importStar(require("eta"));
|
|
7
7
|
const base_1 = require("./base");
|
|
8
|
+
const types_1 = require("../types");
|
|
8
9
|
const field_1 = require("../utils/field");
|
|
9
10
|
const relationships_1 = require("../utils/relationships");
|
|
10
11
|
const casing_1 = require("../utils/casing");
|
|
11
12
|
const events_1 = require("../events");
|
|
13
|
+
const guards_1 = require("../guards");
|
|
12
14
|
const migration_file_1 = require("../ddl/migration-file");
|
|
13
15
|
const pluralize_1 = tslib_1.__importDefault(require("pluralize"));
|
|
14
16
|
/**
|
|
@@ -279,8 +281,11 @@ ${downStatements || " pass"}
|
|
|
279
281
|
},
|
|
280
282
|
];
|
|
281
283
|
}
|
|
282
|
-
async generateIndexModule(entities, _apiType) {
|
|
283
|
-
const content = await this.renderTemplate("./index-module", {
|
|
284
|
+
async generateIndexModule(entities, _apiType, opts) {
|
|
285
|
+
const content = await this.renderTemplate("./index-module", {
|
|
286
|
+
entities,
|
|
287
|
+
hasAuth: Boolean(opts === null || opts === void 0 ? void 0 : opts.auth),
|
|
288
|
+
});
|
|
284
289
|
return [
|
|
285
290
|
{
|
|
286
291
|
path: "__init__.py",
|
|
@@ -320,10 +325,27 @@ ${downStatements || " pass"}
|
|
|
320
325
|
},
|
|
321
326
|
];
|
|
322
327
|
}
|
|
323
|
-
async generateGuards(
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
328
|
+
async generateGuards(entities, auth) {
|
|
329
|
+
if (!auth)
|
|
330
|
+
return [];
|
|
331
|
+
if ((0, guards_1.hasScopedEntities)(entities))
|
|
332
|
+
console.warn("[apso] scopeBy is enforced in TypeScript services only; this Python service authenticates requests but does not filter rows by tenant");
|
|
333
|
+
let data;
|
|
334
|
+
if ((0, types_1.isJwtAuth)(auth)) {
|
|
335
|
+
data = { jwt: (0, types_1.resolveJwtAuth)(auth) };
|
|
336
|
+
}
|
|
337
|
+
else if ((0, types_1.isApiKeyAuth)(auth)) {
|
|
338
|
+
const apiKey = (0, guards_1.apiKeyTemplateData)((0, types_1.resolveApiKeyAuth)(auth), entities);
|
|
339
|
+
data = {
|
|
340
|
+
apiKey: { ...apiKey, pyModule: apiKey.apiKeyEntity.toLowerCase() },
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
else {
|
|
344
|
+
throw new Error(`auth.provider "${auth.provider}" is generated for TypeScript services only. For Python, use "jwt" (Better Auth issues JWTs with its JWT plugin) or "api-key".`);
|
|
345
|
+
}
|
|
346
|
+
return [
|
|
347
|
+
{ path: "auth.py", content: await this.renderTemplate("./auth", data) },
|
|
348
|
+
];
|
|
327
349
|
}
|
|
328
350
|
async generateQueryUtils(_entities, _apiType) {
|
|
329
351
|
const queryContent = await this.renderTemplate("./utils/query", {});
|
|
@@ -16,8 +16,9 @@ export declare class TypeScriptGenerator extends BaseGenerator {
|
|
|
16
16
|
generateModule(options: EntityGenerationOptions): Promise<GeneratedFile[]>;
|
|
17
17
|
generateMigration(_options: MigrationGenerationOptions): Promise<GeneratedFile[]>;
|
|
18
18
|
generateEnums(entities: Entity[], apiType: string): Promise<GeneratedFile[]>;
|
|
19
|
-
generateIndexModule(entities: Entity[], apiType: string,
|
|
19
|
+
generateIndexModule(entities: Entity[], apiType: string, opts?: {
|
|
20
20
|
emitEvents?: boolean;
|
|
21
|
+
auth?: AuthConfig;
|
|
21
22
|
}): Promise<GeneratedFile[]>;
|
|
22
23
|
generateGuards(entities: Entity[], auth?: AuthConfig): Promise<GeneratedFile[]>;
|
|
23
24
|
/**
|
|
@@ -109,7 +109,8 @@ class TypeScriptGenerator extends base_1.BaseGenerator {
|
|
|
109
109
|
];
|
|
110
110
|
}
|
|
111
111
|
async generateController(options) {
|
|
112
|
-
|
|
112
|
+
var _a, _b;
|
|
113
|
+
const { entity, relationships, relationshipMap, auth } = options;
|
|
113
114
|
const { name: entityName } = entity;
|
|
114
115
|
const relationshipsForTemplate = (0, relationships_1.getRelationshipForTemplate)(entityName, relationships);
|
|
115
116
|
// Handle nested relationships for joins
|
|
@@ -135,6 +136,12 @@ class TypeScriptGenerator extends base_1.BaseGenerator {
|
|
|
135
136
|
primaryKeyType,
|
|
136
137
|
associations: [...new Set(relationshipsForTemplate)],
|
|
137
138
|
nestedJoinsFile,
|
|
139
|
+
// Direct scopeBy fields @CrudAuth adds to every query once a caller is
|
|
140
|
+
// authenticated. Nested scopes (task.workspaceId) are checked per row by
|
|
141
|
+
// ScopeGuard only.
|
|
142
|
+
crudAuth: auth
|
|
143
|
+
? ((_b = (_a = (0, guards_1.getScopedEntities)([entity])[0]) === null || _a === void 0 ? void 0 : _a.scopes) !== null && _b !== void 0 ? _b : []).filter((s) => s.direct)
|
|
144
|
+
: [],
|
|
138
145
|
};
|
|
139
146
|
const controllerContent = await this.renderTemplate("./rest/controller-rest", data);
|
|
140
147
|
const specContent = await this.renderTemplate("./rest/controller-rest-spec", data);
|
|
@@ -307,8 +314,13 @@ class TypeScriptGenerator extends base_1.BaseGenerator {
|
|
|
307
314
|
},
|
|
308
315
|
];
|
|
309
316
|
}
|
|
310
|
-
async generateIndexModule(entities, apiType,
|
|
311
|
-
|
|
317
|
+
async generateIndexModule(entities, apiType, opts) {
|
|
318
|
+
// generateGuards emits GuardsModule whenever auth or scopeBy is configured;
|
|
319
|
+
// listing it here is what turns its global guards on.
|
|
320
|
+
const content = await this.renderTemplate(`./${apiType}/index-module-${apiType}`, {
|
|
321
|
+
entities,
|
|
322
|
+
hasGuards: Boolean(opts === null || opts === void 0 ? void 0 : opts.auth) || (0, guards_1.hasScopedEntities)(entities),
|
|
323
|
+
});
|
|
312
324
|
return [
|
|
313
325
|
{
|
|
314
326
|
path: "index.ts",
|
|
@@ -319,20 +331,45 @@ class TypeScriptGenerator extends base_1.BaseGenerator {
|
|
|
319
331
|
async generateGuards(entities, auth) {
|
|
320
332
|
const hasScopes = (0, guards_1.hasScopedEntities)(entities);
|
|
321
333
|
const normalizedAuth = this.normalizeAuthConfig(auth);
|
|
322
|
-
|
|
334
|
+
const jwt = auth && (0, auth_1.isJwtAuth)(auth) ? (0, auth_1.resolveJwtAuth)(auth) : undefined;
|
|
335
|
+
const apiKey = auth && (0, auth_1.isApiKeyAuth)(auth)
|
|
336
|
+
? (0, guards_1.apiKeyTemplateData)((0, auth_1.resolveApiKeyAuth)(auth), entities)
|
|
337
|
+
: undefined;
|
|
338
|
+
const hasAuth = Boolean(normalizedAuth || jwt || apiKey);
|
|
339
|
+
if (!hasScopes && !hasAuth) {
|
|
323
340
|
return [];
|
|
324
341
|
}
|
|
325
342
|
const scopedEntities = (0, guards_1.getScopedEntities)(entities);
|
|
326
343
|
const files = [];
|
|
344
|
+
const authEntities = normalizedAuth
|
|
345
|
+
? [
|
|
346
|
+
normalizedAuth.sessionEntity,
|
|
347
|
+
normalizedAuth.userEntity,
|
|
348
|
+
...(normalizedAuth.accountUserEntity
|
|
349
|
+
? [normalizedAuth.accountUserEntity]
|
|
350
|
+
: []),
|
|
351
|
+
]
|
|
352
|
+
: apiKey
|
|
353
|
+
? [apiKey.apiKeyEntity]
|
|
354
|
+
: [];
|
|
327
355
|
const templateData = {
|
|
328
356
|
scopedEntities,
|
|
329
357
|
authConfig: normalizedAuth,
|
|
358
|
+
hasAuth,
|
|
359
|
+
authProvider: auth === null || auth === void 0 ? void 0 : auth.provider,
|
|
360
|
+
authEntities,
|
|
361
|
+
jwt,
|
|
362
|
+
apiKey,
|
|
330
363
|
generatedAt: new Date().toISOString(),
|
|
331
364
|
generatedBy: "Apso CLI",
|
|
332
365
|
};
|
|
333
366
|
// Generate auth.guard.ts if auth is configured
|
|
334
|
-
if (
|
|
335
|
-
const authGuardContent = await this.renderTemplate(
|
|
367
|
+
if (hasAuth) {
|
|
368
|
+
const authGuardContent = await this.renderTemplate(jwt
|
|
369
|
+
? "./guards/jwt-auth.guard.eta"
|
|
370
|
+
: apiKey
|
|
371
|
+
? "./guards/api-key-auth.guard.eta"
|
|
372
|
+
: "./guards/auth.guard.eta", templateData);
|
|
336
373
|
files.push({
|
|
337
374
|
path: "guards/auth.guard.ts",
|
|
338
375
|
content: authGuardContent,
|
package/dist/lib/guards.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Entity } from "./types";
|
|
1
|
+
import { Entity, ResolvedApiKeyAuth } from "./types";
|
|
2
2
|
/**
|
|
3
3
|
* Represents a scope field configuration for template rendering
|
|
4
4
|
*/
|
|
@@ -28,3 +28,13 @@ export declare function getScopedEntities(entities: Entity[]): ScopedEntityConfi
|
|
|
28
28
|
* Checks if any entities have scopeBy configured.
|
|
29
29
|
*/
|
|
30
30
|
export declare function hasScopedEntities(entities: Entity[]): boolean;
|
|
31
|
+
/** API key settings plus what the key entity offers, as the guard templates use them. */
|
|
32
|
+
export interface ApiKeyTemplateData extends ResolvedApiKeyAuth {
|
|
33
|
+
hasStatus: boolean;
|
|
34
|
+
hasExpiresAt: boolean;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Check the API key entity exists and has the hash field, and note whether it
|
|
38
|
+
* has `status` and `expiresAt`, which the guard then enforces.
|
|
39
|
+
*/
|
|
40
|
+
export declare function apiKeyTemplateData(apiKey: ResolvedApiKeyAuth, entities: Entity[]): ApiKeyTemplateData;
|
package/dist/lib/guards.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.hasScopedEntities = exports.getScopedEntities = void 0;
|
|
3
|
+
exports.apiKeyTemplateData = exports.hasScopedEntities = exports.getScopedEntities = void 0;
|
|
4
4
|
const tslib_1 = require("tslib");
|
|
5
5
|
const pluralize_1 = tslib_1.__importDefault(require("pluralize"));
|
|
6
6
|
/**
|
|
@@ -62,3 +62,22 @@ function hasScopedEntities(entities) {
|
|
|
62
62
|
return entities.some((entity) => entity.scopeBy);
|
|
63
63
|
}
|
|
64
64
|
exports.hasScopedEntities = hasScopedEntities;
|
|
65
|
+
/**
|
|
66
|
+
* Check the API key entity exists and has the hash field, and note whether it
|
|
67
|
+
* has `status` and `expiresAt`, which the guard then enforces.
|
|
68
|
+
*/
|
|
69
|
+
function apiKeyTemplateData(apiKey, entities) {
|
|
70
|
+
var _a;
|
|
71
|
+
const entity = entities.find((e) => e.name === apiKey.apiKeyEntity);
|
|
72
|
+
if (!entity)
|
|
73
|
+
throw new Error(`auth.apiKeyEntity "${apiKey.apiKeyEntity}" is not an entity in .apsorc; add it with a "${apiKey.keyField}" text field`);
|
|
74
|
+
const fields = new Set(((_a = entity.fields) !== null && _a !== void 0 ? _a : []).map((f) => f.name));
|
|
75
|
+
if (!fields.has(apiKey.keyField))
|
|
76
|
+
throw new Error(`Entity ${apiKey.apiKeyEntity} needs a "${apiKey.keyField}" text field holding each key's hex SHA-256 (auth.keyField)`);
|
|
77
|
+
return {
|
|
78
|
+
...apiKey,
|
|
79
|
+
hasStatus: fields.has("status"),
|
|
80
|
+
hasExpiresAt: fields.has("expiresAt"),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
exports.apiKeyTemplateData = apiKeyTemplateData;
|
|
@@ -4,6 +4,7 @@ package routes
|
|
|
4
4
|
|
|
5
5
|
import (
|
|
6
6
|
<% if (httpEntities.length > 0) { %> "app/autogen/handlers"
|
|
7
|
+
<% } %><% if (it.hasAuth) { %> "app/autogen/middleware"
|
|
7
8
|
<% } %> "app/autogen/services"
|
|
8
9
|
"github.com/gin-gonic/gin"
|
|
9
10
|
"gorm.io/gorm"
|
|
@@ -11,6 +12,11 @@ import (
|
|
|
11
12
|
|
|
12
13
|
// RegisterAllRoutes registers all entity routes
|
|
13
14
|
func RegisterAllRoutes(r *gin.RouterGroup, db *gorm.DB) {
|
|
15
|
+
<% if (it.hasAuth) { %>
|
|
16
|
+
// Every route in this group requires authentication (.apsorc auth).
|
|
17
|
+
r.Use(middleware.AuthMiddleware(db))
|
|
18
|
+
|
|
19
|
+
<% } %>
|
|
14
20
|
<% httpEntities.forEach((entity) => { %>
|
|
15
21
|
// <%= entity.name %> routes
|
|
16
22
|
<%= entity.camelName %>Service := services.New<%= entity.name %>Service(db)
|