@appxdigital/appx-core 0.1.121-beta.9 → 0.1.121
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/CHANGELOG.md +32 -1
- package/PRERELEASES.md +107 -0
- package/dist/common/config/permissionsConfigTypes.d.ts +19 -3
- package/dist/common/config/permissionsConfigTypes.d.ts.map +1 -1
- package/dist/config/generate-dtos.js +101 -15
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +15 -0
- package/dist/modules/auth/auth.module.d.ts +16 -0
- package/dist/modules/auth/auth.module.d.ts.map +1 -1
- package/dist/modules/auth/auth.module.js +45 -16
- package/dist/modules/core/core.controller.d.ts.map +1 -1
- package/dist/modules/core/core.controller.js +3 -23
- package/dist/prisma/permissions-validator.d.ts +51 -0
- package/dist/prisma/permissions-validator.d.ts.map +1 -0
- package/dist/prisma/permissions-validator.js +142 -0
- package/dist/prisma/prisma.service.d.ts +61 -3
- package/dist/prisma/prisma.service.d.ts.map +1 -1
- package/dist/prisma/prisma.service.js +245 -33
- package/docs/README.md +2 -0
- package/docs/authentication.md +220 -0
- package/docs/dtos.md +158 -0
- package/docs/limitations.md +7 -1
- package/docs/permissions.md +58 -3
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -51,6 +51,7 @@ A scalar field is included unless it is: the primary key (`@id`), a server-manag
|
|
|
51
51
|
- `app.module.ts` uses `coreEnvFilePath()` for `envFilePath`.
|
|
52
52
|
- Generated `package.json` adds `helmet`; the `start` script sets `NODE_ENV=development`.
|
|
53
53
|
- `AuthController.getAllSessions` / `closeSpecificSession` use `@UseGuards(AuthenticatedGuard)`.
|
|
54
|
+
- `app.module.ts` imports `AuthModule.forRoot()` (not the bare `AuthModule`) — see the migration note below.
|
|
54
55
|
|
|
55
56
|
### Migration
|
|
56
57
|
|
|
@@ -111,7 +112,37 @@ async create(@Body() data: Create<Model>Dto) { return super.create(data as any);
|
|
|
111
112
|
@Put(':id') @Permission('updateMany')
|
|
112
113
|
async update(@Param('id') id: string, @Body() data: Update<Model>Dto) { return super.update(id, data as any); }
|
|
113
114
|
```
|
|
114
|
-
(Or delete an un-customized controller and re-run `appx generate` for the new template.) Add custom validation in the hand-owned `dto/*.dto.ts` subclasses — never in the `.generated.dto.ts` base. Lock sensitive columns with `/// @NoWrite` (all roles) or `restrictedFields` on the permission (per role), then regenerate.
|
|
115
|
+
(Or delete an un-customized controller and re-run `appx generate` for the new template.) Add custom validation in the hand-owned `dto/*.dto.ts` subclasses — never in the `.generated.dto.ts` base. Lock sensitive columns with `/// @NoWrite` (all roles) or `restrictedFields` on the permission (per role), then regenerate. To drop a field from one endpoint's DTO, extend `OmitType(<Model>GeneratedDto, ['field'] as const)`. Full details in [`docs/dtos.md`](../docs/dtos.md).
|
|
116
|
+
|
|
117
|
+
> **Nested relation writes are now an explicit, permission-gated allowlist — this is a behaviour restriction; a migrating agent MUST flag it to the developer.** Generated **create** DTOs accept only two nested operators, each authorized by ABAC: **`create`** (checked against the related model's `create` rule) and **`connect`** (checked against the related model's new dedicated **`connect`** permission — a `connect` rule is required; no rule ⇒ refused). Every other operator (`set`, `disconnect`, `update`, `upsert`, `delete`, …) is rejected (`400`). Nested writes are available on **`create` only** — `update` resolves to `updateMany`, whose payload is scalar-only. **Keep `forbidNonWhitelisted: true` on (fail loud); never `whitelist`-strip on writes.**
|
|
118
|
+
>
|
|
119
|
+
> **For AI agents doing this migration on an existing project:** this can break existing behaviour. Before deploying, tell the developer explicitly that (a) any nested operator other than `create`/`connect` used through CRUD will now `400`, (b) every model that is nested-`connect`ed now needs a `connect` permission or the request fails, and (c) nested writes no longer work on update endpoints. Do not proceed silently. See [`docs/dtos.md`](../docs/dtos.md#relations--nested-writes) and [`docs/permissions.md`](../docs/permissions.md).
|
|
120
|
+
|
|
121
|
+
> ## ⚠️ Relationship authorization moved to `connect` — this WILL break configs; the app refuses to boot until migrated
|
|
122
|
+
>
|
|
123
|
+
> **Every foreign-key reference a write establishes is now authorized by the target model's `connect` rule** — on a `create` (nested `connect`, nested `create`, **or a plain scalar FK** like `ownerId: 7`) **and on an `update` that re-points a foreign key** (`{ teacherId: 5 }` / `{ teacherId: { set: 5 } }`). A **`create` condition now judges the created model's own scalar fields only** — it must NOT reach across a relation. The one thing not re-checked is the foreign key Prisma auto-fills for a child nested inside its parent's `create` (the parent already authorized itself).
|
|
124
|
+
>
|
|
125
|
+
> **The framework validates this at boot.** If a model a role can create **or update** can set a foreign key whose target has **no `connect` rule** for that role, **the app refuses to start** (setting it would `403`). A required-on-create FK reports as such; a `@NoWrite`/`@Role(none)` FK column is exempt (it can't be written); a `create` condition that references a relation, or a relation-scoped `connect` on a non-writable field, are also boot errors.
|
|
126
|
+
>
|
|
127
|
+
> **`restrictedFields` removed.** The per-role body-strip option on a permission action (`restrictedFields: ['x']`) is gone. Migrate: mark a never-client-writable column `/// @NoWrite` (or `/// @Role(none)`); drop a field from one endpoint with `OmitType`; force an owner id with `setUserIdField`; and for a genuinely per-role write difference, use a dedicated endpoint (generic CRUD can't vary a field by role). See [`docs/dtos.md`](../docs/dtos.md#per-role-differences).
|
|
128
|
+
>
|
|
129
|
+
> **Required migration for existing projects:**
|
|
130
|
+
> 1. **Move relationship checks out of `create` conditions and into `connect` rules.** A rule like `ExamQuestion.create: { conditions: { exam: { course: {...} } } }` must become `Course.connect: { conditions: {...} }` (or `Exam.connect`), reached via the FK on the create. A `create` condition may only constrain the model's **own** columns (e.g. `{ ownerId: $USER_ID }`, which stays valid and is still checked).
|
|
131
|
+
> 2. **Add a `connect` rule to every model referenced by a foreign key on a create you allow.** For unrestricted roles that already have `create: 'ALL'`, add `connect: 'ALL'`. Start the app — the boot error lists every missing `<Model>.<Role>.connect` by name; add each until it starts. If a *specific* relation needs its own (stricter) rule — "a class's teacher must be a teacher" — declare it on the source instead: `Source[Role].relations.<field>.connect`. That also satisfies the requirement, so the target model then needs no `connect` rule for that reference.
|
|
132
|
+
> 3. Because one `connect` rule serves every relation that points at a model (it is keyed on `[model][role]`, not per-relation), make its conditions the **union** of what any referencing relation legitimately needs; enforce anything finer in an explicit endpoint.
|
|
133
|
+
>
|
|
134
|
+
> **For AI agents:** this is a behaviour restriction on a production app. Before deploying, tell the developer that (a) any create that sets a foreign key to a model without a `connect` rule now fails — and the app won't boot until those rules exist, and (b) any `create` condition that referenced a relation no longer enforces it there and must be re-expressed as a `connect` rule. Do not proceed silently. The boot-time error message names exactly what to add. See [`docs/permissions.md`](../docs/permissions.md#the-connect-action--authorizes-every-relationship-a-create-establishes).
|
|
135
|
+
|
|
136
|
+
**6. Auth module — `AuthModule` → `AuthModule.forRoot()` (`src/app.module.ts`).**
|
|
137
|
+
The bare `AuthModule` no longer registers anything; wire it as `AuthModule.forRoot()`
|
|
138
|
+
(all providers + the built-in `/auth` controller). This exists so the controller
|
|
139
|
+
can be conditionally omitted: to override the auth endpoints (e.g. widen
|
|
140
|
+
registration to accept `name`), import `AuthModule.forRoot({ controller: false })`
|
|
141
|
+
and register your own controller that `extends AuthController` at the same `/auth`
|
|
142
|
+
prefix. The auth building blocks (`AuthController`, `AuthService`, `RegisterDto`,
|
|
143
|
+
`UserDto`, the Passport strategies, guards) are now exported from the package root
|
|
144
|
+
— stop importing them from `@appxdigital/appx-core/dist/...`. Full guide:
|
|
145
|
+
[`docs/authentication.md`](../docs/authentication.md).
|
|
115
146
|
|
|
116
147
|
**5. Create-permission enforcement (behaviour change — review before upgrading).**
|
|
117
148
|
- Ensure every model/role that legitimately creates records has an explicit `create` permission (or `'ALL'`). Without one, create now returns `403` (previously unchecked). Internal framework flows (registration, sessions, tokens) are unaffected.
|
package/PRERELEASES.md
CHANGED
|
@@ -31,6 +31,113 @@ Prereleases are excluded from normal semver range resolution, so a project on
|
|
|
31
31
|
|
|
32
32
|
---
|
|
33
33
|
|
|
34
|
+
## [0.1.121-beta.16] — unreleased
|
|
35
|
+
|
|
36
|
+
**Fix — `AuthModule.forRoot({ controller: false })` now actually omits the
|
|
37
|
+
controller.** In beta.15 the controller was also declared on the static `@Module()`,
|
|
38
|
+
and NestJS *extends* (does not replace) that metadata with `forRoot()`'s — so
|
|
39
|
+
`AuthController` stayed registered and overriding `/auth/register` produced a
|
|
40
|
+
duplicate route (the base `RegisterDto` won → extra fields like `name` were
|
|
41
|
+
rejected). All wiring now lives in `forRoot()`.
|
|
42
|
+
|
|
43
|
+
- **Migration:** import **`AuthModule.forRoot()`**, not the bare `AuthModule` (which
|
|
44
|
+
now registers nothing). The scaffold is updated. `forRoot({ controller: false })`
|
|
45
|
+
omits the built-in controller so your own can take over `/auth`. Regression
|
|
46
|
+
test added (`test/unit/auth-module.spec.ts`).
|
|
47
|
+
|
|
48
|
+
## [0.1.121-beta.15] — unreleased
|
|
49
|
+
|
|
50
|
+
**Library / docs. Additive — no behaviour change.** Auth is now extensible from
|
|
51
|
+
the package root instead of deep `dist/` imports.
|
|
52
|
+
|
|
53
|
+
- **Exported the auth building blocks** from `@appxdigital/appx-core`:
|
|
54
|
+
`AuthController`, `AuthService`, `RegisterDto`, `UserDto`, `AuthField`,
|
|
55
|
+
`LocalStrategy`, `JwtStrategy`, `RefreshTokenStrategy`, `LocalAuthGuard`,
|
|
56
|
+
`RefreshTokenGuard`, `SessionSerializer`, `SessionAuthGuard`. Stop importing
|
|
57
|
+
these from `@appxdigital/appx-core/dist/...` — deep paths are internal.
|
|
58
|
+
- **`AuthModule.forRoot({ controller?: boolean })`** — importing
|
|
59
|
+
`AuthModule.forRoot({ controller: false })` registers every auth provider
|
|
60
|
+
globally but **not** the built-in `AuthController`, so you can register your own
|
|
61
|
+
controller (e.g. `extends AuthController`) at the same `/auth` prefix with no
|
|
62
|
+
duplicate route. Importing the plain `AuthModule` is unchanged (still registers
|
|
63
|
+
the controller).
|
|
64
|
+
- **New doc `docs/authentication.md`** — the built-in `/auth` surface and how to
|
|
65
|
+
override framework internals (widen `RegisterDto` to accept e.g. `name`, extend
|
|
66
|
+
`AuthService`, swap a Passport strategy), wired at the same prefix in `AppModule`.
|
|
67
|
+
|
|
68
|
+
## [0.1.121-beta.14] — unreleased
|
|
69
|
+
|
|
70
|
+
**Library / types.** `RolePermissions.relations` is now usable in a properly
|
|
71
|
+
typed `PermissionsConfigType` (no `as any`) — the catch-all index signature no
|
|
72
|
+
longer collides with the `relations` key. Guarded by a type-contract test. The
|
|
73
|
+
boot validator now takes `PermissionsConfigType` directly. No runtime change.
|
|
74
|
+
|
|
75
|
+
## [0.1.121-beta.13] — unreleased
|
|
76
|
+
|
|
77
|
+
**Library.** **Relation-scoped `connect` rules.** A `connect` rule can now be
|
|
78
|
+
declared on the **source** model, keyed by the relation field
|
|
79
|
+
(`relations: { teacher: { connect: { conditions: {...} } } }`), to constrain who
|
|
80
|
+
may be attached *through that relation* — something the per-target-model
|
|
81
|
+
`connect` rule can't express (e.g. "a class's teacher must be a teacher"). When
|
|
82
|
+
both the relation rule and the target's `connect` rule exist they are **ANDed**
|
|
83
|
+
(the relation rule only strengthens); when only the relation rule exists it
|
|
84
|
+
stands alone, so the target model needs no `connect` rule. The boot validator
|
|
85
|
+
accepts either placement. Additive and backward-compatible — no `relations` block
|
|
86
|
+
means unchanged behaviour. See `docs/permissions.md`.
|
|
87
|
+
|
|
88
|
+
**Connect authorization now also covers `update`.** Re-pointing a foreign key on
|
|
89
|
+
an `update` (`{ teacherId: 5 }` or `{ teacherId: { set: 5 } }`) is authorized by
|
|
90
|
+
the same `connect` rules as `create` — previously the update path set foreign
|
|
91
|
+
keys with no connect check. The `relations` config is shared across create and
|
|
92
|
+
update.
|
|
93
|
+
|
|
94
|
+
**Boot validator is now strict about all settable FKs.** Any foreign key a role
|
|
95
|
+
can set on create **or** update whose target lacks a `connect` rule is a boot
|
|
96
|
+
**error** (previously optional/update FKs were warnings). `@NoWrite` / `@Role(none)`
|
|
97
|
+
columns are exempt (never client-writable), and a relation-scoped `connect` on a
|
|
98
|
+
non-writable field is flagged as a contradiction.
|
|
99
|
+
|
|
100
|
+
**`restrictedFields` removed (breaking).** The per-role body-strip option on a
|
|
101
|
+
permission action is gone; the `CoreController` no longer strips fields. Migrate
|
|
102
|
+
to `/// @NoWrite` / `OmitType` / `setUserIdField` / a dedicated endpoint — see the
|
|
103
|
+
`[0.1.121]` migration in `CHANGELOG.md` and `docs/dtos.md`.
|
|
104
|
+
|
|
105
|
+
## [0.1.121-beta.12] — unreleased
|
|
106
|
+
|
|
107
|
+
**Library.** **Relationship authorization moved entirely to the `connect` action, with boot-time config validation.** Two fixes from an adopter report plus the model change:
|
|
108
|
+
|
|
109
|
+
- **Fix — create condition vs the `connect` form.** A create condition on a scalar FK (`{ ownerId: $USER_ID }`) is now satisfied when the association is supplied via `owner: { connect: { id } }`, not only the raw scalar. Resolved from relation metadata (no naming assumptions).
|
|
110
|
+
- **Model change (behaviour restriction — review on upgrade).** A `create` condition judges the model's **own scalar fields only**; **every foreign-key reference a create establishes — raw scalar FK, `connect`, or nested `create` — is authorized by the target model's `connect` rule** (default-deny, no bypass). The auto-filled back-FK to a same-request nesting parent is trusted. This fixes nested `create`s whose child rule referenced the parent (previously a spurious `403`) and closes the raw-FK bypass.
|
|
111
|
+
- **Boot validation.** The config is checked against the schema at startup: a required FK whose target has no `connect` rule for a creating role, or a `create` condition that references a relation, **rejects boot** (optional FK → warning). The error names each missing `<Model>.<Role>.connect`.
|
|
112
|
+
- **Migration is mandatory** — see the `[0.1.121]` banner in `CHANGELOG.md` (“Relationship authorization moved to `connect`”). Move relation-reaching create conditions to `connect` rules and add a `connect` rule for every FK-referenced model. An agent doing the migration must warn the developer.
|
|
113
|
+
|
|
114
|
+
## [0.1.121-beta.11] — unreleased
|
|
115
|
+
|
|
116
|
+
**Docs.** No code change. Documented that nested writes over HTTP CRUD are
|
|
117
|
+
**one level deep** (a nested `create` DTO exposes the related model's scalars +
|
|
118
|
+
`connect`, not its own relations, to avoid circular DTOs). The proxy's
|
|
119
|
+
authorization is recursive; the DTO surface is the cap — deeper nesting `400`s,
|
|
120
|
+
and multi-level nested writes belong in an explicit endpoint. See `docs/dtos.md`
|
|
121
|
+
and `docs/limitations.md`.
|
|
122
|
+
|
|
123
|
+
## [0.1.121-beta.10] — unreleased
|
|
124
|
+
|
|
125
|
+
**Library.** **Nested relation writes on generated CRUD are now an explicit,
|
|
126
|
+
permission-gated allowlist.** A `create` body may nest **`create`** and
|
|
127
|
+
**`connect`** on its relations; both are authorized by ABAC (`create` → the
|
|
128
|
+
related model's `create` rule, recursively; `connect` → the related model's new
|
|
129
|
+
dedicated **`connect`** permission — a `connect` rule is required, because being
|
|
130
|
+
able to *read* a record does not authorize associating it). Every other nested
|
|
131
|
+
operator (`set` / `disconnect` / `update` / `upsert` / `delete` / …) is rejected
|
|
132
|
+
at both the DTO layer (not emitted → `400`) and the proxy (throws). Nested
|
|
133
|
+
writes are available on **`create` only** (`update` resolves to `updateMany`,
|
|
134
|
+
which is scalar-only).
|
|
135
|
+
|
|
136
|
+
- **Behaviour restriction — review on upgrade.** If a project used other nested
|
|
137
|
+
operators through CRUD, or connected models without a `connect` rule, those
|
|
138
|
+
requests now fail. Add `connect` rules where needed; move unsupported nested
|
|
139
|
+
writes to explicit endpoints. Keep `forbidNonWhitelisted: true` (fail loud).
|
|
140
|
+
|
|
34
141
|
## [0.1.121-beta.9] — unreleased
|
|
35
142
|
|
|
36
143
|
**Docs / packaging.** No code change.
|
|
@@ -48,15 +48,31 @@ export type FieldConditions<M extends ModelKey<RuntimeClient>, A extends Methods
|
|
|
48
48
|
export interface ActionPermission<M extends ModelKey<RuntimeClient> = ModelKey<RuntimeClient>, A extends MethodsWithWhere<RuntimeClient, M> = MethodsWithWhere<RuntimeClient, M>> {
|
|
49
49
|
conditions: FieldConditions<M, A> | FieldConditions<M, A>[];
|
|
50
50
|
setUserIdField?: string;
|
|
51
|
-
restrictedFields?: string[];
|
|
52
51
|
}
|
|
52
|
+
/**
|
|
53
|
+
* A relation-scoped `connect` rule, declared on the SOURCE model and keyed by
|
|
54
|
+
* the relation field. It authorizes attaching a target through *that specific
|
|
55
|
+
* relation* (e.g. who may be a class's `teacher`). When the target model also
|
|
56
|
+
* declares a `connect` rule, the two are ANDed (the relation rule can only
|
|
57
|
+
* strengthen, never weaken, the target's rule). When only the relation rule
|
|
58
|
+
* exists, it stands alone — the target model then needs no `connect` rule.
|
|
59
|
+
*/
|
|
60
|
+
export interface RelationConnectRule {
|
|
61
|
+
connect: 'ALL' | {
|
|
62
|
+
conditions?: unknown | unknown[];
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
export type RelationConnectMap = {
|
|
66
|
+
[relationField: string]: RelationConnectRule;
|
|
67
|
+
};
|
|
53
68
|
export type RolePermissions<M extends ModelKey<RuntimeClient> = ModelKey<RuntimeClient>> = {
|
|
54
69
|
[A in MethodsWithWhere<RuntimeClient, M>]?: 'ALL' | ActionPermission<M, A>;
|
|
55
70
|
} & {
|
|
56
|
-
|
|
71
|
+
relations?: RelationConnectMap;
|
|
72
|
+
} & {
|
|
73
|
+
[custom: string]: 'ALL' | RelationConnectMap | {
|
|
57
74
|
conditions?: unknown | unknown[];
|
|
58
75
|
setUserIdField?: string;
|
|
59
|
-
restrictedFields?: string[];
|
|
60
76
|
};
|
|
61
77
|
};
|
|
62
78
|
export type ModelPermissions<M extends ModelKey<RuntimeClient> = ModelKey<RuntimeClient>> = {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"permissionsConfigTypes.d.ts","sourceRoot":"","sources":["../../../src/common/config/permissionsConfigTypes.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"permissionsConfigTypes.d.ts","sourceRoot":"","sources":["../../../src/common/config/permissionsConfigTypes.ts"],"names":[],"mappings":"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"}
|
|
@@ -45,7 +45,14 @@ const utils_1 = require("./utils");
|
|
|
45
45
|
* - a server-managed timestamp (`@default(now())` or `@updatedAt`),
|
|
46
46
|
* - annotated `/// @NoWrite`, or
|
|
47
47
|
* - annotated `/// @Role(none)` (readable by nobody ⇒ not CRUD-writable).
|
|
48
|
-
*
|
|
48
|
+
*
|
|
49
|
+
* Nested relation writes (CREATE DTOs only — `updateMany` takes scalar data):
|
|
50
|
+
* Each relation gets a nested-write member exposing ONLY the operators the
|
|
51
|
+
* data-access proxy authorizes: `create` (validated against the related
|
|
52
|
+
* model's writable scalars) and `connect` (validated against the related
|
|
53
|
+
* model's unique fields). Other operators (`set`/`disconnect`/`update`/…) are
|
|
54
|
+
* deliberately NOT emitted, so the whitelist pipe rejects them. The proxy
|
|
55
|
+
* enforces the matching ABAC rule for each (`create` rule / `connect` rule).
|
|
49
56
|
*/
|
|
50
57
|
const cwd = process.cwd();
|
|
51
58
|
// Read the consumer's generated Prisma client for the DMMF (authoritative
|
|
@@ -54,6 +61,9 @@ const cwd = process.cwd();
|
|
|
54
61
|
const { Prisma } = require(path.join(cwd, 'node_modules', '@prisma/client'));
|
|
55
62
|
const generatedDtoRoot = path.join(cwd, 'src/generated/dto');
|
|
56
63
|
const modulesRoot = path.join(cwd, 'src/modules');
|
|
64
|
+
const modelsByName = {};
|
|
65
|
+
for (const m of Prisma.dmmf.datamodel.models)
|
|
66
|
+
modelsByName[m.name] = m;
|
|
57
67
|
function isNoWrite(doc) {
|
|
58
68
|
if (!doc)
|
|
59
69
|
return false;
|
|
@@ -98,7 +108,7 @@ function mapField(field) {
|
|
|
98
108
|
return { validator: 'Allow', tsType: 'any' };
|
|
99
109
|
}
|
|
100
110
|
}
|
|
101
|
-
function writableFields(model) {
|
|
111
|
+
function writableFields(model, omit = []) {
|
|
102
112
|
return model.fields.filter((f) => {
|
|
103
113
|
if (f.kind === 'object')
|
|
104
114
|
return false; // relation navigation
|
|
@@ -108,25 +118,32 @@ function writableFields(model) {
|
|
|
108
118
|
return false;
|
|
109
119
|
if (isNoWrite(f.documentation))
|
|
110
120
|
return false;
|
|
121
|
+
if (omit.includes(f.name))
|
|
122
|
+
return false;
|
|
111
123
|
return true;
|
|
112
124
|
});
|
|
113
125
|
}
|
|
114
|
-
/**
|
|
115
|
-
function
|
|
116
|
-
|
|
117
|
-
|
|
126
|
+
/** id + unique scalar fields (for a connect-by DTO). */
|
|
127
|
+
function connectFields(model) {
|
|
128
|
+
return model.fields.filter((f) => f.kind !== 'object' && (f.isId || f.isUnique));
|
|
129
|
+
}
|
|
130
|
+
/** The scalar FK column(s) the parent sets automatically — omit from a nested create. */
|
|
131
|
+
function backFkColumns(relatedModel, relationName) {
|
|
132
|
+
const inverse = relatedModel.fields.find((f) => f.kind === 'object' && f.relationName === relationName && (f.relationFromFields || []).length > 0);
|
|
133
|
+
return inverse ? inverse.relationFromFields || [] : [];
|
|
134
|
+
}
|
|
135
|
+
/** Renders class-body lines for a set of scalar/enum fields; collects imports. */
|
|
136
|
+
function renderScalarLines(fields, mode, validatorImports, enumImports) {
|
|
118
137
|
const lines = [];
|
|
119
138
|
for (const field of fields) {
|
|
120
139
|
const m = mapField(field);
|
|
121
140
|
validatorImports.add(m.validator);
|
|
122
141
|
if (m.enumArg)
|
|
123
142
|
enumImports.add(m.enumArg);
|
|
124
|
-
// update: everything optional. create: optional if not required or
|
|
125
|
-
const optional = mode
|
|
143
|
+
// update / connect: everything optional. create: optional if not required or defaulted.
|
|
144
|
+
const optional = mode !== 'create' || !field.isRequired || field.hasDefaultValue;
|
|
126
145
|
const isList = field.isList;
|
|
127
146
|
const tsType = isList ? `${m.tsType}[]` : m.tsType;
|
|
128
|
-
// Compose the validator decorator. @Allow takes no options; for other
|
|
129
|
-
// validators on a list field, add `{ each: true }` (+ @IsArray).
|
|
130
147
|
let validatorDecorator;
|
|
131
148
|
if (m.validator === 'Allow') {
|
|
132
149
|
validatorDecorator = '@Allow()';
|
|
@@ -153,19 +170,89 @@ function renderBaseDto(className, fields, mode) {
|
|
|
153
170
|
lines.push(` ${field.name}${optional ? '?' : '!'}: ${tsType};`);
|
|
154
171
|
lines.push('');
|
|
155
172
|
}
|
|
173
|
+
return lines;
|
|
174
|
+
}
|
|
175
|
+
function renderClass(className, bodyLines) {
|
|
176
|
+
return `export class ${className} {\n` + (bodyLines.length ? bodyLines.join('\n') : '') + `}\n`;
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* For a model's CREATE DTO, render the nested-write classes for each relation
|
|
180
|
+
* plus the property lines to add to the parent class. Nested create classes are
|
|
181
|
+
* scalars-only (no deeper relations) so there are no circular imports.
|
|
182
|
+
*/
|
|
183
|
+
function renderRelationNestedWrites(model, validatorImports, enumImports, transformerImports) {
|
|
184
|
+
const classes = [];
|
|
185
|
+
const parentLines = [];
|
|
186
|
+
const relations = model.fields.filter((f) => f.kind === 'object');
|
|
187
|
+
for (const rel of relations) {
|
|
188
|
+
const related = modelsByName[rel.type];
|
|
189
|
+
if (!related)
|
|
190
|
+
continue;
|
|
191
|
+
const relCap = rel.name.charAt(0).toUpperCase() + rel.name.slice(1);
|
|
192
|
+
const createCls = `${model.name}${relCap}CreateNestedDto`;
|
|
193
|
+
const connectCls = `${model.name}${relCap}ConnectNestedDto`;
|
|
194
|
+
const writeCls = `${model.name}${relCap}NestedWriteDto`;
|
|
195
|
+
// create: related writable scalars minus the back-reference FK.
|
|
196
|
+
const omit = backFkColumns(related, rel.relationName);
|
|
197
|
+
classes.push(renderClass(createCls, renderScalarLines(writableFields(related, omit), 'create', validatorImports, enumImports)));
|
|
198
|
+
// connect: related unique fields (all optional).
|
|
199
|
+
const conn = connectFields(related);
|
|
200
|
+
classes.push(renderClass(connectCls, renderScalarLines(conn, 'connect', validatorImports, enumImports)));
|
|
201
|
+
// nested-write: create? / connect? only.
|
|
202
|
+
transformerImports.add('Type');
|
|
203
|
+
validatorImports.add('IsOptional');
|
|
204
|
+
validatorImports.add('ValidateNested');
|
|
205
|
+
const eachArg = rel.isList ? '{ each: true }' : '';
|
|
206
|
+
const arrSuffix = rel.isList ? '[]' : '';
|
|
207
|
+
if (rel.isList)
|
|
208
|
+
validatorImports.add('IsArray');
|
|
209
|
+
const opLines = [];
|
|
210
|
+
for (const [op, cls] of [['create', createCls], ['connect', connectCls]]) {
|
|
211
|
+
if (rel.isList)
|
|
212
|
+
opLines.push(' @IsArray()');
|
|
213
|
+
opLines.push(' @IsOptional()');
|
|
214
|
+
opLines.push(` @ValidateNested(${eachArg})`);
|
|
215
|
+
opLines.push(` @Type(() => ${cls})`);
|
|
216
|
+
opLines.push(` ${op}?: ${cls}${arrSuffix};`);
|
|
217
|
+
opLines.push('');
|
|
218
|
+
}
|
|
219
|
+
classes.push(renderClass(writeCls, opLines));
|
|
220
|
+
// property on the parent CREATE DTO.
|
|
221
|
+
parentLines.push(' @IsOptional()');
|
|
222
|
+
parentLines.push(' @ValidateNested()');
|
|
223
|
+
parentLines.push(` @Type(() => ${writeCls})`);
|
|
224
|
+
parentLines.push(` ${rel.name}?: ${writeCls};`);
|
|
225
|
+
parentLines.push('');
|
|
226
|
+
}
|
|
227
|
+
return { classes, parentLines };
|
|
228
|
+
}
|
|
229
|
+
/** Renders a base DTO file (nested relation writes on create only). */
|
|
230
|
+
function renderBaseDto(className, model, mode) {
|
|
231
|
+
const validatorImports = new Set();
|
|
232
|
+
const enumImports = new Set();
|
|
233
|
+
const transformerImports = new Set();
|
|
234
|
+
const scalarLines = renderScalarLines(writableFields(model), mode, validatorImports, enumImports);
|
|
235
|
+
let nested = { classes: [], parentLines: [] };
|
|
236
|
+
if (mode === 'create') {
|
|
237
|
+
nested = renderRelationNestedWrites(model, validatorImports, enumImports, transformerImports);
|
|
238
|
+
}
|
|
239
|
+
const parentClass = renderClass(className, [...scalarLines, ...nested.parentLines]);
|
|
156
240
|
const importLines = [];
|
|
157
241
|
if (validatorImports.size > 0) {
|
|
158
242
|
importLines.push(`import { ${[...validatorImports].sort().join(', ')} } from 'class-validator';`);
|
|
159
243
|
}
|
|
244
|
+
if (transformerImports.size > 0) {
|
|
245
|
+
importLines.push(`import { ${[...transformerImports].sort().join(', ')} } from 'class-transformer';`);
|
|
246
|
+
}
|
|
160
247
|
if (enumImports.size > 0) {
|
|
161
248
|
importLines.push(`import { ${[...enumImports].sort().join(', ')} } from '@prisma/client';`);
|
|
162
249
|
}
|
|
163
250
|
return (utils_1.GENERATED_BANNER +
|
|
164
251
|
'\n' +
|
|
165
252
|
(importLines.length ? importLines.join('\n') + '\n\n' : '') +
|
|
166
|
-
|
|
167
|
-
(
|
|
168
|
-
|
|
253
|
+
// nested classes first (referenced by @Type in the parent).
|
|
254
|
+
(nested.classes.length ? nested.classes.join('\n') + '\n' : '') +
|
|
255
|
+
parentClass);
|
|
169
256
|
}
|
|
170
257
|
/** Renders the hand-owned subclass (generated once, then customizable). */
|
|
171
258
|
function renderSubclassDto(subclass, baseClass, importPath) {
|
|
@@ -182,14 +269,13 @@ for (const model of Prisma.dmmf.datamodel.models) {
|
|
|
182
269
|
const folder = (0, utils_1.pascalToKebabCase)(modelName); // kebab, e.g. project-member
|
|
183
270
|
if (utils_1.IGNORE_FOLDERS.includes(folder))
|
|
184
271
|
continue;
|
|
185
|
-
const fields = writableFields(model);
|
|
186
272
|
for (const mode of ['create', 'update']) {
|
|
187
273
|
const capMode = mode.charAt(0).toUpperCase() + mode.slice(1);
|
|
188
274
|
const baseClass = `${capMode}${modelName}GeneratedDto`;
|
|
189
275
|
const subclass = `${capMode}${modelName}Dto`;
|
|
190
276
|
// Base (overwritten, gitignored).
|
|
191
277
|
const basePath = path.join(generatedDtoRoot, folder, `${mode}-${folder}.generated.dto.ts`);
|
|
192
|
-
(0, utils_1.writeGeneratedFile)(basePath, renderBaseDto(baseClass,
|
|
278
|
+
(0, utils_1.writeGeneratedFile)(basePath, renderBaseDto(baseClass, model, mode));
|
|
193
279
|
// Subclass (once, hand-owned, committed).
|
|
194
280
|
const subPath = path.join(modulesRoot, folder, 'dto', `${mode}-${folder}.dto.ts`);
|
|
195
281
|
const importPath = `../../../generated/dto/${folder}/${mode}-${folder}.generated.dto`;
|
package/dist/index.d.ts
CHANGED
|
@@ -6,6 +6,18 @@ export * from './graphql/graphql.module';
|
|
|
6
6
|
export * from './graphql/generic.resolver';
|
|
7
7
|
export * from './tasks/session-cleanup.service';
|
|
8
8
|
export * from './modules/auth/auth.module';
|
|
9
|
+
export * from './modules/auth/auth.controller';
|
|
10
|
+
export * from './modules/auth/auth.service';
|
|
11
|
+
export * from './modules/auth/dto/register.dto';
|
|
12
|
+
export * from './modules/auth/dto/user.dto';
|
|
13
|
+
export * from './modules/auth/auth-field.decorator';
|
|
14
|
+
export * from './modules/auth/local.strategy';
|
|
15
|
+
export * from './modules/auth/local-auth.guard';
|
|
16
|
+
export * from './modules/auth/jwt.strategy';
|
|
17
|
+
export * from './modules/auth/refresh-token.strategy';
|
|
18
|
+
export * from './modules/auth/refresh-token.guard';
|
|
19
|
+
export * from './modules/auth/session/session-serializer';
|
|
20
|
+
export * from './modules/auth/session/session-auth.guard';
|
|
9
21
|
export * from './modules/auth/session/session-store';
|
|
10
22
|
export * from './common/guards/rbac.guard';
|
|
11
23
|
export * from './common/guards/user-population.guard';
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,4BAA4B,CAAC;AAC3C,cAAc,6BAA6B,CAAC;AAC5C,cAAc,gCAAgC,CAAC;AAC/C,cAAc,yBAAyB,CAAC;AACxC,cAAc,0BAA0B,CAAC;AACzC,cAAc,4BAA4B,CAAC;AAC3C,cAAc,iCAAiC,CAAC;AAChD,cAAc,4BAA4B,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,4BAA4B,CAAC;AAC3C,cAAc,6BAA6B,CAAC;AAC5C,cAAc,gCAAgC,CAAC;AAC/C,cAAc,yBAAyB,CAAC;AACxC,cAAc,0BAA0B,CAAC;AACzC,cAAc,4BAA4B,CAAC;AAC3C,cAAc,iCAAiC,CAAC;AAChD,cAAc,4BAA4B,CAAC;AAI3C,cAAc,gCAAgC,CAAC;AAC/C,cAAc,6BAA6B,CAAC;AAC5C,cAAc,iCAAiC,CAAC;AAChD,cAAc,6BAA6B,CAAC;AAC5C,cAAc,qCAAqC,CAAC;AACpD,cAAc,+BAA+B,CAAC;AAC9C,cAAc,iCAAiC,CAAC;AAChD,cAAc,6BAA6B,CAAC;AAC5C,cAAc,uCAAuC,CAAC;AACtD,cAAc,oCAAoC,CAAC;AACnD,cAAc,2CAA2C,CAAC;AAC1D,cAAc,2CAA2C,CAAC;AAC1D,cAAc,sCAAsC,CAAC;AACrD,cAAc,4BAA4B,CAAC;AAC3C,cAAc,uCAAuC,CAAC;AACtD,cAAc,yBAAyB,CAAC;AACxC,cAAc,0CAA0C,CAAC;AACzD,cAAc,wCAAwC,CAAC;AACvD,cAAc,iCAAiC,CAAC;AAChD,cAAc,6CAA6C,CAAC;AAC5D,cAAc,oBAAoB,CAAC;AACnC,cAAc,mCAAmC,CAAC;AAClD,cAAc,2CAA2C,CAAC;AAC1D,cAAc,wCAAwC,CAAC;AACvD,cAAc,+CAA+C,CAAC;AAC9D,cAAc,uCAAuC,CAAC;AACtD,cAAc,oCAAoC,CAAC;AACnD,cAAc,0CAA0C,CAAC;AACzD,cAAc,oCAAoC,CAAC;AACnD,cAAc,4BAA4B,CAAC;AAC3C,cAAc,wCAAwC,CAAC;AACvD,cAAc,oCAAoC,CAAC;AACnD,cAAc,+BAA+B,CAAC;AAC9C,cAAc,qCAAqC,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -22,6 +22,21 @@ __exportStar(require("./graphql/graphql.module"), exports);
|
|
|
22
22
|
__exportStar(require("./graphql/generic.resolver"), exports);
|
|
23
23
|
__exportStar(require("./tasks/session-cleanup.service"), exports);
|
|
24
24
|
__exportStar(require("./modules/auth/auth.module"), exports);
|
|
25
|
+
// Auth building blocks — exported so consumers can extend/override them
|
|
26
|
+
// (custom AuthController, widened RegisterDto, custom Passport strategy) without
|
|
27
|
+
// deep-importing from `dist/`. See docs/authentication.md.
|
|
28
|
+
__exportStar(require("./modules/auth/auth.controller"), exports);
|
|
29
|
+
__exportStar(require("./modules/auth/auth.service"), exports);
|
|
30
|
+
__exportStar(require("./modules/auth/dto/register.dto"), exports);
|
|
31
|
+
__exportStar(require("./modules/auth/dto/user.dto"), exports);
|
|
32
|
+
__exportStar(require("./modules/auth/auth-field.decorator"), exports);
|
|
33
|
+
__exportStar(require("./modules/auth/local.strategy"), exports);
|
|
34
|
+
__exportStar(require("./modules/auth/local-auth.guard"), exports);
|
|
35
|
+
__exportStar(require("./modules/auth/jwt.strategy"), exports);
|
|
36
|
+
__exportStar(require("./modules/auth/refresh-token.strategy"), exports);
|
|
37
|
+
__exportStar(require("./modules/auth/refresh-token.guard"), exports);
|
|
38
|
+
__exportStar(require("./modules/auth/session/session-serializer"), exports);
|
|
39
|
+
__exportStar(require("./modules/auth/session/session-auth.guard"), exports);
|
|
25
40
|
__exportStar(require("./modules/auth/session/session-store"), exports);
|
|
26
41
|
__exportStar(require("./common/guards/rbac.guard"), exports);
|
|
27
42
|
__exportStar(require("./common/guards/user-population.guard"), exports);
|
|
@@ -1,3 +1,19 @@
|
|
|
1
|
+
import { DynamicModule } from '@nestjs/common';
|
|
1
2
|
export declare class AuthModule {
|
|
3
|
+
/**
|
|
4
|
+
* Wires the auth module. **Always use `AuthModule.forRoot()`** — importing the
|
|
5
|
+
* bare `AuthModule` registers nothing (all wiring is here so the controller
|
|
6
|
+
* can be conditionally omitted).
|
|
7
|
+
*
|
|
8
|
+
* `AuthModule.forRoot()` registers the built-in `AuthController` (default).
|
|
9
|
+
* `AuthModule.forRoot({ controller: false })` registers every auth provider
|
|
10
|
+
* globally (AuthService, Jwt, Passport, strategies, guards) but NOT the
|
|
11
|
+
* controller — so you can register your OWN controller (e.g. one that
|
|
12
|
+
* `extends AuthController`) at the same `/auth` prefix with no duplicate
|
|
13
|
+
* route. See docs/authentication.md.
|
|
14
|
+
*/
|
|
15
|
+
static forRoot(options?: {
|
|
16
|
+
controller?: boolean;
|
|
17
|
+
}): DynamicModule;
|
|
2
18
|
}
|
|
3
19
|
//# sourceMappingURL=auth.module.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth.module.d.ts","sourceRoot":"","sources":["../../../src/modules/auth/auth.module.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"auth.module.d.ts","sourceRoot":"","sources":["../../../src/modules/auth/auth.module.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,aAAa,EAAiB,MAAM,gBAAgB,CAAC;AAkC7D,qBAEa,UAAU;IACnB;;;;;;;;;;;OAWG;IACH,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE;QAAC,UAAU,CAAC,EAAE,OAAO,CAAA;KAAC,GAAG,aAAa;CAUlE"}
|
|
@@ -5,6 +5,7 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
|
|
|
5
5
|
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
6
|
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
7
|
};
|
|
8
|
+
var AuthModule_1;
|
|
8
9
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
10
|
exports.AuthModule = void 0;
|
|
10
11
|
const common_1 = require("@nestjs/common");
|
|
@@ -20,22 +21,50 @@ const jwt_auth_guard_1 = require("./jwt-auth.guard");
|
|
|
20
21
|
const authenticated_guard_1 = require("./authenticated.guard");
|
|
21
22
|
const jwt_strategy_1 = require("./jwt.strategy");
|
|
22
23
|
const refresh_token_strategy_1 = require("./refresh-token.strategy");
|
|
23
|
-
|
|
24
|
+
// All wiring lives in forRoot() so the controller can be conditionally omitted.
|
|
25
|
+
// The @Module() decorator is intentionally empty: NestJS *extends* (concatenates)
|
|
26
|
+
// the decorator's metadata onto a DynamicModule, so a controller declared there
|
|
27
|
+
// could never be removed by forRoot({ controller: false }).
|
|
28
|
+
const authImports = [
|
|
29
|
+
config_1.ConfigModule,
|
|
30
|
+
user_module_1.UserModule,
|
|
31
|
+
passport_1.PassportModule.register({ session: true }),
|
|
32
|
+
jwt_1.JwtModule.registerAsync({
|
|
33
|
+
imports: [config_1.ConfigModule],
|
|
34
|
+
useFactory: async (config) => ({
|
|
35
|
+
secret: config.get('JWT_SECRET'),
|
|
36
|
+
signOptions: { expiresIn: config.get('JWT_EXPIRES_IN', '60m') },
|
|
37
|
+
}),
|
|
38
|
+
inject: [config_1.ConfigService],
|
|
39
|
+
}),
|
|
40
|
+
];
|
|
41
|
+
const authProviders = [auth_service_1.AuthService, local_strategy_1.LocalStrategy, session_serializer_1.SessionSerializer, jwt_strategy_1.JwtStrategy, jwt_auth_guard_1.JwtAuthGuard, authenticated_guard_1.AuthenticatedGuard, refresh_token_strategy_1.RefreshTokenStrategy];
|
|
42
|
+
const authExports = [auth_service_1.AuthService, jwt_auth_guard_1.JwtAuthGuard, authenticated_guard_1.AuthenticatedGuard, jwt_1.JwtModule];
|
|
43
|
+
let AuthModule = exports.AuthModule = AuthModule_1 = class AuthModule {
|
|
44
|
+
/**
|
|
45
|
+
* Wires the auth module. **Always use `AuthModule.forRoot()`** — importing the
|
|
46
|
+
* bare `AuthModule` registers nothing (all wiring is here so the controller
|
|
47
|
+
* can be conditionally omitted).
|
|
48
|
+
*
|
|
49
|
+
* `AuthModule.forRoot()` registers the built-in `AuthController` (default).
|
|
50
|
+
* `AuthModule.forRoot({ controller: false })` registers every auth provider
|
|
51
|
+
* globally (AuthService, Jwt, Passport, strategies, guards) but NOT the
|
|
52
|
+
* controller — so you can register your OWN controller (e.g. one that
|
|
53
|
+
* `extends AuthController`) at the same `/auth` prefix with no duplicate
|
|
54
|
+
* route. See docs/authentication.md.
|
|
55
|
+
*/
|
|
56
|
+
static forRoot(options) {
|
|
57
|
+
return {
|
|
58
|
+
module: AuthModule_1,
|
|
59
|
+
global: true,
|
|
60
|
+
imports: authImports,
|
|
61
|
+
providers: authProviders,
|
|
62
|
+
controllers: options?.controller === false ? [] : [auth_controller_1.AuthController],
|
|
63
|
+
exports: authExports,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
24
66
|
};
|
|
25
|
-
exports.AuthModule = AuthModule = __decorate([
|
|
67
|
+
exports.AuthModule = AuthModule = AuthModule_1 = __decorate([
|
|
26
68
|
(0, common_1.Global)(),
|
|
27
|
-
(0, common_1.Module)({
|
|
28
|
-
imports: [config_1.ConfigModule, user_module_1.UserModule, passport_1.PassportModule.register({ session: true }),
|
|
29
|
-
jwt_1.JwtModule.registerAsync({
|
|
30
|
-
imports: [config_1.ConfigModule],
|
|
31
|
-
useFactory: async (config) => ({
|
|
32
|
-
secret: config.get('JWT_SECRET'),
|
|
33
|
-
signOptions: { expiresIn: config.get('JWT_EXPIRES_IN', '60m') },
|
|
34
|
-
}),
|
|
35
|
-
inject: [config_1.ConfigService],
|
|
36
|
-
}),],
|
|
37
|
-
providers: [auth_service_1.AuthService, local_strategy_1.LocalStrategy, session_serializer_1.SessionSerializer, jwt_strategy_1.JwtStrategy, jwt_auth_guard_1.JwtAuthGuard, authenticated_guard_1.AuthenticatedGuard, refresh_token_strategy_1.RefreshTokenStrategy],
|
|
38
|
-
controllers: [auth_controller_1.AuthController],
|
|
39
|
-
exports: [auth_service_1.AuthService, jwt_auth_guard_1.JwtAuthGuard, authenticated_guard_1.AuthenticatedGuard, jwt_1.JwtModule],
|
|
40
|
-
})
|
|
69
|
+
(0, common_1.Module)({})
|
|
41
70
|
], AuthModule);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"core.controller.d.ts","sourceRoot":"","sources":["../../../src/modules/core/core.controller.ts"],"names":[],"mappings":"AACA,OAAO,EAAC,WAAW,EAAC,MAAM,gBAAgB,CAAC;AAI3C,OAAO,EAAC,kBAAkB,EAAC,MAAM,yCAAyC,CAAC;
|
|
1
|
+
{"version":3,"file":"core.controller.d.ts","sourceRoot":"","sources":["../../../src/modules/core/core.controller.ts"],"names":[],"mappings":"AACA,OAAO,EAAC,WAAW,EAAC,MAAM,gBAAgB,CAAC;AAI3C,OAAO,EAAC,kBAAkB,EAAC,MAAM,yCAAyC,CAAC;AAQ3E,8BACsB,cAAc,CAAC,CAAC;IAM9B,SAAS,CAAC,QAAQ,CAAC,OAAO,EAAE,WAAW,CAAC,CAAC,CAAC;IAL9C,MAAM,KAAK,UAAU,IAAI,MAAM,CAE9B;IAED,SAAS,aACc,OAAO,EAAE,WAAW,CAAC,CAAC,CAAC;IAK9C,SAAS,CAAC,QAAQ,CAAC,kBAAkB,EAAG,kBAAkB,CAAC;IAIrD,OAAO;IAMP,OAAO,CAAc,EAAE,EAAE,MAAM;IAM/B,MAAM,CAAS,IAAI,EAAE,OAAO,CAAC,CAAC,CAAC,GAAG;QAAC,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAA;KAAC;IAiBtD,MAAM,CAAc,EAAE,EAAE,MAAM,EAAU,IAAI,EAAE,OAAO,CAAC,CAAC,CAAC;IAMxD,MAAM,CAAc,EAAE,EAAE,MAAM;CAGvC"}
|
|
@@ -39,33 +39,13 @@ let CoreController = exports.CoreController = CoreController_1 = class CoreContr
|
|
|
39
39
|
const model = this.constructor.entityName;
|
|
40
40
|
const rolePermissions = this.permissionsService.getPermissionsConfig()[model]?.[role];
|
|
41
41
|
const actionPermission = rolePermissions?.['create'];
|
|
42
|
-
if (typeof actionPermission !== 'string' && actionPermission) {
|
|
43
|
-
//
|
|
44
|
-
|
|
45
|
-
for (const field of (actionPermission.restrictedFields ?? [])) {
|
|
46
|
-
delete data[field];
|
|
47
|
-
}
|
|
48
|
-
if (actionPermission.setUserIdField) {
|
|
49
|
-
data[actionPermission.setUserIdField] = user.id;
|
|
50
|
-
}
|
|
42
|
+
if (typeof actionPermission !== 'string' && actionPermission?.setUserIdField) {
|
|
43
|
+
// Force the server-owned user id (so a client can't forge ownership).
|
|
44
|
+
data[actionPermission.setUserIdField] = user.id;
|
|
51
45
|
}
|
|
52
46
|
return this.service.create(data);
|
|
53
47
|
}
|
|
54
48
|
async update(id, data) {
|
|
55
|
-
const user = nestjs_request_context_1.RequestContext.currentContext.req.user;
|
|
56
|
-
const role = user?.role || 'GUEST';
|
|
57
|
-
const model = this.constructor.entityName;
|
|
58
|
-
const rolePermissions = this.permissionsService.getPermissionsConfig()[model]?.[role];
|
|
59
|
-
// The action key is 'updateMany' (matches @Permission('updateMany') and
|
|
60
|
-
// the proxy) — reading 'update' here meant restrictedFields never applied.
|
|
61
|
-
const actionPermission = rolePermissions?.['updateMany'];
|
|
62
|
-
let restrictedFields = [];
|
|
63
|
-
if (typeof actionPermission !== 'string' && actionPermission?.restrictedFields) {
|
|
64
|
-
restrictedFields = actionPermission.restrictedFields;
|
|
65
|
-
}
|
|
66
|
-
for (const field of restrictedFields) {
|
|
67
|
-
delete data[field];
|
|
68
|
-
}
|
|
69
49
|
return this.service.updateById(id, data);
|
|
70
50
|
}
|
|
71
51
|
async delete(id) {
|