@simple-auth-kit/cli 1.4.3 → 1.5.1
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/lib/copy.ts +1 -1
- package/package.json +1 -1
- package/registry/combos/express-drizzle/shared/src/{auth-core-error.middleware.ts → middleware/auth-core-error.middleware.ts} +17 -4
- package/registry/combos/express-drizzle/shared/src/{auth.middleware.ts → middleware/auth.middleware.ts} +23 -7
- package/registry/combos/{express-prisma/shared/src → express-drizzle/shared/src/middleware}/response-envelope.middleware.ts +15 -3
- package/registry/combos/express-drizzle/shared/src/{oauth.repository.ts → repositories/oauth.repository.ts} +32 -9
- package/registry/combos/express-drizzle/shared/src/repositories/password-reset.repository.ts +59 -0
- package/registry/combos/express-drizzle/shared/src/{session.repository.ts → repositories/session.repository.ts} +52 -14
- package/registry/combos/express-drizzle/shared/src/{two-factor.repository.ts → repositories/two-factor.repository.ts} +20 -6
- package/registry/combos/express-drizzle/shared/src/request-context.ts +1 -1
- package/registry/combos/express-drizzle/shared/src/routers/auth.router.ts +395 -0
- package/registry/combos/express-drizzle/variants/base/src/create-auth-app.ts +52 -19
- package/registry/combos/express-drizzle/variants/base/src/{authz.middleware.ts → middleware/authz.middleware.ts} +15 -7
- package/registry/combos/express-drizzle/variants/base/src/{audit-log.repository.ts → repositories/audit-log.repository.ts} +28 -10
- package/registry/combos/express-drizzle/variants/base/src/{rbac.repository.ts → repositories/rbac.repository.ts} +239 -48
- package/registry/combos/express-drizzle/variants/base/src/routers/admin.router.ts +497 -0
- package/registry/combos/express-drizzle/variants/base/src/{auth.service.ts → services/auth.service.ts} +480 -109
- package/registry/combos/express-drizzle/variants/workspaces/src/create-auth-app.ts +69 -23
- package/registry/combos/express-drizzle/variants/workspaces/src/{authz.middleware.ts → middleware/authz.middleware.ts} +32 -12
- package/registry/combos/express-drizzle/variants/workspaces/src/openapi-workspace.ts +147 -28
- package/registry/combos/express-drizzle/variants/workspaces/src/{audit-log.repository.ts → repositories/audit-log.repository.ts} +30 -11
- package/registry/combos/express-drizzle/variants/workspaces/src/{rbac.repository.ts → repositories/rbac.repository.ts} +319 -70
- package/registry/combos/express-drizzle/variants/workspaces/src/{workspace.repository.ts → repositories/workspace.repository.ts} +127 -30
- package/registry/combos/express-drizzle/variants/workspaces/src/routers/admin.router.ts +534 -0
- package/registry/combos/express-drizzle/variants/workspaces/src/routers/workspace.router.ts +194 -0
- package/registry/combos/express-drizzle/variants/workspaces/src/{auth.service.ts → services/auth.service.ts} +544 -122
- package/registry/combos/express-prisma/shared/src/{auth-core-error.middleware.ts → middleware/auth-core-error.middleware.ts} +17 -4
- package/registry/combos/express-prisma/shared/src/{auth.middleware.ts → middleware/auth.middleware.ts} +19 -6
- package/registry/combos/{express-drizzle/shared/src → express-prisma/shared/src/middleware}/response-envelope.middleware.ts +15 -3
- package/registry/combos/express-prisma/shared/src/{oauth.repository.ts → repositories/oauth.repository.ts} +27 -9
- package/registry/combos/express-prisma/shared/src/repositories/password-reset.repository.ts +55 -0
- package/registry/combos/express-prisma/shared/src/{session.repository.ts → repositories/session.repository.ts} +30 -9
- package/registry/combos/express-prisma/shared/src/{two-factor.repository.ts → repositories/two-factor.repository.ts} +11 -4
- package/registry/combos/express-prisma/shared/src/request-context.ts +1 -1
- package/registry/combos/express-prisma/shared/src/routers/auth.router.ts +398 -0
- package/registry/combos/express-prisma/variants/base/src/create-auth-app.ts +56 -21
- package/registry/combos/express-prisma/variants/base/src/{authz.middleware.ts → middleware/authz.middleware.ts} +15 -7
- package/registry/combos/express-prisma/variants/base/src/rbac.defaults.ts +66 -18
- package/registry/combos/express-prisma/variants/base/src/{audit-log.repository.ts → repositories/audit-log.repository.ts} +16 -5
- package/registry/combos/express-prisma/variants/base/src/{rbac.repository.ts → repositories/rbac.repository.ts} +199 -48
- package/registry/combos/express-prisma/variants/base/src/routers/admin.router.ts +508 -0
- package/registry/combos/express-prisma/variants/base/src/seed.ts +77 -19
- package/registry/combos/express-prisma/variants/base/src/{auth.service.ts → services/auth.service.ts} +454 -105
- package/registry/combos/express-prisma/variants/workspaces/src/create-auth-app.ts +80 -25
- package/registry/combos/express-prisma/variants/workspaces/src/{authz.middleware.ts → middleware/authz.middleware.ts} +32 -12
- package/registry/combos/express-prisma/variants/workspaces/src/openapi-workspace.ts +147 -28
- package/registry/combos/express-prisma/variants/workspaces/src/rbac.defaults.ts +72 -20
- package/registry/combos/express-prisma/variants/workspaces/src/{audit-log.repository.ts → repositories/audit-log.repository.ts} +16 -5
- package/registry/combos/express-prisma/variants/workspaces/src/{rbac.repository.ts → repositories/rbac.repository.ts} +272 -64
- package/registry/combos/express-prisma/variants/workspaces/src/{workspace.repository.ts → repositories/workspace.repository.ts} +86 -23
- package/registry/combos/express-prisma/variants/workspaces/src/routers/admin.router.ts +545 -0
- package/registry/combos/express-prisma/variants/workspaces/src/routers/workspace.router.ts +194 -0
- package/registry/combos/express-prisma/variants/workspaces/src/seed.ts +108 -28
- package/registry/combos/express-prisma/variants/workspaces/src/{auth.service.ts → services/auth.service.ts} +518 -119
- package/registry/combos/nestjs-drizzle/shared/src/{auth.controller.ts → controllers/auth.controller.ts} +6 -6
- package/registry/combos/nestjs-drizzle/shared/src/{auth-core-error.filter.ts → filters/auth-core-error.filter.ts} +29 -5
- package/registry/combos/nestjs-drizzle/shared/src/{ability.guard.ts → guards/ability.guard.ts} +20 -7
- package/registry/combos/nestjs-drizzle/shared/src/{auth.guard.ts → guards/auth.guard.ts} +19 -7
- package/registry/combos/{nestjs-prisma/shared/src → nestjs-drizzle/shared/src/interceptors}/response.interceptor.ts +10 -2
- package/registry/combos/nestjs-drizzle/shared/src/repositories/oauth.repository.ts +63 -0
- package/registry/combos/nestjs-drizzle/shared/src/repositories/password-reset.repository.ts +59 -0
- package/registry/combos/nestjs-drizzle/shared/src/{session.repository.ts → repositories/session.repository.ts} +52 -14
- package/registry/combos/nestjs-drizzle/shared/src/{two-factor.repository.ts → repositories/two-factor.repository.ts} +21 -7
- package/registry/combos/nestjs-drizzle/shared/src/request-context.ts +1 -1
- package/registry/combos/nestjs-drizzle/test/prove-cycle.ts +1097 -307
- package/registry/combos/nestjs-drizzle/variants/base/src/auth.module.ts +44 -20
- package/registry/combos/nestjs-drizzle/variants/base/src/{admin.controller.ts → controllers/admin.controller.ts} +228 -62
- package/registry/combos/nestjs-drizzle/variants/base/src/{authz.guard.ts → guards/authz.guard.ts} +12 -5
- package/registry/combos/nestjs-drizzle/variants/base/src/{audit-log.repository.ts → repositories/audit-log.repository.ts} +27 -10
- package/registry/combos/nestjs-drizzle/variants/base/src/{rbac.repository.ts → repositories/rbac.repository.ts} +249 -51
- package/registry/combos/nestjs-drizzle/variants/base/src/{auth.service.ts → services/auth.service.ts} +499 -116
- package/registry/combos/nestjs-drizzle/variants/workspaces/src/auth.module.ts +48 -22
- package/registry/combos/nestjs-drizzle/variants/workspaces/src/{admin.controller.ts → controllers/admin.controller.ts} +269 -70
- package/registry/combos/nestjs-drizzle/variants/workspaces/src/{workspace.controller.ts → controllers/workspace.controller.ts} +93 -26
- package/registry/combos/nestjs-drizzle/variants/workspaces/src/{authz.guard.ts → guards/authz.guard.ts} +26 -8
- package/registry/combos/nestjs-drizzle/variants/workspaces/src/{audit-log.repository.ts → repositories/audit-log.repository.ts} +29 -11
- package/registry/combos/nestjs-drizzle/variants/workspaces/src/{rbac.repository.ts → repositories/rbac.repository.ts} +323 -71
- package/registry/combos/nestjs-drizzle/variants/workspaces/src/{workspace.repository.ts → repositories/workspace.repository.ts} +131 -30
- package/registry/combos/nestjs-drizzle/variants/workspaces/src/services/auth.service.ts +988 -0
- package/registry/combos/nestjs-prisma/shared/src/{auth.controller.ts → controllers/auth.controller.ts} +6 -6
- package/registry/combos/nestjs-prisma/shared/src/{auth-core-error.filter.ts → filters/auth-core-error.filter.ts} +29 -5
- package/registry/combos/nestjs-prisma/shared/src/{ability.guard.ts → guards/ability.guard.ts} +20 -7
- package/registry/combos/nestjs-prisma/shared/src/{auth.guard.ts → guards/auth.guard.ts} +19 -7
- package/registry/combos/{nestjs-drizzle/shared/src → nestjs-prisma/shared/src/interceptors}/response.interceptor.ts +10 -2
- package/registry/combos/nestjs-prisma/shared/src/{oauth.repository.ts → repositories/oauth.repository.ts} +25 -8
- package/registry/combos/nestjs-prisma/shared/src/repositories/password-reset.repository.ts +55 -0
- package/registry/combos/nestjs-prisma/shared/src/{session.repository.ts → repositories/session.repository.ts} +30 -9
- package/registry/combos/nestjs-prisma/shared/src/{two-factor.repository.ts → repositories/two-factor.repository.ts} +11 -4
- package/registry/combos/nestjs-prisma/shared/src/request-context.ts +1 -1
- package/registry/combos/nestjs-prisma/test/prove-cycle.ts +1097 -307
- package/registry/combos/nestjs-prisma/variants/base/src/auth.module.ts +58 -27
- package/registry/combos/nestjs-prisma/variants/base/src/{admin.controller.ts → controllers/admin.controller.ts} +469 -110
- package/registry/combos/nestjs-prisma/variants/base/src/{audit-log.gateway.ts → gateways/audit-log.gateway.ts} +22 -6
- package/registry/combos/nestjs-prisma/variants/base/src/{authz.guard.ts → guards/authz.guard.ts} +12 -5
- package/registry/combos/nestjs-prisma/variants/base/src/rbac.defaults.ts +97 -26
- package/registry/combos/nestjs-prisma/variants/base/src/{audit-log.repository.ts → repositories/audit-log.repository.ts} +16 -5
- package/registry/combos/nestjs-prisma/variants/base/src/{country.repository.ts → repositories/country.repository.ts} +98 -25
- package/registry/combos/nestjs-prisma/variants/base/src/{customer.repository.ts → repositories/customer.repository.ts} +106 -27
- package/registry/combos/nestjs-prisma/variants/base/src/{language.repository.ts → repositories/language.repository.ts} +93 -25
- package/registry/combos/nestjs-prisma/variants/base/src/{rbac.repository.ts → repositories/rbac.repository.ts} +229 -55
- package/registry/combos/nestjs-prisma/variants/base/src/seed.ts +77 -19
- package/registry/combos/nestjs-prisma/variants/base/src/services/auth.service.ts +1064 -0
- package/registry/combos/nestjs-prisma/variants/workspaces/src/auth.module.ts +54 -26
- package/registry/combos/nestjs-prisma/variants/workspaces/src/{admin.controller.ts → controllers/admin.controller.ts} +544 -128
- package/registry/combos/nestjs-prisma/variants/workspaces/src/{workspace.controller.ts → controllers/workspace.controller.ts} +93 -26
- package/registry/combos/nestjs-prisma/variants/workspaces/src/{authz.guard.ts → guards/authz.guard.ts} +26 -8
- package/registry/combos/nestjs-prisma/variants/workspaces/src/rbac.defaults.ts +103 -28
- package/registry/combos/nestjs-prisma/variants/workspaces/src/{audit-log.repository.ts → repositories/audit-log.repository.ts} +16 -5
- package/registry/combos/nestjs-prisma/variants/workspaces/src/{country.repository.ts → repositories/country.repository.ts} +127 -26
- package/registry/combos/nestjs-prisma/variants/workspaces/src/{customer.repository.ts → repositories/customer.repository.ts} +137 -28
- package/registry/combos/nestjs-prisma/variants/workspaces/src/repositories/language.repository.ts +243 -0
- package/registry/combos/nestjs-prisma/variants/workspaces/src/{rbac.repository.ts → repositories/rbac.repository.ts} +314 -74
- package/registry/combos/nestjs-prisma/variants/workspaces/src/{workspace.repository.ts → repositories/workspace.repository.ts} +94 -24
- package/registry/combos/nestjs-prisma/variants/workspaces/src/seed.ts +108 -28
- package/registry/combos/nestjs-prisma/variants/workspaces/src/services/auth.service.ts +961 -0
- package/registry.json +4 -4
- package/simple-auth-kit.ts +42 -11
- package/registry/combos/express-drizzle/shared/src/auth.router.ts +0 -266
- package/registry/combos/express-drizzle/shared/src/password-reset.repository.ts +0 -35
- package/registry/combos/express-drizzle/variants/base/src/admin.router.ts +0 -307
- package/registry/combos/express-drizzle/variants/workspaces/src/admin.router.ts +0 -323
- package/registry/combos/express-drizzle/variants/workspaces/src/workspace.router.ts +0 -123
- package/registry/combos/express-prisma/shared/src/auth.router.ts +0 -269
- package/registry/combos/express-prisma/shared/src/password-reset.repository.ts +0 -29
- package/registry/combos/express-prisma/variants/base/src/admin.router.ts +0 -314
- package/registry/combos/express-prisma/variants/workspaces/src/admin.router.ts +0 -330
- package/registry/combos/express-prisma/variants/workspaces/src/workspace.router.ts +0 -123
- package/registry/combos/nestjs-drizzle/shared/src/oauth.repository.ts +0 -36
- package/registry/combos/nestjs-drizzle/shared/src/password-reset.repository.ts +0 -29
- package/registry/combos/nestjs-drizzle/variants/workspaces/src/auth.service.ts +0 -553
- package/registry/combos/nestjs-prisma/shared/src/password-reset.repository.ts +0 -29
- package/registry/combos/nestjs-prisma/variants/base/src/auth.service.ts +0 -618
- package/registry/combos/nestjs-prisma/variants/workspaces/src/auth.service.ts +0 -509
- package/registry/combos/nestjs-prisma/variants/workspaces/src/language.repository.ts +0 -143
|
@@ -22,11 +22,11 @@ import {
|
|
|
22
22
|
ApiTags,
|
|
23
23
|
} from "@nestjs/swagger";
|
|
24
24
|
import type { Request } from "express";
|
|
25
|
-
import { AUTH_CONFIG, AuthConfig } from "
|
|
26
|
-
import { AuthGuard } from "
|
|
27
|
-
import { AuthService } from "
|
|
28
|
-
import { AuthzGuard } from "
|
|
29
|
-
import { Authenticated, Public } from "
|
|
25
|
+
import { AUTH_CONFIG, AuthConfig } from "../auth.config.js";
|
|
26
|
+
import { AuthGuard } from "../guards/auth.guard.js";
|
|
27
|
+
import { AuthService } from "../services/auth.service.js";
|
|
28
|
+
import { AuthzGuard } from "../guards/authz.guard.js";
|
|
29
|
+
import { Authenticated, Public } from "../route-tiers.js";
|
|
30
30
|
import {
|
|
31
31
|
AuthTokensDto,
|
|
32
32
|
ChangePasswordDto,
|
|
@@ -46,7 +46,7 @@ import {
|
|
|
46
46
|
SignupDto,
|
|
47
47
|
TwoFactorCodeDto,
|
|
48
48
|
UpdateUserDto,
|
|
49
|
-
} from "
|
|
49
|
+
} from "../dto/auth.dto.js";
|
|
50
50
|
|
|
51
51
|
function requireString(value: unknown, field: string): string {
|
|
52
52
|
if (typeof value !== "string" || value.length === 0)
|
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
ArgumentsHost,
|
|
3
|
+
Catch,
|
|
4
|
+
ExceptionFilter,
|
|
5
|
+
HttpException,
|
|
6
|
+
HttpStatus,
|
|
7
|
+
} from "@nestjs/common";
|
|
2
8
|
import { AuthCoreError } from "@/lib/auth/core/types.js";
|
|
3
9
|
|
|
4
10
|
/**
|
|
@@ -14,19 +20,37 @@ export class AuthCoreErrorFilter implements ExceptionFilter {
|
|
|
14
20
|
const res = host.switchToHttp().getResponse();
|
|
15
21
|
|
|
16
22
|
if (exception instanceof AuthCoreError) {
|
|
17
|
-
res
|
|
23
|
+
res
|
|
24
|
+
.status(HttpStatus.UNAUTHORIZED)
|
|
25
|
+
.json({
|
|
26
|
+
success: false,
|
|
27
|
+
statusCode: HttpStatus.UNAUTHORIZED,
|
|
28
|
+
code: exception.code,
|
|
29
|
+
message: exception.message,
|
|
30
|
+
});
|
|
18
31
|
return;
|
|
19
32
|
}
|
|
20
33
|
|
|
21
34
|
if (exception instanceof HttpException) {
|
|
22
35
|
const status = exception.getStatus();
|
|
23
36
|
const body = exception.getResponse();
|
|
24
|
-
const shaped =
|
|
25
|
-
|
|
37
|
+
const shaped =
|
|
38
|
+
typeof body === "string"
|
|
39
|
+
? { message: body }
|
|
40
|
+
: (body as Record<string, unknown>);
|
|
41
|
+
res
|
|
42
|
+
.status(status)
|
|
43
|
+
.json({ success: false, statusCode: status, ...shaped });
|
|
26
44
|
return;
|
|
27
45
|
}
|
|
28
46
|
|
|
29
47
|
// Anything not thrown deliberately — don't leak internals in the response.
|
|
30
|
-
res
|
|
48
|
+
res
|
|
49
|
+
.status(HttpStatus.INTERNAL_SERVER_ERROR)
|
|
50
|
+
.json({
|
|
51
|
+
success: false,
|
|
52
|
+
statusCode: HttpStatus.INTERNAL_SERVER_ERROR,
|
|
53
|
+
message: "internal server error",
|
|
54
|
+
});
|
|
31
55
|
}
|
|
32
56
|
}
|
package/registry/combos/nestjs-drizzle/shared/src/{ability.guard.ts → guards/ability.guard.ts}
RENAMED
|
@@ -1,7 +1,13 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
CanActivate,
|
|
3
|
+
ExecutionContext,
|
|
4
|
+
ForbiddenException,
|
|
5
|
+
Inject,
|
|
6
|
+
Injectable,
|
|
7
|
+
} from "@nestjs/common";
|
|
2
8
|
import { Reflector } from "@nestjs/core";
|
|
3
|
-
import { ABILITY_SUBJECT } from "
|
|
4
|
-
import { CHECK_ABILITY_KEY } from "
|
|
9
|
+
import { ABILITY_SUBJECT } from "../ability.js";
|
|
10
|
+
import { CHECK_ABILITY_KEY } from "../route-tiers.js";
|
|
5
11
|
|
|
6
12
|
/**
|
|
7
13
|
* The authorization boundary for tier-3 routes. This is the *only* thing standing between a
|
|
@@ -30,16 +36,23 @@ export class AbilityGuard implements CanActivate {
|
|
|
30
36
|
constructor(@Inject(Reflector) private readonly reflector: Reflector) {}
|
|
31
37
|
|
|
32
38
|
canActivate(context: ExecutionContext): boolean {
|
|
33
|
-
const required = this.reflector.getAllAndOverride<string[] | undefined>(
|
|
39
|
+
const required = this.reflector.getAllAndOverride<string[] | undefined>(
|
|
40
|
+
CHECK_ABILITY_KEY,
|
|
41
|
+
[context.getHandler(), context.getClass()],
|
|
42
|
+
);
|
|
34
43
|
if (!required?.length) {
|
|
35
|
-
throw new Error(
|
|
44
|
+
throw new Error(
|
|
45
|
+
`${context.getClass().name}.${context.getHandler().name} is guarded by AbilityGuard but declares no @CheckAbility`,
|
|
46
|
+
);
|
|
36
47
|
}
|
|
37
48
|
|
|
38
49
|
const ability = context.switchToHttp().getRequest().ability;
|
|
39
|
-
if (!ability)
|
|
50
|
+
if (!ability)
|
|
51
|
+
throw new ForbiddenException("no authorization context for this request");
|
|
40
52
|
|
|
41
53
|
for (const permission of required) {
|
|
42
|
-
if (!ability.can(permission, ABILITY_SUBJECT))
|
|
54
|
+
if (!ability.can(permission, ABILITY_SUBJECT))
|
|
55
|
+
throw new ForbiddenException(`missing permission: ${permission}`);
|
|
43
56
|
}
|
|
44
57
|
return true;
|
|
45
58
|
}
|
|
@@ -1,8 +1,14 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
CanActivate,
|
|
3
|
+
ExecutionContext,
|
|
4
|
+
Inject,
|
|
5
|
+
Injectable,
|
|
6
|
+
UnauthorizedException,
|
|
7
|
+
} from "@nestjs/common";
|
|
2
8
|
import { verifyAccessToken } from "@/lib/auth/core/token-service.js";
|
|
3
|
-
import { KeyProviderService } from "
|
|
4
|
-
import "
|
|
5
|
-
import { SessionRepository } from "
|
|
9
|
+
import { KeyProviderService } from "../key-provider.js";
|
|
10
|
+
import "../request-context.js";
|
|
11
|
+
import { SessionRepository } from "../repositories/session.repository.js";
|
|
6
12
|
|
|
7
13
|
/**
|
|
8
14
|
* Authentication only — proves who the caller is and populates `req.auth`. Authorization
|
|
@@ -19,15 +25,21 @@ export class AuthGuard implements CanActivate {
|
|
|
19
25
|
async canActivate(context: ExecutionContext): Promise<boolean> {
|
|
20
26
|
const req = context.switchToHttp().getRequest();
|
|
21
27
|
const header: string | undefined = req.headers["authorization"];
|
|
22
|
-
if (!header?.startsWith("Bearer "))
|
|
28
|
+
if (!header?.startsWith("Bearer "))
|
|
29
|
+
throw new UnauthorizedException("missing bearer token");
|
|
23
30
|
|
|
24
31
|
try {
|
|
25
32
|
req.auth = await verifyAccessToken(
|
|
26
|
-
{
|
|
33
|
+
{
|
|
34
|
+
secret: this.keys.secret,
|
|
35
|
+
isDenylisted: (jti) => this.sessions.isDenylisted(jti),
|
|
36
|
+
},
|
|
27
37
|
header.slice("Bearer ".length),
|
|
28
38
|
);
|
|
29
39
|
} catch (err) {
|
|
30
|
-
throw new UnauthorizedException(
|
|
40
|
+
throw new UnauthorizedException(
|
|
41
|
+
err instanceof Error ? err.message : "invalid access token",
|
|
42
|
+
);
|
|
31
43
|
}
|
|
32
44
|
return true;
|
|
33
45
|
}
|
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
CallHandler,
|
|
3
|
+
ExecutionContext,
|
|
4
|
+
Injectable,
|
|
5
|
+
NestInterceptor,
|
|
6
|
+
} from "@nestjs/common";
|
|
2
7
|
import { Observable, map } from "rxjs";
|
|
3
8
|
import type { Request, Response } from "express";
|
|
4
9
|
|
|
@@ -32,7 +37,10 @@ function deriveMessage(method: string): string {
|
|
|
32
37
|
|
|
33
38
|
@Injectable()
|
|
34
39
|
export class ResponseInterceptor<T> implements NestInterceptor<T, Envelope<T>> {
|
|
35
|
-
intercept(
|
|
40
|
+
intercept(
|
|
41
|
+
context: ExecutionContext,
|
|
42
|
+
next: CallHandler<T>,
|
|
43
|
+
): Observable<Envelope<T>> {
|
|
36
44
|
const http = context.switchToHttp();
|
|
37
45
|
const req = http.getRequest<Request>();
|
|
38
46
|
const res = http.getResponse<Response>();
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { BadRequestException, Inject, Injectable } from "@nestjs/common";
|
|
2
|
+
import { and, eq } from "drizzle-orm";
|
|
3
|
+
import type { OAuthStoreDeps } from "@/lib/auth/core/oauth.js";
|
|
4
|
+
import { DRIZZLE_DB, type Database } from "../db.js";
|
|
5
|
+
import { oauthAccounts, users } from "../schema.js";
|
|
6
|
+
import { toId } from "../id.helper.js";
|
|
7
|
+
|
|
8
|
+
@Injectable()
|
|
9
|
+
export class OAuthRepository implements OAuthStoreDeps {
|
|
10
|
+
constructor(@Inject(DRIZZLE_DB) private readonly db: Database) {}
|
|
11
|
+
|
|
12
|
+
async findAccountByProvider(
|
|
13
|
+
provider: string,
|
|
14
|
+
providerAccountId: string,
|
|
15
|
+
): Promise<{ userId: string } | null> {
|
|
16
|
+
const [row] = await this.db
|
|
17
|
+
.select()
|
|
18
|
+
.from(oauthAccounts)
|
|
19
|
+
.where(
|
|
20
|
+
and(
|
|
21
|
+
eq(oauthAccounts.provider, provider),
|
|
22
|
+
eq(oauthAccounts.providerAccountId, providerAccountId),
|
|
23
|
+
),
|
|
24
|
+
)
|
|
25
|
+
.limit(1);
|
|
26
|
+
return row ? { userId: row.userId.toString() } : null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async linkAccount(input: {
|
|
30
|
+
userId: string;
|
|
31
|
+
provider: string;
|
|
32
|
+
providerAccountId: string;
|
|
33
|
+
email?: string;
|
|
34
|
+
}): Promise<void> {
|
|
35
|
+
await this.db
|
|
36
|
+
.insert(oauthAccounts)
|
|
37
|
+
.values({ ...input, userId: toId(input.userId) });
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async findUserByVerifiedEmail(email: string): Promise<{ id: string } | null> {
|
|
41
|
+
const [row] = await this.db
|
|
42
|
+
.select()
|
|
43
|
+
.from(users)
|
|
44
|
+
.where(eq(users.email, email))
|
|
45
|
+
.limit(1);
|
|
46
|
+
return row ? { id: row.id.toString() } : null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** OAuth-only signup: no password is ever set, which is why `User.passwordHash` is nullable. */
|
|
50
|
+
async createUserFromOAuth(input: {
|
|
51
|
+
email?: string;
|
|
52
|
+
}): Promise<{ id: string }> {
|
|
53
|
+
if (!input.email)
|
|
54
|
+
throw new BadRequestException(
|
|
55
|
+
"OAuth provider did not return an email address",
|
|
56
|
+
);
|
|
57
|
+
const [row] = await this.db
|
|
58
|
+
.insert(users)
|
|
59
|
+
.values({ email: input.email, passwordHash: null })
|
|
60
|
+
.returning();
|
|
61
|
+
return { id: row.id.toString() };
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { Inject, Injectable } from "@nestjs/common";
|
|
2
|
+
import { eq } from "drizzle-orm";
|
|
3
|
+
import type { PasswordResetStoreDeps } from "@/lib/auth/core/password-reset.js";
|
|
4
|
+
import { DRIZZLE_DB, type Database } from "../db.js";
|
|
5
|
+
import { passwordResetTokens, users } from "../schema.js";
|
|
6
|
+
import { toId } from "../id.helper.js";
|
|
7
|
+
|
|
8
|
+
@Injectable()
|
|
9
|
+
export class PasswordResetRepository implements PasswordResetStoreDeps {
|
|
10
|
+
constructor(@Inject(DRIZZLE_DB) private readonly db: Database) {}
|
|
11
|
+
|
|
12
|
+
async saveResetToken(input: {
|
|
13
|
+
userId: string;
|
|
14
|
+
tokenHash: string;
|
|
15
|
+
expiresAt: string;
|
|
16
|
+
}): Promise<void> {
|
|
17
|
+
await this.db
|
|
18
|
+
.insert(passwordResetTokens)
|
|
19
|
+
.values({
|
|
20
|
+
userId: toId(input.userId),
|
|
21
|
+
tokenHash: input.tokenHash,
|
|
22
|
+
expiresAt: new Date(input.expiresAt),
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async findValidResetToken(
|
|
27
|
+
tokenHash: string,
|
|
28
|
+
): Promise<{
|
|
29
|
+
userId: string;
|
|
30
|
+
expiresAt: string;
|
|
31
|
+
consumedAt: string | null;
|
|
32
|
+
} | null> {
|
|
33
|
+
const [row] = await this.db
|
|
34
|
+
.select()
|
|
35
|
+
.from(passwordResetTokens)
|
|
36
|
+
.where(eq(passwordResetTokens.tokenHash, tokenHash))
|
|
37
|
+
.limit(1);
|
|
38
|
+
if (!row) return null;
|
|
39
|
+
return {
|
|
40
|
+
userId: row.userId.toString(),
|
|
41
|
+
expiresAt: row.expiresAt.toISOString(),
|
|
42
|
+
consumedAt: row.consumedAt?.toISOString() ?? null,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async consumeResetToken(tokenHash: string): Promise<void> {
|
|
47
|
+
await this.db
|
|
48
|
+
.update(passwordResetTokens)
|
|
49
|
+
.set({ consumedAt: new Date() })
|
|
50
|
+
.where(eq(passwordResetTokens.tokenHash, tokenHash));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async setPasswordHash(userId: string, passwordHash: string): Promise<void> {
|
|
54
|
+
await this.db
|
|
55
|
+
.update(users)
|
|
56
|
+
.set({ passwordHash })
|
|
57
|
+
.where(eq(users.id, toId(userId)));
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -2,12 +2,16 @@ import { randomUUID } from "node:crypto";
|
|
|
2
2
|
import { Inject, Injectable } from "@nestjs/common";
|
|
3
3
|
import { and, eq, isNull, ne, sql } from "drizzle-orm";
|
|
4
4
|
import type { SessionStoreDeps } from "@/lib/auth/core/session-policy.js";
|
|
5
|
-
import type {
|
|
5
|
+
import type {
|
|
6
|
+
AuditEvent,
|
|
7
|
+
Revoker,
|
|
8
|
+
SessionRecord,
|
|
9
|
+
} from "@/lib/auth/core/types.js";
|
|
6
10
|
import { AuditLogRepository } from "./audit-log.repository.js";
|
|
7
|
-
import { AUTH_CONFIG, AuthConfig } from "
|
|
8
|
-
import { DRIZZLE_DB, type Database } from "
|
|
9
|
-
import { denylistedAccessTokens, sessions } from "
|
|
10
|
-
import { toId, toIdOrNull } from "
|
|
11
|
+
import { AUTH_CONFIG, AuthConfig } from "../auth.config.js";
|
|
12
|
+
import { DRIZZLE_DB, type Database } from "../db.js";
|
|
13
|
+
import { denylistedAccessTokens, sessions } from "../schema.js";
|
|
14
|
+
import { toId, toIdOrNull } from "../id.helper.js";
|
|
11
15
|
|
|
12
16
|
function toSessionRecord(row: typeof sessions.$inferSelect): SessionRecord {
|
|
13
17
|
return {
|
|
@@ -55,7 +59,11 @@ export class SessionRepository implements SessionStoreDeps {
|
|
|
55
59
|
}
|
|
56
60
|
|
|
57
61
|
async findSession(id: string): Promise<SessionRecord | null> {
|
|
58
|
-
const [row] = await this.db
|
|
62
|
+
const [row] = await this.db
|
|
63
|
+
.select()
|
|
64
|
+
.from(sessions)
|
|
65
|
+
.where(eq(sessions.id, toId(id)))
|
|
66
|
+
.limit(1);
|
|
59
67
|
return row ? toSessionRecord(row) : null;
|
|
60
68
|
}
|
|
61
69
|
|
|
@@ -83,7 +91,12 @@ export class SessionRepository implements SessionStoreDeps {
|
|
|
83
91
|
* `createdBy` is the user themselves. A session is only ever created by the person logging in,
|
|
84
92
|
* so recording it costs nothing and leaves the column meaningful rather than always null.
|
|
85
93
|
*/
|
|
86
|
-
async createSession(input: {
|
|
94
|
+
async createSession(input: {
|
|
95
|
+
userId: string;
|
|
96
|
+
userAgent?: string;
|
|
97
|
+
ip?: string;
|
|
98
|
+
provider?: string;
|
|
99
|
+
}): Promise<SessionRecord> {
|
|
87
100
|
const userIdBig = toId(input.userId);
|
|
88
101
|
const [row] = await this.db
|
|
89
102
|
.insert(sessions)
|
|
@@ -104,15 +117,33 @@ export class SessionRepository implements SessionStoreDeps {
|
|
|
104
117
|
async revokeAllByUser(userId: string, revoker?: Revoker): Promise<void> {
|
|
105
118
|
await this.db
|
|
106
119
|
.update(sessions)
|
|
107
|
-
.set({
|
|
108
|
-
|
|
120
|
+
.set({
|
|
121
|
+
...revocationFields(revoker),
|
|
122
|
+
sessionVersion: sql`${sessions.sessionVersion} + 1`,
|
|
123
|
+
})
|
|
124
|
+
.where(
|
|
125
|
+
and(eq(sessions.userId, toId(userId)), isNull(sessions.revokedAt)),
|
|
126
|
+
);
|
|
109
127
|
}
|
|
110
128
|
|
|
111
|
-
async revokeAllByUserExcept(
|
|
129
|
+
async revokeAllByUserExcept(
|
|
130
|
+
userId: string,
|
|
131
|
+
keepSessionId: string,
|
|
132
|
+
revoker?: Revoker,
|
|
133
|
+
): Promise<void> {
|
|
112
134
|
await this.db
|
|
113
135
|
.update(sessions)
|
|
114
|
-
.set({
|
|
115
|
-
|
|
136
|
+
.set({
|
|
137
|
+
...revocationFields(revoker),
|
|
138
|
+
sessionVersion: sql`${sessions.sessionVersion} + 1`,
|
|
139
|
+
})
|
|
140
|
+
.where(
|
|
141
|
+
and(
|
|
142
|
+
eq(sessions.userId, toId(userId)),
|
|
143
|
+
ne(sessions.id, toId(keepSessionId)),
|
|
144
|
+
isNull(sessions.revokedAt),
|
|
145
|
+
),
|
|
146
|
+
);
|
|
116
147
|
}
|
|
117
148
|
|
|
118
149
|
async denylistJti(jti: string, ttlSeconds: number): Promise<void> {
|
|
@@ -120,11 +151,18 @@ export class SessionRepository implements SessionStoreDeps {
|
|
|
120
151
|
await this.db
|
|
121
152
|
.insert(denylistedAccessTokens)
|
|
122
153
|
.values({ jti, expiresAt })
|
|
123
|
-
.onConflictDoUpdate({
|
|
154
|
+
.onConflictDoUpdate({
|
|
155
|
+
target: denylistedAccessTokens.jti,
|
|
156
|
+
set: { expiresAt },
|
|
157
|
+
});
|
|
124
158
|
}
|
|
125
159
|
|
|
126
160
|
async isDenylisted(jti: string): Promise<boolean> {
|
|
127
|
-
const [row] = await this.db
|
|
161
|
+
const [row] = await this.db
|
|
162
|
+
.select()
|
|
163
|
+
.from(denylistedAccessTokens)
|
|
164
|
+
.where(eq(denylistedAccessTokens.jti, jti))
|
|
165
|
+
.limit(1);
|
|
128
166
|
if (!row) return false;
|
|
129
167
|
if (row.expiresAt.getTime() < Date.now()) return false; // expired; a cleanup job reaps these, see Phase 6
|
|
130
168
|
return true;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { Inject, Injectable } from "@nestjs/common";
|
|
2
2
|
import { and, eq, isNull } from "drizzle-orm";
|
|
3
3
|
import { verifyBackupCode } from "@/lib/auth/core/two-factor.js";
|
|
4
|
-
import { DRIZZLE_DB, type Database } from "
|
|
5
|
-
import { twoFactorBackupCodes } from "
|
|
4
|
+
import { DRIZZLE_DB, type Database } from "../db.js";
|
|
5
|
+
import { twoFactorBackupCodes } from "../schema.js";
|
|
6
6
|
|
|
7
7
|
// Takes bigint directly, not string: every caller already has the user row's id in hand from
|
|
8
8
|
// its own database lookup, so there's nothing to parse here.
|
|
@@ -12,24 +12,38 @@ export class TwoFactorRepository {
|
|
|
12
12
|
|
|
13
13
|
/** Replaces any prior set — re-confirming enrollment invalidates old backup codes. */
|
|
14
14
|
async saveBackupCodes(userId: bigint, hashes: string[]): Promise<void> {
|
|
15
|
-
await this.db
|
|
15
|
+
await this.db
|
|
16
|
+
.delete(twoFactorBackupCodes)
|
|
17
|
+
.where(eq(twoFactorBackupCodes.userId, userId));
|
|
16
18
|
if (hashes.length === 0) return;
|
|
17
|
-
await this.db
|
|
19
|
+
await this.db
|
|
20
|
+
.insert(twoFactorBackupCodes)
|
|
21
|
+
.values(hashes.map((codeHash) => ({ userId, codeHash })));
|
|
18
22
|
}
|
|
19
23
|
|
|
20
24
|
async consumeBackupCode(userId: bigint, code: string): Promise<boolean> {
|
|
21
25
|
const unused = await this.db
|
|
22
26
|
.select()
|
|
23
27
|
.from(twoFactorBackupCodes)
|
|
24
|
-
.where(
|
|
28
|
+
.where(
|
|
29
|
+
and(
|
|
30
|
+
eq(twoFactorBackupCodes.userId, userId),
|
|
31
|
+
isNull(twoFactorBackupCodes.usedAt),
|
|
32
|
+
),
|
|
33
|
+
);
|
|
25
34
|
const match = unused.find((row) => verifyBackupCode(code, row.codeHash));
|
|
26
35
|
if (!match) return false;
|
|
27
36
|
|
|
28
|
-
await this.db
|
|
37
|
+
await this.db
|
|
38
|
+
.update(twoFactorBackupCodes)
|
|
39
|
+
.set({ usedAt: new Date() })
|
|
40
|
+
.where(eq(twoFactorBackupCodes.id, match.id));
|
|
29
41
|
return true;
|
|
30
42
|
}
|
|
31
43
|
|
|
32
44
|
async clearBackupCodes(userId: bigint): Promise<void> {
|
|
33
|
-
await this.db
|
|
45
|
+
await this.db
|
|
46
|
+
.delete(twoFactorBackupCodes)
|
|
47
|
+
.where(eq(twoFactorBackupCodes.userId, userId));
|
|
34
48
|
}
|
|
35
49
|
}
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
// for its side effect installs the Express `Request` augmentation.
|
|
15
15
|
import type { AccessTokenClaims } from "@/lib/auth/core/types.js";
|
|
16
16
|
import type { AppAbility } from "./ability.js";
|
|
17
|
-
import type { AuthzContext } from "./authz.guard.js";
|
|
17
|
+
import type { AuthzContext } from "./guards/authz.guard.js";
|
|
18
18
|
|
|
19
19
|
declare module "express" {
|
|
20
20
|
interface Request {
|