@ftisindia/create-app 0.1.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.
Files changed (151) hide show
  1. package/LICENSE +144 -0
  2. package/bin/index.mjs +2 -0
  3. package/package.json +36 -0
  4. package/src/copy.mjs +45 -0
  5. package/src/log.mjs +23 -0
  6. package/src/main.mjs +221 -0
  7. package/src/run.mjs +52 -0
  8. package/src/substitute.mjs +40 -0
  9. package/template/.env.example +36 -0
  10. package/template/.github/workflows/ci.yml +36 -0
  11. package/template/.husky/pre-commit +1 -0
  12. package/template/README.md +146 -0
  13. package/template/_editorconfig +8 -0
  14. package/template/_gitignore +7 -0
  15. package/template/_nvmrc +1 -0
  16. package/template/_package.json +107 -0
  17. package/template/_prettierignore +5 -0
  18. package/template/_prettierrc +6 -0
  19. package/template/docs/API_REFERENCE.md +123 -0
  20. package/template/docs/GETTING_STARTED.md +65 -0
  21. package/template/docs/MODULE_COMPLETION_CHECKLIST.md +40 -0
  22. package/template/docs/OAUTH.md +46 -0
  23. package/template/docs/SAMPLE_MODULE.md +23 -0
  24. package/template/docs/api.http +269 -0
  25. package/template/eslint.config.mjs +51 -0
  26. package/template/nest-cli.json +8 -0
  27. package/template/prisma/migrations/20260530000000_init/migration.sql +248 -0
  28. package/template/prisma/schema.prisma +299 -0
  29. package/template/prisma/seed.ts +44 -0
  30. package/template/scripts/db-create.mjs +126 -0
  31. package/template/scripts/gen-module.mjs +217 -0
  32. package/template/scripts/seed-test-user-org.ts +264 -0
  33. package/template/scripts/setup-local.mjs +224 -0
  34. package/template/scripts/test-db.mjs +69 -0
  35. package/template/src/app.module.ts +58 -0
  36. package/template/src/common/decorators/.gitkeep +1 -0
  37. package/template/src/common/dto/error-response.dto.ts +17 -0
  38. package/template/src/common/dto/membership-response.dto.ts +51 -0
  39. package/template/src/common/dto/mutation-response.dto.ts +11 -0
  40. package/template/src/common/dto/role-summary.dto.ts +18 -0
  41. package/template/src/common/dto/user-summary.dto.ts +23 -0
  42. package/template/src/common/enums/.gitkeep +1 -0
  43. package/template/src/common/filters/.gitkeep +1 -0
  44. package/template/src/common/filters/http-exception.filter.ts +78 -0
  45. package/template/src/common/guards/.gitkeep +1 -0
  46. package/template/src/common/interceptors/.gitkeep +1 -0
  47. package/template/src/common/pipes/.gitkeep +1 -0
  48. package/template/src/common/swagger/api-error-responses.ts +54 -0
  49. package/template/src/common/types/.gitkeep +1 -0
  50. package/template/src/config/app.config.ts +7 -0
  51. package/template/src/config/auth.config.ts +33 -0
  52. package/template/src/config/database.config.ts +6 -0
  53. package/template/src/config/env.validation.ts +131 -0
  54. package/template/src/config/index.ts +5 -0
  55. package/template/src/config/rbac.config.ts +6 -0
  56. package/template/src/database/prisma/prisma-transaction.ts +22 -0
  57. package/template/src/database/prisma/prisma.module.ts +9 -0
  58. package/template/src/database/prisma/prisma.service.ts +16 -0
  59. package/template/src/main.ts +42 -0
  60. package/template/src/modules/access-control/access-control.module.ts +24 -0
  61. package/template/src/modules/access-control/application/route-registry.validator.ts +289 -0
  62. package/template/src/modules/access-control/application/services/ability.factory.ts +28 -0
  63. package/template/src/modules/access-control/application/services/access-control.service.ts +478 -0
  64. package/template/src/modules/access-control/application/services/permission.guard.ts +77 -0
  65. package/template/src/modules/access-control/application/services/rbac-cache.service.ts +148 -0
  66. package/template/src/modules/access-control/dto/access-control-response.dto.ts +79 -0
  67. package/template/src/modules/access-control/dto/create-role.dto.ts +18 -0
  68. package/template/src/modules/access-control/dto/update-role-permissions.dto.ts +23 -0
  69. package/template/src/modules/access-control/dto/update-role.dto.ts +19 -0
  70. package/template/src/modules/access-control/presentation/access-control.controller.ts +157 -0
  71. package/template/src/modules/access-control/presentation/permissions.decorator.ts +8 -0
  72. package/template/src/modules/access-control/presentation/public.decorator.ts +7 -0
  73. package/template/src/modules/access-control/types/permission-key.ts +37 -0
  74. package/template/src/modules/access-control/types/rbac-context.ts +11 -0
  75. package/template/src/modules/access-control/types/route-permission-registry.ts +129 -0
  76. package/template/src/modules/audit/application/services/audit.service.ts +97 -0
  77. package/template/src/modules/audit/audit.module.ts +13 -0
  78. package/template/src/modules/audit/dto/audit-response.dto.ts +75 -0
  79. package/template/src/modules/audit/dto/list-audit-logs-query.dto.ts +75 -0
  80. package/template/src/modules/audit/presentation/audit.controller.ts +37 -0
  81. package/template/src/modules/auth/application/services/auth.service.ts +509 -0
  82. package/template/src/modules/auth/application/services/password.service.ts +15 -0
  83. package/template/src/modules/auth/application/services/token.service.ts +95 -0
  84. package/template/src/modules/auth/auth.module.ts +73 -0
  85. package/template/src/modules/auth/dto/auth-response.dto.ts +29 -0
  86. package/template/src/modules/auth/dto/login.dto.ts +15 -0
  87. package/template/src/modules/auth/dto/logout.dto.ts +3 -0
  88. package/template/src/modules/auth/dto/oauth-exchange.dto.ts +15 -0
  89. package/template/src/modules/auth/dto/refresh-token.dto.ts +14 -0
  90. package/template/src/modules/auth/dto/signup.dto.ts +27 -0
  91. package/template/src/modules/auth/infrastructure/passport/google-auth.guard.ts +27 -0
  92. package/template/src/modules/auth/infrastructure/passport/google.strategy.ts +56 -0
  93. package/template/src/modules/auth/infrastructure/passport/jwt-auth.guard.ts +5 -0
  94. package/template/src/modules/auth/infrastructure/passport/jwt.strategy.ts +43 -0
  95. package/template/src/modules/auth/presentation/auth.controller.ts +148 -0
  96. package/template/src/modules/auth/presentation/current-user.decorator.ts +11 -0
  97. package/template/src/modules/auth/presentation/google-oauth-exception.filter.ts +33 -0
  98. package/template/src/modules/auth/types/authenticated-user.ts +7 -0
  99. package/template/src/modules/auth/types/google-auth-profile.ts +6 -0
  100. package/template/src/modules/auth/types/jwt-payload.ts +5 -0
  101. package/template/src/modules/health/dto/health-response.dto.ts +9 -0
  102. package/template/src/modules/health/health.module.ts +7 -0
  103. package/template/src/modules/health/presentation/health.controller.ts +33 -0
  104. package/template/src/modules/invitations/application/services/invitations.service.ts +967 -0
  105. package/template/src/modules/invitations/dto/accept-invitation.dto.ts +24 -0
  106. package/template/src/modules/invitations/dto/create-invitation.dto.ts +100 -0
  107. package/template/src/modules/invitations/dto/invitation-response.dto.ts +108 -0
  108. package/template/src/modules/invitations/dto/invitation-token.dto.ts +15 -0
  109. package/template/src/modules/invitations/invitations.module.ts +12 -0
  110. package/template/src/modules/invitations/presentation/invitations.controller.ts +149 -0
  111. package/template/src/modules/memberships/application/services/memberships.service.ts +455 -0
  112. package/template/src/modules/memberships/dto/transfer-owner.dto.ts +11 -0
  113. package/template/src/modules/memberships/dto/update-billing-contact.dto.ts +8 -0
  114. package/template/src/modules/memberships/dto/update-membership-owner.dto.ts +8 -0
  115. package/template/src/modules/memberships/dto/update-membership-role.dto.ts +11 -0
  116. package/template/src/modules/memberships/dto/update-membership-status.dto.ts +9 -0
  117. package/template/src/modules/memberships/memberships.module.ts +12 -0
  118. package/template/src/modules/memberships/presentation/memberships.controller.ts +193 -0
  119. package/template/src/modules/organisations/application/services/organisations.service.ts +147 -0
  120. package/template/src/modules/organisations/dto/create-organisation.dto.ts +32 -0
  121. package/template/src/modules/organisations/dto/organisation-response.dto.ts +62 -0
  122. package/template/src/modules/organisations/infrastructure/repositories/organisations.repository.ts +24 -0
  123. package/template/src/modules/organisations/organisations.module.ts +12 -0
  124. package/template/src/modules/organisations/presentation/organisations.controller.ts +37 -0
  125. package/template/src/modules/organisations/types/default-organisation-data.ts +18 -0
  126. package/template/src/modules/platform-admin/.gitkeep +1 -0
  127. package/template/src/modules/request-context/application/services/request-context.service.ts +79 -0
  128. package/template/src/modules/request-context/presentation/org-scope.guard.ts +26 -0
  129. package/template/src/modules/request-context/presentation/request-context.interceptor.ts +26 -0
  130. package/template/src/modules/request-context/presentation/request-context.middleware.ts +31 -0
  131. package/template/src/modules/request-context/request-context.module.ts +25 -0
  132. package/template/src/modules/request-context/types/request-context.ts +29 -0
  133. package/template/src/modules/sample/application/services/sample.service.ts +67 -0
  134. package/template/src/modules/sample/dto/sample-echo.dto.ts +10 -0
  135. package/template/src/modules/sample/dto/sample-response.dto.ts +41 -0
  136. package/template/src/modules/sample/presentation/sample.controller.ts +63 -0
  137. package/template/src/modules/sample/sample.module.ts +11 -0
  138. package/template/src/modules/settings/application/services/settings.service.ts +139 -0
  139. package/template/src/modules/settings/dto/setting-response.dto.ts +27 -0
  140. package/template/src/modules/settings/dto/update-setting.dto.ts +16 -0
  141. package/template/src/modules/settings/presentation/settings.controller.ts +66 -0
  142. package/template/src/modules/settings/settings.module.ts +12 -0
  143. package/template/src/modules/settings/types/setting-definitions.ts +104 -0
  144. package/template/src/modules/users/.gitkeep +1 -0
  145. package/template/test/.gitkeep +1 -0
  146. package/template/test/jest-e2e.json +9 -0
  147. package/template/test/permission.guard.spec.ts +22 -0
  148. package/template/test/route-registry.validator.spec.ts +90 -0
  149. package/template/test/security.e2e-spec.ts +102 -0
  150. package/template/tsconfig.build.json +4 -0
  151. package/template/tsconfig.json +18 -0
@@ -0,0 +1,289 @@
1
+ import {
2
+ Injectable,
3
+ OnApplicationBootstrap,
4
+ RequestMethod,
5
+ } from "@nestjs/common";
6
+ import { METHOD_METADATA, PATH_METADATA } from "@nestjs/common/constants";
7
+ import { DiscoveryService, MetadataScanner, Reflector } from "@nestjs/core";
8
+ import { REQUIRED_PERMISSIONS_KEY } from "../presentation/permissions.decorator";
9
+ import {
10
+ PermissionKey,
11
+ permissionKeys,
12
+ RoutePermissionEntry,
13
+ } from "../types/permission-key";
14
+ import { routePermissionRegistry } from "../types/route-permission-registry";
15
+
16
+ type ControllerInstance = Record<string, unknown>;
17
+ type RouteHandler = (...args: unknown[]) => unknown;
18
+ type RoutePermissionDiff = {
19
+ key: string;
20
+ registryPermissions: PermissionKey[];
21
+ enforcedPermissions: PermissionKey[];
22
+ };
23
+
24
+ @Injectable()
25
+ export class RouteRegistryValidator implements OnApplicationBootstrap {
26
+ constructor(
27
+ private readonly discovery: DiscoveryService,
28
+ private readonly metadataScanner: MetadataScanner,
29
+ private readonly reflector: Reflector,
30
+ ) {}
31
+
32
+ onApplicationBootstrap() {
33
+ const enforcedRoutes = this.discoverPermissionRoutes();
34
+ const registryRoutes = routePermissionRegistry.map(normalizeEntry);
35
+
36
+ const enforcedByRoute = mapByRoute(
37
+ enforcedRoutes,
38
+ "enforced route metadata",
39
+ );
40
+ const registryByRoute = mapByRoute(
41
+ registryRoutes,
42
+ "routePermissionRegistry",
43
+ );
44
+ const missingFromRegistry = enforcedRoutes.filter(
45
+ (entry) => !registryByRoute.has(routeKey(entry)),
46
+ );
47
+ const staleRegistryEntries = registryRoutes.filter(
48
+ (entry) => !enforcedByRoute.has(routeKey(entry)),
49
+ );
50
+ const permissionMismatches = collectPermissionMismatches(
51
+ registryByRoute,
52
+ enforcedByRoute,
53
+ );
54
+ const unknownPermissions = collectUnknownPermissions([
55
+ ...registryRoutes,
56
+ ...enforcedRoutes,
57
+ ]);
58
+
59
+ if (
60
+ missingFromRegistry.length > 0 ||
61
+ staleRegistryEntries.length > 0 ||
62
+ permissionMismatches.length > 0 ||
63
+ unknownPermissions.length > 0
64
+ ) {
65
+ throw new Error(
66
+ formatRegistryDriftError({
67
+ missingFromRegistry,
68
+ staleRegistryEntries,
69
+ permissionMismatches,
70
+ unknownPermissions,
71
+ }),
72
+ );
73
+ }
74
+ }
75
+
76
+ private discoverPermissionRoutes() {
77
+ const entries: RoutePermissionEntry[] = [];
78
+
79
+ for (const wrapper of this.discovery.getControllers()) {
80
+ const instance = wrapper.instance as ControllerInstance | undefined;
81
+ const metatype = wrapper.metatype;
82
+ if (!instance || !metatype) {
83
+ continue;
84
+ }
85
+
86
+ const controllerPaths = toPathParts(
87
+ this.reflector.get(PATH_METADATA, metatype),
88
+ );
89
+ const prototype = Object.getPrototypeOf(instance) as object | null;
90
+
91
+ for (const methodName of this.metadataScanner.getAllMethodNames(
92
+ prototype,
93
+ )) {
94
+ const handler = instance[methodName];
95
+ if (typeof handler !== "function") {
96
+ continue;
97
+ }
98
+
99
+ const requestMethod = this.reflector.get<RequestMethod | undefined>(
100
+ METHOD_METADATA,
101
+ handler,
102
+ );
103
+ if (requestMethod === undefined) {
104
+ continue;
105
+ }
106
+
107
+ const permissions = this.reflector.getAllAndOverride<PermissionKey[]>(
108
+ REQUIRED_PERMISSIONS_KEY,
109
+ [handler as RouteHandler, metatype],
110
+ );
111
+ if (!permissions || permissions.length === 0) {
112
+ continue;
113
+ }
114
+
115
+ const methodPaths = toPathParts(
116
+ this.reflector.get(PATH_METADATA, handler),
117
+ );
118
+ for (const path of combinePaths(controllerPaths, methodPaths)) {
119
+ entries.push(
120
+ normalizeEntry({
121
+ method: requestMethodToString(requestMethod),
122
+ path,
123
+ permissions,
124
+ }),
125
+ );
126
+ }
127
+ }
128
+ }
129
+
130
+ return entries;
131
+ }
132
+ }
133
+
134
+ function collectPermissionMismatches(
135
+ registryByRoute: Map<string, RoutePermissionEntry>,
136
+ enforcedByRoute: Map<string, RoutePermissionEntry>,
137
+ ) {
138
+ const mismatches: RoutePermissionDiff[] = [];
139
+
140
+ for (const [key, enforced] of enforcedByRoute) {
141
+ const registered = registryByRoute.get(key);
142
+ if (!registered) {
143
+ continue;
144
+ }
145
+
146
+ if (
147
+ permissionListKey(registered.permissions) !==
148
+ permissionListKey(enforced.permissions)
149
+ ) {
150
+ mismatches.push({
151
+ key,
152
+ registryPermissions: registered.permissions,
153
+ enforcedPermissions: enforced.permissions,
154
+ });
155
+ }
156
+ }
157
+
158
+ return mismatches;
159
+ }
160
+
161
+ function collectUnknownPermissions(entries: RoutePermissionEntry[]) {
162
+ const known = new Set<string>(permissionKeys);
163
+ return [...new Set(entries.flatMap((entry) => entry.permissions))].filter(
164
+ (permission) => !known.has(permission),
165
+ );
166
+ }
167
+
168
+ function formatRegistryDriftError({
169
+ missingFromRegistry,
170
+ staleRegistryEntries,
171
+ permissionMismatches,
172
+ unknownPermissions,
173
+ }: {
174
+ missingFromRegistry: RoutePermissionEntry[];
175
+ staleRegistryEntries: RoutePermissionEntry[];
176
+ permissionMismatches: RoutePermissionDiff[];
177
+ unknownPermissions: string[];
178
+ }) {
179
+ const sections = ["Route permission registry drift detected."];
180
+
181
+ if (missingFromRegistry.length > 0) {
182
+ sections.push(
183
+ [
184
+ "Permission-gated routes missing from routePermissionRegistry:",
185
+ ...missingFromRegistry.map(formatEntry),
186
+ ].join("\n"),
187
+ );
188
+ }
189
+
190
+ if (staleRegistryEntries.length > 0) {
191
+ sections.push(
192
+ [
193
+ "routePermissionRegistry entries without matching permission-gated routes:",
194
+ ...staleRegistryEntries.map(formatEntry),
195
+ ].join("\n"),
196
+ );
197
+ }
198
+
199
+ if (permissionMismatches.length > 0) {
200
+ sections.push(
201
+ [
202
+ "routePermissionRegistry permission mismatches:",
203
+ ...permissionMismatches.map(
204
+ (diff) =>
205
+ `- ${diff.key}: registry [${diff.registryPermissions.join(", ")}], enforced [${diff.enforcedPermissions.join(", ")}]`,
206
+ ),
207
+ ].join("\n"),
208
+ );
209
+ }
210
+
211
+ if (unknownPermissions.length > 0) {
212
+ sections.push(
213
+ [
214
+ "Unknown permission keys referenced by guarded routes or routePermissionRegistry:",
215
+ ...unknownPermissions.map((permission) => `- ${permission}`),
216
+ ].join("\n"),
217
+ );
218
+ }
219
+
220
+ return sections.join("\n\n");
221
+ }
222
+
223
+ function mapByRoute(entries: RoutePermissionEntry[], source: string) {
224
+ const byRoute = new Map<string, RoutePermissionEntry>();
225
+
226
+ for (const entry of entries) {
227
+ const key = routeKey(entry);
228
+ if (byRoute.has(key)) {
229
+ throw new Error(`Duplicate ${source} entry for ${key}.`);
230
+ }
231
+
232
+ byRoute.set(key, entry);
233
+ }
234
+
235
+ return byRoute;
236
+ }
237
+
238
+ function normalizeEntry(entry: RoutePermissionEntry): RoutePermissionEntry {
239
+ return {
240
+ method: entry.method.toUpperCase(),
241
+ path: normalizePath(entry.path),
242
+ permissions: [...new Set(entry.permissions)].sort(),
243
+ };
244
+ }
245
+
246
+ function combinePaths(controllerPaths: string[], methodPaths: string[]) {
247
+ const paths: string[] = [];
248
+
249
+ for (const controllerPath of controllerPaths) {
250
+ for (const methodPath of methodPaths) {
251
+ paths.push(normalizePath([controllerPath, methodPath].join("/")));
252
+ }
253
+ }
254
+
255
+ return paths;
256
+ }
257
+
258
+ function toPathParts(metadata: string | string[] | undefined) {
259
+ if (Array.isArray(metadata)) {
260
+ return metadata.length > 0 ? metadata : [""];
261
+ }
262
+
263
+ return [metadata ?? ""];
264
+ }
265
+
266
+ function normalizePath(path: string) {
267
+ const trimmed = path
268
+ .split("/")
269
+ .filter((part) => part.length > 0)
270
+ .join("/");
271
+
272
+ return trimmed ? `/${trimmed}` : "/";
273
+ }
274
+
275
+ function requestMethodToString(method: RequestMethod) {
276
+ return RequestMethod[method];
277
+ }
278
+
279
+ function routeKey(entry: RoutePermissionEntry) {
280
+ return `${entry.method} ${entry.path}`;
281
+ }
282
+
283
+ function permissionListKey(permissions: PermissionKey[]) {
284
+ return permissions.join("\0");
285
+ }
286
+
287
+ function formatEntry(entry: RoutePermissionEntry) {
288
+ return `- ${routeKey(entry)} -> [${entry.permissions.join(", ")}]`;
289
+ }
@@ -0,0 +1,28 @@
1
+ import {
2
+ AbilityBuilder,
3
+ createMongoAbility,
4
+ MongoAbility,
5
+ } from "@casl/ability";
6
+ import { Injectable } from "@nestjs/common";
7
+ import { permissionKeyToRule } from "../../types/permission-key";
8
+ import { RbacContext } from "../../types/rbac-context";
9
+
10
+ export type AppAbility = MongoAbility<[string, string]>;
11
+
12
+ @Injectable()
13
+ export class AbilityFactory {
14
+ createForContext(context: RbacContext): AppAbility {
15
+ const { can, build } = new AbilityBuilder<AppAbility>(createMongoAbility);
16
+
17
+ if (context.isOwner) {
18
+ can("manage", "all");
19
+ }
20
+
21
+ for (const key of context.permissionKeys) {
22
+ const { action, subject } = permissionKeyToRule(key);
23
+ can(action, subject);
24
+ }
25
+
26
+ return build();
27
+ }
28
+ }