@appxdigital/appx-core 0.1.123 → 0.1.125-beta.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/CHANGELOG.md +35 -0
- package/PRERELEASES.md +20 -0
- package/README.md +22 -1
- package/dist/common/interfaces/user.interface.d.ts +12 -0
- package/dist/common/interfaces/user.interface.d.ts.map +1 -1
- package/dist/common/types.d.ts +1 -1
- package/dist/common/types.d.ts.map +1 -1
- package/dist/common/utils/coerce-id.util.d.ts +30 -0
- package/dist/common/utils/coerce-id.util.d.ts.map +1 -0
- package/dist/common/utils/coerce-id.util.js +27 -0
- package/dist/config/generate-all.js +2 -2
- package/dist/config/generate-models.js +2 -2
- package/dist/config/generate-session-schema.js +18 -1
- package/dist/config/utils.d.ts +15 -0
- package/dist/config/utils.d.ts.map +1 -1
- package/dist/config/utils.js +49 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/modules/auth/auth.controller.d.ts.map +1 -1
- package/dist/modules/auth/auth.controller.js +4 -1
- package/dist/modules/auth/auth.service.d.ts +3 -2
- package/dist/modules/auth/auth.service.d.ts.map +1 -1
- package/dist/modules/auth/auth.service.js +3 -2
- package/dist/modules/auth/session/session-serializer.d.ts.map +1 -1
- package/dist/modules/auth/session/session-serializer.js +5 -1
- package/dist/modules/core/core.service.d.ts.map +1 -1
- package/dist/modules/core/core.service.js +5 -9
- package/docs/authentication.md +40 -0
- package/docs/limitations.md +1 -0
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -14,6 +14,41 @@ This project follows the `MAJOR.MINOR.PATCH` scheme in `package.json`.
|
|
|
14
14
|
|
|
15
15
|
---
|
|
16
16
|
|
|
17
|
+
## [0.1.125] — 2026-07-29
|
|
18
|
+
|
|
19
|
+
A string (uuid/cuid) `User.id` is now supported end-to-end, and the CLI's `create` gains a non-interactive mode plus far better failure diagnostics.
|
|
20
|
+
|
|
21
|
+
### String `User.id` support (library)
|
|
22
|
+
|
|
23
|
+
The framework was already id-type-agnostic in generic CRUD (it reads the PK type from the schema before coercing), but the auth module hardcoded `Int` in a few places — most importantly `SessionSerializer.deserializeUser`, which did `Number(userId)`: `NaN` for a uuid, so **session auth failed on every request** for string-id projects. All of it is fixed via one shared `coerceId(delegate, id)` helper (a `String` PK passes through, anything else is number-coerced):
|
|
24
|
+
|
|
25
|
+
- `SessionSerializer.deserializeUser` — was `Number(userId)`.
|
|
26
|
+
- `GET /auth/sessions/:userId` — was `parseInt(userId, 10)`, which silently truncates a uuid starting with digits (`'3f2a…'` → `3`, the **wrong user's sessions**).
|
|
27
|
+
- `getSessionsByUserId` / `revokeRefreshTokensForUser` accept `UserId` (`string | number`, exported); the `express-session` `SessionData.userId` augmentation is widened the same way.
|
|
28
|
+
- `coerceId` and `UserId` are exported from the package root for consumer overrides.
|
|
29
|
+
|
|
30
|
+
**Using a string id is a schema-only change** — set `User.id String @default(uuid())` and switch `Session.userId` / `UserRefreshToken.userId` to `String` with it. No code overrides needed. See the new section in [`docs/authentication.md`](./docs/authentication.md#using-a-string-uuid--cuid-userid). Int-keyed projects are unaffected (drop-in).
|
|
31
|
+
|
|
32
|
+
### CLI (`@appxdigital/appx-core-cli`)
|
|
33
|
+
|
|
34
|
+
- **Non-interactive `create`.** `appx-core create --yes` (or any value flag) skips the wizard — for CI, scripts, and non-TTY contexts. Unset values take the wizard's defaults; the database check fails fast with the reason instead of re-prompting. Flags: `--name`, `--db-provider`, `--db-host`, `--db-port`, `--db-user`, `--db-password`, `--db-name`, `--show-output`.
|
|
35
|
+
- **`create` failures now show the real error.** Hidden commands piped their output to nowhere, so an init failure surfaced as an opaque stack trace with `stdout/stderr: null`; the tail of the failing command's output is now printed.
|
|
36
|
+
- **Prisma is preflighted and invoked from the project.** After `npm install`, `create` verifies the project's own `node_modules/.bin/prisma` exists (an `~/.npmrc` with `legacy-peer-deps`/`omit=peer` silently skips the peer deps that carry it) and runs every migrate step via that explicit path — never a `PATH` lookup.
|
|
37
|
+
|
|
38
|
+
Drop-in: no changes required in existing projects.
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## [0.1.124] — 2026-07-23
|
|
43
|
+
|
|
44
|
+
### Fixed
|
|
45
|
+
|
|
46
|
+
- **`appx-core generate` and `generate models` now run the project's own Prisma.** Both shelled out to a bare `prisma generate`, which resolves from `PATH`. With the CLI installed **globally** the project's `node_modules/.bin` is not on `PATH`, so Prisma resolved to a *global* install — a different version than the project's — or was not found at all. The executable is now resolved inside the project (walking up from the working directory, so hoisted / monorepo layouts work), and its `.bin` is prepended to `PATH` so the generator binaries Prisma spawns (`prisma-nestjs-graphql`, …) also resolve from the project. A project with no local Prisma now fails with an actionable message instead of silently generating against a global one.
|
|
47
|
+
|
|
48
|
+
Drop-in: no changes required in your project.
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
17
52
|
## [0.1.123] — 2026-07-22
|
|
18
53
|
|
|
19
54
|
Two changes: the `generate` command is split into a deploy-safe pass + a module wizard, and generated CRUD DTOs now type each scalar column to match its Prisma model type.
|
package/PRERELEASES.md
CHANGED
|
@@ -31,6 +31,26 @@ Prereleases are excluded from normal semver range resolution, so a project on
|
|
|
31
31
|
|
|
32
32
|
---
|
|
33
33
|
|
|
34
|
+
## [0.1.125-beta.0] — library
|
|
35
|
+
|
|
36
|
+
Prerelease of the string-`User.id` support that ships in `[0.1.125]` — see
|
|
37
|
+
[`CHANGELOG.md`](./CHANGELOG.md#01125--2026-07-29) for the full notes. Same
|
|
38
|
+
content as the production cut; published to `beta` so a uuid-keyed project can
|
|
39
|
+
validate before promoting.
|
|
40
|
+
|
|
41
|
+
- Session auth, `GET /auth/sessions/:userId`, and refresh-token revocation now
|
|
42
|
+
coerce ids to the `User.id` type from your schema (`coerceId`, exported).
|
|
43
|
+
- `UserId` (`string | number`) exported; `SessionData.userId` widened.
|
|
44
|
+
- Int-keyed projects: drop-in, no changes.
|
|
45
|
+
|
|
46
|
+
## [cli-1.0.25-beta.0] — CLI
|
|
47
|
+
|
|
48
|
+
Prerelease of the CLI changes in `[0.1.125]`'s CLI section: non-interactive
|
|
49
|
+
`create --yes` (+ per-answer flags), failure output no longer swallowed, prisma
|
|
50
|
+
preflighted and run from the project's own `node_modules/.bin`. Also includes
|
|
51
|
+
the self-update mechanism and channel-matched library pinning shipped in
|
|
52
|
+
cli-1.0.23.
|
|
53
|
+
|
|
34
54
|
## [0.1.121-beta.16] — unreleased
|
|
35
55
|
|
|
36
56
|
**Fix — `AuthModule.forRoot({ controller: false })` now actually omits the
|
package/README.md
CHANGED
|
@@ -124,12 +124,22 @@ npm install -g @appxdigital/appx-core-cli
|
|
|
124
124
|
npm install -g @appxdigital/appx-core-cli@beta # CLI prerelease
|
|
125
125
|
```
|
|
126
126
|
|
|
127
|
-
`appx-core create` scaffolds a project
|
|
127
|
+
`appx-core create` pins the library to **the CLI's own channel** — a stable CLI scaffolds a project depending on the stable library, a beta CLI (`@beta`) one depending on the beta library. To move an existing project across channels, install it there directly:
|
|
128
128
|
|
|
129
129
|
```bash
|
|
130
130
|
npm install @appxdigital/appx-core@beta # inside your project
|
|
131
131
|
```
|
|
132
132
|
|
|
133
|
+
**Staying up to date (automatic).** The CLI keeps itself current. On each run it checks the registry in the background (throttled), and when a newer version is published **on your channel** it installs it before your *next* command and re-launches into the new binary — so a beta install stays on `beta`, a stable install stays on `latest`. The check never blocks your command, and a failed/offline check is silently ignored.
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
appx-core update # update now, on your current channel
|
|
137
|
+
appx-core update --channel beta # switch to the beta channel (or --channel production to switch back)
|
|
138
|
+
APPX_CORE_NO_AUTO_UPDATE=1 … # disable the background auto-update entirely
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Install logs (only written when an update runs) go to `~/.appx-core/last-install.log`.
|
|
142
|
+
|
|
133
143
|
### 2) Create a New Project
|
|
134
144
|
|
|
135
145
|
```bash
|
|
@@ -137,6 +147,17 @@ appx-core create
|
|
|
137
147
|
cd <your-project>
|
|
138
148
|
```
|
|
139
149
|
|
|
150
|
+
**Non-interactive (CI / scripts).** Passing `--yes` — or any value flag — skips the wizard entirely; anything not given uses the wizard's default. No prompt is ever opened, so it's safe without a TTY. The database check still runs and fails fast with the reason instead of re-prompting.
|
|
151
|
+
|
|
152
|
+
```bash
|
|
153
|
+
appx-core create --yes \
|
|
154
|
+
--name my-app \
|
|
155
|
+
--db-provider mysql --db-host 127.0.0.1 --db-port 3306 \
|
|
156
|
+
--db-user root --db-password secret --db-name my_app_db
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Flags: `--name`, `--db-provider mysql|postgresql`, `--db-host`, `--db-port`, `--db-user`, `--db-password`, `--db-name`, `--show-output`. File upload isn't configured on this path — run `appx-core setup:fileupload` inside the project afterwards.
|
|
160
|
+
|
|
140
161
|
### 3) Configure Environment
|
|
141
162
|
|
|
142
163
|
`appx-core create` **already writes a `.env`** at the project root, with secure
|
|
@@ -3,6 +3,18 @@ export interface UserCreateInput {
|
|
|
3
3
|
password: string;
|
|
4
4
|
name?: string;
|
|
5
5
|
}
|
|
6
|
+
/**
|
|
7
|
+
* The type of a user's primary key as accepted at API boundaries. `User.id` is a
|
|
8
|
+
* `String` (uuid/cuid) or an `Int` (autoincrement) depending on the consumer's
|
|
9
|
+
* schema, so anything that *accepts* a user id (route params, service methods,
|
|
10
|
+
* session data) should accept both. Framework code coerces to the concrete type
|
|
11
|
+
* at the data-access boundary via `coerceId`.
|
|
12
|
+
*
|
|
13
|
+
* Note `User.id` below stays `string`: the authenticated principal is treated as
|
|
14
|
+
* an opaque string id throughout auth/session, and widening it here would force
|
|
15
|
+
* casts in consumer override code. Use `UserId` for *inputs*, not the principal.
|
|
16
|
+
*/
|
|
17
|
+
export type UserId = string | number;
|
|
6
18
|
export interface User {
|
|
7
19
|
id: string;
|
|
8
20
|
email: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"user.interface.d.ts","sourceRoot":"","sources":["../../../src/common/interfaces/user.interface.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,eAAe;IAC5B,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,IAAI;IACjB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,aAAa,CAAC,EAAE,MAAM,CAAC;CAC1B"}
|
|
1
|
+
{"version":3,"file":"user.interface.d.ts","sourceRoot":"","sources":["../../../src/common/interfaces/user.interface.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,eAAe;IAC5B,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;;;;;;;;;GAUG;AACH,MAAM,MAAM,MAAM,GAAG,MAAM,GAAG,MAAM,CAAC;AAErC,MAAM,WAAW,IAAI;IACjB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,aAAa,CAAC,EAAE,MAAM,CAAC;CAC1B"}
|
package/dist/common/types.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/common/types.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,YAAY,EAAC,MAAM,gBAAgB,CAAC;AAC5C,OAAO,iBAAiB,CAAC;AAEzB,MAAM,MAAM,IAAI,CAAC,CAAC,IAAI,KAAK,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,CAAC,CAAC;AAEhD,OAAO,CAAC,MAAM,CAAC;IACX,UAAU,OAAO,CAAC;QACd,UAAU,OAAO;YACb,MAAM,CAAC,EAAE,YAAY,CAAC;YAEtB,KAAK,CAAC,IAAI,EAAE,GAAG,EAAE,IAAI,EAAE,CAAC,GAAG,EAAE,GAAG,KAAK,IAAI,GAAG,IAAI,CAAC;YAEjD,MAAM,CAAC,QAAQ,EAAE,CAAC,GAAG,EAAE,GAAG,KAAK,IAAI,GAAG,IAAI,CAAC;YAE3C,IAAI,CAAC,EAAE,IAAI,GAAG,SAAS,CAAC;SAC3B;QAED,UAAU,IAAK,SAAQ,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC;SACzC;KACJ;CACJ;AAED,OAAO,QAAQ,iBAAiB,CAAC;IAC7B,UAAU,WAAW;
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/common/types.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,YAAY,EAAC,MAAM,gBAAgB,CAAC;AAC5C,OAAO,iBAAiB,CAAC;AAEzB,MAAM,MAAM,IAAI,CAAC,CAAC,IAAI,KAAK,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,CAAC,CAAC;AAEhD,OAAO,CAAC,MAAM,CAAC;IACX,UAAU,OAAO,CAAC;QACd,UAAU,OAAO;YACb,MAAM,CAAC,EAAE,YAAY,CAAC;YAEtB,KAAK,CAAC,IAAI,EAAE,GAAG,EAAE,IAAI,EAAE,CAAC,GAAG,EAAE,GAAG,KAAK,IAAI,GAAG,IAAI,CAAC;YAEjD,MAAM,CAAC,QAAQ,EAAE,CAAC,GAAG,EAAE,GAAG,KAAK,IAAI,GAAG,IAAI,CAAC;YAE3C,IAAI,CAAC,EAAE,IAAI,GAAG,SAAS,CAAC;SAC3B;QAED,UAAU,IAAK,SAAQ,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC;SACzC;KACJ;CACJ;AAED,OAAO,QAAQ,iBAAiB,CAAC;IAC7B,UAAU,WAAW;QAIjB,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC;KAC3B;CACJ"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The shape we need off a Prisma model delegate: its runtime field metadata.
|
|
3
|
+
* A delegate (e.g. `prisma.user`) exposes `fields.id.typeName` — the scalar
|
|
4
|
+
* type name of the primary key ('String', 'Int', 'BigInt', …). This passes
|
|
5
|
+
* through the PrismaService proxy unchanged (the proxy only wraps method calls).
|
|
6
|
+
*/
|
|
7
|
+
export interface IdBearingDelegate {
|
|
8
|
+
fields?: {
|
|
9
|
+
id?: {
|
|
10
|
+
typeName?: string;
|
|
11
|
+
};
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Coerce an id — as it arrives over HTTP (route params) or from a session
|
|
16
|
+
* (always a string) — to the runtime type the model's primary key expects.
|
|
17
|
+
*
|
|
18
|
+
* String primary keys (uuid/cuid) pass through unchanged; every other PK type
|
|
19
|
+
* is converted with `Number()`, matching Prisma's expectation for `Int` ids.
|
|
20
|
+
* The type is read from the delegate's own field metadata, so this is correct
|
|
21
|
+
* whether `User.id` is `Int @default(autoincrement())` or `String @default(uuid())`.
|
|
22
|
+
*
|
|
23
|
+
* This is the single source of truth for id coercion — generic CRUD
|
|
24
|
+
* (`CoreService`) and the auth module both call it, so a string-keyed model
|
|
25
|
+
* behaves consistently everywhere. Do NOT reintroduce `Number(id)` / `parseInt`
|
|
26
|
+
* at call sites: `Number('3f2a…')` is `NaN` and `parseInt('3f2a…', 10)` is `3`,
|
|
27
|
+
* i.e. a silent wrong-row lookup for UUID ids.
|
|
28
|
+
*/
|
|
29
|
+
export declare function coerceId(delegate: IdBearingDelegate, id: string | number): string | number;
|
|
30
|
+
//# sourceMappingURL=coerce-id.util.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"coerce-id.util.d.ts","sourceRoot":"","sources":["../../../src/common/utils/coerce-id.util.ts"],"names":[],"mappings":"AAEA;;;;;GAKG;AACH,MAAM,WAAW,iBAAiB;IAC9B,MAAM,CAAC,EAAE;QAAC,EAAE,CAAC,EAAE;YAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;SAAC,CAAA;KAAC,CAAC;CACvC;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,QAAQ,CAAC,QAAQ,EAAE,iBAAiB,EAAE,EAAE,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,CAM1F"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.coerceId = void 0;
|
|
4
|
+
const common_1 = require("@nestjs/common");
|
|
5
|
+
/**
|
|
6
|
+
* Coerce an id — as it arrives over HTTP (route params) or from a session
|
|
7
|
+
* (always a string) — to the runtime type the model's primary key expects.
|
|
8
|
+
*
|
|
9
|
+
* String primary keys (uuid/cuid) pass through unchanged; every other PK type
|
|
10
|
+
* is converted with `Number()`, matching Prisma's expectation for `Int` ids.
|
|
11
|
+
* The type is read from the delegate's own field metadata, so this is correct
|
|
12
|
+
* whether `User.id` is `Int @default(autoincrement())` or `String @default(uuid())`.
|
|
13
|
+
*
|
|
14
|
+
* This is the single source of truth for id coercion — generic CRUD
|
|
15
|
+
* (`CoreService`) and the auth module both call it, so a string-keyed model
|
|
16
|
+
* behaves consistently everywhere. Do NOT reintroduce `Number(id)` / `parseInt`
|
|
17
|
+
* at call sites: `Number('3f2a…')` is `NaN` and `parseInt('3f2a…', 10)` is `3`,
|
|
18
|
+
* i.e. a silent wrong-row lookup for UUID ids.
|
|
19
|
+
*/
|
|
20
|
+
function coerceId(delegate, id) {
|
|
21
|
+
const idField = delegate?.fields?.id;
|
|
22
|
+
if (!idField) {
|
|
23
|
+
throw new common_1.BadRequestException(`Model does not have an 'id' field`);
|
|
24
|
+
}
|
|
25
|
+
return idField.typeName === 'String' ? String(id) : Number(id);
|
|
26
|
+
}
|
|
27
|
+
exports.coerceId = coerceId;
|
|
@@ -15,11 +15,11 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
15
15
|
* Scaffolding CRUD modules (which mutates code) is the separate, interactive
|
|
16
16
|
* `appx generate models` wizard.
|
|
17
17
|
*/
|
|
18
|
-
const child_process_1 = require("child_process");
|
|
19
18
|
const utils_1 = require("./utils");
|
|
20
19
|
const generate_dtos_1 = require("./generate-dtos");
|
|
21
20
|
console.log('Running Prisma Generate (client + GraphQL artifacts)...');
|
|
22
|
-
|
|
21
|
+
// Project-local prisma: the CLI is usually global, so PATH has no project .bin.
|
|
22
|
+
(0, utils_1.runProjectBin)('prisma', 'generate');
|
|
23
23
|
console.log('Generating DTO base classes...');
|
|
24
24
|
(0, generate_dtos_1.generateDtoBases)((0, utils_1.loadAllModels)());
|
|
25
25
|
console.log('Deploy-safe generation complete — src/generated only, no code changes.');
|
|
@@ -22,7 +22,6 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
22
22
|
* It first runs the deploy-safe pass (prisma generate + DTO bases), so the
|
|
23
23
|
* scaffolded resolver/controller imports resolve.
|
|
24
24
|
*/
|
|
25
|
-
const child_process_1 = require("child_process");
|
|
26
25
|
const utils_1 = require("./utils");
|
|
27
26
|
const generate_dtos_1 = require("./generate-dtos");
|
|
28
27
|
const generate_modules_1 = require("./generate-modules");
|
|
@@ -37,7 +36,8 @@ const dynamicImport = new Function('m', 'return import(m)');
|
|
|
37
36
|
async function main() {
|
|
38
37
|
// 1) Deploy-safe pass first — refresh client + GraphQL artifacts + DTO bases.
|
|
39
38
|
console.log('Running Prisma Generate (client + GraphQL artifacts)...');
|
|
40
|
-
|
|
39
|
+
// Project-local prisma: the CLI is usually global, so PATH has no project .bin.
|
|
40
|
+
(0, utils_1.runProjectBin)('prisma', 'generate');
|
|
41
41
|
(0, generate_dtos_1.generateDtoBases)((0, utils_1.loadAllModels)());
|
|
42
42
|
// 2) Candidates = non-framework models with no module yet. "Has a module"
|
|
43
43
|
// means either the canonical folder exists OR some existing module already
|
|
@@ -39,6 +39,22 @@ const doesSessionModelExist = () => {
|
|
|
39
39
|
}
|
|
40
40
|
return false;
|
|
41
41
|
};
|
|
42
|
+
/**
|
|
43
|
+
* Read the type of `User.id` from schema.prisma so the generated `Session.userId`
|
|
44
|
+
* foreign key matches it. A consumer who changes `User.id` to `String` (uuid/cuid)
|
|
45
|
+
* must not have an `Int` FK written back by tooling. Defaults to `Int` when the
|
|
46
|
+
* User model or its id can't be found.
|
|
47
|
+
*/
|
|
48
|
+
const getUserIdType = () => {
|
|
49
|
+
if (!fs.existsSync(schemaPath))
|
|
50
|
+
return 'Int';
|
|
51
|
+
const schemaContent = fs.readFileSync(schemaPath, 'utf-8');
|
|
52
|
+
const userModel = schemaContent.match(/model\s+User\s*{[\s\S]*?}/i);
|
|
53
|
+
if (!userModel)
|
|
54
|
+
return 'Int';
|
|
55
|
+
const idField = userModel[0].match(/^\s*id\s+(\w+)/m);
|
|
56
|
+
return idField ? idField[1] : 'Int';
|
|
57
|
+
};
|
|
42
58
|
/**
|
|
43
59
|
* Ensure that the session schema exists if it is not already defined in schema.prisma
|
|
44
60
|
*/
|
|
@@ -49,13 +65,14 @@ const ensureSessionSchemaExists = () => {
|
|
|
49
65
|
}
|
|
50
66
|
if (!fs.existsSync(sessionSchemaPath)) {
|
|
51
67
|
console.log('Session schema not found in session.prisma. Creating it...');
|
|
68
|
+
const userIdType = getUserIdType();
|
|
52
69
|
const sessionSchemaContent = `
|
|
53
70
|
model Session {
|
|
54
71
|
id String @id
|
|
55
72
|
sid String @unique
|
|
56
73
|
data String
|
|
57
74
|
expiresAt DateTime
|
|
58
|
-
userId
|
|
75
|
+
userId ${userIdType}?
|
|
59
76
|
}
|
|
60
77
|
`;
|
|
61
78
|
fs.writeFileSync(sessionSchemaPath, sessionSchemaContent);
|
package/dist/config/utils.d.ts
CHANGED
|
@@ -1,3 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Run a project-local CLI binary (e.g. `prisma`) from the PROJECT's node_modules.
|
|
3
|
+
*
|
|
4
|
+
* The `appx-core` CLI is normally installed **globally**, so the project's
|
|
5
|
+
* `node_modules/.bin` is not on PATH — a bare `prisma generate` would resolve to
|
|
6
|
+
* a global Prisma (a different version than the project's, or nothing at all).
|
|
7
|
+
* We resolve the executable inside the project and prepend its `.bin` to PATH so
|
|
8
|
+
* that anything the tool spawns — Prisma's generator binaries such as
|
|
9
|
+
* `prisma-nestjs-graphql` — also resolves from the project.
|
|
10
|
+
*
|
|
11
|
+
* Throws with an actionable message rather than silently falling back to a
|
|
12
|
+
* global install, since generating against the wrong Prisma version is exactly
|
|
13
|
+
* the failure this avoids.
|
|
14
|
+
*/
|
|
15
|
+
export declare const runProjectBin: (bin: string, args: string, cwd?: string) => void;
|
|
1
16
|
export declare const kebabToPascalCase: (str: string) => string;
|
|
2
17
|
export declare const pascalToKebabCase: (str: string) => string;
|
|
3
18
|
export declare const createFileIfNotExists: (filePath: string, content: string) => void;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../src/config/utils.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../src/config/utils.ts"],"names":[],"mappings":"AAuBA;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,aAAa,QAAS,MAAM,QAAQ,MAAM,QAAO,MAAM,KAAmB,IAatF,CAAC;AAEF,eAAO,MAAM,iBAAiB,QAAS,MAAM,WAK5C,CAAC;AAEF,eAAO,MAAM,iBAAiB,QAAS,MAAM,WAK5C,CAAC;AAEF,eAAO,MAAM,qBAAqB,aAAc,MAAM,WAAW,MAAM,SAStE,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,gBAAgB,QAGoD,CAAC;AAElF;;;;GAIG;AACH,eAAO,MAAM,kBAAkB,aAAc,MAAM,WAAW,MAAM,SAInE,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,gBAAgB,UAA0C,CAAC;AAExE;;;;;GAKG;AACH,eAAO,MAAM,WAAW,cAAe,MAAM,KAAG,MAAsC,CAAC;AAEvF;;;;;;GAMG;AACH,eAAO,MAAM,aAAa,QAAO,GAAG,EAInC,CAAC;AAEF,eAAO,MAAM,UAAU,QAAO,GAAG,EAAuE,CAAC;AAEzG,iFAAiF;AACjF,eAAO,MAAM,YAAY,cAAe,MAAM,KAAG,OAGhD,CAAC;AAEF;;;;;;;;GAQG;AACH,eAAO,MAAM,oBAAoB,aAAc,MAAM,EAAE,KAAG,IAAI,MAAM,CAQnE,CAAC;AAEF,6EAA6E;AAC7E,eAAO,MAAM,WAAW,QAAO,IAAI,MAAM,CAYxC,CAAC"}
|
package/dist/config/utils.js
CHANGED
|
@@ -23,9 +23,57 @@ var __importStar = (this && this.__importStar) || function (mod) {
|
|
|
23
23
|
return result;
|
|
24
24
|
};
|
|
25
25
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
26
|
-
exports.ownedModels = exports.modelsServedBySource = exports.moduleExists = exports.loadModels = exports.loadAllModels = exports.modelFolder = exports.FRAMEWORK_MODELS = exports.writeGeneratedFile = exports.GENERATED_BANNER = exports.createFileIfNotExists = exports.pascalToKebabCase = exports.kebabToPascalCase = void 0;
|
|
26
|
+
exports.ownedModels = exports.modelsServedBySource = exports.moduleExists = exports.loadModels = exports.loadAllModels = exports.modelFolder = exports.FRAMEWORK_MODELS = exports.writeGeneratedFile = exports.GENERATED_BANNER = exports.createFileIfNotExists = exports.pascalToKebabCase = exports.kebabToPascalCase = exports.runProjectBin = void 0;
|
|
27
27
|
const fs = __importStar(require("fs"));
|
|
28
28
|
const path = __importStar(require("path"));
|
|
29
|
+
const child_process_1 = require("child_process");
|
|
30
|
+
/**
|
|
31
|
+
* Locate a binary inside the project's own `node_modules/.bin`, walking up from
|
|
32
|
+
* `cwd` so hoisted / monorepo layouts resolve too. Returns null when the project
|
|
33
|
+
* doesn't provide it (i.e. only a global install would be found on PATH).
|
|
34
|
+
*/
|
|
35
|
+
const findProjectBin = (bin, cwd) => {
|
|
36
|
+
const name = process.platform === 'win32' ? `${bin}.cmd` : bin;
|
|
37
|
+
let dir = path.resolve(cwd);
|
|
38
|
+
// eslint-disable-next-line no-constant-condition
|
|
39
|
+
while (true) {
|
|
40
|
+
const binDir = path.join(dir, 'node_modules', '.bin');
|
|
41
|
+
const exe = path.join(binDir, name);
|
|
42
|
+
if (fs.existsSync(exe))
|
|
43
|
+
return { exe, binDir };
|
|
44
|
+
const parent = path.dirname(dir);
|
|
45
|
+
if (parent === dir)
|
|
46
|
+
return null;
|
|
47
|
+
dir = parent;
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
/**
|
|
51
|
+
* Run a project-local CLI binary (e.g. `prisma`) from the PROJECT's node_modules.
|
|
52
|
+
*
|
|
53
|
+
* The `appx-core` CLI is normally installed **globally**, so the project's
|
|
54
|
+
* `node_modules/.bin` is not on PATH — a bare `prisma generate` would resolve to
|
|
55
|
+
* a global Prisma (a different version than the project's, or nothing at all).
|
|
56
|
+
* We resolve the executable inside the project and prepend its `.bin` to PATH so
|
|
57
|
+
* that anything the tool spawns — Prisma's generator binaries such as
|
|
58
|
+
* `prisma-nestjs-graphql` — also resolves from the project.
|
|
59
|
+
*
|
|
60
|
+
* Throws with an actionable message rather than silently falling back to a
|
|
61
|
+
* global install, since generating against the wrong Prisma version is exactly
|
|
62
|
+
* the failure this avoids.
|
|
63
|
+
*/
|
|
64
|
+
const runProjectBin = (bin, args, cwd = process.cwd()) => {
|
|
65
|
+
const found = findProjectBin(bin, cwd);
|
|
66
|
+
if (!found) {
|
|
67
|
+
throw new Error(`Could not find "${bin}" in this project's node_modules. ` +
|
|
68
|
+
`Run "npm install" in the project root and try again.`);
|
|
69
|
+
}
|
|
70
|
+
(0, child_process_1.execSync)(`"${found.exe}" ${args}`.trim(), {
|
|
71
|
+
cwd,
|
|
72
|
+
stdio: 'inherit',
|
|
73
|
+
env: { ...process.env, PATH: `${found.binDir}${path.delimiter}${process.env.PATH ?? ''}` },
|
|
74
|
+
});
|
|
75
|
+
};
|
|
76
|
+
exports.runProjectBin = runProjectBin;
|
|
29
77
|
const kebabToPascalCase = (str) => {
|
|
30
78
|
return str
|
|
31
79
|
.split("-")
|
package/dist/index.d.ts
CHANGED
|
@@ -24,6 +24,8 @@ export * from './common/guards/user-population.guard';
|
|
|
24
24
|
export * from './modules/common.module';
|
|
25
25
|
export * from './common/interceptors/prisma.interceptor';
|
|
26
26
|
export * from './common/decorators/scalar-field.decorator';
|
|
27
|
+
export * from './common/utils/coerce-id.util';
|
|
28
|
+
export * from './common/interfaces/user.interface';
|
|
27
29
|
export * from './common/config/permissionsConfigTypes';
|
|
28
30
|
export * from './common/config/adminConfigType';
|
|
29
31
|
export * from './common/config/permissions.config.provider';
|
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;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,4CAA4C,CAAC;AAC3D,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"}
|
|
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,4CAA4C,CAAC;AAC3D,cAAc,+BAA+B,CAAC;AAC9C,cAAc,oCAAoC,CAAC;AACnD,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
|
@@ -43,6 +43,8 @@ __exportStar(require("./common/guards/user-population.guard"), exports);
|
|
|
43
43
|
__exportStar(require("./modules/common.module"), exports);
|
|
44
44
|
__exportStar(require("./common/interceptors/prisma.interceptor"), exports);
|
|
45
45
|
__exportStar(require("./common/decorators/scalar-field.decorator"), exports);
|
|
46
|
+
__exportStar(require("./common/utils/coerce-id.util"), exports);
|
|
47
|
+
__exportStar(require("./common/interfaces/user.interface"), exports);
|
|
46
48
|
__exportStar(require("./common/config/permissionsConfigTypes"), exports);
|
|
47
49
|
__exportStar(require("./common/config/adminConfigType"), exports);
|
|
48
50
|
__exportStar(require("./common/config/permissions.config.provider"), exports);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth.controller.d.ts","sourceRoot":"","sources":["../../../src/modules/auth/auth.controller.ts"],"names":[],"mappings":"AACA,OAAO,EAAC,WAAW,EAAC,MAAM,gBAAgB,CAAC;AAC3C,OAAO,EAAC,WAAW,EAAC,MAAM,oBAAoB,CAAC;AAG/C,OAAO,EAAC,OAAO,EAAE,QAAQ,EAAC,MAAM,SAAS,CAAC;AAI1C,qBACa,cAAc;IACX,SAAS,CAAC,QAAQ,CAAC,WAAW,EAAE,WAAW;gBAAxB,WAAW,EAAE,WAAW;IAGjD,QAAQ,CAAS,WAAW,EAAE,WAAW;;;;IAMzC,KAAK,CAAQ,GAAG,EAAE,OAAO,EAAS,GAAG,EAAE,QAAQ;IAW/C,MAAM,CAAQ,GAAG,EAAE,OAAO,EAAS,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC;IAoBhE,EAAE,CAAQ,GAAG,EAAE,OAAO;;;IAQtB,iBAAiB,CAAQ,GAAG,EAAE,OAAO;;;IAOrC,gBAAgB,CAAQ,GAAG,EAAE,OAAO,EAAS,GAAG,EAAE,QAAQ;IAM1D,cAAc,CAAkB,MAAM,EAAE,MAAM,EAAS,GAAG,EAAE,OAAO;;;
|
|
1
|
+
{"version":3,"file":"auth.controller.d.ts","sourceRoot":"","sources":["../../../src/modules/auth/auth.controller.ts"],"names":[],"mappings":"AACA,OAAO,EAAC,WAAW,EAAC,MAAM,gBAAgB,CAAC;AAC3C,OAAO,EAAC,WAAW,EAAC,MAAM,oBAAoB,CAAC;AAG/C,OAAO,EAAC,OAAO,EAAE,QAAQ,EAAC,MAAM,SAAS,CAAC;AAI1C,qBACa,cAAc;IACX,SAAS,CAAC,QAAQ,CAAC,WAAW,EAAE,WAAW;gBAAxB,WAAW,EAAE,WAAW;IAGjD,QAAQ,CAAS,WAAW,EAAE,WAAW;;;;IAMzC,KAAK,CAAQ,GAAG,EAAE,OAAO,EAAS,GAAG,EAAE,QAAQ;IAW/C,MAAM,CAAQ,GAAG,EAAE,OAAO,EAAS,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC;IAoBhE,EAAE,CAAQ,GAAG,EAAE,OAAO;;;IAQtB,iBAAiB,CAAQ,GAAG,EAAE,OAAO;;;IAOrC,gBAAgB,CAAQ,GAAG,EAAE,OAAO,EAAS,GAAG,EAAE,QAAQ;IAM1D,cAAc,CAAkB,MAAM,EAAE,MAAM,EAAS,GAAG,EAAE,OAAO;;;IAgBnE,oBAAoB,CACF,SAAS,EAAE,MAAM,EAC9B,GAAG,EAAE,OAAO,EACZ,GAAG,EAAE,QAAQ;IAgBlB,QAAQ,CAAQ,GAAG,EAAE,OAAO;;;;;IAM5B,aAAa,CAAQ,GAAG,EAAE,OAAO;;;;IAOjC,SAAS,CAAQ,GAAG,EAAE,OAAO;;;CAQtC"}
|
|
@@ -70,7 +70,10 @@ let AuthController = exports.AuthController = class AuthController {
|
|
|
70
70
|
if (req.user?.role !== 'ADMIN') {
|
|
71
71
|
throw new common_1.ForbiddenException('Access denied.');
|
|
72
72
|
}
|
|
73
|
-
|
|
73
|
+
// Pass the raw param through; the service coerces it to the User PK's
|
|
74
|
+
// actual type. `parseInt('3f2a…', 10)` was `3` — a silent wrong-user
|
|
75
|
+
// lookup for uuid ids — while `parseInt('f3a2…')` was `NaN`.
|
|
76
|
+
const sessions = await this.authService.getSessionsByUserId(userId);
|
|
74
77
|
return { sessions };
|
|
75
78
|
}
|
|
76
79
|
async closeSpecificSession(sessionId, req, res) {
|
|
@@ -4,6 +4,7 @@ import { PrismaService } from '../../prisma/prisma.service';
|
|
|
4
4
|
import { ConfigService } from '@nestjs/config';
|
|
5
5
|
import { Request, Response } from 'express';
|
|
6
6
|
import { JwtService } from "@nestjs/jwt";
|
|
7
|
+
import { UserId } from '../../common/interfaces/user.interface';
|
|
7
8
|
export declare class AuthService {
|
|
8
9
|
protected readonly userService: UserService;
|
|
9
10
|
protected readonly prisma: PrismaService;
|
|
@@ -24,7 +25,7 @@ export declare class AuthService {
|
|
|
24
25
|
getCurrentUser(req: Request): Promise<any>;
|
|
25
26
|
getActiveSessions(req: Request): Promise<any>;
|
|
26
27
|
closeAllUserSessions(req: Request, res: Response): Promise<void>;
|
|
27
|
-
getSessionsByUserId(userId:
|
|
28
|
+
getSessionsByUserId(userId: UserId): Promise<any>;
|
|
28
29
|
closeSpecificSession(sessionId: string): Promise<boolean>;
|
|
29
30
|
validateJwtPayload(payload: any): Promise<any>;
|
|
30
31
|
private generateTokens;
|
|
@@ -38,6 +39,6 @@ export declare class AuthService {
|
|
|
38
39
|
refresh_token: string;
|
|
39
40
|
user: any;
|
|
40
41
|
}>;
|
|
41
|
-
revokeRefreshTokensForUser(userId:
|
|
42
|
+
revokeRefreshTokensForUser(userId: UserId): Promise<void>;
|
|
42
43
|
}
|
|
43
44
|
//# sourceMappingURL=auth.service.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth.service.d.ts","sourceRoot":"","sources":["../../../src/modules/auth/auth.service.ts"],"names":[],"mappings":"AACA,OAAO,EAAC,WAAW,EAAC,MAAM,sBAAsB,CAAC;AACjD,OAAO,EAAC,WAAW,EAAC,MAAM,oBAAoB,CAAC;AAE/C,OAAO,EAAC,aAAa,EAAC,MAAM,6BAA6B,CAAC;AAC1D,OAAO,EAAC,aAAa,EAAC,MAAM,gBAAgB,CAAC;AAC7C,OAAO,EAAC,OAAO,EAAE,QAAQ,EAAC,MAAM,SAAS,CAAC;AAC1C,OAAO,EAAC,UAAU,EAAiB,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"auth.service.d.ts","sourceRoot":"","sources":["../../../src/modules/auth/auth.service.ts"],"names":[],"mappings":"AACA,OAAO,EAAC,WAAW,EAAC,MAAM,sBAAsB,CAAC;AACjD,OAAO,EAAC,WAAW,EAAC,MAAM,oBAAoB,CAAC;AAE/C,OAAO,EAAC,aAAa,EAAC,MAAM,6BAA6B,CAAC;AAC1D,OAAO,EAAC,aAAa,EAAC,MAAM,gBAAgB,CAAC;AAC7C,OAAO,EAAC,OAAO,EAAE,QAAQ,EAAC,MAAM,SAAS,CAAC;AAC1C,OAAO,EAAC,UAAU,EAAiB,MAAM,aAAa,CAAC;AAGvD,OAAO,EAAC,MAAM,EAAC,MAAM,wCAAwC,CAAC;AAI9D,qBACa,WAAW;IAIhB,SAAS,CAAC,QAAQ,CAAC,WAAW,EAAE,WAAW;IAC3C,SAAS,CAAC,QAAQ,CAAC,MAAM,EAAE,aAAa;IACxC,SAAS,CAAC,QAAQ,CAAC,UAAU,EAAE,UAAU;IACzC,SAAS,CAAC,QAAQ,CAAC,aAAa,EAAE,aAAa;IANnD,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAS;gBAGpB,WAAW,EAAE,WAAW,EACxB,MAAM,EAAE,aAAa,EACrB,UAAU,EAAE,UAAU,EACtB,aAAa,EAAE,aAAa;IAK7C,QAAQ,CAAC,WAAW,EAAE,WAAW;;;;IA2BjC,KAAK,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,GAAG,CAAA;KAAC,CAAC;IA8B1D,MAAM,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC;IAgBlD,YAAY,CACd,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,EAChB,aAAa,EAAE,MAAM,GACtB,OAAO,CAAC,GAAG,CAAC;IAuBT,cAAc,CAAC,GAAG,EAAE,OAAO;IAW3B,iBAAiB,CAAC,GAAG,EAAE,OAAO;IAW9B,oBAAoB,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ;IA0BhD,mBAAmB,CAAC,MAAM,EAAE,MAAM;IAMlC,oBAAoB,CAAC,SAAS,EAAE,MAAM;IAOtC,kBAAkB,CAAC,OAAO,EAAE,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC;YActC,cAAc;IAkCtB,aAAa,CAAC,yBAAyB,EAAE,GAAG,GAAG,OAAO,CAAC;QAAC,YAAY,EAAE,MAAM,CAAC;QAAC,aAAa,EAAE,MAAM,CAAA;KAAC,CAAC;IA4B3G,OAAO,CAAC,WAAW;IAUb,QAAQ,CAAC,iBAAiB,EAAE,GAAG,GAAG,OAAO,CAAC;QAAC,YAAY,EAAE,MAAM,CAAC;QAAC,aAAa,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,GAAG,CAAA;KAAC,CAAC;IAcnG,0BAA0B,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAUlE"}
|
|
@@ -40,6 +40,7 @@ const prisma_service_1 = require("../../prisma/prisma.service");
|
|
|
40
40
|
const config_1 = require("@nestjs/config");
|
|
41
41
|
const jwt_1 = require("@nestjs/jwt");
|
|
42
42
|
const cuid2_1 = require("@paralleldrive/cuid2");
|
|
43
|
+
const coerce_id_util_1 = require("../../common/utils/coerce-id.util");
|
|
43
44
|
let AuthService = exports.AuthService = class AuthService {
|
|
44
45
|
constructor(userService, prisma, jwtService, configService) {
|
|
45
46
|
this.userService = userService;
|
|
@@ -172,7 +173,7 @@ let AuthService = exports.AuthService = class AuthService {
|
|
|
172
173
|
}
|
|
173
174
|
async getSessionsByUserId(userId) {
|
|
174
175
|
return this.prisma.session.findMany({
|
|
175
|
-
where: { userId, expiresAt: { gte: new Date() } },
|
|
176
|
+
where: { userId: (0, coerce_id_util_1.coerceId)(this.prisma.user, userId), expiresAt: { gte: new Date() } },
|
|
176
177
|
});
|
|
177
178
|
}
|
|
178
179
|
async closeSpecificSession(sessionId) {
|
|
@@ -272,7 +273,7 @@ let AuthService = exports.AuthService = class AuthService {
|
|
|
272
273
|
}
|
|
273
274
|
async revokeRefreshTokensForUser(userId) {
|
|
274
275
|
await this.prisma.userRefreshToken.updateMany({
|
|
275
|
-
where: { userId: userId, revokedAt: null },
|
|
276
|
+
where: { userId: (0, coerce_id_util_1.coerceId)(this.prisma.user, userId), revokedAt: null },
|
|
276
277
|
data: { revokedAt: new Date() },
|
|
277
278
|
},
|
|
278
279
|
// @ts-ignore
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session-serializer.d.ts","sourceRoot":"","sources":["../../../../src/modules/auth/session/session-serializer.ts"],"names":[],"mappings":"AACA,OAAO,EAAC,kBAAkB,EAAC,MAAM,kBAAkB,CAAC;AACpD,OAAO,EAAC,aAAa,EAAC,MAAM,gCAAgC,CAAC;
|
|
1
|
+
{"version":3,"file":"session-serializer.d.ts","sourceRoot":"","sources":["../../../../src/modules/auth/session/session-serializer.ts"],"names":[],"mappings":"AACA,OAAO,EAAC,kBAAkB,EAAC,MAAM,kBAAkB,CAAC;AACpD,OAAO,EAAC,aAAa,EAAC,MAAM,gCAAgC,CAAC;AAG7D,qBACa,iBAAkB,SAAQ,kBAAkB;IACzC,OAAO,CAAC,MAAM;gBAAN,MAAM,EAAE,aAAa;IAIzC,aAAa,CAAC,IAAI,EAAE,GAAG,EAAE,IAAI,EAAE,QAAQ;IAIjC,eAAe,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,QAAQ;CAwBvD"}
|
|
@@ -13,6 +13,7 @@ exports.SessionSerializer = void 0;
|
|
|
13
13
|
const common_1 = require("@nestjs/common");
|
|
14
14
|
const passport_1 = require("@nestjs/passport");
|
|
15
15
|
const prisma_service_1 = require("../../../prisma/prisma.service");
|
|
16
|
+
const coerce_id_util_1 = require("../../../common/utils/coerce-id.util");
|
|
16
17
|
let SessionSerializer = exports.SessionSerializer = class SessionSerializer extends passport_1.PassportSerializer {
|
|
17
18
|
constructor(prisma) {
|
|
18
19
|
super();
|
|
@@ -27,7 +28,10 @@ let SessionSerializer = exports.SessionSerializer = class SessionSerializer exte
|
|
|
27
28
|
}
|
|
28
29
|
try {
|
|
29
30
|
const user = await this.prisma.user.findFirstOrThrow({
|
|
30
|
-
|
|
31
|
+
// Coerce to the User PK's actual type. `Number(userId)` here made
|
|
32
|
+
// session auth fail closed for string (uuid/cuid) ids — Number of
|
|
33
|
+
// a uuid is NaN, so findFirstOrThrow threw on every request.
|
|
34
|
+
where: { id: (0, coerce_id_util_1.coerceId)(this.prisma.user, userId) },
|
|
31
35
|
select: {
|
|
32
36
|
id: true,
|
|
33
37
|
role: true,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"core.service.d.ts","sourceRoot":"","sources":["../../../src/modules/core/core.service.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"core.service.d.ts","sourceRoot":"","sources":["../../../src/modules/core/core.service.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAC,YAAY,EAAC,MAAM,gBAAgB,CAAC;AAEjD,qBACa,WAAW,CAAC,CAAC;IAElB,SAAS,CAAC,QAAQ,CAAC,aAAa,EAAE,YAAY,CAAC,CAAC,CAAC;gBAA9B,aAAa,EAAE,YAAY,CAAC,CAAC,CAAC;IAIrD;;;;OAIG;IACG,OAAO,CAAC,MAAM,GAAE,GAAQ,GAAG,OAAO,CAAC,CAAC,EAAE,CAAC;IAQ7C;;;;;OAKG;IACG,OAAO,CAAC,KAAK,EAAE,GAAG,EAAE,OAAO,GAAE,GAAQ,GAAG,OAAO,CAAC,CAAC,CAAC;IAYxD;;;;OAIG;IACG,QAAQ,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,CAAC;IAUtC;;;;OAIG;IACG,MAAM,CAAC,IAAI,EAAE,GAAG,GAAG,OAAO,CAAC,CAAC,CAAC;IAQnC;;;;;OAKG;IACG,UAAU,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,EAAE,IAAI,EAAE,GAAG,GAAG,OAAO,CAAC,CAAC,CAAC;IAW5D;;;;OAIG;IACG,UAAU,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,CAAC,CAAC;IAejD,OAAO,CAAC,yBAAyB;CAGpC"}
|
|
@@ -12,6 +12,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
12
12
|
exports.CoreService = void 0;
|
|
13
13
|
const common_1 = require("@nestjs/common");
|
|
14
14
|
const error_handler_1 = require("../../common/utils/error-handler");
|
|
15
|
+
const coerce_id_util_1 = require("../../common/utils/coerce-id.util");
|
|
15
16
|
let CoreService = exports.CoreService = class CoreService {
|
|
16
17
|
constructor(modelDelegate) {
|
|
17
18
|
this.modelDelegate = modelDelegate;
|
|
@@ -105,17 +106,12 @@ let CoreService = exports.CoreService = class CoreService {
|
|
|
105
106
|
}
|
|
106
107
|
}
|
|
107
108
|
/*
|
|
108
|
-
* Generates a 'where' clause based on the model's ID field.
|
|
109
|
+
* Generates a 'where' clause based on the model's ID field. Coercion to the
|
|
110
|
+
* PK's concrete type (String passes through, otherwise Number) is centralised
|
|
111
|
+
* in `coerceId` and shared with the auth module.
|
|
109
112
|
*/
|
|
110
113
|
_generateWhereFromIdField(id) {
|
|
111
|
-
|
|
112
|
-
if (!idField) {
|
|
113
|
-
throw new common_1.BadRequestException(`Model does not have an 'id' field`);
|
|
114
|
-
}
|
|
115
|
-
if (idField.typeName != "String") {
|
|
116
|
-
id = Number(id);
|
|
117
|
-
}
|
|
118
|
-
return { id: id };
|
|
114
|
+
return { id: (0, coerce_id_util_1.coerceId)(this.modelDelegate, id) };
|
|
119
115
|
}
|
|
120
116
|
};
|
|
121
117
|
exports.CoreService = CoreService = __decorate([
|
package/docs/authentication.md
CHANGED
|
@@ -218,3 +218,43 @@ exported from `@appxdigital/appx-core`. To override inner functionality:
|
|
|
218
218
|
|
|
219
219
|
If a class you need to extend isn't exported, that's a framework gap — open it
|
|
220
220
|
rather than deep-importing from `dist/`.
|
|
221
|
+
|
|
222
|
+
## Using a string (UUID / cuid) `User.id`
|
|
223
|
+
|
|
224
|
+
The scaffold defaults to `User.id Int @default(autoincrement())`, but a **string
|
|
225
|
+
primary key is fully supported** — teams migrating in from Supabase, Firebase,
|
|
226
|
+
Hasura, etc. usually keep their existing UUIDs. The framework reads the id's
|
|
227
|
+
type from your schema and coerces ids to it at the data-access boundary
|
|
228
|
+
(`coerceId`), so generic CRUD, sessions, and the auth module all handle a string
|
|
229
|
+
`User.id` without any override.
|
|
230
|
+
|
|
231
|
+
To use it, change the primary key **and the two foreign keys that reference it**
|
|
232
|
+
together — they must match:
|
|
233
|
+
|
|
234
|
+
```prisma
|
|
235
|
+
model User {
|
|
236
|
+
id String @id @default(uuid()) // or @default(cuid())
|
|
237
|
+
// …
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
model Session {
|
|
241
|
+
// …
|
|
242
|
+
userId String? // was Int?
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
model UserRefreshToken {
|
|
246
|
+
// …
|
|
247
|
+
userId String // was Int
|
|
248
|
+
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
249
|
+
}
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
That's the whole change — no code overrides are required. A `prisma db pull`
|
|
253
|
+
against an existing UUID-keyed database already emits `String @id`, so the pulled
|
|
254
|
+
schema and the framework agree.
|
|
255
|
+
|
|
256
|
+
> **Why it's just the schema:** ids arrive over HTTP and in sessions as strings.
|
|
257
|
+
> The framework never hardcodes `Number(id)` — it converts based on the PK type,
|
|
258
|
+
> so a `String` id passes through untouched while an `Int` id is still parsed.
|
|
259
|
+
> Accept a user id as `UserId` (`string | number`) in your own signatures if you
|
|
260
|
+
> extend the auth service; it's exported from the package root.
|
package/docs/limitations.md
CHANGED
|
@@ -52,6 +52,7 @@ Generated CRUD accepts nested `create`/`connect` **one level deep only** — a `
|
|
|
52
52
|
- **No audit logging of denials.** A refused request returns `403` but emits no structured audit event; per-request diagnostics are opt-in via `prismaService.debugQueries(true)`.
|
|
53
53
|
- **Exposed models are a full bypass.** `@ExposeModels` / `@Permission(a, [models])` / `withExposedModels` disable row *and* field filtering for the listed models within that scope. Keep the scope as narrow as possible.
|
|
54
54
|
- **`connect` defaults to one rule per target model; override per relation when needed.** By default every foreign key that points at model `T` is authorized by the single `T.connect` rule for the role. When a specific relation needs a different (stricter) rule — "a class's teacher must be a teacher" — declare a **relation-scoped** `connect` on the source model (`Source[role].relations[field].connect`); it is ANDed with `T.connect`, or stands alone if `T` has none. See [permissions.md](./permissions.md#two-places-to-declare-a-connect-rule). A relation rule can only strengthen, never loosen, the target rule.
|
|
55
|
+
- **A string `User.id` needs its two foreign keys changed with it.** `User.id` may be `String` (uuid/cuid) instead of the scaffold's `Int` — the framework is id-type-agnostic and coerces accordingly (see [authentication.md](./authentication.md#using-a-string-uuid--cuid-userid)). But `Session.userId` and `UserRefreshToken.userId` must be switched to `String` in the same edit; leaving one as `Int` is a schema/relation mismatch Prisma will reject at migrate time. Change all three together.
|
|
55
56
|
- **The config is validated at boot; a bad config stops the app.** A create condition that references a relation, or a required foreign key whose target lacks a `connect` rule for a role that can create, is a startup error (not a runtime one). This is deliberate — the alternative is a silent runtime `403`. The error message names exactly what to fix.
|
|
56
57
|
|
|
57
58
|
---
|
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@appxdigital/appx-core",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.125-beta.0",
|
|
4
4
|
"description": "Appx Core is a library that provides a set of tools to help you build your application faster.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
7
7
|
"scripts": {
|
|
8
8
|
"build": "tsc",
|
|
9
|
-
"build:cli": "npm --prefix cli ci",
|
|
9
|
+
"build:cli": "node scripts/sync-cli-core-version.mjs && npm --prefix cli ci",
|
|
10
10
|
"build:all": "npm run build && npm run build:cli",
|
|
11
11
|
"db-pull": "npx prisma db pull && prisma-case-format --table-case pascal --field-case camel --enum-case pascal --file prisma/schema.prisma",
|
|
12
12
|
"test": "jest",
|