@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
|
@@ -10,8 +10,14 @@ import {
|
|
|
10
10
|
signAppleClientSecret,
|
|
11
11
|
verifyIdTokenAndExtractProfile,
|
|
12
12
|
} from "@/lib/auth/core/oauth.js";
|
|
13
|
-
import {
|
|
14
|
-
|
|
13
|
+
import {
|
|
14
|
+
requestPasswordReset as coreRequestPasswordReset,
|
|
15
|
+
resetPassword as coreResetPassword,
|
|
16
|
+
} from "@/lib/auth/core/password-reset.js";
|
|
17
|
+
import {
|
|
18
|
+
checkRateLimit,
|
|
19
|
+
type RateLimitDeps,
|
|
20
|
+
} from "@/lib/auth/core/rate-limit.js";
|
|
15
21
|
import {
|
|
16
22
|
blockUser,
|
|
17
23
|
createSession,
|
|
@@ -22,21 +28,46 @@ import {
|
|
|
22
28
|
revokeSession,
|
|
23
29
|
rotateRefreshToken,
|
|
24
30
|
} from "@/lib/auth/core/session-policy.js";
|
|
25
|
-
import {
|
|
26
|
-
|
|
31
|
+
import {
|
|
32
|
+
signAccessToken,
|
|
33
|
+
signRefreshToken,
|
|
34
|
+
signTwoFactorChallengeToken,
|
|
35
|
+
verifyRefreshToken,
|
|
36
|
+
verifyTwoFactorChallengeToken,
|
|
37
|
+
} from "@/lib/auth/core/token-service.js";
|
|
38
|
+
import {
|
|
39
|
+
buildTotpProvisioningUri,
|
|
40
|
+
generateBackupCodes,
|
|
41
|
+
generateTotpSecret,
|
|
42
|
+
verifyTotpCode,
|
|
43
|
+
} from "@/lib/auth/core/two-factor.js";
|
|
27
44
|
import type { Revoker } from "@/lib/auth/core/types.js";
|
|
28
|
-
import type { AuthConfig } from "
|
|
29
|
-
import { HttpError } from "
|
|
30
|
-
import {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
import {
|
|
37
|
-
import {
|
|
38
|
-
import {
|
|
39
|
-
import {
|
|
45
|
+
import type { AuthConfig } from "../auth.config.js";
|
|
46
|
+
import { HttpError } from "../http-error.js";
|
|
47
|
+
import {
|
|
48
|
+
AuditLogEntry,
|
|
49
|
+
AuditLogListFilter,
|
|
50
|
+
AuditLogRepository,
|
|
51
|
+
toAuditLogEntry,
|
|
52
|
+
} from "../repositories/audit-log.repository.js";
|
|
53
|
+
import type { Paginated } from "../pagination.js";
|
|
54
|
+
import { PrismaClient } from "../../generated/prisma/client.js";
|
|
55
|
+
import { KeyProviderService } from "../key-provider.js";
|
|
56
|
+
import { OAuthRepository } from "../repositories/oauth.repository.js";
|
|
57
|
+
import { PasswordResetRepository } from "../repositories/password-reset.repository.js";
|
|
58
|
+
import {
|
|
59
|
+
PermissionInput,
|
|
60
|
+
PermissionSummary,
|
|
61
|
+
RbacRepository,
|
|
62
|
+
RoleSummary,
|
|
63
|
+
toUserSummary,
|
|
64
|
+
UserListFilter,
|
|
65
|
+
UserListResult,
|
|
66
|
+
UserSummary,
|
|
67
|
+
} from "../repositories/rbac.repository.js";
|
|
68
|
+
import { SessionRepository } from "../repositories/session.repository.js";
|
|
69
|
+
import { TwoFactorRepository } from "../repositories/two-factor.repository.js";
|
|
70
|
+
import { toId, toIdOrNull } from "../id.helper.js";
|
|
40
71
|
|
|
41
72
|
export interface AuthTokens {
|
|
42
73
|
accessToken: string;
|
|
@@ -91,7 +122,9 @@ export class AuthService {
|
|
|
91
122
|
userAgent?: string;
|
|
92
123
|
ip?: string;
|
|
93
124
|
}): Promise<AuthTokens> {
|
|
94
|
-
const existing = await this.prisma.user.findUnique({
|
|
125
|
+
const existing = await this.prisma.user.findUnique({
|
|
126
|
+
where: { email: input.email },
|
|
127
|
+
});
|
|
95
128
|
if (existing) throw new HttpError(409, "email already registered");
|
|
96
129
|
|
|
97
130
|
const passwordHash = await hashPassword(input.password);
|
|
@@ -109,82 +142,174 @@ export class AuthService {
|
|
|
109
142
|
// The signup default is whichever roles are flagged `isDefault` in the database, not a name
|
|
110
143
|
// spelled in code — see rbac.defaults.ts.
|
|
111
144
|
await this.rbac.assignDefaultRoles(user.id);
|
|
112
|
-
return this.issueSessionTokens(user, {
|
|
145
|
+
return this.issueSessionTokens(user, {
|
|
146
|
+
userAgent: input.userAgent,
|
|
147
|
+
ip: input.ip,
|
|
148
|
+
});
|
|
113
149
|
}
|
|
114
150
|
|
|
115
151
|
/** Returns full tokens directly, or a short-lived challenge if the account has 2FA enabled — see `loginTwoFactor`. */
|
|
116
|
-
async login(input: {
|
|
117
|
-
|
|
152
|
+
async login(input: {
|
|
153
|
+
identifier: string;
|
|
154
|
+
password: string;
|
|
155
|
+
userAgent?: string;
|
|
156
|
+
ip?: string;
|
|
157
|
+
}): Promise<AuthTokens | TwoFactorChallenge> {
|
|
158
|
+
const { allowed } = await checkRateLimit(
|
|
159
|
+
this.rateLimit,
|
|
160
|
+
"login",
|
|
161
|
+
input.identifier,
|
|
162
|
+
);
|
|
118
163
|
if (!allowed) throw new HttpError(429, "too many login attempts");
|
|
119
164
|
|
|
120
165
|
const user = await this.prisma.user.findFirst({
|
|
121
|
-
where: {
|
|
166
|
+
where: {
|
|
167
|
+
OR: [
|
|
168
|
+
{ email: input.identifier },
|
|
169
|
+
{ username: input.identifier },
|
|
170
|
+
{ phone: input.identifier },
|
|
171
|
+
],
|
|
172
|
+
},
|
|
122
173
|
});
|
|
123
|
-
if (
|
|
174
|
+
if (
|
|
175
|
+
!user ||
|
|
176
|
+
user.blocked ||
|
|
177
|
+
!user.isActive ||
|
|
178
|
+
user.isDeleted ||
|
|
179
|
+
!user.passwordHash
|
|
180
|
+
)
|
|
181
|
+
throw new HttpError(401, "invalid credentials");
|
|
124
182
|
|
|
125
183
|
const valid = await verifyPassword(user.passwordHash, input.password);
|
|
126
184
|
if (!valid) throw new HttpError(401, "invalid credentials");
|
|
127
185
|
|
|
128
186
|
if (user.twoFactorEnabled) {
|
|
129
187
|
const key = await this.keys.getActiveKey();
|
|
130
|
-
const { token } = await signTwoFactorChallengeToken(
|
|
188
|
+
const { token } = await signTwoFactorChallengeToken(
|
|
189
|
+
{ activeKey: key },
|
|
190
|
+
user.id.toString(),
|
|
191
|
+
); // core-facing: sub must be string
|
|
131
192
|
return { twoFactorRequired: true, challengeToken: token };
|
|
132
193
|
}
|
|
133
194
|
|
|
134
|
-
return this.issueSessionTokens(user, {
|
|
195
|
+
return this.issueSessionTokens(user, {
|
|
196
|
+
userAgent: input.userAgent,
|
|
197
|
+
ip: input.ip,
|
|
198
|
+
});
|
|
135
199
|
}
|
|
136
200
|
|
|
137
201
|
/** Completes the challenge `login()` returned when 2FA is enabled — a TOTP code or an unused backup code. */
|
|
138
|
-
async loginTwoFactor(input: {
|
|
139
|
-
|
|
202
|
+
async loginTwoFactor(input: {
|
|
203
|
+
challengeToken: string;
|
|
204
|
+
code: string;
|
|
205
|
+
userAgent?: string;
|
|
206
|
+
ip?: string;
|
|
207
|
+
}): Promise<AuthTokens> {
|
|
208
|
+
const { sub } = await verifyTwoFactorChallengeToken(
|
|
209
|
+
{ secret: this.keys.secret },
|
|
210
|
+
input.challengeToken,
|
|
211
|
+
);
|
|
140
212
|
|
|
141
|
-
const user = await this.prisma.user.findUnique({
|
|
142
|
-
|
|
213
|
+
const user = await this.prisma.user.findUnique({
|
|
214
|
+
where: { id: toId(sub) },
|
|
215
|
+
});
|
|
216
|
+
if (
|
|
217
|
+
!user ||
|
|
218
|
+
user.blocked ||
|
|
219
|
+
!user.isActive ||
|
|
220
|
+
user.isDeleted ||
|
|
221
|
+
!user.twoFactorEnabled ||
|
|
222
|
+
!user.twoFactorSecret
|
|
223
|
+
)
|
|
224
|
+
throw new HttpError(401, "invalid credentials");
|
|
143
225
|
|
|
144
226
|
const validTotp = verifyTotpCode(user.twoFactorSecret, input.code);
|
|
145
|
-
const validBackup =
|
|
227
|
+
const validBackup =
|
|
228
|
+
!validTotp &&
|
|
229
|
+
(await this.twoFactor.consumeBackupCode(user.id, input.code));
|
|
146
230
|
if (!validTotp && !validBackup) {
|
|
147
|
-
await this.sessions.appendAuditEvent({
|
|
231
|
+
await this.sessions.appendAuditEvent({
|
|
232
|
+
type: "two_factor_challenge_failed",
|
|
233
|
+
userId: user.id.toString(),
|
|
234
|
+
}); // core-facing: string
|
|
148
235
|
throw new HttpError(401, "invalid two-factor code");
|
|
149
236
|
}
|
|
150
237
|
|
|
151
|
-
return this.issueSessionTokens(user, {
|
|
238
|
+
return this.issueSessionTokens(user, {
|
|
239
|
+
userAgent: input.userAgent,
|
|
240
|
+
ip: input.ip,
|
|
241
|
+
});
|
|
152
242
|
}
|
|
153
243
|
|
|
154
|
-
async enrollTwoFactor(
|
|
244
|
+
async enrollTwoFactor(
|
|
245
|
+
userId: string,
|
|
246
|
+
): Promise<{ secret: string; provisioningUri: string }> {
|
|
155
247
|
const idBig = toId(userId);
|
|
156
248
|
const user = await this.getUserOrThrow(userId);
|
|
157
249
|
const secret = generateTotpSecret();
|
|
158
250
|
// Not enabled yet — confirmTwoFactor() flips that, so an abandoned enrollment never locks the account out.
|
|
159
|
-
await this.prisma.user.update({
|
|
160
|
-
|
|
251
|
+
await this.prisma.user.update({
|
|
252
|
+
where: { id: idBig },
|
|
253
|
+
data: { twoFactorSecret: secret, twoFactorEnabled: false },
|
|
254
|
+
});
|
|
255
|
+
return {
|
|
256
|
+
secret,
|
|
257
|
+
provisioningUri: buildTotpProvisioningUri({
|
|
258
|
+
secret,
|
|
259
|
+
accountName: user.email,
|
|
260
|
+
issuer: this.config.twoFactorIssuer,
|
|
261
|
+
}),
|
|
262
|
+
};
|
|
161
263
|
}
|
|
162
264
|
|
|
163
|
-
async confirmTwoFactor(
|
|
265
|
+
async confirmTwoFactor(
|
|
266
|
+
userId: string,
|
|
267
|
+
code: string,
|
|
268
|
+
): Promise<{ backupCodes: string[] }> {
|
|
164
269
|
const idBig = toId(userId);
|
|
165
270
|
const user = await this.getUserOrThrow(userId);
|
|
166
|
-
if (!user.twoFactorSecret)
|
|
167
|
-
|
|
271
|
+
if (!user.twoFactorSecret)
|
|
272
|
+
throw new HttpError(400, "call enrollTwoFactor first");
|
|
273
|
+
if (!verifyTotpCode(user.twoFactorSecret, code))
|
|
274
|
+
throw new HttpError(401, "invalid two-factor code");
|
|
168
275
|
|
|
169
276
|
const backupCodes = generateBackupCodes();
|
|
170
|
-
await this.twoFactor.saveBackupCodes(
|
|
171
|
-
|
|
172
|
-
|
|
277
|
+
await this.twoFactor.saveBackupCodes(
|
|
278
|
+
idBig,
|
|
279
|
+
backupCodes.map((c) => c.hash),
|
|
280
|
+
);
|
|
281
|
+
await this.prisma.user.update({
|
|
282
|
+
where: { id: idBig },
|
|
283
|
+
data: { twoFactorEnabled: true },
|
|
284
|
+
});
|
|
285
|
+
await this.sessions.appendAuditEvent({
|
|
286
|
+
type: "two_factor_enabled",
|
|
287
|
+
userId,
|
|
288
|
+
});
|
|
173
289
|
return { backupCodes: backupCodes.map((c) => c.code) };
|
|
174
290
|
}
|
|
175
291
|
|
|
176
292
|
async disableTwoFactor(userId: string, code: string): Promise<void> {
|
|
177
293
|
const idBig = toId(userId);
|
|
178
294
|
const user = await this.getUserOrThrow(userId);
|
|
179
|
-
if (!user.twoFactorEnabled || !user.twoFactorSecret)
|
|
295
|
+
if (!user.twoFactorEnabled || !user.twoFactorSecret)
|
|
296
|
+
throw new HttpError(400, "two-factor is not enabled");
|
|
180
297
|
|
|
181
298
|
const validTotp = verifyTotpCode(user.twoFactorSecret, code);
|
|
182
|
-
const validBackup =
|
|
183
|
-
|
|
299
|
+
const validBackup =
|
|
300
|
+
!validTotp && (await this.twoFactor.consumeBackupCode(idBig, code));
|
|
301
|
+
if (!validTotp && !validBackup)
|
|
302
|
+
throw new HttpError(401, "invalid two-factor code");
|
|
184
303
|
|
|
185
|
-
await this.prisma.user.update({
|
|
304
|
+
await this.prisma.user.update({
|
|
305
|
+
where: { id: idBig },
|
|
306
|
+
data: { twoFactorEnabled: false, twoFactorSecret: null },
|
|
307
|
+
});
|
|
186
308
|
await this.twoFactor.clearBackupCodes(idBig);
|
|
187
|
-
await this.sessions.appendAuditEvent({
|
|
309
|
+
await this.sessions.appendAuditEvent({
|
|
310
|
+
type: "two_factor_disabled",
|
|
311
|
+
userId,
|
|
312
|
+
});
|
|
188
313
|
}
|
|
189
314
|
|
|
190
315
|
/** Always succeeds from the caller's point of view — an unknown email or a throttled request looks identical, to avoid enumeration. */
|
|
@@ -192,15 +317,27 @@ export class AuthService {
|
|
|
192
317
|
const user = await this.prisma.user.findUnique({ where: { email } });
|
|
193
318
|
if (!user || user.isDeleted) return;
|
|
194
319
|
|
|
195
|
-
const { allowed } = await checkRateLimit(
|
|
320
|
+
const { allowed } = await checkRateLimit(
|
|
321
|
+
this.rateLimit,
|
|
322
|
+
"password-reset",
|
|
323
|
+
email,
|
|
324
|
+
);
|
|
196
325
|
if (!allowed) return;
|
|
197
326
|
|
|
198
|
-
const { token } = await coreRequestPasswordReset(
|
|
199
|
-
|
|
327
|
+
const { token } = await coreRequestPasswordReset(
|
|
328
|
+
this.passwordReset,
|
|
329
|
+
user.id.toString(),
|
|
330
|
+
);
|
|
331
|
+
if (this.config.sendPasswordResetEmail)
|
|
332
|
+
await this.config.sendPasswordResetEmail(email, token);
|
|
200
333
|
}
|
|
201
334
|
|
|
202
335
|
async resetPassword(token: string, newPassword: string): Promise<void> {
|
|
203
|
-
const { userId } = await coreResetPassword(
|
|
336
|
+
const { userId } = await coreResetPassword(
|
|
337
|
+
this.passwordReset,
|
|
338
|
+
token,
|
|
339
|
+
newPassword,
|
|
340
|
+
);
|
|
204
341
|
// The old password is no longer trusted, so neither are its sessions. The revoker is the user
|
|
205
342
|
// themselves — they proved control of the account by consuming the reset token.
|
|
206
343
|
await revokeAllSessionsForUser(this.sessions, userId, { userId });
|
|
@@ -208,22 +345,40 @@ export class AuthService {
|
|
|
208
345
|
|
|
209
346
|
/** Builds the redirect URL for `provider`. `state` is an opaque anti-CSRF nonce the provider echoes back verbatim. */
|
|
210
347
|
async startOAuth(provider: string): Promise<{ url: string }> {
|
|
211
|
-
const state = Buffer.from(JSON.stringify({ nonce: randomUUID() })).toString(
|
|
348
|
+
const state = Buffer.from(JSON.stringify({ nonce: randomUUID() })).toString(
|
|
349
|
+
"base64url",
|
|
350
|
+
);
|
|
212
351
|
|
|
213
352
|
if (provider === "google") {
|
|
214
353
|
const creds = this.config.oauthProviders.google;
|
|
215
354
|
if (!creds) throw new HttpError(400, "google OAuth is not configured");
|
|
216
|
-
return {
|
|
355
|
+
return {
|
|
356
|
+
url: buildAuthorizationUrl(GOOGLE_OIDC_PROVIDER, {
|
|
357
|
+
clientId: creds.clientId,
|
|
358
|
+
redirectUri: creds.redirectUri,
|
|
359
|
+
state,
|
|
360
|
+
}),
|
|
361
|
+
};
|
|
217
362
|
}
|
|
218
363
|
if (provider === "apple") {
|
|
219
364
|
const creds = this.config.oauthProviders.apple;
|
|
220
365
|
if (!creds) throw new HttpError(400, "apple OAuth is not configured");
|
|
221
|
-
return {
|
|
366
|
+
return {
|
|
367
|
+
url: buildAuthorizationUrl(APPLE_OIDC_PROVIDER, {
|
|
368
|
+
clientId: creds.clientId,
|
|
369
|
+
redirectUri: creds.redirectUri,
|
|
370
|
+
state,
|
|
371
|
+
}),
|
|
372
|
+
};
|
|
222
373
|
}
|
|
223
374
|
throw new HttpError(400, `unknown OAuth provider "${provider}"`);
|
|
224
375
|
}
|
|
225
376
|
|
|
226
|
-
async completeOAuthCallback(
|
|
377
|
+
async completeOAuthCallback(
|
|
378
|
+
provider: string,
|
|
379
|
+
code: string,
|
|
380
|
+
_state: string,
|
|
381
|
+
): Promise<AuthTokens> {
|
|
227
382
|
let providerDescriptor: OAuthProviderDescriptor;
|
|
228
383
|
let clientId: string;
|
|
229
384
|
let idToken: string;
|
|
@@ -234,47 +389,91 @@ export class AuthService {
|
|
|
234
389
|
providerDescriptor = GOOGLE_OIDC_PROVIDER;
|
|
235
390
|
clientId = creds.clientId;
|
|
236
391
|
idToken = (
|
|
237
|
-
await exchangeCodeForTokens(GOOGLE_OIDC_PROVIDER, {
|
|
392
|
+
await exchangeCodeForTokens(GOOGLE_OIDC_PROVIDER, {
|
|
393
|
+
clientId: creds.clientId,
|
|
394
|
+
clientSecret: creds.clientSecret,
|
|
395
|
+
redirectUri: creds.redirectUri,
|
|
396
|
+
code,
|
|
397
|
+
})
|
|
238
398
|
).idToken;
|
|
239
399
|
} else if (provider === "apple") {
|
|
240
400
|
const creds = this.config.oauthProviders.apple;
|
|
241
401
|
if (!creds) throw new HttpError(400, "apple OAuth is not configured");
|
|
242
402
|
providerDescriptor = APPLE_OIDC_PROVIDER;
|
|
243
403
|
clientId = creds.clientId;
|
|
244
|
-
const clientSecret = await signAppleClientSecret({
|
|
245
|
-
|
|
404
|
+
const clientSecret = await signAppleClientSecret({
|
|
405
|
+
teamId: creds.teamId,
|
|
406
|
+
clientId: creds.clientId,
|
|
407
|
+
keyId: creds.keyId,
|
|
408
|
+
privateKeyPem: creds.privateKey,
|
|
409
|
+
});
|
|
410
|
+
idToken = (
|
|
411
|
+
await exchangeCodeForTokens(APPLE_OIDC_PROVIDER, {
|
|
412
|
+
clientId: creds.clientId,
|
|
413
|
+
clientSecret,
|
|
414
|
+
redirectUri: creds.redirectUri,
|
|
415
|
+
code,
|
|
416
|
+
})
|
|
417
|
+
).idToken;
|
|
246
418
|
} else {
|
|
247
419
|
throw new HttpError(400, `unknown OAuth provider "${provider}"`);
|
|
248
420
|
}
|
|
249
421
|
|
|
250
|
-
const profile = await verifyIdTokenAndExtractProfile(providerDescriptor, {
|
|
251
|
-
|
|
422
|
+
const profile = await verifyIdTokenAndExtractProfile(providerDescriptor, {
|
|
423
|
+
clientId,
|
|
424
|
+
idToken,
|
|
425
|
+
});
|
|
426
|
+
const { userId } = await completeOAuthLogin(this.oauth, {
|
|
427
|
+
provider,
|
|
428
|
+
profile,
|
|
429
|
+
});
|
|
252
430
|
|
|
253
431
|
const user = await this.getUserOrThrow(userId);
|
|
254
|
-
if (user.blocked || !user.isActive || user.isDeleted)
|
|
432
|
+
if (user.blocked || !user.isActive || user.isDeleted)
|
|
433
|
+
throw new HttpError(401, "account is blocked");
|
|
255
434
|
// A user this login just created holds no roles at all; give them the defaults. Guarded on
|
|
256
435
|
// "holds none" rather than "was created", so it can never re-add a role an administrator
|
|
257
436
|
// revoked from someone who has been logging in for months.
|
|
258
|
-
if (!(await this.rbac.hasAnyRole(user.id)))
|
|
437
|
+
if (!(await this.rbac.hasAnyRole(user.id)))
|
|
438
|
+
await this.rbac.assignDefaultRoles(user.id);
|
|
259
439
|
return this.issueSessionTokens(user, { provider });
|
|
260
440
|
}
|
|
261
441
|
|
|
262
|
-
async listAuditLog(
|
|
442
|
+
async listAuditLog(
|
|
443
|
+
filter: AuditLogListFilter,
|
|
444
|
+
): Promise<Paginated<AuditLogEntry>> {
|
|
263
445
|
const { items, meta } = await this.auditLog.list(filter);
|
|
264
446
|
return { items: items.map(toAuditLogEntry), meta };
|
|
265
447
|
}
|
|
266
448
|
|
|
267
449
|
/** `req.auth` (the JWT claims) never carries 2FA status, so `/auth/me` fetches it fresh — the one bit of the response that isn't just echoing the token. */
|
|
268
|
-
async getTwoFactorStatus(
|
|
450
|
+
async getTwoFactorStatus(
|
|
451
|
+
userId: string,
|
|
452
|
+
): Promise<{ twoFactorEnabled: boolean }> {
|
|
269
453
|
const user = await this.getUserOrThrow(userId);
|
|
270
454
|
return { twoFactorEnabled: user.twoFactorEnabled };
|
|
271
455
|
}
|
|
272
456
|
|
|
273
|
-
async listActiveSessions(
|
|
457
|
+
async listActiveSessions(
|
|
458
|
+
userId: string,
|
|
459
|
+
): Promise<
|
|
460
|
+
Array<{
|
|
461
|
+
id: string;
|
|
462
|
+
createdAt: string;
|
|
463
|
+
expiresAt: string;
|
|
464
|
+
provider?: string;
|
|
465
|
+
userAgent?: string;
|
|
466
|
+
ip?: string;
|
|
467
|
+
}>
|
|
468
|
+
> {
|
|
274
469
|
// "Active" is now three conditions, not one: not revoked, and not past its own absolute
|
|
275
470
|
// expiry. `sessions_user_active_idx` is (user_id, is_revoked, expires_at) for exactly this read.
|
|
276
471
|
const rows = await this.prisma.session.findMany({
|
|
277
|
-
where: {
|
|
472
|
+
where: {
|
|
473
|
+
userId: toId(userId),
|
|
474
|
+
isRevoked: false,
|
|
475
|
+
expiresAt: { gt: new Date() },
|
|
476
|
+
},
|
|
278
477
|
orderBy: { createdAt: "desc" },
|
|
279
478
|
});
|
|
280
479
|
return rows.map((row) => ({
|
|
@@ -315,13 +514,24 @@ export class AuthService {
|
|
|
315
514
|
|
|
316
515
|
async updateUser(
|
|
317
516
|
userId: string,
|
|
318
|
-
input: {
|
|
517
|
+
input: {
|
|
518
|
+
firstName?: string | null;
|
|
519
|
+
lastName?: string | null;
|
|
520
|
+
displayName?: string | null;
|
|
521
|
+
phone?: string | null;
|
|
522
|
+
username?: string | null;
|
|
523
|
+
photo?: string | null;
|
|
524
|
+
},
|
|
319
525
|
actorUserId: string | null,
|
|
320
526
|
): Promise<UserSummary> {
|
|
321
527
|
return this.rbac.updateUser(userId, input, actorUserId);
|
|
322
528
|
}
|
|
323
529
|
|
|
324
|
-
async deleteUser(
|
|
530
|
+
async deleteUser(
|
|
531
|
+
userId: string,
|
|
532
|
+
actorUserId: string | null,
|
|
533
|
+
reason?: string,
|
|
534
|
+
): Promise<void> {
|
|
325
535
|
await this.rbac.deleteUser(userId, actorUserId, reason);
|
|
326
536
|
}
|
|
327
537
|
|
|
@@ -331,7 +541,10 @@ export class AuthService {
|
|
|
331
541
|
}
|
|
332
542
|
|
|
333
543
|
/** Defines a permission, or edits one — including deactivating it. The write path for "the catalog is editable in the database". */
|
|
334
|
-
async definePermission(
|
|
544
|
+
async definePermission(
|
|
545
|
+
input: PermissionInput,
|
|
546
|
+
actorUserId: string | null,
|
|
547
|
+
): Promise<PermissionSummary> {
|
|
335
548
|
return this.rbac.upsertPermission(input, actorUserId);
|
|
336
549
|
}
|
|
337
550
|
|
|
@@ -339,53 +552,106 @@ export class AuthService {
|
|
|
339
552
|
return { roles: await this.rbac.listRoles() };
|
|
340
553
|
}
|
|
341
554
|
|
|
342
|
-
async createRole(
|
|
555
|
+
async createRole(
|
|
556
|
+
input: {
|
|
557
|
+
slug: string;
|
|
558
|
+
name?: string;
|
|
559
|
+
displayName?: string;
|
|
560
|
+
description?: string | null;
|
|
561
|
+
},
|
|
562
|
+
actorUserId: string | null,
|
|
563
|
+
): Promise<RoleSummary> {
|
|
343
564
|
return this.rbac.createRole(input, actorUserId);
|
|
344
565
|
}
|
|
345
566
|
|
|
346
567
|
async updateRole(
|
|
347
568
|
roleId: string,
|
|
348
|
-
input: {
|
|
569
|
+
input: {
|
|
570
|
+
name?: string;
|
|
571
|
+
displayName?: string;
|
|
572
|
+
description?: string | null;
|
|
573
|
+
isActive?: boolean;
|
|
574
|
+
},
|
|
349
575
|
actorUserId: string | null,
|
|
350
576
|
): Promise<RoleSummary> {
|
|
351
577
|
return this.rbac.updateRole(roleId, input, actorUserId);
|
|
352
578
|
}
|
|
353
579
|
|
|
354
|
-
async deleteRole(
|
|
580
|
+
async deleteRole(
|
|
581
|
+
roleId: string,
|
|
582
|
+
actorUserId: string | null,
|
|
583
|
+
reason?: string,
|
|
584
|
+
): Promise<void> {
|
|
355
585
|
await this.rbac.deleteRole(roleId, actorUserId, reason);
|
|
356
586
|
}
|
|
357
587
|
|
|
358
|
-
async attachPermissionToRole(
|
|
588
|
+
async attachPermissionToRole(
|
|
589
|
+
roleId: string,
|
|
590
|
+
permissionSlug: string,
|
|
591
|
+
actorUserId: string | null,
|
|
592
|
+
): Promise<void> {
|
|
359
593
|
await this.rbac.attachPermissionToRole(roleId, permissionSlug, actorUserId);
|
|
360
594
|
}
|
|
361
595
|
|
|
362
596
|
async assignRole(userId: string, roleSlug: string): Promise<void> {
|
|
363
597
|
await this.rbac.assignRoleToUser(userId, roleSlug);
|
|
364
|
-
await this.sessions.appendAuditEvent({
|
|
598
|
+
await this.sessions.appendAuditEvent({
|
|
599
|
+
type: "role_assigned",
|
|
600
|
+
userId,
|
|
601
|
+
role: roleSlug,
|
|
602
|
+
});
|
|
365
603
|
}
|
|
366
604
|
|
|
367
605
|
async revokeRole(userId: string, roleSlug: string): Promise<void> {
|
|
368
606
|
await this.rbac.revokeRoleFromUser(userId, roleSlug);
|
|
369
|
-
await this.sessions.appendAuditEvent({
|
|
607
|
+
await this.sessions.appendAuditEvent({
|
|
608
|
+
type: "role_revoked",
|
|
609
|
+
userId,
|
|
610
|
+
role: roleSlug,
|
|
611
|
+
});
|
|
370
612
|
}
|
|
371
613
|
|
|
372
|
-
async grantPermission(
|
|
614
|
+
async grantPermission(
|
|
615
|
+
userId: string,
|
|
616
|
+
permissionSlug: string,
|
|
617
|
+
actorUserId: string | null,
|
|
618
|
+
): Promise<void> {
|
|
373
619
|
await this.rbac.grantPermissionToUser(userId, permissionSlug, actorUserId);
|
|
374
|
-
await this.sessions.appendAuditEvent({
|
|
620
|
+
await this.sessions.appendAuditEvent({
|
|
621
|
+
type: "permission_granted",
|
|
622
|
+
userId,
|
|
623
|
+
permission: permissionSlug,
|
|
624
|
+
});
|
|
375
625
|
}
|
|
376
626
|
|
|
377
|
-
async revokePermission(
|
|
627
|
+
async revokePermission(
|
|
628
|
+
userId: string,
|
|
629
|
+
permissionSlug: string,
|
|
630
|
+
): Promise<void> {
|
|
378
631
|
await this.rbac.revokePermissionFromUser(userId, permissionSlug);
|
|
379
|
-
await this.sessions.appendAuditEvent({
|
|
632
|
+
await this.sessions.appendAuditEvent({
|
|
633
|
+
type: "permission_revoked",
|
|
634
|
+
userId,
|
|
635
|
+
permission: permissionSlug,
|
|
636
|
+
});
|
|
380
637
|
}
|
|
381
638
|
|
|
382
639
|
async refresh(refreshToken: string): Promise<Omit<AuthTokens, "sessionId">> {
|
|
383
640
|
const key = await this.keys.getActiveKey();
|
|
384
|
-
const presented = await verifyRefreshToken(
|
|
385
|
-
|
|
641
|
+
const presented = await verifyRefreshToken(
|
|
642
|
+
{ secret: this.keys.secret },
|
|
643
|
+
refreshToken,
|
|
644
|
+
);
|
|
645
|
+
const { session, nextJti } = await rotateRefreshToken(
|
|
646
|
+
this.sessions,
|
|
647
|
+
presented,
|
|
648
|
+
);
|
|
386
649
|
|
|
387
|
-
const user = await this.prisma.user.findUnique({
|
|
388
|
-
|
|
650
|
+
const user = await this.prisma.user.findUnique({
|
|
651
|
+
where: { id: toId(session.userId) },
|
|
652
|
+
});
|
|
653
|
+
if (!user || user.blocked || !user.isActive || user.isDeleted)
|
|
654
|
+
throw new HttpError(401, "invalid credentials");
|
|
389
655
|
|
|
390
656
|
// Identity only. Authorization is resolved from the database on every request (the authz middleware),
|
|
391
657
|
// so a token issued before a grant is as authoritative as one issued after it.
|
|
@@ -396,38 +662,81 @@ export class AuthService {
|
|
|
396
662
|
);
|
|
397
663
|
const refresh = await signRefreshToken(
|
|
398
664
|
{ activeKey: key },
|
|
399
|
-
{
|
|
665
|
+
{
|
|
666
|
+
sub: user.id.toString(),
|
|
667
|
+
sessionId: session.id,
|
|
668
|
+
sv: session.sessionVersion,
|
|
669
|
+
},
|
|
400
670
|
{ jti: nextJti, ttlSeconds: this.config.refreshTokenTtlSeconds },
|
|
401
671
|
);
|
|
402
672
|
return { accessToken: access.token, refreshToken: refresh.token };
|
|
403
673
|
}
|
|
404
674
|
|
|
405
|
-
async logout(
|
|
675
|
+
async logout(
|
|
676
|
+
sessionId: string,
|
|
677
|
+
accessJti: string,
|
|
678
|
+
accessRemainingTtlSeconds: number,
|
|
679
|
+
revoker?: Revoker,
|
|
680
|
+
): Promise<void> {
|
|
406
681
|
await revokeSession(this.sessions, sessionId, revoker);
|
|
407
|
-
await revokeAccessToken(
|
|
682
|
+
await revokeAccessToken(
|
|
683
|
+
this.sessions,
|
|
684
|
+
accessJti,
|
|
685
|
+
accessRemainingTtlSeconds,
|
|
686
|
+
);
|
|
408
687
|
}
|
|
409
688
|
|
|
410
689
|
async logoutAll(userId: string, revoker?: Revoker): Promise<void> {
|
|
411
690
|
await revokeAllSessionsForUser(this.sessions, userId, revoker);
|
|
412
691
|
}
|
|
413
692
|
|
|
414
|
-
async logoutOthers(
|
|
415
|
-
|
|
693
|
+
async logoutOthers(
|
|
694
|
+
userId: string,
|
|
695
|
+
keepSessionId: string,
|
|
696
|
+
revoker?: Revoker,
|
|
697
|
+
): Promise<void> {
|
|
698
|
+
await revokeOtherSessionsForUser(
|
|
699
|
+
this.sessions,
|
|
700
|
+
userId,
|
|
701
|
+
keepSessionId,
|
|
702
|
+
revoker,
|
|
703
|
+
);
|
|
416
704
|
}
|
|
417
705
|
|
|
418
|
-
async changePassword(
|
|
706
|
+
async changePassword(
|
|
707
|
+
userId: string,
|
|
708
|
+
currentSessionId: string,
|
|
709
|
+
input: { currentPassword: string; newPassword: string },
|
|
710
|
+
): Promise<void> {
|
|
419
711
|
const userIdBig = toId(userId);
|
|
420
|
-
const user = await this.prisma.user.findUnique({
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
712
|
+
const user = await this.prisma.user.findUnique({
|
|
713
|
+
where: { id: userIdBig },
|
|
714
|
+
});
|
|
715
|
+
if (
|
|
716
|
+
!user ||
|
|
717
|
+
user.blocked ||
|
|
718
|
+
!user.isActive ||
|
|
719
|
+
user.isDeleted ||
|
|
720
|
+
!user.passwordHash
|
|
721
|
+
)
|
|
722
|
+
throw new HttpError(401, "invalid credentials");
|
|
723
|
+
|
|
724
|
+
const valid = await verifyPassword(
|
|
725
|
+
user.passwordHash,
|
|
726
|
+
input.currentPassword,
|
|
727
|
+
);
|
|
424
728
|
if (!valid) throw new HttpError(401, "invalid credentials");
|
|
425
729
|
|
|
426
730
|
const passwordHash = await hashPassword(input.newPassword);
|
|
427
|
-
await this.prisma.user.update({
|
|
731
|
+
await this.prisma.user.update({
|
|
732
|
+
where: { id: userIdBig },
|
|
733
|
+
data: { passwordHash, updatedBy: userIdBig },
|
|
734
|
+
});
|
|
428
735
|
// The old password is no longer trusted everywhere else it's signed in — but leave the
|
|
429
736
|
// session making this very call alone, the same courtesy `logoutOthers` extends.
|
|
430
|
-
await revokeOtherSessionsForUser(this.sessions, userId, currentSessionId, {
|
|
737
|
+
await revokeOtherSessionsForUser(this.sessions, userId, currentSessionId, {
|
|
738
|
+
userId,
|
|
739
|
+
});
|
|
431
740
|
}
|
|
432
741
|
|
|
433
742
|
/** Backs both `GET`- and `PATCH /auth/me` — roles/blocked/lastLogin are an admin's business, not this endpoint's. */
|
|
@@ -457,13 +766,25 @@ export class AuthService {
|
|
|
457
766
|
*/
|
|
458
767
|
async updateProfile(
|
|
459
768
|
userId: string,
|
|
460
|
-
input: {
|
|
769
|
+
input: {
|
|
770
|
+
firstName?: string | null;
|
|
771
|
+
lastName?: string | null;
|
|
772
|
+
displayName?: string | null;
|
|
773
|
+
phone?: string | null;
|
|
774
|
+
username?: string | null;
|
|
775
|
+
photo?: string | null;
|
|
776
|
+
},
|
|
461
777
|
): Promise<SelfProfile> {
|
|
462
|
-
return this.toSelfProfile(
|
|
778
|
+
return this.toSelfProfile(
|
|
779
|
+
await this.rbac.updateUser(userId, input, userId),
|
|
780
|
+
);
|
|
463
781
|
}
|
|
464
782
|
|
|
465
783
|
async block(userId: string, revoker?: Revoker): Promise<void> {
|
|
466
|
-
await this.prisma.user.update({
|
|
784
|
+
await this.prisma.user.update({
|
|
785
|
+
where: { id: toId(userId) },
|
|
786
|
+
data: { blocked: true, updatedBy: toIdOrNull(revoker?.userId) },
|
|
787
|
+
});
|
|
467
788
|
// The administrator, not the blocked user, is what lands in `sessions.revoked_by`.
|
|
468
789
|
await blockUser(this.sessions, userId, revoker);
|
|
469
790
|
// Belt and braces. The block is enforced on the authentication path — login and refresh both
|
|
@@ -474,7 +795,10 @@ export class AuthService {
|
|
|
474
795
|
}
|
|
475
796
|
|
|
476
797
|
async unblock(userId: string, revoker?: Revoker): Promise<void> {
|
|
477
|
-
await this.prisma.user.update({
|
|
798
|
+
await this.prisma.user.update({
|
|
799
|
+
where: { id: toId(userId) },
|
|
800
|
+
data: { blocked: false, updatedBy: toIdOrNull(revoker?.userId) },
|
|
801
|
+
});
|
|
478
802
|
}
|
|
479
803
|
|
|
480
804
|
/**
|
|
@@ -482,18 +806,26 @@ export class AuthService {
|
|
|
482
806
|
* security/moderation action. Both independently deny login; see the note on the `User` model.
|
|
483
807
|
*/
|
|
484
808
|
async deactivate(userId: string, revoker?: Revoker): Promise<void> {
|
|
485
|
-
await this.prisma.user.update({
|
|
809
|
+
await this.prisma.user.update({
|
|
810
|
+
where: { id: toId(userId) },
|
|
811
|
+
data: { isActive: false, updatedBy: toIdOrNull(revoker?.userId) },
|
|
812
|
+
});
|
|
486
813
|
await deactivateUser(this.sessions, userId, revoker);
|
|
487
814
|
await this.rbac.invalidateUser(userId);
|
|
488
815
|
}
|
|
489
816
|
|
|
490
817
|
async activate(userId: string, revoker?: Revoker): Promise<void> {
|
|
491
|
-
await this.prisma.user.update({
|
|
818
|
+
await this.prisma.user.update({
|
|
819
|
+
where: { id: toId(userId) },
|
|
820
|
+
data: { isActive: true, updatedBy: toIdOrNull(revoker?.userId) },
|
|
821
|
+
});
|
|
492
822
|
}
|
|
493
823
|
|
|
494
824
|
/** Nest's filter turns Prisma's `findUniqueOrThrow` into a 404; without one, this does it by hand. */
|
|
495
825
|
private async getUserOrThrow(userId: string) {
|
|
496
|
-
const user = await this.prisma.user.findUnique({
|
|
826
|
+
const user = await this.prisma.user.findUnique({
|
|
827
|
+
where: { id: toId(userId) },
|
|
828
|
+
});
|
|
497
829
|
if (!user) throw new HttpError(404, `user "${userId}" not found`);
|
|
498
830
|
return user;
|
|
499
831
|
}
|
|
@@ -501,8 +833,14 @@ export class AuthService {
|
|
|
501
833
|
/** The access token identifies the user and the session and carries no authorization at all
|
|
502
834
|
* — see the authz middleware. Takes the raw Prisma user row (bigint id) — every call site
|
|
503
835
|
* already has one in hand. */
|
|
504
|
-
private async issueSessionTokens(
|
|
505
|
-
|
|
836
|
+
private async issueSessionTokens(
|
|
837
|
+
user: { id: bigint },
|
|
838
|
+
meta: { userAgent?: string; ip?: string; provider?: string },
|
|
839
|
+
): Promise<AuthTokens> {
|
|
840
|
+
const session = await createSession(this.sessions, {
|
|
841
|
+
userId: user.id.toString(),
|
|
842
|
+
...meta,
|
|
843
|
+
});
|
|
506
844
|
const key = await this.keys.getActiveKey();
|
|
507
845
|
|
|
508
846
|
const access = await signAccessToken(
|
|
@@ -512,10 +850,21 @@ export class AuthService {
|
|
|
512
850
|
);
|
|
513
851
|
const refresh = await signRefreshToken(
|
|
514
852
|
{ activeKey: key },
|
|
515
|
-
{
|
|
516
|
-
|
|
853
|
+
{
|
|
854
|
+
sub: user.id.toString(),
|
|
855
|
+
sessionId: session.id,
|
|
856
|
+
sv: session.sessionVersion,
|
|
857
|
+
},
|
|
858
|
+
{
|
|
859
|
+
jti: session.currentRefreshJti,
|
|
860
|
+
ttlSeconds: this.config.refreshTokenTtlSeconds,
|
|
861
|
+
},
|
|
517
862
|
);
|
|
518
863
|
|
|
519
|
-
return {
|
|
864
|
+
return {
|
|
865
|
+
accessToken: access.token,
|
|
866
|
+
refreshToken: refresh.token,
|
|
867
|
+
sessionId: session.id,
|
|
868
|
+
};
|
|
520
869
|
}
|
|
521
870
|
}
|