@appxdigital/appx-core 0.1.121-beta.14 → 0.1.121-beta.16
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 +12 -0
- package/PRERELEASES.md +41 -0
- 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/docs/README.md +1 -0
- package/docs/authentication.md +220 -0
- 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
|
|
|
@@ -132,6 +133,17 @@ async update(@Param('id') id: string, @Body() data: Update<Model>Dto) { return s
|
|
|
132
133
|
>
|
|
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).
|
|
134
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).
|
|
146
|
+
|
|
135
147
|
**5. Create-permission enforcement (behaviour change — review before upgrading).**
|
|
136
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.
|
|
137
149
|
- If you supplied arbitrary values for a field a `create` condition constrains (e.g. an owner id), those requests now return `403` unless the value matches — use `setUserIdField` to have the framework set it.
|
package/PRERELEASES.md
CHANGED
|
@@ -31,6 +31,47 @@ 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
|
+
|
|
34
75
|
## [0.1.121-beta.13] — unreleased
|
|
35
76
|
|
|
36
77
|
**Library.** **Relation-scoped `connect` rules.** A `connect` rule can now be
|
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);
|
package/docs/README.md
CHANGED
|
@@ -7,6 +7,7 @@ Authoritative usage documentation for `@appxdigital/appx-core`. **This folder is
|
|
|
7
7
|
- **[permissions.md](./permissions.md)** — the authorization model: the `@Permission` and `@ExposeModels` decorators, the `permissions.config.ts` shape, roles/actions, row-level `conditions`, field-level control, action fallbacks, and the `GUEST` (unauthenticated) flow.
|
|
8
8
|
- **[dtos.md](./dtos.md)** — the generated CRUD validation DTOs: their two-file structure, **how to remove a field** (`OmitType` / `@NoWrite`), how to add validation, and why nested relation writes don't belong on generic CRUD.
|
|
9
9
|
- **[data-access.md](./data-access.md)** — how you talk to the database: the Prisma proxy (`prisma.model.*`), how ABAC filtering and field omission are enforced on every call, the redirected single-record methods, relation behaviour, bypass hatches, and transactions.
|
|
10
|
+
- **[authentication.md](./authentication.md)** — the built-in `/auth` surface, and **how to override framework internals** (extend the exported `AuthController`/`AuthService`/`RegisterDto`/Passport strategies and register your own at the same prefix via `AuthModule.forRoot({ controller: false })`) — e.g. adding `name` to registration.
|
|
10
11
|
- **[graphql.md](./graphql.md)** — the **read-only** GraphQL API generated from your schema: queries, nested selections, and how ABAC filters them.
|
|
11
12
|
- **[limitations.md](./limitations.md)** — **known limitations and features that are NOT ready for use** (e.g. `aggregate`, GraphQL writes). Read this before relying on any method not covered in the guides above.
|
|
12
13
|
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
# Authentication & overriding framework internals
|
|
2
|
+
|
|
3
|
+
The framework ships a working auth surface. When you need to change part of it —
|
|
4
|
+
add a field to registration, add endpoints, swap a Passport strategy — you don't
|
|
5
|
+
fork it: you **extend the exported classes and register your own versions**. This
|
|
6
|
+
page shows the pattern (auth is the main example, but it applies to any exported
|
|
7
|
+
framework class).
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## What `AuthModule` provides
|
|
12
|
+
|
|
13
|
+
**Always wire it as `AuthModule.forRoot()`** — importing the bare `AuthModule`
|
|
14
|
+
registers nothing (all wiring lives in `forRoot()` so the controller can be
|
|
15
|
+
conditionally omitted; see the override section). `AuthModule.forRoot()` registers,
|
|
16
|
+
under the `/auth` prefix:
|
|
17
|
+
|
|
18
|
+
| Route | Guard | Body |
|
|
19
|
+
|---|---|---|
|
|
20
|
+
| `POST /auth/register` | — | `RegisterDto` (`email`, `password`) |
|
|
21
|
+
| `POST /auth/login` | `LocalAuthGuard` | credentials |
|
|
22
|
+
| `POST /auth/login/jwt` | `LocalAuthGuard` | credentials → tokens |
|
|
23
|
+
| `POST /auth/refresh` | `RefreshTokenGuard` | refresh token |
|
|
24
|
+
| `POST /auth/logout` · `logout/jwt` | `AuthenticatedGuard` | — |
|
|
25
|
+
| `GET /auth/me`, `GET/POST /auth/sessions…` | `AuthenticatedGuard` | — |
|
|
26
|
+
|
|
27
|
+
plus the providers (all **global**): `AuthService`, the Passport strategies
|
|
28
|
+
(`LocalStrategy`, `JwtStrategy`, `RefreshTokenStrategy`), the guards
|
|
29
|
+
(`LocalAuthGuard`, `JwtAuthGuard`, `AuthenticatedGuard`, `RefreshTokenGuard`,
|
|
30
|
+
`SessionAuthGuard`), and `SessionSerializer`.
|
|
31
|
+
|
|
32
|
+
**Registration is intentionally narrow.** `RegisterDto` accepts only `email` +
|
|
33
|
+
`password`, and `AuthService.register` creates the user with `BYPASS_FILTERING`
|
|
34
|
+
(it's an unauthenticated flow, so ABAC create-enforcement can't gate it). The
|
|
35
|
+
`RegisterDto` whitelist is therefore the *only* thing controlling which fields a
|
|
36
|
+
sign-up may set — which is exactly why widening it is a deliberate override, not
|
|
37
|
+
a config toggle.
|
|
38
|
+
|
|
39
|
+
All of these are exported from the package root — **import from
|
|
40
|
+
`@appxdigital/appx-core`, never from `@appxdigital/appx-core/dist/...`** (deep
|
|
41
|
+
paths are internal and can move between versions):
|
|
42
|
+
|
|
43
|
+
```ts
|
|
44
|
+
import {
|
|
45
|
+
AuthModule, AuthController, AuthService,
|
|
46
|
+
RegisterDto, UserDto, AuthField,
|
|
47
|
+
LocalStrategy, JwtStrategy, RefreshTokenStrategy,
|
|
48
|
+
} from '@appxdigital/appx-core';
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## The override pattern
|
|
54
|
+
|
|
55
|
+
To replace or extend an auth endpoint **at the same `/auth` prefix**:
|
|
56
|
+
|
|
57
|
+
1. **Extend** the framework class (controller / service / strategy).
|
|
58
|
+
2. Put your version in **your own module**.
|
|
59
|
+
3. Import **`AuthModule.forRoot({ controller: false })`** — this keeps every auth
|
|
60
|
+
provider global but drops the built-in `AuthController`, so *your* controller
|
|
61
|
+
owns `/auth` with no duplicate route.
|
|
62
|
+
4. Register your module in `AppModule` (in place of a bare `AuthModule`).
|
|
63
|
+
|
|
64
|
+
```ts
|
|
65
|
+
// app.module.ts
|
|
66
|
+
@Module({
|
|
67
|
+
imports: [
|
|
68
|
+
AppxCoreModule.forRoot(PermissionsConfig),
|
|
69
|
+
AuthModule.forRoot({ controller: false }), // providers only, no built-in controller
|
|
70
|
+
MyAuthModule, // your controller/service live here
|
|
71
|
+
// …
|
|
72
|
+
],
|
|
73
|
+
})
|
|
74
|
+
export class AppModule {}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
```ts
|
|
78
|
+
// my-auth.module.ts
|
|
79
|
+
@Module({
|
|
80
|
+
controllers: [MyAuthController],
|
|
81
|
+
providers: [/* MyAuthService, MyLocalStrategy, … as needed */],
|
|
82
|
+
})
|
|
83
|
+
export class MyAuthModule {}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
> **Why `forRoot({ controller: false })`?** If the built-in controller *and* your
|
|
87
|
+
> own `/auth` controller are both registered, you get two handlers for the same
|
|
88
|
+
> routes (Express runs the first-registered, so the base `RegisterDto` would win
|
|
89
|
+
> and reject your extra fields). `forRoot({ controller: false })` drops the built-in
|
|
90
|
+
> controller while keeping every provider global, so your controller takes over
|
|
91
|
+
> cleanly. Use plain `AuthModule.forRoot()` (no options) when you're *not*
|
|
92
|
+
> overriding the controller.
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
## Recipe 1 — add a field to registration (e.g. `name`)
|
|
97
|
+
|
|
98
|
+
Extend `RegisterDto`, then override the inherited `register` handler:
|
|
99
|
+
|
|
100
|
+
```ts
|
|
101
|
+
// register-local.dto.ts
|
|
102
|
+
import { RegisterDto } from '@appxdigital/appx-core';
|
|
103
|
+
import { IsOptional, IsString } from 'class-validator';
|
|
104
|
+
|
|
105
|
+
export class RegisterLocalDto extends RegisterDto {
|
|
106
|
+
@IsOptional()
|
|
107
|
+
@IsString()
|
|
108
|
+
name?: string;
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
```ts
|
|
113
|
+
// my-auth.controller.ts
|
|
114
|
+
import { Body, Controller, Post } from '@nestjs/common';
|
|
115
|
+
import { AuthController } from '@appxdigital/appx-core';
|
|
116
|
+
import { RegisterLocalDto } from './register-local.dto';
|
|
117
|
+
|
|
118
|
+
@Controller('auth')
|
|
119
|
+
export class MyAuthController extends AuthController {
|
|
120
|
+
@Post('register')
|
|
121
|
+
async register(@Body() dto: RegisterLocalDto) {
|
|
122
|
+
return this.authService.register(dto); // authService is `protected` on AuthController
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
`AuthService.register` spreads the DTO straight into the user create, so `name`
|
|
128
|
+
persists (your `User` model must have the column). Everything else on
|
|
129
|
+
`AuthController` — login, refresh, sessions — is inherited unchanged. The global
|
|
130
|
+
validation pipe now validates the body against `RegisterLocalDto`, so `name` is
|
|
131
|
+
accepted and any *other* unknown field still `400`s.
|
|
132
|
+
|
|
133
|
+
> Keep registration a **narrow allowlist** — only add fields a self-service
|
|
134
|
+
> sign-up should legitimately set. Never widen it to privileged columns (`role`,
|
|
135
|
+
> tenant, ownership ids); mark those `/// @NoWrite` and set them server-side.
|
|
136
|
+
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
## Recipe 2 — extend the service (add methods / change behaviour)
|
|
140
|
+
|
|
141
|
+
Extend `AuthService` and forward its constructor. Its signature is
|
|
142
|
+
`(userService, prisma, jwtService, configService)`:
|
|
143
|
+
|
|
144
|
+
```ts
|
|
145
|
+
import { Injectable } from '@nestjs/common';
|
|
146
|
+
import { AuthService, PrismaService } from '@appxdigital/appx-core';
|
|
147
|
+
import { UserService } from '../user/user.service';
|
|
148
|
+
import { ConfigService } from '@nestjs/config';
|
|
149
|
+
import { JwtService } from '@nestjs/jwt';
|
|
150
|
+
|
|
151
|
+
@Injectable()
|
|
152
|
+
export class MyAuthService extends AuthService {
|
|
153
|
+
constructor(
|
|
154
|
+
userService: UserService,
|
|
155
|
+
prisma: PrismaService,
|
|
156
|
+
jwtService: JwtService,
|
|
157
|
+
configService: ConfigService,
|
|
158
|
+
) {
|
|
159
|
+
super(userService, prisma, jwtService, configService);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async handleForgotPassword(email: string) { /* … */ }
|
|
163
|
+
}
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
Provide `MyAuthService` in your module and inject it in your controller
|
|
167
|
+
(`super(myAuthService)` if the controller extends `AuthController`). Because you
|
|
168
|
+
imported `AuthModule.forRoot({ controller: false })`, `UserService`, `JwtService`,
|
|
169
|
+
`PrismaService`, and `ConfigService` are all already available to inject.
|
|
170
|
+
|
|
171
|
+
---
|
|
172
|
+
|
|
173
|
+
## Recipe 3 — override a Passport strategy
|
|
174
|
+
|
|
175
|
+
The built-in `LocalStrategy` reads the username/password field names from
|
|
176
|
+
`@AuthField(...)` metadata on `UserDto` and delegates to
|
|
177
|
+
`AuthService.validateUser`. To change credential validation, extend it (or
|
|
178
|
+
`JwtStrategy` / `RefreshTokenStrategy`) and provide your version:
|
|
179
|
+
|
|
180
|
+
```ts
|
|
181
|
+
import { Injectable } from '@nestjs/common';
|
|
182
|
+
import { LocalStrategy } from '@appxdigital/appx-core';
|
|
183
|
+
|
|
184
|
+
@Injectable()
|
|
185
|
+
export class MyLocalStrategy extends LocalStrategy {
|
|
186
|
+
async validate(username: string, password: string) {
|
|
187
|
+
// custom checks, then fall back to the framework behaviour:
|
|
188
|
+
return super.validate(username, password);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
```ts
|
|
194
|
+
@Module({
|
|
195
|
+
controllers: [MyAuthController],
|
|
196
|
+
providers: [MyLocalStrategy], // Passport picks up the last-registered strategy of this name
|
|
197
|
+
})
|
|
198
|
+
export class MyAuthModule {}
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
> Passport registers strategies by name. Providing your own `LocalStrategy`
|
|
202
|
+
> subclass in your module (with `AuthModule.forRoot({ controller: false })` for
|
|
203
|
+
> the rest) makes yours the active `local` strategy.
|
|
204
|
+
|
|
205
|
+
---
|
|
206
|
+
|
|
207
|
+
## The general principle
|
|
208
|
+
|
|
209
|
+
This isn't auth-specific. Every framework class you might need to specialise is
|
|
210
|
+
exported from `@appxdigital/appx-core`. To override inner functionality:
|
|
211
|
+
|
|
212
|
+
1. **Extend** the exported class.
|
|
213
|
+
2. **Provide** your subclass in your own module (a subclass provider shadows the
|
|
214
|
+
base where you inject it).
|
|
215
|
+
3. When the framework module would *also* register the thing you're replacing
|
|
216
|
+
(a controller, a route), import it in a mode that omits that piece — e.g.
|
|
217
|
+
`AuthModule.forRoot({ controller: false })` — so there's no duplicate.
|
|
218
|
+
|
|
219
|
+
If a class you need to extend isn't exported, that's a framework gap — open it
|
|
220
|
+
rather than deep-importing from `dist/`.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@appxdigital/appx-core",
|
|
3
|
-
"version": "0.1.121-beta.
|
|
3
|
+
"version": "0.1.121-beta.16",
|
|
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",
|