@simple-auth-kit/cli 1.4.3 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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 +57 -22
- 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 +67 -19
- 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 +78 -20
- 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 +81 -26
- 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 +73 -21
- 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 +109 -29
- 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 +59 -28
- 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 +98 -27
- 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 +78 -20
- 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 +55 -27
- 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 +104 -29
- 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 +109 -29
- package/registry/combos/nestjs-prisma/variants/workspaces/src/services/auth.service.ts +961 -0
- package/registry.json +4 -4
- package/simple-auth-kit.ts +29 -2
- 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,22 +28,47 @@ 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 type { AuthzContext } from "
|
|
31
|
-
import {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
import {
|
|
38
|
-
import {
|
|
39
|
-
import {
|
|
40
|
-
import {
|
|
45
|
+
import type { AuthConfig } from "../auth.config.js";
|
|
46
|
+
import { HttpError } from "../http-error.js";
|
|
47
|
+
import type { AuthzContext } from "../middleware/authz.middleware.js";
|
|
48
|
+
import {
|
|
49
|
+
AuditLogEntry,
|
|
50
|
+
AuditLogListFilter,
|
|
51
|
+
AuditLogRepository,
|
|
52
|
+
toAuditLogEntry,
|
|
53
|
+
} from "../repositories/audit-log.repository.js";
|
|
54
|
+
import type { Paginated } from "../pagination.js";
|
|
55
|
+
import { PrismaClient } from "../../generated/prisma/client.js";
|
|
56
|
+
import { KeyProviderService } from "../key-provider.js";
|
|
57
|
+
import { OAuthRepository } from "../repositories/oauth.repository.js";
|
|
58
|
+
import { PasswordResetRepository } from "../repositories/password-reset.repository.js";
|
|
59
|
+
import {
|
|
60
|
+
MemberListFilter,
|
|
61
|
+
MemberListResult,
|
|
62
|
+
MemberSummary,
|
|
63
|
+
PermissionInput,
|
|
64
|
+
PermissionSummary,
|
|
65
|
+
RbacRepository,
|
|
66
|
+
RoleSummary,
|
|
67
|
+
toMemberSummary,
|
|
68
|
+
} from "../repositories/rbac.repository.js";
|
|
69
|
+
import { SessionRepository } from "../repositories/session.repository.js";
|
|
70
|
+
import { TwoFactorRepository } from "../repositories/two-factor.repository.js";
|
|
71
|
+
import { toId, toIdOrNull } from "../id.helper.js";
|
|
41
72
|
|
|
42
73
|
export interface AuthTokens {
|
|
43
74
|
accessToken: string;
|
|
@@ -92,7 +123,9 @@ export class AuthService {
|
|
|
92
123
|
userAgent?: string;
|
|
93
124
|
ip?: string;
|
|
94
125
|
}): Promise<AuthTokens> {
|
|
95
|
-
const existing = await this.prisma.user.findUnique({
|
|
126
|
+
const existing = await this.prisma.user.findUnique({
|
|
127
|
+
where: { email: input.email },
|
|
128
|
+
});
|
|
96
129
|
if (existing) throw new HttpError(409, "email already registered");
|
|
97
130
|
|
|
98
131
|
const passwordHash = await hashPassword(input.password);
|
|
@@ -107,82 +140,174 @@ export class AuthService {
|
|
|
107
140
|
username: input.username,
|
|
108
141
|
},
|
|
109
142
|
});
|
|
110
|
-
return this.issueSessionTokens(user, {
|
|
143
|
+
return this.issueSessionTokens(user, {
|
|
144
|
+
userAgent: input.userAgent,
|
|
145
|
+
ip: input.ip,
|
|
146
|
+
});
|
|
111
147
|
}
|
|
112
148
|
|
|
113
149
|
/** Returns full tokens directly, or a short-lived challenge if the account has 2FA enabled — see `loginTwoFactor`. */
|
|
114
|
-
async login(input: {
|
|
115
|
-
|
|
150
|
+
async login(input: {
|
|
151
|
+
identifier: string;
|
|
152
|
+
password: string;
|
|
153
|
+
userAgent?: string;
|
|
154
|
+
ip?: string;
|
|
155
|
+
}): Promise<AuthTokens | TwoFactorChallenge> {
|
|
156
|
+
const { allowed } = await checkRateLimit(
|
|
157
|
+
this.rateLimit,
|
|
158
|
+
"login",
|
|
159
|
+
input.identifier,
|
|
160
|
+
);
|
|
116
161
|
if (!allowed) throw new HttpError(429, "too many login attempts");
|
|
117
162
|
|
|
118
163
|
const user = await this.prisma.user.findFirst({
|
|
119
|
-
where: {
|
|
164
|
+
where: {
|
|
165
|
+
OR: [
|
|
166
|
+
{ email: input.identifier },
|
|
167
|
+
{ username: input.identifier },
|
|
168
|
+
{ phone: input.identifier },
|
|
169
|
+
],
|
|
170
|
+
},
|
|
120
171
|
});
|
|
121
|
-
if (
|
|
172
|
+
if (
|
|
173
|
+
!user ||
|
|
174
|
+
user.blocked ||
|
|
175
|
+
!user.isActive ||
|
|
176
|
+
user.isDeleted ||
|
|
177
|
+
!user.passwordHash
|
|
178
|
+
)
|
|
179
|
+
throw new HttpError(401, "invalid credentials");
|
|
122
180
|
|
|
123
181
|
const valid = await verifyPassword(user.passwordHash, input.password);
|
|
124
182
|
if (!valid) throw new HttpError(401, "invalid credentials");
|
|
125
183
|
|
|
126
184
|
if (user.twoFactorEnabled) {
|
|
127
185
|
const key = await this.keys.getActiveKey();
|
|
128
|
-
const { token } = await signTwoFactorChallengeToken(
|
|
186
|
+
const { token } = await signTwoFactorChallengeToken(
|
|
187
|
+
{ activeKey: key },
|
|
188
|
+
user.id.toString(),
|
|
189
|
+
); // core-facing: sub must be string
|
|
129
190
|
return { twoFactorRequired: true, challengeToken: token };
|
|
130
191
|
}
|
|
131
192
|
|
|
132
|
-
return this.issueSessionTokens(user, {
|
|
193
|
+
return this.issueSessionTokens(user, {
|
|
194
|
+
userAgent: input.userAgent,
|
|
195
|
+
ip: input.ip,
|
|
196
|
+
});
|
|
133
197
|
}
|
|
134
198
|
|
|
135
199
|
/** Completes the challenge `login()` returned when 2FA is enabled — a TOTP code or an unused backup code. */
|
|
136
|
-
async loginTwoFactor(input: {
|
|
137
|
-
|
|
200
|
+
async loginTwoFactor(input: {
|
|
201
|
+
challengeToken: string;
|
|
202
|
+
code: string;
|
|
203
|
+
userAgent?: string;
|
|
204
|
+
ip?: string;
|
|
205
|
+
}): Promise<AuthTokens> {
|
|
206
|
+
const { sub } = await verifyTwoFactorChallengeToken(
|
|
207
|
+
{ secret: this.keys.secret },
|
|
208
|
+
input.challengeToken,
|
|
209
|
+
);
|
|
138
210
|
|
|
139
|
-
const user = await this.prisma.user.findUnique({
|
|
140
|
-
|
|
211
|
+
const user = await this.prisma.user.findUnique({
|
|
212
|
+
where: { id: toId(sub) },
|
|
213
|
+
});
|
|
214
|
+
if (
|
|
215
|
+
!user ||
|
|
216
|
+
user.blocked ||
|
|
217
|
+
!user.isActive ||
|
|
218
|
+
user.isDeleted ||
|
|
219
|
+
!user.twoFactorEnabled ||
|
|
220
|
+
!user.twoFactorSecret
|
|
221
|
+
)
|
|
222
|
+
throw new HttpError(401, "invalid credentials");
|
|
141
223
|
|
|
142
224
|
const validTotp = verifyTotpCode(user.twoFactorSecret, input.code);
|
|
143
|
-
const validBackup =
|
|
225
|
+
const validBackup =
|
|
226
|
+
!validTotp &&
|
|
227
|
+
(await this.twoFactor.consumeBackupCode(user.id, input.code));
|
|
144
228
|
if (!validTotp && !validBackup) {
|
|
145
|
-
await this.sessions.appendAuditEvent({
|
|
229
|
+
await this.sessions.appendAuditEvent({
|
|
230
|
+
type: "two_factor_challenge_failed",
|
|
231
|
+
userId: user.id.toString(),
|
|
232
|
+
}); // core-facing: string
|
|
146
233
|
throw new HttpError(401, "invalid two-factor code");
|
|
147
234
|
}
|
|
148
235
|
|
|
149
|
-
return this.issueSessionTokens(user, {
|
|
236
|
+
return this.issueSessionTokens(user, {
|
|
237
|
+
userAgent: input.userAgent,
|
|
238
|
+
ip: input.ip,
|
|
239
|
+
});
|
|
150
240
|
}
|
|
151
241
|
|
|
152
|
-
async enrollTwoFactor(
|
|
242
|
+
async enrollTwoFactor(
|
|
243
|
+
userId: string,
|
|
244
|
+
): Promise<{ secret: string; provisioningUri: string }> {
|
|
153
245
|
const idBig = toId(userId);
|
|
154
246
|
const user = await this.getUserOrThrow(userId);
|
|
155
247
|
const secret = generateTotpSecret();
|
|
156
248
|
// Not enabled yet — confirmTwoFactor() flips that, so an abandoned enrollment never locks the account out.
|
|
157
|
-
await this.prisma.user.update({
|
|
158
|
-
|
|
249
|
+
await this.prisma.user.update({
|
|
250
|
+
where: { id: idBig },
|
|
251
|
+
data: { twoFactorSecret: secret, twoFactorEnabled: false },
|
|
252
|
+
});
|
|
253
|
+
return {
|
|
254
|
+
secret,
|
|
255
|
+
provisioningUri: buildTotpProvisioningUri({
|
|
256
|
+
secret,
|
|
257
|
+
accountName: user.email,
|
|
258
|
+
issuer: this.config.twoFactorIssuer,
|
|
259
|
+
}),
|
|
260
|
+
};
|
|
159
261
|
}
|
|
160
262
|
|
|
161
|
-
async confirmTwoFactor(
|
|
263
|
+
async confirmTwoFactor(
|
|
264
|
+
userId: string,
|
|
265
|
+
code: string,
|
|
266
|
+
): Promise<{ backupCodes: string[] }> {
|
|
162
267
|
const idBig = toId(userId);
|
|
163
268
|
const user = await this.getUserOrThrow(userId);
|
|
164
|
-
if (!user.twoFactorSecret)
|
|
165
|
-
|
|
269
|
+
if (!user.twoFactorSecret)
|
|
270
|
+
throw new HttpError(400, "call enrollTwoFactor first");
|
|
271
|
+
if (!verifyTotpCode(user.twoFactorSecret, code))
|
|
272
|
+
throw new HttpError(401, "invalid two-factor code");
|
|
166
273
|
|
|
167
274
|
const backupCodes = generateBackupCodes();
|
|
168
|
-
await this.twoFactor.saveBackupCodes(
|
|
169
|
-
|
|
170
|
-
|
|
275
|
+
await this.twoFactor.saveBackupCodes(
|
|
276
|
+
idBig,
|
|
277
|
+
backupCodes.map((c) => c.hash),
|
|
278
|
+
);
|
|
279
|
+
await this.prisma.user.update({
|
|
280
|
+
where: { id: idBig },
|
|
281
|
+
data: { twoFactorEnabled: true },
|
|
282
|
+
});
|
|
283
|
+
await this.sessions.appendAuditEvent({
|
|
284
|
+
type: "two_factor_enabled",
|
|
285
|
+
userId,
|
|
286
|
+
});
|
|
171
287
|
return { backupCodes: backupCodes.map((c) => c.code) };
|
|
172
288
|
}
|
|
173
289
|
|
|
174
290
|
async disableTwoFactor(userId: string, code: string): Promise<void> {
|
|
175
291
|
const idBig = toId(userId);
|
|
176
292
|
const user = await this.getUserOrThrow(userId);
|
|
177
|
-
if (!user.twoFactorEnabled || !user.twoFactorSecret)
|
|
293
|
+
if (!user.twoFactorEnabled || !user.twoFactorSecret)
|
|
294
|
+
throw new HttpError(400, "two-factor is not enabled");
|
|
178
295
|
|
|
179
296
|
const validTotp = verifyTotpCode(user.twoFactorSecret, code);
|
|
180
|
-
const validBackup =
|
|
181
|
-
|
|
297
|
+
const validBackup =
|
|
298
|
+
!validTotp && (await this.twoFactor.consumeBackupCode(idBig, code));
|
|
299
|
+
if (!validTotp && !validBackup)
|
|
300
|
+
throw new HttpError(401, "invalid two-factor code");
|
|
182
301
|
|
|
183
|
-
await this.prisma.user.update({
|
|
302
|
+
await this.prisma.user.update({
|
|
303
|
+
where: { id: idBig },
|
|
304
|
+
data: { twoFactorEnabled: false, twoFactorSecret: null },
|
|
305
|
+
});
|
|
184
306
|
await this.twoFactor.clearBackupCodes(idBig);
|
|
185
|
-
await this.sessions.appendAuditEvent({
|
|
307
|
+
await this.sessions.appendAuditEvent({
|
|
308
|
+
type: "two_factor_disabled",
|
|
309
|
+
userId,
|
|
310
|
+
});
|
|
186
311
|
}
|
|
187
312
|
|
|
188
313
|
/** Always succeeds from the caller's point of view — an unknown email or a throttled request looks identical, to avoid enumeration. */
|
|
@@ -190,15 +315,27 @@ export class AuthService {
|
|
|
190
315
|
const user = await this.prisma.user.findUnique({ where: { email } });
|
|
191
316
|
if (!user || user.isDeleted) return;
|
|
192
317
|
|
|
193
|
-
const { allowed } = await checkRateLimit(
|
|
318
|
+
const { allowed } = await checkRateLimit(
|
|
319
|
+
this.rateLimit,
|
|
320
|
+
"password-reset",
|
|
321
|
+
email,
|
|
322
|
+
);
|
|
194
323
|
if (!allowed) return;
|
|
195
324
|
|
|
196
|
-
const { token } = await coreRequestPasswordReset(
|
|
197
|
-
|
|
325
|
+
const { token } = await coreRequestPasswordReset(
|
|
326
|
+
this.passwordReset,
|
|
327
|
+
user.id.toString(),
|
|
328
|
+
);
|
|
329
|
+
if (this.config.sendPasswordResetEmail)
|
|
330
|
+
await this.config.sendPasswordResetEmail(email, token);
|
|
198
331
|
}
|
|
199
332
|
|
|
200
333
|
async resetPassword(token: string, newPassword: string): Promise<void> {
|
|
201
|
-
const { userId } = await coreResetPassword(
|
|
334
|
+
const { userId } = await coreResetPassword(
|
|
335
|
+
this.passwordReset,
|
|
336
|
+
token,
|
|
337
|
+
newPassword,
|
|
338
|
+
);
|
|
202
339
|
// The old password is no longer trusted, so neither are its sessions. The revoker is the user
|
|
203
340
|
// themselves — they proved control of the account by consuming the reset token.
|
|
204
341
|
await revokeAllSessionsForUser(this.sessions, userId, { userId });
|
|
@@ -206,22 +343,40 @@ export class AuthService {
|
|
|
206
343
|
|
|
207
344
|
/** Builds the redirect URL for `provider`. `state` is an opaque anti-CSRF nonce the provider echoes back verbatim. */
|
|
208
345
|
async startOAuth(provider: string): Promise<{ url: string }> {
|
|
209
|
-
const state = Buffer.from(JSON.stringify({ nonce: randomUUID() })).toString(
|
|
346
|
+
const state = Buffer.from(JSON.stringify({ nonce: randomUUID() })).toString(
|
|
347
|
+
"base64url",
|
|
348
|
+
);
|
|
210
349
|
|
|
211
350
|
if (provider === "google") {
|
|
212
351
|
const creds = this.config.oauthProviders.google;
|
|
213
352
|
if (!creds) throw new HttpError(400, "google OAuth is not configured");
|
|
214
|
-
return {
|
|
353
|
+
return {
|
|
354
|
+
url: buildAuthorizationUrl(GOOGLE_OIDC_PROVIDER, {
|
|
355
|
+
clientId: creds.clientId,
|
|
356
|
+
redirectUri: creds.redirectUri,
|
|
357
|
+
state,
|
|
358
|
+
}),
|
|
359
|
+
};
|
|
215
360
|
}
|
|
216
361
|
if (provider === "apple") {
|
|
217
362
|
const creds = this.config.oauthProviders.apple;
|
|
218
363
|
if (!creds) throw new HttpError(400, "apple OAuth is not configured");
|
|
219
|
-
return {
|
|
364
|
+
return {
|
|
365
|
+
url: buildAuthorizationUrl(APPLE_OIDC_PROVIDER, {
|
|
366
|
+
clientId: creds.clientId,
|
|
367
|
+
redirectUri: creds.redirectUri,
|
|
368
|
+
state,
|
|
369
|
+
}),
|
|
370
|
+
};
|
|
220
371
|
}
|
|
221
372
|
throw new HttpError(400, `unknown OAuth provider "${provider}"`);
|
|
222
373
|
}
|
|
223
374
|
|
|
224
|
-
async completeOAuthCallback(
|
|
375
|
+
async completeOAuthCallback(
|
|
376
|
+
provider: string,
|
|
377
|
+
code: string,
|
|
378
|
+
_state: string,
|
|
379
|
+
): Promise<AuthTokens> {
|
|
225
380
|
let providerDescriptor: OAuthProviderDescriptor;
|
|
226
381
|
let clientId: string;
|
|
227
382
|
let idToken: string;
|
|
@@ -232,44 +387,91 @@ export class AuthService {
|
|
|
232
387
|
providerDescriptor = GOOGLE_OIDC_PROVIDER;
|
|
233
388
|
clientId = creds.clientId;
|
|
234
389
|
idToken = (
|
|
235
|
-
await exchangeCodeForTokens(GOOGLE_OIDC_PROVIDER, {
|
|
390
|
+
await exchangeCodeForTokens(GOOGLE_OIDC_PROVIDER, {
|
|
391
|
+
clientId: creds.clientId,
|
|
392
|
+
clientSecret: creds.clientSecret,
|
|
393
|
+
redirectUri: creds.redirectUri,
|
|
394
|
+
code,
|
|
395
|
+
})
|
|
236
396
|
).idToken;
|
|
237
397
|
} else if (provider === "apple") {
|
|
238
398
|
const creds = this.config.oauthProviders.apple;
|
|
239
399
|
if (!creds) throw new HttpError(400, "apple OAuth is not configured");
|
|
240
400
|
providerDescriptor = APPLE_OIDC_PROVIDER;
|
|
241
401
|
clientId = creds.clientId;
|
|
242
|
-
const clientSecret = await signAppleClientSecret({
|
|
243
|
-
|
|
402
|
+
const clientSecret = await signAppleClientSecret({
|
|
403
|
+
teamId: creds.teamId,
|
|
404
|
+
clientId: creds.clientId,
|
|
405
|
+
keyId: creds.keyId,
|
|
406
|
+
privateKeyPem: creds.privateKey,
|
|
407
|
+
});
|
|
408
|
+
idToken = (
|
|
409
|
+
await exchangeCodeForTokens(APPLE_OIDC_PROVIDER, {
|
|
410
|
+
clientId: creds.clientId,
|
|
411
|
+
clientSecret,
|
|
412
|
+
redirectUri: creds.redirectUri,
|
|
413
|
+
code,
|
|
414
|
+
})
|
|
415
|
+
).idToken;
|
|
244
416
|
} else {
|
|
245
417
|
throw new HttpError(400, `unknown OAuth provider "${provider}"`);
|
|
246
418
|
}
|
|
247
419
|
|
|
248
|
-
const profile = await verifyIdTokenAndExtractProfile(providerDescriptor, {
|
|
249
|
-
|
|
420
|
+
const profile = await verifyIdTokenAndExtractProfile(providerDescriptor, {
|
|
421
|
+
clientId,
|
|
422
|
+
idToken,
|
|
423
|
+
});
|
|
424
|
+
const { userId } = await completeOAuthLogin(this.oauth, {
|
|
425
|
+
provider,
|
|
426
|
+
profile,
|
|
427
|
+
});
|
|
250
428
|
|
|
251
429
|
const user = await this.getUserOrThrow(userId);
|
|
252
|
-
if (user.blocked || !user.isActive || user.isDeleted)
|
|
430
|
+
if (user.blocked || !user.isActive || user.isDeleted)
|
|
431
|
+
throw new HttpError(401, "account is blocked");
|
|
253
432
|
return this.issueSessionTokens(user, { provider });
|
|
254
433
|
}
|
|
255
434
|
|
|
256
435
|
/** Pinned to the caller's workspace — the filter argument cannot widen it. */
|
|
257
|
-
async listAuditLog(
|
|
258
|
-
|
|
436
|
+
async listAuditLog(
|
|
437
|
+
ctx: AuthzContext,
|
|
438
|
+
filter: AuditLogListFilter,
|
|
439
|
+
): Promise<Paginated<AuditLogEntry>> {
|
|
440
|
+
const { items, meta } = await this.auditLog.list({
|
|
441
|
+
...filter,
|
|
442
|
+
workspaceId: ctx.workspaceId,
|
|
443
|
+
});
|
|
259
444
|
return { items: items.map(toAuditLogEntry), meta };
|
|
260
445
|
}
|
|
261
446
|
|
|
262
447
|
/** `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. */
|
|
263
|
-
async getTwoFactorStatus(
|
|
448
|
+
async getTwoFactorStatus(
|
|
449
|
+
userId: string,
|
|
450
|
+
): Promise<{ twoFactorEnabled: boolean }> {
|
|
264
451
|
const user = await this.getUserOrThrow(userId);
|
|
265
452
|
return { twoFactorEnabled: user.twoFactorEnabled };
|
|
266
453
|
}
|
|
267
454
|
|
|
268
|
-
async listActiveSessions(
|
|
455
|
+
async listActiveSessions(
|
|
456
|
+
userId: string,
|
|
457
|
+
): Promise<
|
|
458
|
+
Array<{
|
|
459
|
+
id: string;
|
|
460
|
+
createdAt: string;
|
|
461
|
+
expiresAt: string;
|
|
462
|
+
provider?: string;
|
|
463
|
+
userAgent?: string;
|
|
464
|
+
ip?: string;
|
|
465
|
+
}>
|
|
466
|
+
> {
|
|
269
467
|
// "Active" is now three conditions, not one: not revoked, and not past its own absolute
|
|
270
468
|
// expiry. `sessions_user_active_idx` is (user_id, is_revoked, expires_at) for exactly this read.
|
|
271
469
|
const rows = await this.prisma.session.findMany({
|
|
272
|
-
where: {
|
|
470
|
+
where: {
|
|
471
|
+
userId: toId(userId),
|
|
472
|
+
isRevoked: false,
|
|
473
|
+
expiresAt: { gt: new Date() },
|
|
474
|
+
},
|
|
273
475
|
orderBy: { createdAt: "desc" },
|
|
274
476
|
});
|
|
275
477
|
return rows.map((row) => ({
|
|
@@ -288,8 +490,14 @@ export class AuthService {
|
|
|
288
490
|
// database through a workspace-scoped query. That is the security property this model rests on:
|
|
289
491
|
// an admin of one workspace has no expressible way to name a row in another.
|
|
290
492
|
|
|
291
|
-
async listUsers(
|
|
292
|
-
|
|
493
|
+
async listUsers(
|
|
494
|
+
ctx: AuthzContext,
|
|
495
|
+
filter: MemberListFilter,
|
|
496
|
+
): Promise<MemberListResult> {
|
|
497
|
+
const { items, meta } = await this.rbac.listMembers(
|
|
498
|
+
ctx.workspaceId,
|
|
499
|
+
filter,
|
|
500
|
+
);
|
|
293
501
|
return { items: items.map(toMemberSummary), meta };
|
|
294
502
|
}
|
|
295
503
|
|
|
@@ -300,13 +508,25 @@ export class AuthService {
|
|
|
300
508
|
async updateUser(
|
|
301
509
|
ctx: AuthzContext,
|
|
302
510
|
userId: string,
|
|
303
|
-
input: {
|
|
511
|
+
input: {
|
|
512
|
+
firstName?: string | null;
|
|
513
|
+
lastName?: string | null;
|
|
514
|
+
displayName?: string | null;
|
|
515
|
+
phone?: string | null;
|
|
516
|
+
username?: string | null;
|
|
517
|
+
photo?: string | null;
|
|
518
|
+
},
|
|
304
519
|
actorUserId: string | null,
|
|
305
520
|
): Promise<MemberSummary> {
|
|
306
521
|
return this.rbac.updateMember(ctx.workspaceId, userId, input, actorUserId);
|
|
307
522
|
}
|
|
308
523
|
|
|
309
|
-
async deleteUser(
|
|
524
|
+
async deleteUser(
|
|
525
|
+
ctx: AuthzContext,
|
|
526
|
+
userId: string,
|
|
527
|
+
actorUserId: string | null,
|
|
528
|
+
reason?: string,
|
|
529
|
+
): Promise<void> {
|
|
310
530
|
await this.rbac.deleteMember(ctx.workspaceId, userId, actorUserId, reason);
|
|
311
531
|
}
|
|
312
532
|
|
|
@@ -319,7 +539,10 @@ export class AuthService {
|
|
|
319
539
|
}
|
|
320
540
|
|
|
321
541
|
/** Defines a permission, or edits one — including deactivating it. The write path for "the catalog is editable in the database". */
|
|
322
|
-
async definePermission(
|
|
542
|
+
async definePermission(
|
|
543
|
+
input: PermissionInput,
|
|
544
|
+
actorUserId: string | null,
|
|
545
|
+
): Promise<PermissionSummary> {
|
|
323
546
|
return this.rbac.upsertPermission(input, actorUserId);
|
|
324
547
|
}
|
|
325
548
|
|
|
@@ -329,7 +552,12 @@ export class AuthService {
|
|
|
329
552
|
|
|
330
553
|
async createRole(
|
|
331
554
|
ctx: AuthzContext,
|
|
332
|
-
input: {
|
|
555
|
+
input: {
|
|
556
|
+
slug: string;
|
|
557
|
+
name?: string;
|
|
558
|
+
displayName?: string;
|
|
559
|
+
description?: string | null;
|
|
560
|
+
},
|
|
333
561
|
actorUserId: string | null,
|
|
334
562
|
): Promise<RoleSummary> {
|
|
335
563
|
return this.rbac.createRole(ctx.workspaceId, input, actorUserId);
|
|
@@ -338,47 +566,114 @@ export class AuthService {
|
|
|
338
566
|
async updateRole(
|
|
339
567
|
ctx: AuthzContext,
|
|
340
568
|
roleId: string,
|
|
341
|
-
input: {
|
|
569
|
+
input: {
|
|
570
|
+
name?: string;
|
|
571
|
+
displayName?: string;
|
|
572
|
+
description?: string | null;
|
|
573
|
+
isActive?: boolean;
|
|
574
|
+
},
|
|
342
575
|
actorUserId: string | null,
|
|
343
576
|
): Promise<RoleSummary> {
|
|
344
577
|
return this.rbac.updateRole(ctx.workspaceId, roleId, input, actorUserId);
|
|
345
578
|
}
|
|
346
579
|
|
|
347
|
-
async deleteRole(
|
|
580
|
+
async deleteRole(
|
|
581
|
+
ctx: AuthzContext,
|
|
582
|
+
roleId: string,
|
|
583
|
+
actorUserId: string | null,
|
|
584
|
+
reason?: string,
|
|
585
|
+
): Promise<void> {
|
|
348
586
|
await this.rbac.deleteRole(ctx.workspaceId, roleId, actorUserId, reason);
|
|
349
587
|
}
|
|
350
588
|
|
|
351
|
-
async attachPermissionToRole(
|
|
352
|
-
|
|
589
|
+
async attachPermissionToRole(
|
|
590
|
+
ctx: AuthzContext,
|
|
591
|
+
roleId: string,
|
|
592
|
+
permissionSlug: string,
|
|
593
|
+
actorUserId: string | null,
|
|
594
|
+
): Promise<void> {
|
|
595
|
+
await this.rbac.attachPermissionToRole(
|
|
596
|
+
ctx.workspaceId,
|
|
597
|
+
roleId,
|
|
598
|
+
permissionSlug,
|
|
599
|
+
actorUserId,
|
|
600
|
+
);
|
|
353
601
|
}
|
|
354
602
|
|
|
355
|
-
async assignRole(
|
|
603
|
+
async assignRole(
|
|
604
|
+
ctx: AuthzContext,
|
|
605
|
+
userId: string,
|
|
606
|
+
roleSlug: string,
|
|
607
|
+
): Promise<void> {
|
|
356
608
|
await this.rbac.assignRoleToMember(ctx.workspaceId, userId, roleSlug);
|
|
357
|
-
await this.auditLog.append(
|
|
609
|
+
await this.auditLog.append(
|
|
610
|
+
{ type: "role_assigned", userId, role: roleSlug },
|
|
611
|
+
{ workspaceId: ctx.workspaceId },
|
|
612
|
+
);
|
|
358
613
|
}
|
|
359
614
|
|
|
360
|
-
async revokeRole(
|
|
615
|
+
async revokeRole(
|
|
616
|
+
ctx: AuthzContext,
|
|
617
|
+
userId: string,
|
|
618
|
+
roleSlug: string,
|
|
619
|
+
): Promise<void> {
|
|
361
620
|
await this.rbac.revokeRoleFromMember(ctx.workspaceId, userId, roleSlug);
|
|
362
|
-
await this.auditLog.append(
|
|
621
|
+
await this.auditLog.append(
|
|
622
|
+
{ type: "role_revoked", userId, role: roleSlug },
|
|
623
|
+
{ workspaceId: ctx.workspaceId },
|
|
624
|
+
);
|
|
363
625
|
}
|
|
364
626
|
|
|
365
|
-
async grantPermission(
|
|
366
|
-
|
|
367
|
-
|
|
627
|
+
async grantPermission(
|
|
628
|
+
ctx: AuthzContext,
|
|
629
|
+
userId: string,
|
|
630
|
+
permissionSlug: string,
|
|
631
|
+
actorUserId: string | null,
|
|
632
|
+
): Promise<void> {
|
|
633
|
+
await this.rbac.grantPermissionToMember(
|
|
634
|
+
ctx.workspaceId,
|
|
635
|
+
userId,
|
|
636
|
+
permissionSlug,
|
|
637
|
+
actorUserId,
|
|
638
|
+
);
|
|
639
|
+
await this.auditLog.append(
|
|
640
|
+
{ type: "permission_granted", userId, permission: permissionSlug },
|
|
641
|
+
{ workspaceId: ctx.workspaceId },
|
|
642
|
+
);
|
|
368
643
|
}
|
|
369
644
|
|
|
370
|
-
async revokePermission(
|
|
371
|
-
|
|
372
|
-
|
|
645
|
+
async revokePermission(
|
|
646
|
+
ctx: AuthzContext,
|
|
647
|
+
userId: string,
|
|
648
|
+
permissionSlug: string,
|
|
649
|
+
): Promise<void> {
|
|
650
|
+
await this.rbac.revokePermissionFromMember(
|
|
651
|
+
ctx.workspaceId,
|
|
652
|
+
userId,
|
|
653
|
+
permissionSlug,
|
|
654
|
+
);
|
|
655
|
+
await this.auditLog.append(
|
|
656
|
+
{ type: "permission_revoked", userId, permission: permissionSlug },
|
|
657
|
+
{ workspaceId: ctx.workspaceId },
|
|
658
|
+
);
|
|
373
659
|
}
|
|
374
660
|
|
|
375
661
|
async refresh(refreshToken: string): Promise<Omit<AuthTokens, "sessionId">> {
|
|
376
662
|
const key = await this.keys.getActiveKey();
|
|
377
|
-
const presented = await verifyRefreshToken(
|
|
378
|
-
|
|
663
|
+
const presented = await verifyRefreshToken(
|
|
664
|
+
{ secret: this.keys.secret },
|
|
665
|
+
refreshToken,
|
|
666
|
+
);
|
|
667
|
+
const { session, nextJti } = await rotateRefreshToken(
|
|
668
|
+
this.sessions,
|
|
669
|
+
presented,
|
|
670
|
+
);
|
|
379
671
|
|
|
380
|
-
const user = await this.prisma.user.findUnique({
|
|
381
|
-
|
|
672
|
+
const user = await this.prisma.user.findUnique({
|
|
673
|
+
where: { id: toId(session.userId) },
|
|
674
|
+
});
|
|
675
|
+
if (!user || user.blocked || !user.isActive || user.isDeleted)
|
|
676
|
+
throw new HttpError(401, "invalid credentials");
|
|
382
677
|
|
|
383
678
|
const access = await signAccessToken(
|
|
384
679
|
{ activeKey: key },
|
|
@@ -387,38 +682,81 @@ export class AuthService {
|
|
|
387
682
|
);
|
|
388
683
|
const refresh = await signRefreshToken(
|
|
389
684
|
{ activeKey: key },
|
|
390
|
-
{
|
|
685
|
+
{
|
|
686
|
+
sub: user.id.toString(),
|
|
687
|
+
sessionId: session.id,
|
|
688
|
+
sv: session.sessionVersion,
|
|
689
|
+
},
|
|
391
690
|
{ jti: nextJti, ttlSeconds: this.config.refreshTokenTtlSeconds },
|
|
392
691
|
);
|
|
393
692
|
return { accessToken: access.token, refreshToken: refresh.token };
|
|
394
693
|
}
|
|
395
694
|
|
|
396
|
-
async logout(
|
|
695
|
+
async logout(
|
|
696
|
+
sessionId: string,
|
|
697
|
+
accessJti: string,
|
|
698
|
+
accessRemainingTtlSeconds: number,
|
|
699
|
+
revoker?: Revoker,
|
|
700
|
+
): Promise<void> {
|
|
397
701
|
await revokeSession(this.sessions, sessionId, revoker);
|
|
398
|
-
await revokeAccessToken(
|
|
702
|
+
await revokeAccessToken(
|
|
703
|
+
this.sessions,
|
|
704
|
+
accessJti,
|
|
705
|
+
accessRemainingTtlSeconds,
|
|
706
|
+
);
|
|
399
707
|
}
|
|
400
708
|
|
|
401
709
|
async logoutAll(userId: string, revoker?: Revoker): Promise<void> {
|
|
402
710
|
await revokeAllSessionsForUser(this.sessions, userId, revoker);
|
|
403
711
|
}
|
|
404
712
|
|
|
405
|
-
async logoutOthers(
|
|
406
|
-
|
|
713
|
+
async logoutOthers(
|
|
714
|
+
userId: string,
|
|
715
|
+
keepSessionId: string,
|
|
716
|
+
revoker?: Revoker,
|
|
717
|
+
): Promise<void> {
|
|
718
|
+
await revokeOtherSessionsForUser(
|
|
719
|
+
this.sessions,
|
|
720
|
+
userId,
|
|
721
|
+
keepSessionId,
|
|
722
|
+
revoker,
|
|
723
|
+
);
|
|
407
724
|
}
|
|
408
725
|
|
|
409
|
-
async changePassword(
|
|
726
|
+
async changePassword(
|
|
727
|
+
userId: string,
|
|
728
|
+
currentSessionId: string,
|
|
729
|
+
input: { currentPassword: string; newPassword: string },
|
|
730
|
+
): Promise<void> {
|
|
410
731
|
const userIdBig = toId(userId);
|
|
411
|
-
const user = await this.prisma.user.findUnique({
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
732
|
+
const user = await this.prisma.user.findUnique({
|
|
733
|
+
where: { id: userIdBig },
|
|
734
|
+
});
|
|
735
|
+
if (
|
|
736
|
+
!user ||
|
|
737
|
+
user.blocked ||
|
|
738
|
+
!user.isActive ||
|
|
739
|
+
user.isDeleted ||
|
|
740
|
+
!user.passwordHash
|
|
741
|
+
)
|
|
742
|
+
throw new HttpError(401, "invalid credentials");
|
|
743
|
+
|
|
744
|
+
const valid = await verifyPassword(
|
|
745
|
+
user.passwordHash,
|
|
746
|
+
input.currentPassword,
|
|
747
|
+
);
|
|
415
748
|
if (!valid) throw new HttpError(401, "invalid credentials");
|
|
416
749
|
|
|
417
750
|
const passwordHash = await hashPassword(input.newPassword);
|
|
418
|
-
await this.prisma.user.update({
|
|
751
|
+
await this.prisma.user.update({
|
|
752
|
+
where: { id: userIdBig },
|
|
753
|
+
data: { passwordHash, updatedBy: userIdBig },
|
|
754
|
+
});
|
|
419
755
|
// The old password is no longer trusted everywhere else it's signed in — but leave the
|
|
420
756
|
// session making this very call alone, the same courtesy `logoutOthers` extends.
|
|
421
|
-
await revokeOtherSessionsForUser(this.sessions, userId, currentSessionId, {
|
|
757
|
+
await revokeOtherSessionsForUser(this.sessions, userId, currentSessionId, {
|
|
758
|
+
userId,
|
|
759
|
+
});
|
|
422
760
|
}
|
|
423
761
|
|
|
424
762
|
/** Backs both `GET`- and `PATCH /auth/me` — deliberately not workspace-scoped, see `updateProfile`. */
|
|
@@ -449,7 +787,9 @@ export class AuthService {
|
|
|
449
787
|
/** Self-service — no `users:manage` permission required, callable by anyone on their own row, and deliberately not workspace-scoped, see `updateProfile`. */
|
|
450
788
|
async getProfile(userId: string): Promise<SelfProfile> {
|
|
451
789
|
const userIdBig = toId(userId);
|
|
452
|
-
const user = await this.prisma.user.findUnique({
|
|
790
|
+
const user = await this.prisma.user.findUnique({
|
|
791
|
+
where: { id: userIdBig, isDeleted: false },
|
|
792
|
+
});
|
|
453
793
|
if (!user) throw new HttpError(401, "invalid credentials");
|
|
454
794
|
return this.toSelfProfile(user);
|
|
455
795
|
}
|
|
@@ -462,12 +802,24 @@ export class AuthService {
|
|
|
462
802
|
*/
|
|
463
803
|
async updateProfile(
|
|
464
804
|
userId: string,
|
|
465
|
-
input: {
|
|
805
|
+
input: {
|
|
806
|
+
firstName?: string | null;
|
|
807
|
+
lastName?: string | null;
|
|
808
|
+
displayName?: string | null;
|
|
809
|
+
phone?: string | null;
|
|
810
|
+
username?: string | null;
|
|
811
|
+
photo?: string | null;
|
|
812
|
+
},
|
|
466
813
|
): Promise<SelfProfile> {
|
|
467
814
|
const userIdBig = toId(userId);
|
|
468
|
-
const existing = await this.prisma.user.findUnique({
|
|
815
|
+
const existing = await this.prisma.user.findUnique({
|
|
816
|
+
where: { id: userIdBig, isDeleted: false },
|
|
817
|
+
});
|
|
469
818
|
if (!existing) throw new HttpError(401, "invalid credentials");
|
|
470
|
-
const user = await this.prisma.user.update({
|
|
819
|
+
const user = await this.prisma.user.update({
|
|
820
|
+
where: { id: userIdBig },
|
|
821
|
+
data: { ...input, updatedBy: userIdBig },
|
|
822
|
+
});
|
|
471
823
|
return this.toSelfProfile(user);
|
|
472
824
|
}
|
|
473
825
|
|
|
@@ -476,9 +828,16 @@ export class AuthService {
|
|
|
476
828
|
* caller's workspace — otherwise an admin of one workspace could disable an account they
|
|
477
829
|
* have no relationship with.
|
|
478
830
|
*/
|
|
479
|
-
async block(
|
|
831
|
+
async block(
|
|
832
|
+
ctx: AuthzContext,
|
|
833
|
+
userId: string,
|
|
834
|
+
revoker?: Revoker,
|
|
835
|
+
): Promise<void> {
|
|
480
836
|
await this.rbac.requireMember(ctx.workspaceId, userId);
|
|
481
|
-
await this.prisma.user.update({
|
|
837
|
+
await this.prisma.user.update({
|
|
838
|
+
where: { id: toId(userId) },
|
|
839
|
+
data: { blocked: true, updatedBy: toIdOrNull(revoker?.userId) },
|
|
840
|
+
});
|
|
482
841
|
// The administrator, not the blocked user, is what lands in `sessions.revoked_by`.
|
|
483
842
|
await blockUser(this.sessions, userId, revoker);
|
|
484
843
|
// Belt and braces. The block is enforced on the authentication path — login and refresh both
|
|
@@ -488,30 +847,53 @@ export class AuthService {
|
|
|
488
847
|
await this.rbac.invalidateMember(userId, ctx.workspaceId);
|
|
489
848
|
}
|
|
490
849
|
|
|
491
|
-
async unblock(
|
|
850
|
+
async unblock(
|
|
851
|
+
ctx: AuthzContext,
|
|
852
|
+
userId: string,
|
|
853
|
+
revoker?: Revoker,
|
|
854
|
+
): Promise<void> {
|
|
492
855
|
await this.rbac.requireMember(ctx.workspaceId, userId);
|
|
493
|
-
await this.prisma.user.update({
|
|
856
|
+
await this.prisma.user.update({
|
|
857
|
+
where: { id: toId(userId) },
|
|
858
|
+
data: { blocked: false, updatedBy: toIdOrNull(revoker?.userId) },
|
|
859
|
+
});
|
|
494
860
|
}
|
|
495
861
|
|
|
496
862
|
/**
|
|
497
863
|
* A routine administrative on/off toggle — distinct from `block`/`unblock`, which is a
|
|
498
864
|
* security/moderation action. Both independently deny login; see the note on the `User` model.
|
|
499
865
|
*/
|
|
500
|
-
async deactivate(
|
|
866
|
+
async deactivate(
|
|
867
|
+
ctx: AuthzContext,
|
|
868
|
+
userId: string,
|
|
869
|
+
revoker?: Revoker,
|
|
870
|
+
): Promise<void> {
|
|
501
871
|
await this.rbac.requireMember(ctx.workspaceId, userId);
|
|
502
|
-
await this.prisma.user.update({
|
|
872
|
+
await this.prisma.user.update({
|
|
873
|
+
where: { id: toId(userId) },
|
|
874
|
+
data: { isActive: false, updatedBy: toIdOrNull(revoker?.userId) },
|
|
875
|
+
});
|
|
503
876
|
await deactivateUser(this.sessions, userId, revoker);
|
|
504
877
|
await this.rbac.invalidateMember(userId, ctx.workspaceId);
|
|
505
878
|
}
|
|
506
879
|
|
|
507
|
-
async activate(
|
|
880
|
+
async activate(
|
|
881
|
+
ctx: AuthzContext,
|
|
882
|
+
userId: string,
|
|
883
|
+
revoker?: Revoker,
|
|
884
|
+
): Promise<void> {
|
|
508
885
|
await this.rbac.requireMember(ctx.workspaceId, userId);
|
|
509
|
-
await this.prisma.user.update({
|
|
886
|
+
await this.prisma.user.update({
|
|
887
|
+
where: { id: toId(userId) },
|
|
888
|
+
data: { isActive: true, updatedBy: toIdOrNull(revoker?.userId) },
|
|
889
|
+
});
|
|
510
890
|
}
|
|
511
891
|
|
|
512
892
|
/** Nest's filter turns Prisma's `findUniqueOrThrow` into a 404; without one, this does it by hand. */
|
|
513
893
|
private async getUserOrThrow(userId: string) {
|
|
514
|
-
const user = await this.prisma.user.findUnique({
|
|
894
|
+
const user = await this.prisma.user.findUnique({
|
|
895
|
+
where: { id: toId(userId) },
|
|
896
|
+
});
|
|
515
897
|
if (!user) throw new HttpError(404, `user "${userId}" not found`);
|
|
516
898
|
return user;
|
|
517
899
|
}
|
|
@@ -522,8 +904,14 @@ export class AuthService {
|
|
|
522
904
|
* Each request resolves its own — see AuthzGuard. Takes the raw Prisma user row (bigint id) —
|
|
523
905
|
* every call site already has one in hand.
|
|
524
906
|
*/
|
|
525
|
-
private async issueSessionTokens(
|
|
526
|
-
|
|
907
|
+
private async issueSessionTokens(
|
|
908
|
+
user: { id: bigint },
|
|
909
|
+
meta: { userAgent?: string; ip?: string; provider?: string },
|
|
910
|
+
): Promise<AuthTokens> {
|
|
911
|
+
const session = await createSession(this.sessions, {
|
|
912
|
+
userId: user.id.toString(),
|
|
913
|
+
...meta,
|
|
914
|
+
});
|
|
527
915
|
const key = await this.keys.getActiveKey();
|
|
528
916
|
|
|
529
917
|
const access = await signAccessToken(
|
|
@@ -533,10 +921,21 @@ export class AuthService {
|
|
|
533
921
|
);
|
|
534
922
|
const refresh = await signRefreshToken(
|
|
535
923
|
{ activeKey: key },
|
|
536
|
-
{
|
|
537
|
-
|
|
924
|
+
{
|
|
925
|
+
sub: user.id.toString(),
|
|
926
|
+
sessionId: session.id,
|
|
927
|
+
sv: session.sessionVersion,
|
|
928
|
+
},
|
|
929
|
+
{
|
|
930
|
+
jti: session.currentRefreshJti,
|
|
931
|
+
ttlSeconds: this.config.refreshTokenTtlSeconds,
|
|
932
|
+
},
|
|
538
933
|
);
|
|
539
934
|
|
|
540
|
-
return {
|
|
935
|
+
return {
|
|
936
|
+
accessToken: access.token,
|
|
937
|
+
refreshToken: refresh.token,
|
|
938
|
+
sessionId: session.id,
|
|
939
|
+
};
|
|
541
940
|
}
|
|
542
941
|
}
|