@ftisindia/create-app 0.1.4 → 0.1.6
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/template/.env.example +31 -0
- package/template/README.md +61 -0
- package/template/_gitignore +6 -0
- package/template/_package.json +6 -0
- package/template/docs/FORMS.md +169 -0
- package/template/docs/FORMS_CHECKLIST.md +61 -0
- package/template/docs/REPORTS.md +246 -0
- package/template/docs/REPORTS_CHECKLIST.md +97 -0
- package/template/prisma/migrations/20260612000000_add_form_builder/migration.sql +147 -0
- package/template/prisma/migrations/20260613000000_add_report_builder/migration.sql +129 -0
- package/template/prisma/schema.prisma +285 -0
- package/template/scripts/export-openapi.ts +85 -0
- package/template/scripts/gen-form.mjs +149 -0
- package/template/scripts/push-form.ts +124 -0
- package/template/src/app.module.ts +29 -8
- package/template/src/common/dto/membership-response.dto.ts +1 -0
- package/template/src/common/dto/role-summary.dto.ts +3 -3
- package/template/src/common/dto/user-summary.dto.ts +3 -3
- package/template/src/config/app.config.ts +6 -1
- package/template/src/config/env.validation.ts +45 -0
- package/template/src/config/forms.config.ts +12 -0
- package/template/src/config/index.ts +2 -0
- package/template/src/config/openapi.ts +12 -0
- package/template/src/config/reports-secret.ts +15 -0
- package/template/src/config/reports.config.ts +16 -0
- package/template/src/main.ts +16 -12
- package/template/src/modules/access-control/access-control.module.ts +2 -1
- package/template/src/modules/access-control/dto/access-control-response.dto.ts +3 -0
- package/template/src/modules/access-control/dto/current-access-control-response.dto.ts +35 -0
- package/template/src/modules/access-control/presentation/current-access-control.controller.ts +40 -0
- package/template/src/modules/access-control/types/permission-key.ts +27 -0
- package/template/src/modules/access-control/types/route-permission-registry.ts +183 -0
- package/template/src/modules/audit/dto/audit-response.dto.ts +7 -3
- package/template/src/modules/auth/auth.module.ts +3 -1
- package/template/src/modules/auth/dto/auth-response.dto.ts +1 -1
- package/template/src/modules/forms/application/services/file-gc.service.ts +85 -0
- package/template/src/modules/forms/application/services/forms-definitions.service.ts +137 -0
- package/template/src/modules/forms/application/services/forms-error.mapper.ts +64 -0
- package/template/src/modules/forms/application/services/forms-export.service.ts +210 -0
- package/template/src/modules/forms/application/services/forms-files.service.ts +164 -0
- package/template/src/modules/forms/application/services/forms-public.service.ts +49 -0
- package/template/src/modules/forms/application/services/forms-settings-reader.service.ts +53 -0
- package/template/src/modules/forms/application/services/forms-submissions.service.ts +103 -0
- package/template/src/modules/forms/application/services/handlers/authenticate.action.ts +37 -0
- package/template/src/modules/forms/application/services/handlers/logging-email.handler.ts +22 -0
- package/template/src/modules/forms/application/services/handlers/send-confirmation-email.action.ts +40 -0
- package/template/src/modules/forms/application/services/handlers/webhook.handler.ts +41 -0
- package/template/src/modules/forms/application/services/outbox-dispatcher.service.ts +109 -0
- package/template/src/modules/forms/dto/create-form-definition.dto.ts +12 -0
- package/template/src/modules/forms/dto/data-source-response.dto.ts +19 -0
- package/template/src/modules/forms/dto/export-submissions-query.dto.ts +33 -0
- package/template/src/modules/forms/dto/file-upload-response.dto.ts +24 -0
- package/template/src/modules/forms/dto/form-definition-response.dto.ts +50 -0
- package/template/src/modules/forms/dto/form-render-response.dto.ts +17 -0
- package/template/src/modules/forms/dto/list-form-definitions-query.dto.ts +10 -0
- package/template/src/modules/forms/dto/list-submissions-query.dto.ts +10 -0
- package/template/src/modules/forms/dto/public-submit-form.dto.ts +24 -0
- package/template/src/modules/forms/dto/set-public-access.dto.ts +8 -0
- package/template/src/modules/forms/dto/submission-response.dto.ts +99 -0
- package/template/src/modules/forms/dto/submit-form.dto.ts +50 -0
- package/template/src/modules/forms/dto/update-form-definition.dto.ts +12 -0
- package/template/src/modules/forms/dto/upload-file-query.dto.ts +33 -0
- package/template/src/modules/forms/dto/validate-submission.dto.ts +22 -0
- package/template/src/modules/forms/examples/abstract-submission.form.json +80 -0
- package/template/src/modules/forms/examples/login.form.json +24 -0
- package/template/src/modules/forms/examples/registration.form.json +44 -0
- package/template/src/modules/forms/forms.module.ts +226 -0
- package/template/src/modules/forms/forms.tokens.ts +6 -0
- package/template/src/modules/forms/infrastructure/audit-sink.adapter.ts +30 -0
- package/template/src/modules/forms/infrastructure/casl-forms-authorization.ts +31 -0
- package/template/src/modules/forms/infrastructure/prisma-tx-runner.ts +17 -0
- package/template/src/modules/forms/infrastructure/registry/form-extension.decorators.ts +17 -0
- package/template/src/modules/forms/infrastructure/registry/registry-bootstrap.service.ts +82 -0
- package/template/src/modules/forms/infrastructure/request-forms-context.ts +60 -0
- package/template/src/modules/forms/infrastructure/schema-check/forms-schema-check.service.ts +76 -0
- package/template/src/modules/forms/infrastructure/storage/local-disk-storage.adapter.ts +43 -0
- package/template/src/modules/forms/infrastructure/stores/index.ts +5 -0
- package/template/src/modules/forms/infrastructure/stores/prisma-action-log.store.ts +37 -0
- package/template/src/modules/forms/infrastructure/stores/prisma-file.store.ts +108 -0
- package/template/src/modules/forms/infrastructure/stores/prisma-form-definition.store.ts +147 -0
- package/template/src/modules/forms/infrastructure/stores/prisma-outbox.store.ts +133 -0
- package/template/src/modules/forms/infrastructure/stores/prisma-submission.store.ts +164 -0
- package/template/src/modules/forms/presentation/forms-data-sources.controller.ts +58 -0
- package/template/src/modules/forms/presentation/forms-definitions.controller.ts +191 -0
- package/template/src/modules/forms/presentation/forms-files.controller.ts +79 -0
- package/template/src/modules/forms/presentation/forms-submissions.controller.ts +154 -0
- package/template/src/modules/forms/presentation/forms-upload.interceptor.ts +33 -0
- package/template/src/modules/forms/presentation/public-forms.controller.ts +51 -0
- package/template/src/modules/invitations/dto/invitation-response.dto.ts +4 -0
- package/template/src/modules/organisations/application/services/organisations.service.ts +67 -1
- package/template/src/modules/organisations/dto/organisation-response.dto.ts +52 -0
- package/template/src/modules/organisations/presentation/organisations.controller.ts +25 -3
- package/template/src/modules/reports/application/services/reports-actions.service.ts +54 -0
- package/template/src/modules/reports/application/services/reports-definitions.service.ts +66 -0
- package/template/src/modules/reports/application/services/reports-error.mapper.ts +97 -0
- package/template/src/modules/reports/application/services/reports-export-dispatcher.service.ts +124 -0
- package/template/src/modules/reports/application/services/reports-exports.service.ts +74 -0
- package/template/src/modules/reports/application/services/reports-queries.service.ts +35 -0
- package/template/src/modules/reports/application/services/reports-settings-reader.service.ts +49 -0
- package/template/src/modules/reports/application/services/reports-views.service.ts +79 -0
- package/template/src/modules/reports/dto/action-result-response.dto.ts +21 -0
- package/template/src/modules/reports/dto/create-report-definition.dto.ts +86 -0
- package/template/src/modules/reports/dto/create-saved-view.dto.ts +26 -0
- package/template/src/modules/reports/dto/execute-action.dto.ts +71 -0
- package/template/src/modules/reports/dto/export-job-response.dto.ts +60 -0
- package/template/src/modules/reports/dto/export-request.dto.ts +34 -0
- package/template/src/modules/reports/dto/list-reports-query.dto.ts +10 -0
- package/template/src/modules/reports/dto/list-views-query.dto.ts +17 -0
- package/template/src/modules/reports/dto/prepare-action-response.dto.ts +14 -0
- package/template/src/modules/reports/dto/prepare-action.dto.ts +27 -0
- package/template/src/modules/reports/dto/query-response.dto.ts +64 -0
- package/template/src/modules/reports/dto/query-spec.dto.ts +120 -0
- package/template/src/modules/reports/dto/report-definition-response.dto.ts +64 -0
- package/template/src/modules/reports/dto/report-meta-query.dto.ts +16 -0
- package/template/src/modules/reports/dto/report-meta-response.dto.ts +113 -0
- package/template/src/modules/reports/dto/saved-view-response.dto.ts +66 -0
- package/template/src/modules/reports/dto/update-report-definition.dto.ts +9 -0
- package/template/src/modules/reports/dto/update-saved-view.dto.ts +27 -0
- package/template/src/modules/reports/examples/abstract-review-board.report.json +54 -0
- package/template/src/modules/reports/examples/org-members.report.json +55 -0
- package/template/src/modules/reports/infrastructure/audit-sink.adapter.ts +31 -0
- package/template/src/modules/reports/infrastructure/casl-reports-authorization.ts +39 -0
- package/template/src/modules/reports/infrastructure/forms-adapter/form-report-source.adapter.ts +292 -0
- package/template/src/modules/reports/infrastructure/forms-adapter/form-row-actions.ts +171 -0
- package/template/src/modules/reports/infrastructure/forms-adapter/forms-bridge-bootstrap.service.ts +32 -0
- package/template/src/modules/reports/infrastructure/prisma-catalog.adapter.ts +95 -0
- package/template/src/modules/reports/infrastructure/prisma-query-executor.ts +103 -0
- package/template/src/modules/reports/infrastructure/prisma-snapshot-runner.ts +47 -0
- package/template/src/modules/reports/infrastructure/prisma-tx-runner.ts +18 -0
- package/template/src/modules/reports/infrastructure/registry/registry-bootstrap.service.ts +61 -0
- package/template/src/modules/reports/infrastructure/registry/report-extension.decorators.ts +14 -0
- package/template/src/modules/reports/infrastructure/reports-job-queue.adapter.ts +28 -0
- package/template/src/modules/reports/infrastructure/request-reports-context.ts +42 -0
- package/template/src/modules/reports/infrastructure/schema-check/reports-schema-check.service.ts +116 -0
- package/template/src/modules/reports/infrastructure/storage/local-disk-export-storage.adapter.ts +79 -0
- package/template/src/modules/reports/infrastructure/stores/index.ts +5 -0
- package/template/src/modules/reports/infrastructure/stores/prisma-bulk-action-run.store.ts +89 -0
- package/template/src/modules/reports/infrastructure/stores/prisma-export-job.store.ts +93 -0
- package/template/src/modules/reports/infrastructure/stores/prisma-report-definition.store.ts +171 -0
- package/template/src/modules/reports/infrastructure/stores/prisma-row-tag.store.ts +110 -0
- package/template/src/modules/reports/infrastructure/stores/prisma-saved-view.store.ts +144 -0
- package/template/src/modules/reports/presentation/reports-actions.controller.ts +83 -0
- package/template/src/modules/reports/presentation/reports-definitions.controller.ts +156 -0
- package/template/src/modules/reports/presentation/reports-export-jobs.controller.ts +61 -0
- package/template/src/modules/reports/presentation/reports-export.controller.ts +76 -0
- package/template/src/modules/reports/presentation/reports-query.controller.ts +52 -0
- package/template/src/modules/reports/presentation/reports-views.controller.ts +140 -0
- package/template/src/modules/reports/reports-forms.module.ts +33 -0
- package/template/src/modules/reports/reports.module.ts +335 -0
- package/template/src/modules/reports/reports.tokens.ts +11 -0
- package/template/src/modules/reports/sources/org-members.source.ts +112 -0
- package/template/src/modules/settings/types/setting-definitions.ts +94 -0
- package/template/test/forms-definitions.e2e-spec.ts +394 -0
- package/template/test/forms-export.e2e-spec.ts +390 -0
- package/template/test/forms-files.e2e-spec.ts +345 -0
- package/template/test/forms-outbox.e2e-spec.ts +309 -0
- package/template/test/forms-permission-sync.spec.ts +27 -0
- package/template/test/forms-public.e2e-spec.ts +269 -0
- package/template/test/forms-schema-check.e2e-spec.ts +65 -0
- package/template/test/forms-submissions.e2e-spec.ts +500 -0
- package/template/test/forms-webhooks.e2e-spec.ts +261 -0
- package/template/test/frontend-bootstrap.spec.ts +181 -0
- package/template/test/reports-advanced.e2e-spec.ts +368 -0
- package/template/test/reports-permission-sync.spec.ts +30 -0
- package/template/test/reports-query.e2e-spec.ts +350 -0
- package/template/test/reports-tiers.e2e-spec.ts +257 -0
- package/template/test/route-registry.validator.spec.ts +34 -0
- package/template/test/security.e2e-spec.ts +134 -2
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'fs';
|
|
2
|
+
import { writeFile } from 'fs/promises';
|
|
3
|
+
import { resolve } from 'path';
|
|
4
|
+
|
|
5
|
+
loadEnvFile();
|
|
6
|
+
ensurePlaceholderEnv();
|
|
7
|
+
|
|
8
|
+
main().catch((error: unknown) => {
|
|
9
|
+
console.error(error);
|
|
10
|
+
process.exit(1);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
async function main() {
|
|
14
|
+
// Imported after env preparation so config validation sees the placeholders.
|
|
15
|
+
const { NestFactory } = await import('@nestjs/core');
|
|
16
|
+
const { SwaggerModule } = await import('@nestjs/swagger');
|
|
17
|
+
const { AppModule } = await import('../src/app.module');
|
|
18
|
+
const { buildOpenApiConfig } = await import('../src/config/openapi');
|
|
19
|
+
|
|
20
|
+
const app = await NestFactory.create(AppModule, {
|
|
21
|
+
logger: false,
|
|
22
|
+
abortOnError: false,
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
const document = SwaggerModule.createDocument(app, buildOpenApiConfig());
|
|
27
|
+
const outputPath = resolve(process.cwd(), 'docs-json.json');
|
|
28
|
+
await writeFile(outputPath, `${JSON.stringify(document, null, 2)}\n`);
|
|
29
|
+
console.log(`OpenAPI document written to ${outputPath}`);
|
|
30
|
+
} finally {
|
|
31
|
+
await app.close().catch(() => undefined);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// The document is built from route metadata only; no server starts and no
|
|
36
|
+
// database connection is made, so missing secrets get safe placeholders.
|
|
37
|
+
function ensurePlaceholderEnv() {
|
|
38
|
+
if (!process.env.DATABASE_URL) {
|
|
39
|
+
process.env.DATABASE_URL =
|
|
40
|
+
'postgresql://placeholder:placeholder@localhost:5432/placeholder';
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (!process.env.JWT_SECRET) {
|
|
44
|
+
process.env.JWT_SECRET = 'openapi-export-placeholder-secret-0123456789';
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function loadEnvFile() {
|
|
49
|
+
const envPath = resolve(process.cwd(), '.env');
|
|
50
|
+
if (!existsSync(envPath)) {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const lines = readFileSync(envPath, 'utf8').split(/\r?\n/);
|
|
55
|
+
for (const line of lines) {
|
|
56
|
+
const trimmed = line.trim();
|
|
57
|
+
if (!trimmed || trimmed.startsWith('#')) {
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const equalsIndex = trimmed.indexOf('=');
|
|
62
|
+
if (equalsIndex === -1) {
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const key = trimmed.slice(0, equalsIndex).trim();
|
|
67
|
+
const rawValue = trimmed.slice(equalsIndex + 1).trim();
|
|
68
|
+
if (!key || process.env[key] !== undefined) {
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
process.env[key] = stripQuotes(rawValue);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function stripQuotes(value: string) {
|
|
77
|
+
if (
|
|
78
|
+
(value.startsWith('"') && value.endsWith('"')) ||
|
|
79
|
+
(value.startsWith("'") && value.endsWith("'"))
|
|
80
|
+
) {
|
|
81
|
+
return value.slice(1, -1);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return value;
|
|
85
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
// Scaffold a form definition + a DB-free lint spec (definitions-as-code).
|
|
2
|
+
//
|
|
3
|
+
// npm run gen:form -- customer-feedback
|
|
4
|
+
// npm run gen:form -- customer-feedback --title "Customer feedback"
|
|
5
|
+
//
|
|
6
|
+
// Generates:
|
|
7
|
+
// src/modules/forms/definitions/<key>.form.json (starter definition)
|
|
8
|
+
// test/<key>.form.spec.ts (meta-schema + publish lint, runs in `npm test`)
|
|
9
|
+
//
|
|
10
|
+
// Load it into an organisation with:
|
|
11
|
+
// npm run forms:push -- --file src/modules/forms/definitions/<key>.form.json --org <org> --user <email> --publish
|
|
12
|
+
|
|
13
|
+
import { existsSync, mkdirSync, writeFileSync } from 'fs';
|
|
14
|
+
import { dirname, join } from 'path';
|
|
15
|
+
|
|
16
|
+
const args = process.argv.slice(2);
|
|
17
|
+
const key = args[0];
|
|
18
|
+
if (!key || key.startsWith('--')) {
|
|
19
|
+
throw new Error('Usage: npm run gen:form -- <form-key> [--title "Display title"]');
|
|
20
|
+
}
|
|
21
|
+
if (!/^[a-z0-9][a-z0-9-]*$/.test(key) || key.length > 100) {
|
|
22
|
+
throw new Error(
|
|
23
|
+
`Form key "${key}" is invalid. Use lowercase letters, digits, and hyphens (must start with a letter or digit).`,
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const titleFlag = args.indexOf('--title');
|
|
28
|
+
const title =
|
|
29
|
+
titleFlag !== -1 && args[titleFlag + 1]
|
|
30
|
+
? args[titleFlag + 1]
|
|
31
|
+
: key
|
|
32
|
+
.split('-')
|
|
33
|
+
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
34
|
+
.join(' ');
|
|
35
|
+
|
|
36
|
+
const definitionPath = join('src', 'modules', 'forms', 'definitions', `${key}.form.json`);
|
|
37
|
+
const specPath = join('test', `${key}.form.spec.ts`);
|
|
38
|
+
|
|
39
|
+
for (const path of [definitionPath, specPath]) {
|
|
40
|
+
if (existsSync(path)) {
|
|
41
|
+
throw new Error(`Refusing to overwrite existing file: ${path}`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const definition = {
|
|
46
|
+
key,
|
|
47
|
+
version: 1,
|
|
48
|
+
title,
|
|
49
|
+
fields: [
|
|
50
|
+
{
|
|
51
|
+
type: 'text',
|
|
52
|
+
name: 'title',
|
|
53
|
+
label: 'Title',
|
|
54
|
+
validators: { required: true, maxLength: 200 },
|
|
55
|
+
reportable: true,
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
type: 'text',
|
|
59
|
+
name: 'notes',
|
|
60
|
+
label: 'Notes',
|
|
61
|
+
validators: { maxLength: 2000 },
|
|
62
|
+
},
|
|
63
|
+
],
|
|
64
|
+
actions: {
|
|
65
|
+
submit: ['validateAll', 'persist'],
|
|
66
|
+
saveDraft: ['persistDraft'],
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const spec = `import { readFileSync } from 'node:fs';
|
|
71
|
+
import { join } from 'node:path';
|
|
72
|
+
import {
|
|
73
|
+
ActionRegistry,
|
|
74
|
+
DEFAULT_ORG_FORMS_POLICY,
|
|
75
|
+
DEFAULT_RULE_LIMITS,
|
|
76
|
+
DataSourceRegistry,
|
|
77
|
+
FieldTypeRegistry,
|
|
78
|
+
lintDefinition,
|
|
79
|
+
lintRules,
|
|
80
|
+
registerBuiltinActions,
|
|
81
|
+
registerCoreFieldTypes,
|
|
82
|
+
validateDefinitionShape,
|
|
83
|
+
} from '@ftisindia/form-builder';
|
|
84
|
+
import type { FormDefinition, SubmissionStore } from '@ftisindia/form-builder';
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* DB-free definition lint for ${key}.form.json — fails the unit-test run the
|
|
88
|
+
* moment the definition would be rejected at save/publish time. If your
|
|
89
|
+
* definition references custom actions or data sources, register matching
|
|
90
|
+
* stubs below (same names/kinds as the real providers).
|
|
91
|
+
*/
|
|
92
|
+
const definition = JSON.parse(
|
|
93
|
+
readFileSync(
|
|
94
|
+
join(__dirname, '..', 'src', 'modules', 'forms', 'definitions', '${key}.form.json'),
|
|
95
|
+
'utf8',
|
|
96
|
+
),
|
|
97
|
+
) as FormDefinition;
|
|
98
|
+
|
|
99
|
+
describe('${key} form definition', () => {
|
|
100
|
+
const fieldTypes = new FieldTypeRegistry();
|
|
101
|
+
registerCoreFieldTypes(fieldTypes);
|
|
102
|
+
|
|
103
|
+
const actions = new ActionRegistry();
|
|
104
|
+
registerBuiltinActions(actions, {
|
|
105
|
+
validate: () => ({ valid: true, errors: [] }),
|
|
106
|
+
submissions: {} as SubmissionStore,
|
|
107
|
+
});
|
|
108
|
+
actions.register({ name: 'sendConfirmationEmail', kind: 'post-commit', execute: async () => ({}) });
|
|
109
|
+
actions.register({
|
|
110
|
+
name: 'authenticate',
|
|
111
|
+
kind: 'transactional',
|
|
112
|
+
dangerous: true,
|
|
113
|
+
execute: async () => ({}),
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('passes the engine meta-schema', () => {
|
|
117
|
+
expect(validateDefinitionShape(definition)).toEqual([]);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('passes publish-stage linting', () => {
|
|
121
|
+
expect(
|
|
122
|
+
lintDefinition(definition, {
|
|
123
|
+
fieldTypes,
|
|
124
|
+
actions,
|
|
125
|
+
dataSources: new DataSourceRegistry(),
|
|
126
|
+
policy: { ...DEFAULT_ORG_FORMS_POLICY, allowedDangerousActions: ['authenticate'] },
|
|
127
|
+
ruleLimits: DEFAULT_RULE_LIMITS,
|
|
128
|
+
lintRules,
|
|
129
|
+
stage: 'publish',
|
|
130
|
+
canWireDangerous: true,
|
|
131
|
+
}),
|
|
132
|
+
).toEqual([]);
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
`;
|
|
136
|
+
|
|
137
|
+
mkdirSync(dirname(definitionPath), { recursive: true });
|
|
138
|
+
writeFileSync(definitionPath, `${JSON.stringify(definition, null, 2)}\n`);
|
|
139
|
+
writeFileSync(specPath, spec);
|
|
140
|
+
|
|
141
|
+
console.log(`Created ${definitionPath}`);
|
|
142
|
+
console.log(`Created ${specPath}`);
|
|
143
|
+
console.log('');
|
|
144
|
+
console.log('Next steps:');
|
|
145
|
+
console.log(' 1. Edit the definition (fields, rules, actions, settings).');
|
|
146
|
+
console.log(' 2. npm test # lint spec runs without a database');
|
|
147
|
+
console.log(
|
|
148
|
+
` 3. npm run forms:push -- --file ${definitionPath.replaceAll('\\', '/')} --org <org-id-or-slug> --user <owner-email> --publish`,
|
|
149
|
+
);
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Push a form-definition JSON file into an organisation — headless
|
|
3
|
+
* definitions-as-code, through the REAL engine path (save-time lint, RBAC,
|
|
4
|
+
* audit rows, versioning). Nothing is bypassed: the script impersonates an
|
|
5
|
+
* existing member (their CASL permissions apply; org owners pass everything).
|
|
6
|
+
*
|
|
7
|
+
* npm run forms:push -- --file src/modules/forms/definitions/my-form.form.json \
|
|
8
|
+
* --org <org-id-or-slug> --user <member-email> [--publish]
|
|
9
|
+
*
|
|
10
|
+
* Idempotent: if the file matches the latest stored version's schema, nothing
|
|
11
|
+
* new is created (with --publish, a matching DRAFT is published in place).
|
|
12
|
+
* Runs under ts-node (it bootstraps Nest — tsx drops decorator metadata).
|
|
13
|
+
*/
|
|
14
|
+
import { readFileSync } from 'node:fs';
|
|
15
|
+
import { NestFactory } from '@nestjs/core';
|
|
16
|
+
import type { FormDefinition } from '@ftisindia/form-builder';
|
|
17
|
+
import { AppModule } from '../src/app.module';
|
|
18
|
+
import { PrismaService } from '../src/database/prisma/prisma.service';
|
|
19
|
+
import { RbacCacheService } from '../src/modules/access-control/application/services/rbac-cache.service';
|
|
20
|
+
import { FormsDefinitionsService } from '../src/modules/forms/application/services/forms-definitions.service';
|
|
21
|
+
import { RequestContextService } from '../src/modules/request-context/application/services/request-context.service';
|
|
22
|
+
|
|
23
|
+
function argValue(flag: string): string | undefined {
|
|
24
|
+
const index = process.argv.indexOf(flag);
|
|
25
|
+
return index !== -1 ? process.argv[index + 1] : undefined;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function stableStringify(value: unknown): string {
|
|
29
|
+
if (Array.isArray(value)) {
|
|
30
|
+
return `[${value.map(stableStringify).join(',')}]`;
|
|
31
|
+
}
|
|
32
|
+
if (value && typeof value === 'object') {
|
|
33
|
+
const entries = Object.entries(value as Record<string, unknown>)
|
|
34
|
+
.filter(([, v]) => v !== undefined)
|
|
35
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
36
|
+
.map(([k, v]) => `${JSON.stringify(k)}:${stableStringify(v)}`);
|
|
37
|
+
return `{${entries.join(',')}}`;
|
|
38
|
+
}
|
|
39
|
+
return JSON.stringify(value);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Version/status are managed by the engine — compare the authored content only. */
|
|
43
|
+
function normalized(schema: FormDefinition): string {
|
|
44
|
+
const { version: _version, status: _status, ...rest } = schema;
|
|
45
|
+
return stableStringify(rest);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function main() {
|
|
49
|
+
const file = argValue('--file');
|
|
50
|
+
const orgRef = argValue('--org');
|
|
51
|
+
const userEmail = argValue('--user');
|
|
52
|
+
const publish = process.argv.includes('--publish');
|
|
53
|
+
if (!file || !orgRef || !userEmail) {
|
|
54
|
+
throw new Error(
|
|
55
|
+
'Usage: npm run forms:push -- --file <path> --org <org-id-or-slug> --user <member-email> [--publish]',
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const definition = JSON.parse(readFileSync(file, 'utf8')) as FormDefinition;
|
|
60
|
+
if (!definition.key) {
|
|
61
|
+
throw new Error(`${file} does not look like a form definition (missing "key").`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const app = await NestFactory.createApplicationContext(AppModule, {
|
|
65
|
+
logger: ['error', 'warn'],
|
|
66
|
+
});
|
|
67
|
+
try {
|
|
68
|
+
const prisma = app.get(PrismaService);
|
|
69
|
+
const user = await prisma.user.findFirst({ where: { email: userEmail } });
|
|
70
|
+
if (!user) {
|
|
71
|
+
throw new Error(`No user found with email "${userEmail}".`);
|
|
72
|
+
}
|
|
73
|
+
const org = await prisma.organisation.findFirst({
|
|
74
|
+
where: { OR: [{ id: orgRef }, { slug: orgRef }] },
|
|
75
|
+
});
|
|
76
|
+
if (!org) {
|
|
77
|
+
throw new Error(`No organisation found with id or slug "${orgRef}".`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Real membership + permissions — the engine's authz seam sees exactly
|
|
81
|
+
// what this user could do over HTTP.
|
|
82
|
+
const rbac = await app.get(RbacCacheService).getContext(user.id, org.id);
|
|
83
|
+
const requestContext = app.get(RequestContextService);
|
|
84
|
+
const definitions = app.get(FormsDefinitionsService);
|
|
85
|
+
|
|
86
|
+
await requestContext.run(
|
|
87
|
+
{ source: 'worker', orgId: org.id, userId: user.id, rbac },
|
|
88
|
+
async () => {
|
|
89
|
+
const latest = await prisma.formDefinition.findFirst({
|
|
90
|
+
where: { orgId: org.id, key: definition.key },
|
|
91
|
+
orderBy: { version: 'desc' },
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
if (latest && normalized(latest.schema as unknown as FormDefinition) === normalized(definition)) {
|
|
95
|
+
if (publish && latest.status === 'DRAFT') {
|
|
96
|
+
await definitions.publish(org.id, definition.key, latest.version);
|
|
97
|
+
console.log(`Unchanged content — published existing draft v${latest.version}.`);
|
|
98
|
+
} else {
|
|
99
|
+
console.log(
|
|
100
|
+
`No changes — "${definition.key}" v${latest.version} (${latest.status}) already matches ${file}.`,
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const created = await definitions.create(org.id, {
|
|
107
|
+
definition: definition as unknown as Record<string, unknown>,
|
|
108
|
+
});
|
|
109
|
+
console.log(`Created "${created.key}" v${created.version} (DRAFT).`);
|
|
110
|
+
if (publish) {
|
|
111
|
+
await definitions.publish(org.id, created.key, created.version);
|
|
112
|
+
console.log(`Published "${created.key}" v${created.version}.`);
|
|
113
|
+
}
|
|
114
|
+
},
|
|
115
|
+
);
|
|
116
|
+
} finally {
|
|
117
|
+
await app.close();
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
main().catch((error) => {
|
|
122
|
+
console.error(error instanceof Error ? error.message : error);
|
|
123
|
+
process.exit(1);
|
|
124
|
+
});
|
|
@@ -2,15 +2,26 @@ import { Module } from '@nestjs/common';
|
|
|
2
2
|
import { ConfigModule } from '@nestjs/config';
|
|
3
3
|
import { APP_GUARD } from '@nestjs/core';
|
|
4
4
|
import { ThrottlerGuard, ThrottlerModule } from '@nestjs/throttler';
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
appConfig,
|
|
7
|
+
authConfig,
|
|
8
|
+
databaseConfig,
|
|
9
|
+
formsConfig,
|
|
10
|
+
rbacConfig,
|
|
11
|
+
reportsConfig,
|
|
12
|
+
validate,
|
|
13
|
+
} from './config';
|
|
6
14
|
import { PrismaModule } from './database/prisma/prisma.module';
|
|
7
15
|
import { AccessControlModule } from './modules/access-control/access-control.module';
|
|
8
16
|
import { AuditModule } from './modules/audit/audit.module';
|
|
9
17
|
import { AuthModule } from './modules/auth/auth.module';
|
|
18
|
+
import { FormsModule } from './modules/forms/forms.module';
|
|
10
19
|
import { HealthModule } from './modules/health/health.module';
|
|
11
20
|
import { InvitationsModule } from './modules/invitations/invitations.module';
|
|
12
21
|
import { MembershipsModule } from './modules/memberships/memberships.module';
|
|
13
22
|
import { OrganisationsModule } from './modules/organisations/organisations.module';
|
|
23
|
+
import { ReportsModule } from './modules/reports/reports.module';
|
|
24
|
+
import { ReportsFormsModule } from './modules/reports/reports-forms.module';
|
|
14
25
|
import { RequestContextModule } from './modules/request-context/request-context.module';
|
|
15
26
|
import { SampleModule } from './modules/sample/sample.module';
|
|
16
27
|
import { SettingsModule } from './modules/settings/settings.module';
|
|
@@ -21,15 +32,20 @@ import { SettingsModule } from './modules/settings/settings.module';
|
|
|
21
32
|
isGlobal: true,
|
|
22
33
|
cache: true,
|
|
23
34
|
expandVariables: true,
|
|
24
|
-
load: [appConfig, authConfig, databaseConfig, rbacConfig],
|
|
35
|
+
load: [appConfig, authConfig, databaseConfig, formsConfig, rbacConfig, reportsConfig],
|
|
25
36
|
validate,
|
|
26
37
|
}),
|
|
27
|
-
ThrottlerModule.forRoot(
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
38
|
+
ThrottlerModule.forRoot({
|
|
39
|
+
throttlers: [
|
|
40
|
+
{
|
|
41
|
+
ttl: 60_000,
|
|
42
|
+
limit: 100,
|
|
43
|
+
},
|
|
44
|
+
],
|
|
45
|
+
// e2e suites create many users/submissions in seconds; rate limiting is
|
|
46
|
+
// not what they test. Production/dev behavior is unchanged.
|
|
47
|
+
skipIf: () => process.env.NODE_ENV === 'test',
|
|
48
|
+
}),
|
|
33
49
|
PrismaModule,
|
|
34
50
|
HealthModule,
|
|
35
51
|
AuthModule,
|
|
@@ -40,6 +56,11 @@ import { SettingsModule } from './modules/settings/settings.module';
|
|
|
40
56
|
MembershipsModule,
|
|
41
57
|
InvitationsModule,
|
|
42
58
|
SettingsModule,
|
|
59
|
+
FormsModule,
|
|
60
|
+
ReportsModule,
|
|
61
|
+
// Optional bridge: adds form-backed reports + delegated grid verbs. Remove
|
|
62
|
+
// this line and ReportsModule keeps working over custom sources alone.
|
|
63
|
+
ReportsFormsModule,
|
|
43
64
|
SampleModule,
|
|
44
65
|
],
|
|
45
66
|
providers: [
|
|
@@ -10,9 +10,9 @@ export class RoleSummaryDto {
|
|
|
10
10
|
@ApiProperty({ example: 'Owner' })
|
|
11
11
|
name!: string;
|
|
12
12
|
|
|
13
|
-
@ApiPropertyOptional({ example: 'Full organisation access.', nullable: true })
|
|
13
|
+
@ApiPropertyOptional({ type: String, example: 'Full organisation access.', nullable: true })
|
|
14
14
|
description?: string | null;
|
|
15
15
|
|
|
16
|
-
@
|
|
17
|
-
isSystemSeeded
|
|
16
|
+
@ApiProperty({ example: true })
|
|
17
|
+
isSystemSeeded!: boolean;
|
|
18
18
|
}
|
|
@@ -7,13 +7,13 @@ export class UserSummaryDto {
|
|
|
7
7
|
})
|
|
8
8
|
id!: string;
|
|
9
9
|
|
|
10
|
-
@ApiPropertyOptional({ example: 'owner@example.com', nullable: true })
|
|
10
|
+
@ApiPropertyOptional({ type: String, example: 'owner@example.com', nullable: true })
|
|
11
11
|
email?: string | null;
|
|
12
12
|
|
|
13
|
-
@ApiPropertyOptional({ example: '+14155552671', nullable: true })
|
|
13
|
+
@ApiPropertyOptional({ type: String, example: '+14155552671', nullable: true })
|
|
14
14
|
mobile?: string | null;
|
|
15
15
|
|
|
16
|
-
@ApiPropertyOptional({ example: 'Starter Owner', nullable: true })
|
|
16
|
+
@ApiPropertyOptional({ type: String, example: 'Starter Owner', nullable: true })
|
|
17
17
|
displayName?: string | null;
|
|
18
18
|
}
|
|
19
19
|
|
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import { registerAs } from '@nestjs/config';
|
|
2
|
-
import { getEnv } from './env.validation';
|
|
2
|
+
import { getEnv, parseCorsOrigins } from './env.validation';
|
|
3
3
|
|
|
4
4
|
export default registerAs('app', () => ({
|
|
5
5
|
nodeEnv: getEnv().NODE_ENV,
|
|
6
6
|
port: getEnv().PORT,
|
|
7
|
+
cors: {
|
|
8
|
+
enabled: getEnv().CORS_ENABLED,
|
|
9
|
+
origins: parseCorsOrigins(getEnv().CORS_ORIGINS),
|
|
10
|
+
credentials: getEnv().CORS_CREDENTIALS,
|
|
11
|
+
},
|
|
7
12
|
}));
|
|
@@ -61,10 +61,48 @@ export const envSchema = z
|
|
|
61
61
|
AUTH_FACEBOOK_ENABLED: booleanFromEnv.default(false),
|
|
62
62
|
AUTH_MOBILE_OTP_ENABLED: booleanFromEnv.default(false),
|
|
63
63
|
AUTH_MAGIC_LINK_ENABLED: booleanFromEnv.default(false),
|
|
64
|
+
CORS_ENABLED: booleanFromEnv.default(false),
|
|
65
|
+
CORS_ORIGINS: z.string().trim().optional().default(''),
|
|
66
|
+
CORS_CREDENTIALS: booleanFromEnv.default(false),
|
|
64
67
|
RBAC_CACHE_TTL_SECONDS: z.coerce.number().int().positive().default(60),
|
|
65
68
|
ORG_CONTEXT_MODE: z.literal('path').default('path'),
|
|
69
|
+
FORMS_OUTBOX_ENABLED: booleanFromEnv.default(true),
|
|
70
|
+
FORMS_OUTBOX_POLL_MS: z.coerce.number().int().positive().default(5000),
|
|
71
|
+
FORMS_FILE_GC_INTERVAL_MS: z.coerce.number().int().positive().default(3_600_000),
|
|
72
|
+
FORMS_FILE_TEMP_TTL_HOURS: z.coerce.number().int().positive().default(24),
|
|
73
|
+
FORMS_MAX_UPLOAD_MB: z.coerce.number().int().positive().default(25),
|
|
74
|
+
FORMS_FILE_STORAGE_DIR: z.string().trim().min(1).default('./var/uploads'),
|
|
75
|
+
FORMS_SCHEMA_CHECK: z.enum(['on', 'off']).default('on'),
|
|
76
|
+
REPORTS_SCHEMA_CHECK: z.enum(['on', 'off']).default('on'),
|
|
77
|
+
// HMAC secret for report cursors and bulk-action tokens (report design
|
|
78
|
+
// §5.3/§6.3). Optional: when empty, a key is derived from JWT_SECRET via
|
|
79
|
+
// HKDF so existing apps need no new mandatory env. Rotating it invalidates
|
|
80
|
+
// outstanding cursors/tokens — clients restart from page one by design.
|
|
81
|
+
REPORTS_TOKEN_SECRET: z.string().trim().optional().default(''),
|
|
82
|
+
// Reports-owned async-export worker + file storage (report design §9).
|
|
83
|
+
REPORTS_EXPORT_WORKER_ENABLED: booleanFromEnv.default(true),
|
|
84
|
+
REPORTS_EXPORT_POLL_MS: z.coerce.number().int().positive().default(5000),
|
|
85
|
+
REPORTS_EXPORT_STORAGE_DIR: z.string().trim().min(1).default('./var/report-exports'),
|
|
66
86
|
})
|
|
67
87
|
.superRefine((env, ctx) => {
|
|
88
|
+
if (env.REPORTS_TOKEN_SECRET.length > 0 && env.REPORTS_TOKEN_SECRET.length < 32) {
|
|
89
|
+
ctx.addIssue({
|
|
90
|
+
code: 'custom',
|
|
91
|
+
path: ['REPORTS_TOKEN_SECRET'],
|
|
92
|
+
message: 'REPORTS_TOKEN_SECRET must be at least 32 characters when set',
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const corsOrigins = parseCorsOrigins(env.CORS_ORIGINS);
|
|
97
|
+
|
|
98
|
+
if (env.CORS_CREDENTIALS && corsOrigins.includes('*')) {
|
|
99
|
+
ctx.addIssue({
|
|
100
|
+
code: 'custom',
|
|
101
|
+
path: ['CORS_ORIGINS'],
|
|
102
|
+
message: 'CORS_ORIGINS cannot include * when CORS_CREDENTIALS=true',
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
68
106
|
if (!env.AUTH_GOOGLE_ENABLED) {
|
|
69
107
|
return;
|
|
70
108
|
}
|
|
@@ -136,6 +174,13 @@ export function getEnv(): Env {
|
|
|
136
174
|
return validatedEnv;
|
|
137
175
|
}
|
|
138
176
|
|
|
177
|
+
export function parseCorsOrigins(value: string) {
|
|
178
|
+
return value
|
|
179
|
+
.split(',')
|
|
180
|
+
.map((origin) => origin.trim())
|
|
181
|
+
.filter(Boolean);
|
|
182
|
+
}
|
|
183
|
+
|
|
139
184
|
function requireHttpsInProduction(ctx: z.RefinementCtx, key: string, value: string) {
|
|
140
185
|
if (new URL(value).protocol === 'https:') {
|
|
141
186
|
return;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { registerAs } from '@nestjs/config';
|
|
2
|
+
import { getEnv } from './env.validation';
|
|
3
|
+
|
|
4
|
+
export default registerAs('forms', () => ({
|
|
5
|
+
outboxEnabled: getEnv().FORMS_OUTBOX_ENABLED,
|
|
6
|
+
outboxPollMs: getEnv().FORMS_OUTBOX_POLL_MS,
|
|
7
|
+
fileGcIntervalMs: getEnv().FORMS_FILE_GC_INTERVAL_MS,
|
|
8
|
+
fileTempTtlHours: getEnv().FORMS_FILE_TEMP_TTL_HOURS,
|
|
9
|
+
maxUploadMb: getEnv().FORMS_MAX_UPLOAD_MB,
|
|
10
|
+
storageDir: getEnv().FORMS_FILE_STORAGE_DIR,
|
|
11
|
+
schemaCheck: getEnv().FORMS_SCHEMA_CHECK,
|
|
12
|
+
}));
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
export { default as appConfig } from './app.config';
|
|
2
2
|
export { default as authConfig } from './auth.config';
|
|
3
3
|
export { default as databaseConfig } from './database.config';
|
|
4
|
+
export { default as formsConfig } from './forms.config';
|
|
4
5
|
export { default as rbacConfig } from './rbac.config';
|
|
6
|
+
export { default as reportsConfig } from './reports.config';
|
|
5
7
|
export { validate } from './env.validation';
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { DocumentBuilder } from '@nestjs/swagger';
|
|
2
|
+
|
|
3
|
+
export function buildOpenApiConfig() {
|
|
4
|
+
return new DocumentBuilder()
|
|
5
|
+
.setTitle('Foundation Starter API')
|
|
6
|
+
.setDescription(
|
|
7
|
+
'Modular-monolith starter API with auth, organisations, RBAC, audit, and settings.',
|
|
8
|
+
)
|
|
9
|
+
.setVersion('0.1.0')
|
|
10
|
+
.addBearerAuth()
|
|
11
|
+
.build();
|
|
12
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { hkdfSync } from 'node:crypto';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Key bytes for report cursor/action-token HMACs (report design §5.3/§6.3).
|
|
5
|
+
* Uses REPORTS_TOKEN_SECRET verbatim when configured; otherwise derives a
|
|
6
|
+
* dedicated 32-byte key from JWT_SECRET via HKDF-SHA256 so the raw JWT secret
|
|
7
|
+
* never signs report tokens directly.
|
|
8
|
+
*/
|
|
9
|
+
export function createHkdfSync(reportsSecret: string, jwtSecret: string): Uint8Array {
|
|
10
|
+
if (reportsSecret.length > 0) {
|
|
11
|
+
return new TextEncoder().encode(reportsSecret);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return new Uint8Array(hkdfSync('sha256', jwtSecret, '', 'ftis-reports-tokens', 32));
|
|
15
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { registerAs } from '@nestjs/config';
|
|
2
|
+
import { getEnv } from './env.validation';
|
|
3
|
+
import { createHkdfSync } from './reports-secret';
|
|
4
|
+
|
|
5
|
+
export default registerAs('reports', () => ({
|
|
6
|
+
schemaCheck: getEnv().REPORTS_SCHEMA_CHECK,
|
|
7
|
+
// Resolved key bytes for cursor/action-token HMACs (report design §5.3/§6.3):
|
|
8
|
+
// REPORTS_TOKEN_SECRET when set, else HKDF-derived from JWT_SECRET so the raw
|
|
9
|
+
// JWT secret never signs report tokens directly.
|
|
10
|
+
tokenSecret: createHkdfSync(getEnv().REPORTS_TOKEN_SECRET, getEnv().JWT_SECRET),
|
|
11
|
+
// Reports-owned async-export worker + file storage (report design §9) — no
|
|
12
|
+
// dependency on the forms outbox or UploadedFile machinery.
|
|
13
|
+
exportWorkerEnabled: getEnv().REPORTS_EXPORT_WORKER_ENABLED,
|
|
14
|
+
exportPollMs: getEnv().REPORTS_EXPORT_POLL_MS,
|
|
15
|
+
exportStorageDir: getEnv().REPORTS_EXPORT_STORAGE_DIR,
|
|
16
|
+
}));
|
package/template/src/main.ts
CHANGED
|
@@ -1,13 +1,27 @@
|
|
|
1
1
|
import { ValidationPipe } from '@nestjs/common';
|
|
2
2
|
import { ConfigService } from '@nestjs/config';
|
|
3
3
|
import { NestFactory } from '@nestjs/core';
|
|
4
|
-
import {
|
|
4
|
+
import { SwaggerModule } from '@nestjs/swagger';
|
|
5
5
|
import { AppModule } from './app.module';
|
|
6
6
|
import { HttpExceptionFilter } from './common/filters/http-exception.filter';
|
|
7
|
+
import { buildOpenApiConfig } from './config/openapi';
|
|
7
8
|
|
|
8
9
|
async function bootstrap() {
|
|
9
10
|
const app = await NestFactory.create(AppModule);
|
|
10
11
|
const config = app.get(ConfigService);
|
|
12
|
+
const cors = config.get<{
|
|
13
|
+
enabled: boolean;
|
|
14
|
+
origins: string[];
|
|
15
|
+
credentials: boolean;
|
|
16
|
+
}>('app.cors');
|
|
17
|
+
|
|
18
|
+
if (cors?.enabled) {
|
|
19
|
+
app.enableCors({
|
|
20
|
+
origin: cors.origins,
|
|
21
|
+
credentials: cors.credentials,
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
|
|
11
25
|
app.enableShutdownHooks();
|
|
12
26
|
app.useGlobalFilters(new HttpExceptionFilter());
|
|
13
27
|
|
|
@@ -19,17 +33,7 @@ async function bootstrap() {
|
|
|
19
33
|
}),
|
|
20
34
|
);
|
|
21
35
|
|
|
22
|
-
const document = SwaggerModule.createDocument(
|
|
23
|
-
app,
|
|
24
|
-
new DocumentBuilder()
|
|
25
|
-
.setTitle('Foundation Starter API')
|
|
26
|
-
.setDescription(
|
|
27
|
-
'Modular-monolith starter API with auth, organisations, RBAC, audit, and settings.',
|
|
28
|
-
)
|
|
29
|
-
.setVersion('0.1.0')
|
|
30
|
-
.addBearerAuth()
|
|
31
|
-
.build(),
|
|
32
|
-
);
|
|
36
|
+
const document = SwaggerModule.createDocument(app, buildOpenApiConfig());
|
|
33
37
|
SwaggerModule.setup('docs', app, document, {
|
|
34
38
|
swaggerOptions: {
|
|
35
39
|
persistAuthorization: true,
|
|
@@ -7,11 +7,12 @@ import { AccessControlService } from './application/services/access-control.serv
|
|
|
7
7
|
import { PermissionGuard } from './application/services/permission.guard';
|
|
8
8
|
import { RbacCacheService } from './application/services/rbac-cache.service';
|
|
9
9
|
import { AccessControlController } from './presentation/access-control.controller';
|
|
10
|
+
import { CurrentAccessControlController } from './presentation/current-access-control.controller';
|
|
10
11
|
|
|
11
12
|
@Global()
|
|
12
13
|
@Module({
|
|
13
14
|
imports: [AuthModule, DiscoveryModule],
|
|
14
|
-
controllers: [AccessControlController],
|
|
15
|
+
controllers: [AccessControlController, CurrentAccessControlController],
|
|
15
16
|
providers: [
|
|
16
17
|
AbilityFactory,
|
|
17
18
|
AccessControlService,
|