@ftisindia/create-app 0.1.5 → 0.2.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/template/.env.example +28 -0
- package/template/README.md +51 -0
- package/template/_gitignore +6 -0
- package/template/_package.json +10 -1
- package/template/docs/FORMS.md +188 -0
- package/template/docs/FORMS_CHECKLIST.md +69 -0
- package/template/docs/REPORTS.md +255 -0
- package/template/docs/REPORTS_CHECKLIST.md +152 -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/migrations/20260616000000_add_form_outbox_claimed_by/migration.sql +5 -0
- package/template/prisma/schema.prisma +289 -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 +30 -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/env.validation.ts +28 -0
- package/template/src/config/forms.config.ts +13 -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 +18 -0
- package/template/src/main.ts +3 -12
- 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 +5 -1
- 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-delivery.transport.ts +319 -0
- package/template/src/modules/forms/application/services/handlers/webhook.handler.ts +89 -0
- package/template/src/modules/forms/application/services/outbox-dispatcher.service.ts +131 -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 +228 -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 +156 -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/dto/organisation-response.dto.ts +1 -0
- 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 +205 -0
- package/template/src/modules/reports/application/services/reports-exports.service.ts +78 -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 +92 -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-captcha.e2e-spec.ts +163 -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 +570 -0
- package/template/test/forms-permission-sync.spec.ts +27 -0
- package/template/test/forms-public.e2e-spec.ts +293 -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-throttling.e2e-spec.ts +146 -0
- package/template/test/forms-webhooks.e2e-spec.ts +403 -0
- package/template/test/jest-e2e.json +1 -0
- package/template/test/reports-advanced.e2e-spec.ts +381 -0
- package/template/test/reports-permission-sync.spec.ts +30 -0
- package/template/test/reports-query.e2e-spec.ts +402 -0
- package/template/test/reports-tiers.e2e-spec.ts +343 -0
- package/template/test/route-registry.validator.spec.ts +22 -0
|
@@ -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,21 @@ 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
|
+
// Most e2e suites create many users/submissions in seconds; enable
|
|
46
|
+
// throttling only in the dedicated abuse-control suite.
|
|
47
|
+
skipIf: () =>
|
|
48
|
+
process.env.NODE_ENV === 'test' && process.env.E2E_THROTTLE_ENABLED !== 'true',
|
|
49
|
+
}),
|
|
33
50
|
PrismaModule,
|
|
34
51
|
HealthModule,
|
|
35
52
|
AuthModule,
|
|
@@ -40,6 +57,11 @@ import { SettingsModule } from './modules/settings/settings.module';
|
|
|
40
57
|
MembershipsModule,
|
|
41
58
|
InvitationsModule,
|
|
42
59
|
SettingsModule,
|
|
60
|
+
FormsModule,
|
|
61
|
+
ReportsModule,
|
|
62
|
+
// Optional bridge: adds form-backed reports + delegated grid verbs. Remove
|
|
63
|
+
// this line and ReportsModule keeps working over custom sources alone.
|
|
64
|
+
ReportsFormsModule,
|
|
43
65
|
SampleModule,
|
|
44
66
|
],
|
|
45
67
|
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
|
|
|
@@ -66,8 +66,36 @@ export const envSchema = z
|
|
|
66
66
|
CORS_CREDENTIALS: booleanFromEnv.default(false),
|
|
67
67
|
RBAC_CACHE_TTL_SECONDS: z.coerce.number().int().positive().default(60),
|
|
68
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_OUTBOX_HEARTBEAT_MS: z.coerce.number().int().positive().default(60_000),
|
|
72
|
+
FORMS_FILE_GC_INTERVAL_MS: z.coerce.number().int().positive().default(3_600_000),
|
|
73
|
+
FORMS_FILE_TEMP_TTL_HOURS: z.coerce.number().int().positive().default(24),
|
|
74
|
+
FORMS_MAX_UPLOAD_MB: z.coerce.number().int().positive().default(25),
|
|
75
|
+
FORMS_FILE_STORAGE_DIR: z.string().trim().min(1).default('./var/uploads'),
|
|
76
|
+
FORMS_SCHEMA_CHECK: z.enum(['on', 'off']).default('on'),
|
|
77
|
+
REPORTS_SCHEMA_CHECK: z.enum(['on', 'off']).default('on'),
|
|
78
|
+
// HMAC secret for report cursors and bulk-action tokens (report design
|
|
79
|
+
// §5.3/§6.3). Optional: when empty, a key is derived from JWT_SECRET via
|
|
80
|
+
// HKDF so existing apps need no new mandatory env. Rotating it invalidates
|
|
81
|
+
// outstanding cursors/tokens — clients restart from page one by design.
|
|
82
|
+
REPORTS_TOKEN_SECRET: z.string().trim().optional().default(''),
|
|
83
|
+
// Reports-owned async-export worker + file storage (report design §9).
|
|
84
|
+
REPORTS_EXPORT_WORKER_ENABLED: booleanFromEnv.default(true),
|
|
85
|
+
REPORTS_EXPORT_POLL_MS: z.coerce.number().int().positive().default(5000),
|
|
86
|
+
REPORTS_EXPORT_STORAGE_DIR: z.string().trim().min(1).default('./var/report-exports'),
|
|
87
|
+
REPORTS_EXPORT_RETENTION_DAYS: z.coerce.number().int().nonnegative().default(7),
|
|
88
|
+
REPORTS_EXPORT_RETENTION_SWEEP_MS: z.coerce.number().int().positive().default(3_600_000),
|
|
69
89
|
})
|
|
70
90
|
.superRefine((env, ctx) => {
|
|
91
|
+
if (env.REPORTS_TOKEN_SECRET.length > 0 && env.REPORTS_TOKEN_SECRET.length < 32) {
|
|
92
|
+
ctx.addIssue({
|
|
93
|
+
code: 'custom',
|
|
94
|
+
path: ['REPORTS_TOKEN_SECRET'],
|
|
95
|
+
message: 'REPORTS_TOKEN_SECRET must be at least 32 characters when set',
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
71
99
|
const corsOrigins = parseCorsOrigins(env.CORS_ORIGINS);
|
|
72
100
|
|
|
73
101
|
if (env.CORS_CREDENTIALS && corsOrigins.includes('*')) {
|
|
@@ -0,0 +1,13 @@
|
|
|
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
|
+
outboxHeartbeatMs: getEnv().FORMS_OUTBOX_HEARTBEAT_MS,
|
|
8
|
+
fileGcIntervalMs: getEnv().FORMS_FILE_GC_INTERVAL_MS,
|
|
9
|
+
fileTempTtlHours: getEnv().FORMS_FILE_TEMP_TTL_HOURS,
|
|
10
|
+
maxUploadMb: getEnv().FORMS_MAX_UPLOAD_MB,
|
|
11
|
+
storageDir: getEnv().FORMS_FILE_STORAGE_DIR,
|
|
12
|
+
schemaCheck: getEnv().FORMS_SCHEMA_CHECK,
|
|
13
|
+
}));
|
|
@@ -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,18 @@
|
|
|
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
|
+
exportRetentionDays: getEnv().REPORTS_EXPORT_RETENTION_DAYS,
|
|
17
|
+
exportRetentionSweepMs: getEnv().REPORTS_EXPORT_RETENTION_SWEEP_MS,
|
|
18
|
+
}));
|
package/template/src/main.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
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);
|
|
@@ -32,17 +33,7 @@ async function bootstrap() {
|
|
|
32
33
|
}),
|
|
33
34
|
);
|
|
34
35
|
|
|
35
|
-
const document = SwaggerModule.createDocument(
|
|
36
|
-
app,
|
|
37
|
-
new DocumentBuilder()
|
|
38
|
-
.setTitle('Foundation Starter API')
|
|
39
|
-
.setDescription(
|
|
40
|
-
'Modular-monolith starter API with auth, organisations, RBAC, audit, and settings.',
|
|
41
|
-
)
|
|
42
|
-
.setVersion('0.1.0')
|
|
43
|
-
.addBearerAuth()
|
|
44
|
-
.build(),
|
|
45
|
-
);
|
|
36
|
+
const document = SwaggerModule.createDocument(app, buildOpenApiConfig());
|
|
46
37
|
SwaggerModule.setup('docs', app, document, {
|
|
47
38
|
swaggerOptions: {
|
|
48
39
|
persistAuthorization: true,
|
|
@@ -20,6 +20,7 @@ export class PermissionResponseDto {
|
|
|
20
20
|
subject!: string;
|
|
21
21
|
|
|
22
22
|
@ApiPropertyOptional({
|
|
23
|
+
type: String,
|
|
23
24
|
example: 'Read organisation memberships.',
|
|
24
25
|
nullable: true,
|
|
25
26
|
})
|
|
@@ -57,6 +58,7 @@ export class RoleResponseDto {
|
|
|
57
58
|
name!: string;
|
|
58
59
|
|
|
59
60
|
@ApiPropertyOptional({
|
|
61
|
+
type: String,
|
|
60
62
|
example: 'Can review support-facing records.',
|
|
61
63
|
nullable: true,
|
|
62
64
|
})
|
|
@@ -83,6 +85,7 @@ export class RoleListResponseDto {
|
|
|
83
85
|
items!: RoleResponseDto[];
|
|
84
86
|
|
|
85
87
|
@ApiPropertyOptional({
|
|
88
|
+
type: String,
|
|
86
89
|
example: 'f602c057-04f4-4ef8-8c84-1b7c62fbf8c5',
|
|
87
90
|
format: 'uuid',
|
|
88
91
|
nullable: true,
|
|
@@ -26,6 +26,10 @@ export class CurrentAccessControlResponseDto {
|
|
|
26
26
|
@ApiProperty({ example: true })
|
|
27
27
|
isBillingContact!: boolean;
|
|
28
28
|
|
|
29
|
-
@ApiProperty({
|
|
29
|
+
@ApiProperty({
|
|
30
|
+
enum: permissionKeys,
|
|
31
|
+
isArray: true,
|
|
32
|
+
example: ['organisations.read', 'settings.read'],
|
|
33
|
+
})
|
|
30
34
|
permissionKeys!: PermissionKey[];
|
|
31
35
|
}
|
|
@@ -17,6 +17,33 @@ export const permissionKeys = [
|
|
|
17
17
|
'settings.update',
|
|
18
18
|
'audit.read',
|
|
19
19
|
'platform.admin',
|
|
20
|
+
// Form builder (@ftisindia/form-builder). Kept as literals so the as-const
|
|
21
|
+
// tuple stays narrow; test/forms-permission-sync.spec.ts asserts these match
|
|
22
|
+
// the engine's FORM_PERMISSION_KEYS export exactly.
|
|
23
|
+
'forms.read',
|
|
24
|
+
'forms.create',
|
|
25
|
+
'forms.update',
|
|
26
|
+
'forms.publish',
|
|
27
|
+
'forms.archive',
|
|
28
|
+
'forms.wireDangerous',
|
|
29
|
+
'forms.managePublicAccess',
|
|
30
|
+
'formSubmissions.read',
|
|
31
|
+
'formSubmissions.create',
|
|
32
|
+
// Row-level edits delegated from report grids (report design §6.1) — and any
|
|
33
|
+
// other surface that patches submissions through the form engine.
|
|
34
|
+
'formSubmissions.update',
|
|
35
|
+
'formSubmissions.export',
|
|
36
|
+
'formDataSources.manage',
|
|
37
|
+
// Report builder (@ftisindia/report-builder). Kept as literals so the
|
|
38
|
+
// as-const tuple stays narrow; test/reports-permission-sync.spec.ts asserts
|
|
39
|
+
// these match the engine's REPORT_PERMISSION_KEYS export exactly.
|
|
40
|
+
'reports.read',
|
|
41
|
+
'reports.create',
|
|
42
|
+
'reports.update',
|
|
43
|
+
'reports.publish',
|
|
44
|
+
'reports.archive',
|
|
45
|
+
'reports.export',
|
|
46
|
+
'reportTags.manage',
|
|
20
47
|
] as const;
|
|
21
48
|
|
|
22
49
|
export type PermissionKey = (typeof permissionKeys)[number];
|
|
@@ -126,4 +126,187 @@ export const routePermissionRegistry: RoutePermissionEntry[] = [
|
|
|
126
126
|
path: '/organisations/:orgId/sample/echo',
|
|
127
127
|
permissions: ['organisations.update'],
|
|
128
128
|
},
|
|
129
|
+
{
|
|
130
|
+
method: 'GET',
|
|
131
|
+
path: '/organisations/:orgId/forms',
|
|
132
|
+
permissions: ['forms.read'],
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
method: 'POST',
|
|
136
|
+
path: '/organisations/:orgId/forms',
|
|
137
|
+
permissions: ['forms.create'],
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
method: 'GET',
|
|
141
|
+
path: '/organisations/:orgId/forms/:formKey',
|
|
142
|
+
permissions: ['forms.read'],
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
method: 'GET',
|
|
146
|
+
path: '/organisations/:orgId/forms/:formKey/render',
|
|
147
|
+
permissions: ['formSubmissions.create'],
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
method: 'GET',
|
|
151
|
+
path: '/organisations/:orgId/forms/:formKey/versions/:version',
|
|
152
|
+
permissions: ['forms.read'],
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
method: 'PATCH',
|
|
156
|
+
path: '/organisations/:orgId/forms/:formKey/versions/:version',
|
|
157
|
+
permissions: ['forms.update'],
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
method: 'POST',
|
|
161
|
+
path: '/organisations/:orgId/forms/:formKey/versions/:version/publish',
|
|
162
|
+
permissions: ['forms.publish'],
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
method: 'POST',
|
|
166
|
+
path: '/organisations/:orgId/forms/:formKey/versions/:version/archive',
|
|
167
|
+
permissions: ['forms.archive'],
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
method: 'POST',
|
|
171
|
+
path: '/organisations/:orgId/forms/:formKey/versions/:version/public-access',
|
|
172
|
+
permissions: ['forms.managePublicAccess'],
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
method: 'POST',
|
|
176
|
+
path: '/organisations/:orgId/forms/:formKey/submissions',
|
|
177
|
+
permissions: ['formSubmissions.create'],
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
method: 'POST',
|
|
181
|
+
path: '/organisations/:orgId/forms/:formKey/submissions/validate',
|
|
182
|
+
permissions: ['formSubmissions.create'],
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
method: 'GET',
|
|
186
|
+
path: '/organisations/:orgId/forms/:formKey/submissions',
|
|
187
|
+
permissions: ['formSubmissions.read'],
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
method: 'GET',
|
|
191
|
+
path: '/organisations/:orgId/forms/:formKey/submissions/:submissionId',
|
|
192
|
+
permissions: ['formSubmissions.read'],
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
method: 'GET',
|
|
196
|
+
path: '/organisations/:orgId/forms/data-sources',
|
|
197
|
+
permissions: ['formDataSources.manage'],
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
method: 'GET',
|
|
201
|
+
path: '/organisations/:orgId/forms/:formKey/data-sources/:dataSourceKey/options',
|
|
202
|
+
permissions: ['formSubmissions.create'],
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
method: 'POST',
|
|
206
|
+
path: '/organisations/:orgId/forms/:formKey/files',
|
|
207
|
+
permissions: ['formSubmissions.create'],
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
method: 'GET',
|
|
211
|
+
path: '/organisations/:orgId/forms/:formKey/files/:fileId',
|
|
212
|
+
permissions: ['formSubmissions.read'],
|
|
213
|
+
},
|
|
214
|
+
{
|
|
215
|
+
method: 'GET',
|
|
216
|
+
path: '/organisations/:orgId/forms/:formKey/submissions/export',
|
|
217
|
+
permissions: ['formSubmissions.export'],
|
|
218
|
+
},
|
|
219
|
+
// Report builder (@ftisindia/report-builder) — report design §10. Row
|
|
220
|
+
// actions carry an additional per-action permission check inside the engine
|
|
221
|
+
// (attach-time and execute-time, §6.1); the route gate below is the base.
|
|
222
|
+
{
|
|
223
|
+
method: 'GET',
|
|
224
|
+
path: '/organisations/:orgId/reports',
|
|
225
|
+
permissions: ['reports.read'],
|
|
226
|
+
},
|
|
227
|
+
{
|
|
228
|
+
method: 'POST',
|
|
229
|
+
path: '/organisations/:orgId/reports',
|
|
230
|
+
permissions: ['reports.create'],
|
|
231
|
+
},
|
|
232
|
+
{
|
|
233
|
+
method: 'GET',
|
|
234
|
+
path: '/organisations/:orgId/reports/exports/:jobId',
|
|
235
|
+
permissions: ['reports.export'],
|
|
236
|
+
},
|
|
237
|
+
{
|
|
238
|
+
method: 'GET',
|
|
239
|
+
path: '/organisations/:orgId/reports/exports/:jobId/download',
|
|
240
|
+
permissions: ['reports.export'],
|
|
241
|
+
},
|
|
242
|
+
{
|
|
243
|
+
method: 'GET',
|
|
244
|
+
path: '/organisations/:orgId/reports/:key',
|
|
245
|
+
permissions: ['reports.read'],
|
|
246
|
+
},
|
|
247
|
+
{
|
|
248
|
+
method: 'PATCH',
|
|
249
|
+
path: '/organisations/:orgId/reports/:key',
|
|
250
|
+
permissions: ['reports.update'],
|
|
251
|
+
},
|
|
252
|
+
{
|
|
253
|
+
method: 'POST',
|
|
254
|
+
path: '/organisations/:orgId/reports/:key/publish',
|
|
255
|
+
permissions: ['reports.publish'],
|
|
256
|
+
},
|
|
257
|
+
{
|
|
258
|
+
method: 'POST',
|
|
259
|
+
path: '/organisations/:orgId/reports/:key/archive',
|
|
260
|
+
permissions: ['reports.archive'],
|
|
261
|
+
},
|
|
262
|
+
{
|
|
263
|
+
method: 'GET',
|
|
264
|
+
path: '/organisations/:orgId/reports/:key/meta',
|
|
265
|
+
permissions: ['reports.read'],
|
|
266
|
+
},
|
|
267
|
+
{
|
|
268
|
+
method: 'POST',
|
|
269
|
+
path: '/organisations/:orgId/reports/:key/query',
|
|
270
|
+
permissions: ['reports.read'],
|
|
271
|
+
},
|
|
272
|
+
{
|
|
273
|
+
method: 'GET',
|
|
274
|
+
path: '/organisations/:orgId/reports/:key/views',
|
|
275
|
+
permissions: ['reports.read'],
|
|
276
|
+
},
|
|
277
|
+
{
|
|
278
|
+
method: 'POST',
|
|
279
|
+
path: '/organisations/:orgId/reports/:key/views',
|
|
280
|
+
permissions: ['reports.read'],
|
|
281
|
+
},
|
|
282
|
+
{
|
|
283
|
+
method: 'POST',
|
|
284
|
+
path: '/organisations/:orgId/reports/:key/views/shared',
|
|
285
|
+
permissions: ['reports.update'],
|
|
286
|
+
},
|
|
287
|
+
{
|
|
288
|
+
method: 'PATCH',
|
|
289
|
+
path: '/organisations/:orgId/reports/:key/views/:viewId',
|
|
290
|
+
permissions: ['reports.read'],
|
|
291
|
+
},
|
|
292
|
+
{
|
|
293
|
+
method: 'DELETE',
|
|
294
|
+
path: '/organisations/:orgId/reports/:key/views/:viewId',
|
|
295
|
+
permissions: ['reports.read'],
|
|
296
|
+
},
|
|
297
|
+
{
|
|
298
|
+
method: 'POST',
|
|
299
|
+
path: '/organisations/:orgId/reports/:key/actions/:name/prepare',
|
|
300
|
+
permissions: ['reports.read'],
|
|
301
|
+
},
|
|
302
|
+
{
|
|
303
|
+
method: 'POST',
|
|
304
|
+
path: '/organisations/:orgId/reports/:key/actions/:name',
|
|
305
|
+
permissions: ['reports.read'],
|
|
306
|
+
},
|
|
307
|
+
{
|
|
308
|
+
method: 'POST',
|
|
309
|
+
path: '/organisations/:orgId/reports/:key/export',
|
|
310
|
+
permissions: ['reports.export'],
|
|
311
|
+
},
|
|
129
312
|
];
|