@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 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';
@@ -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;AAC3C,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"}
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":"AAcA,qBAea,UAAU;CACtB"}
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
- let AuthModule = exports.AuthModule = class AuthModule {
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.14",
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",