@factiii/auth 0.1.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/LICENSE +21 -0
- package/README.md +120 -0
- package/bin/init.mjs +315 -0
- package/dist/chunk-CLHDX2R2.mjs +118 -0
- package/dist/chunk-CLHDX2R2.mjs.map +1 -0
- package/dist/hooks-B4Kl294A.d.mts +400 -0
- package/dist/hooks-B4Kl294A.d.ts +400 -0
- package/dist/index.d.mts +1061 -0
- package/dist/index.d.ts +1061 -0
- package/dist/index.js +2096 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +1947 -0
- package/dist/index.mjs.map +1 -0
- package/dist/validators.d.mts +2 -0
- package/dist/validators.d.ts +2 -0
- package/dist/validators.js +164 -0
- package/dist/validators.js.map +1 -0
- package/dist/validators.mjs +51 -0
- package/dist/validators.mjs.map +1 -0
- package/package.json +104 -0
- package/prisma/schema.prisma +138 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,1947 @@
|
|
|
1
|
+
import {
|
|
2
|
+
biometricVerifySchema,
|
|
3
|
+
changePasswordSchema,
|
|
4
|
+
checkPasswordResetSchema,
|
|
5
|
+
createSchemas,
|
|
6
|
+
deregisterPushTokenSchema,
|
|
7
|
+
disableTwofaSchema,
|
|
8
|
+
endAllSessionsSchema,
|
|
9
|
+
getTwofaSecretSchema,
|
|
10
|
+
loginSchema,
|
|
11
|
+
logoutSchema,
|
|
12
|
+
oAuthLoginSchema,
|
|
13
|
+
otpLoginRequestSchema,
|
|
14
|
+
otpLoginVerifySchema,
|
|
15
|
+
registerPushTokenSchema,
|
|
16
|
+
requestPasswordResetSchema,
|
|
17
|
+
resetPasswordSchema,
|
|
18
|
+
signupSchema,
|
|
19
|
+
twoFaResetSchema,
|
|
20
|
+
twoFaResetVerifySchema,
|
|
21
|
+
twoFaSetupSchema,
|
|
22
|
+
twoFaVerifySchema,
|
|
23
|
+
verifyEmailSchema
|
|
24
|
+
} from "./chunk-CLHDX2R2.mjs";
|
|
25
|
+
|
|
26
|
+
// src/middleware/authGuard.ts
|
|
27
|
+
import { TRPCError } from "@trpc/server";
|
|
28
|
+
|
|
29
|
+
// src/adapters/email.ts
|
|
30
|
+
function createNoopEmailAdapter() {
|
|
31
|
+
return {
|
|
32
|
+
async sendVerificationEmail(email, code) {
|
|
33
|
+
console.log(
|
|
34
|
+
`[NoopEmailAdapter] Would send verification email to ${email} with code ${code}`
|
|
35
|
+
);
|
|
36
|
+
},
|
|
37
|
+
async sendPasswordResetEmail(email, token) {
|
|
38
|
+
console.log(
|
|
39
|
+
`[NoopEmailAdapter] Would send password reset email to ${email} with token ${token}`
|
|
40
|
+
);
|
|
41
|
+
},
|
|
42
|
+
async sendOTPEmail(email, otp) {
|
|
43
|
+
console.log(
|
|
44
|
+
`[NoopEmailAdapter] Would send OTP email to ${email} with code ${otp}`
|
|
45
|
+
);
|
|
46
|
+
},
|
|
47
|
+
async sendLoginNotification(email, browserName, ip) {
|
|
48
|
+
console.log(
|
|
49
|
+
`[NoopEmailAdapter] Would send login notification to ${email} from ${browserName} (${ip})`
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
function createConsoleEmailAdapter() {
|
|
55
|
+
return {
|
|
56
|
+
async sendVerificationEmail(email, code) {
|
|
57
|
+
console.log("\n=== EMAIL: Verification ===");
|
|
58
|
+
console.log(`To: ${email}`);
|
|
59
|
+
console.log(`Code: ${code}`);
|
|
60
|
+
console.log("===========================\n");
|
|
61
|
+
},
|
|
62
|
+
async sendPasswordResetEmail(email, token) {
|
|
63
|
+
console.log("\n=== EMAIL: Password Reset ===");
|
|
64
|
+
console.log(`To: ${email}`);
|
|
65
|
+
console.log(`Token: ${token}`);
|
|
66
|
+
console.log("=============================\n");
|
|
67
|
+
},
|
|
68
|
+
async sendOTPEmail(email, otp) {
|
|
69
|
+
console.log("\n=== EMAIL: OTP Login ===");
|
|
70
|
+
console.log(`To: ${email}`);
|
|
71
|
+
console.log(`OTP: ${otp}`);
|
|
72
|
+
console.log("========================\n");
|
|
73
|
+
},
|
|
74
|
+
async sendLoginNotification(email, browserName, ip) {
|
|
75
|
+
console.log("\n=== EMAIL: Login Notification ===");
|
|
76
|
+
console.log(`To: ${email}`);
|
|
77
|
+
console.log(`Browser: ${browserName}`);
|
|
78
|
+
console.log(`IP: ${ip || "Unknown"}`);
|
|
79
|
+
console.log("=================================\n");
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// src/utilities/config.ts
|
|
85
|
+
var defaultTokenSettings = {
|
|
86
|
+
accessTokenExpiry: "5m",
|
|
87
|
+
passwordResetExpiryMs: 60 * 60 * 1e3,
|
|
88
|
+
// 1 hour
|
|
89
|
+
otpValidityMs: 15 * 60 * 1e3
|
|
90
|
+
// 15 minutes
|
|
91
|
+
};
|
|
92
|
+
var defaultCookieSettings = {
|
|
93
|
+
secure: true,
|
|
94
|
+
sameSite: "Strict",
|
|
95
|
+
httpOnly: true,
|
|
96
|
+
accessTokenPath: "/",
|
|
97
|
+
refreshTokenPath: "/api/trpc/auth.refresh",
|
|
98
|
+
maxAge: 365 * 24 * 60 * 60
|
|
99
|
+
// 1 year in seconds
|
|
100
|
+
};
|
|
101
|
+
var defaultStorageKeys = {
|
|
102
|
+
accessToken: "auth-at",
|
|
103
|
+
refreshToken: "auth-rt"
|
|
104
|
+
};
|
|
105
|
+
var defaultFeatures = {
|
|
106
|
+
twoFa: true,
|
|
107
|
+
oauth: { google: true, apple: true },
|
|
108
|
+
biometric: false,
|
|
109
|
+
emailVerification: true,
|
|
110
|
+
passwordReset: true,
|
|
111
|
+
otpLogin: true
|
|
112
|
+
};
|
|
113
|
+
function createAuthConfig(config) {
|
|
114
|
+
const emailService = config.emailService ?? createNoopEmailAdapter();
|
|
115
|
+
return {
|
|
116
|
+
...config,
|
|
117
|
+
features: { ...defaultFeatures, ...config.features },
|
|
118
|
+
tokenSettings: { ...defaultTokenSettings, ...config.tokenSettings },
|
|
119
|
+
cookieSettings: { ...defaultCookieSettings, ...config.cookieSettings },
|
|
120
|
+
storageKeys: { ...defaultStorageKeys, ...config.storageKeys },
|
|
121
|
+
generateUsername: config.generateUsername ?? (() => `user_${Date.now()}`),
|
|
122
|
+
emailService
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
var defaultAuthConfig = {
|
|
126
|
+
features: defaultFeatures,
|
|
127
|
+
tokenSettings: defaultTokenSettings,
|
|
128
|
+
cookieSettings: defaultCookieSettings,
|
|
129
|
+
storageKeys: defaultStorageKeys
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
// src/utilities/cookies.ts
|
|
133
|
+
var DEFAULT_STORAGE_KEYS = {
|
|
134
|
+
ACCESS_TOKEN: "auth-at",
|
|
135
|
+
REFRESH_TOKEN: "auth-rt"
|
|
136
|
+
};
|
|
137
|
+
function parseAuthCookies(cookieHeader, storageKeys = {
|
|
138
|
+
accessToken: DEFAULT_STORAGE_KEYS.ACCESS_TOKEN,
|
|
139
|
+
refreshToken: DEFAULT_STORAGE_KEYS.REFRESH_TOKEN
|
|
140
|
+
}) {
|
|
141
|
+
if (!cookieHeader) {
|
|
142
|
+
return {};
|
|
143
|
+
}
|
|
144
|
+
const accessToken = cookieHeader.split(`${storageKeys.accessToken}=`)[1]?.split(";")[0];
|
|
145
|
+
const refreshToken = cookieHeader.split(`${storageKeys.refreshToken}=`)[1]?.split(";")[0];
|
|
146
|
+
return {
|
|
147
|
+
accessToken: accessToken || void 0,
|
|
148
|
+
refreshToken: refreshToken || void 0
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
function extractDomain(req) {
|
|
152
|
+
const origin = req.headers.origin;
|
|
153
|
+
if (origin) {
|
|
154
|
+
try {
|
|
155
|
+
return new URL(origin).hostname;
|
|
156
|
+
} catch {
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
const referer = req.headers.referer;
|
|
160
|
+
if (referer) {
|
|
161
|
+
try {
|
|
162
|
+
return new URL(referer).hostname;
|
|
163
|
+
} catch {
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
const host = req.headers.host;
|
|
167
|
+
if (host) {
|
|
168
|
+
return host.split(":")[0];
|
|
169
|
+
}
|
|
170
|
+
return void 0;
|
|
171
|
+
}
|
|
172
|
+
function setAuthCookies(res, credentials, settings, storageKeys = {
|
|
173
|
+
accessToken: DEFAULT_STORAGE_KEYS.ACCESS_TOKEN,
|
|
174
|
+
refreshToken: DEFAULT_STORAGE_KEYS.REFRESH_TOKEN
|
|
175
|
+
}) {
|
|
176
|
+
const cookies = [];
|
|
177
|
+
const domain = settings.domain ?? extractDomain(res.req);
|
|
178
|
+
const expiresDate = settings.maxAge ? new Date(Date.now() + settings.maxAge * 1e3).toUTCString() : void 0;
|
|
179
|
+
if (credentials.refreshToken) {
|
|
180
|
+
const refreshCookie = [
|
|
181
|
+
`${storageKeys.refreshToken}=${credentials.refreshToken}`,
|
|
182
|
+
"HttpOnly",
|
|
183
|
+
settings.secure ? "Secure=true" : "",
|
|
184
|
+
`SameSite=${settings.sameSite}`,
|
|
185
|
+
`Path=${settings.refreshTokenPath}`,
|
|
186
|
+
domain ? `Domain=${domain}` : "",
|
|
187
|
+
`Expires=${expiresDate}`
|
|
188
|
+
].filter(Boolean).join("; ");
|
|
189
|
+
cookies.push(refreshCookie);
|
|
190
|
+
}
|
|
191
|
+
if (credentials.accessToken) {
|
|
192
|
+
const accessCookie = [
|
|
193
|
+
`${storageKeys.accessToken}=${credentials.accessToken}`,
|
|
194
|
+
settings.secure ? "Secure=true" : "",
|
|
195
|
+
`SameSite=${settings.sameSite}`,
|
|
196
|
+
`Path=${settings.accessTokenPath}`,
|
|
197
|
+
domain ? `Domain=${domain}` : "",
|
|
198
|
+
`Expires=${expiresDate}`
|
|
199
|
+
].filter(Boolean).join("; ");
|
|
200
|
+
cookies.push(accessCookie);
|
|
201
|
+
}
|
|
202
|
+
if (cookies.length > 0) {
|
|
203
|
+
res.setHeader("Set-Cookie", cookies);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
function clearAuthCookies(res, settings, storageKeys = {
|
|
207
|
+
accessToken: DEFAULT_STORAGE_KEYS.ACCESS_TOKEN,
|
|
208
|
+
refreshToken: DEFAULT_STORAGE_KEYS.REFRESH_TOKEN
|
|
209
|
+
}) {
|
|
210
|
+
const domain = extractDomain(res.req);
|
|
211
|
+
const expiredDate = (/* @__PURE__ */ new Date(0)).toUTCString();
|
|
212
|
+
const cookies = [
|
|
213
|
+
[
|
|
214
|
+
`${storageKeys.refreshToken}=destroy`,
|
|
215
|
+
"HttpOnly",
|
|
216
|
+
settings.secure ? "Secure=true" : "",
|
|
217
|
+
`SameSite=${settings.sameSite}`,
|
|
218
|
+
`Path=${settings.refreshTokenPath}`,
|
|
219
|
+
domain ? `Domain=${domain}` : "",
|
|
220
|
+
`Expires=${expiredDate}`
|
|
221
|
+
].filter(Boolean).join("; "),
|
|
222
|
+
[
|
|
223
|
+
`${storageKeys.accessToken}=destroy`,
|
|
224
|
+
settings.secure ? "Secure=true" : "",
|
|
225
|
+
`SameSite=${settings.sameSite}`,
|
|
226
|
+
`Path=${settings.accessTokenPath}`,
|
|
227
|
+
domain ? `Domain=${domain}` : "",
|
|
228
|
+
`Expires=${expiredDate}`
|
|
229
|
+
].filter(Boolean).join("; ")
|
|
230
|
+
];
|
|
231
|
+
res.setHeader("Set-Cookie", cookies);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// src/utilities/jwt.ts
|
|
235
|
+
import jwt from "jsonwebtoken";
|
|
236
|
+
function createAccessToken(payload, options) {
|
|
237
|
+
return jwt.sign(payload, options.secret, {
|
|
238
|
+
expiresIn: options.expiresIn
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
function verifyAccessToken(token, options) {
|
|
242
|
+
return jwt.verify(token, options.secret, {
|
|
243
|
+
ignoreExpiration: options.ignoreExpiration ?? false
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
function decodeToken(token) {
|
|
247
|
+
try {
|
|
248
|
+
return jwt.decode(token);
|
|
249
|
+
} catch {
|
|
250
|
+
return null;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
function isJwtError(error) {
|
|
254
|
+
return error instanceof Error && ["TokenExpiredError", "JsonWebTokenError", "NotBeforeError"].includes(
|
|
255
|
+
error.name
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
function isTokenExpiredError(error) {
|
|
259
|
+
return isJwtError(error) && error.name === "TokenExpiredError";
|
|
260
|
+
}
|
|
261
|
+
function isTokenInvalidError(error) {
|
|
262
|
+
return isJwtError(error) && error.name === "JsonWebTokenError";
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// src/middleware/authGuard.ts
|
|
266
|
+
function createAuthGuard(config, t) {
|
|
267
|
+
const storageKeys = config.storageKeys ?? defaultStorageKeys;
|
|
268
|
+
const cookieSettings = { ...defaultCookieSettings, ...config.cookieSettings };
|
|
269
|
+
const revokeSession = async (ctx, sessionId, description, errorStack, path) => {
|
|
270
|
+
clearAuthCookies(ctx.res, cookieSettings, storageKeys);
|
|
271
|
+
if (config.hooks?.logError) {
|
|
272
|
+
try {
|
|
273
|
+
const contextInfo = {
|
|
274
|
+
reason: description,
|
|
275
|
+
sessionId,
|
|
276
|
+
userId: ctx.userId,
|
|
277
|
+
ip: ctx.ip,
|
|
278
|
+
userAgent: ctx.headers["user-agent"],
|
|
279
|
+
...path ? { path } : {},
|
|
280
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
281
|
+
};
|
|
282
|
+
const combinedStack = [
|
|
283
|
+
errorStack ? `Error Stack:
|
|
284
|
+
${errorStack}` : null,
|
|
285
|
+
"Context:",
|
|
286
|
+
JSON.stringify(contextInfo, null, 2)
|
|
287
|
+
].filter(Boolean).join("\n\n");
|
|
288
|
+
await config.hooks.logError({
|
|
289
|
+
type: "SECURITY",
|
|
290
|
+
description: `Session revoked: ${description}`,
|
|
291
|
+
stack: combinedStack,
|
|
292
|
+
ip: ctx.ip,
|
|
293
|
+
userId: ctx.userId ?? null
|
|
294
|
+
});
|
|
295
|
+
} catch {
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
if (sessionId) {
|
|
299
|
+
try {
|
|
300
|
+
await config.prisma.session.update({
|
|
301
|
+
where: { id: sessionId },
|
|
302
|
+
data: { revokedAt: /* @__PURE__ */ new Date() }
|
|
303
|
+
});
|
|
304
|
+
if (config.hooks?.onSessionRevoked) {
|
|
305
|
+
const session = await config.prisma.session.findUnique({
|
|
306
|
+
where: { id: sessionId },
|
|
307
|
+
select: { id: true, userId: true, socketId: true }
|
|
308
|
+
});
|
|
309
|
+
if (session) {
|
|
310
|
+
await config.hooks.onSessionRevoked(
|
|
311
|
+
session.userId,
|
|
312
|
+
session.socketId,
|
|
313
|
+
description
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
} catch {
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
};
|
|
321
|
+
const authGuard = t.middleware(async ({ ctx, meta, next, path }) => {
|
|
322
|
+
const cookies = parseAuthCookies(ctx.headers.cookie, storageKeys);
|
|
323
|
+
const authToken = cookies.accessToken;
|
|
324
|
+
const refreshToken = cookies.refreshToken;
|
|
325
|
+
const userAgent = ctx.headers["user-agent"];
|
|
326
|
+
if (!userAgent) {
|
|
327
|
+
throw new TRPCError({
|
|
328
|
+
code: "BAD_REQUEST",
|
|
329
|
+
message: "User agent is required"
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
if (authToken) {
|
|
333
|
+
try {
|
|
334
|
+
const decodedToken = verifyAccessToken(authToken, {
|
|
335
|
+
secret: config.secrets.jwt,
|
|
336
|
+
ignoreExpiration: meta?.ignoreExpiration ?? false
|
|
337
|
+
});
|
|
338
|
+
if (path === "auth.refresh" && !refreshToken) {
|
|
339
|
+
await revokeSession(
|
|
340
|
+
ctx,
|
|
341
|
+
decodedToken.id,
|
|
342
|
+
"Session revoked: No refresh token",
|
|
343
|
+
void 0,
|
|
344
|
+
path
|
|
345
|
+
);
|
|
346
|
+
throw new TRPCError({
|
|
347
|
+
message: "Unauthorized",
|
|
348
|
+
code: "UNAUTHORIZED"
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
const session = await config.prisma.session.findUnique({
|
|
352
|
+
where: {
|
|
353
|
+
id: decodedToken.id,
|
|
354
|
+
...path === "auth.refresh" ? { refreshToken } : {}
|
|
355
|
+
},
|
|
356
|
+
select: {
|
|
357
|
+
userId: true,
|
|
358
|
+
user: {
|
|
359
|
+
select: {
|
|
360
|
+
status: true,
|
|
361
|
+
verifiedHumanAt: true
|
|
362
|
+
}
|
|
363
|
+
},
|
|
364
|
+
revokedAt: true,
|
|
365
|
+
socketId: true,
|
|
366
|
+
id: true
|
|
367
|
+
}
|
|
368
|
+
});
|
|
369
|
+
if (!session) {
|
|
370
|
+
await revokeSession(
|
|
371
|
+
ctx,
|
|
372
|
+
decodedToken.id,
|
|
373
|
+
"Session revoked: Session not found",
|
|
374
|
+
void 0,
|
|
375
|
+
path
|
|
376
|
+
);
|
|
377
|
+
throw new TRPCError({
|
|
378
|
+
message: "Unauthorized",
|
|
379
|
+
code: "UNAUTHORIZED"
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
if (session.user.status === "BANNED") {
|
|
383
|
+
await revokeSession(
|
|
384
|
+
ctx,
|
|
385
|
+
session.id,
|
|
386
|
+
"Session revoked: User banned",
|
|
387
|
+
void 0,
|
|
388
|
+
path
|
|
389
|
+
);
|
|
390
|
+
throw new TRPCError({
|
|
391
|
+
message: "Unauthorized",
|
|
392
|
+
code: "UNAUTHORIZED"
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
if (config.features?.biometric && config.hooks?.getBiometricTimeout) {
|
|
396
|
+
const timeoutMs = await config.hooks.getBiometricTimeout();
|
|
397
|
+
if (timeoutMs !== null && !["auth.refresh", "auth.verifyBiometric", "auth.logout"].includes(
|
|
398
|
+
path
|
|
399
|
+
)) {
|
|
400
|
+
if (!session.user.verifiedHumanAt) {
|
|
401
|
+
throw new TRPCError({
|
|
402
|
+
message: "Biometric verification not completed. Please verify again.",
|
|
403
|
+
code: "FORBIDDEN"
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
const now = /* @__PURE__ */ new Date();
|
|
407
|
+
const verificationExpiry = new Date(
|
|
408
|
+
session.user.verifiedHumanAt.getTime() + timeoutMs
|
|
409
|
+
);
|
|
410
|
+
if (now > verificationExpiry) {
|
|
411
|
+
throw new TRPCError({
|
|
412
|
+
message: "Biometric verification expired. Please verify again.",
|
|
413
|
+
code: "FORBIDDEN"
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
if (session.revokedAt) {
|
|
419
|
+
await revokeSession(
|
|
420
|
+
ctx,
|
|
421
|
+
session.id,
|
|
422
|
+
"Session revoked: Session already revoked",
|
|
423
|
+
void 0,
|
|
424
|
+
path
|
|
425
|
+
);
|
|
426
|
+
throw new TRPCError({
|
|
427
|
+
message: "Unauthorized",
|
|
428
|
+
code: "UNAUTHORIZED"
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
if (meta?.adminRequired) {
|
|
432
|
+
const admin = await config.prisma.admin.findFirst({
|
|
433
|
+
where: { userId: session.userId },
|
|
434
|
+
select: { ip: true }
|
|
435
|
+
});
|
|
436
|
+
if (!admin || admin.ip !== ctx.ip) {
|
|
437
|
+
await revokeSession(
|
|
438
|
+
ctx,
|
|
439
|
+
session.id,
|
|
440
|
+
"Session revoked: Admin not found or IP mismatch",
|
|
441
|
+
void 0,
|
|
442
|
+
path
|
|
443
|
+
);
|
|
444
|
+
throw new TRPCError({
|
|
445
|
+
message: "Unauthorized",
|
|
446
|
+
code: "UNAUTHORIZED"
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
return next({
|
|
451
|
+
ctx: {
|
|
452
|
+
...ctx,
|
|
453
|
+
userId: session.userId,
|
|
454
|
+
socketId: session.socketId,
|
|
455
|
+
sessionId: session.id,
|
|
456
|
+
refreshToken
|
|
457
|
+
}
|
|
458
|
+
});
|
|
459
|
+
} catch (err) {
|
|
460
|
+
if (err instanceof TRPCError && err.code === "FORBIDDEN") {
|
|
461
|
+
throw err;
|
|
462
|
+
}
|
|
463
|
+
if (!meta?.authRequired) {
|
|
464
|
+
return next({ ctx: { ...ctx, userId: 0 } });
|
|
465
|
+
}
|
|
466
|
+
const errorStack = err instanceof Error ? err.stack : void 0;
|
|
467
|
+
if (isTokenExpiredError(err) || isTokenInvalidError(err)) {
|
|
468
|
+
await revokeSession(
|
|
469
|
+
ctx,
|
|
470
|
+
null,
|
|
471
|
+
isTokenInvalidError(err) ? "Session revoked: Token invalid" : "Session revoked: Token expired",
|
|
472
|
+
errorStack,
|
|
473
|
+
path
|
|
474
|
+
);
|
|
475
|
+
throw new TRPCError({
|
|
476
|
+
message: isTokenInvalidError(err) ? "Token invalid" : "Token expired",
|
|
477
|
+
code: "UNAUTHORIZED"
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
if (err instanceof TRPCError && err.code === "UNAUTHORIZED") {
|
|
481
|
+
await revokeSession(
|
|
482
|
+
ctx,
|
|
483
|
+
null,
|
|
484
|
+
"Session revoked: Unauthorized",
|
|
485
|
+
errorStack,
|
|
486
|
+
path
|
|
487
|
+
);
|
|
488
|
+
throw new TRPCError({
|
|
489
|
+
message: "Unauthorized",
|
|
490
|
+
code: "UNAUTHORIZED"
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
throw err;
|
|
494
|
+
}
|
|
495
|
+
} else {
|
|
496
|
+
if (!meta?.authRequired) {
|
|
497
|
+
return next({ ctx: { ...ctx, userId: 0 } });
|
|
498
|
+
}
|
|
499
|
+
await revokeSession(
|
|
500
|
+
ctx,
|
|
501
|
+
null,
|
|
502
|
+
"Session revoked: No token sent",
|
|
503
|
+
void 0,
|
|
504
|
+
path
|
|
505
|
+
);
|
|
506
|
+
throw new TRPCError({ message: "Unauthorized", code: "UNAUTHORIZED" });
|
|
507
|
+
}
|
|
508
|
+
});
|
|
509
|
+
return authGuard;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// src/procedures/base.ts
|
|
513
|
+
import { randomUUID } from "crypto";
|
|
514
|
+
import { TRPCError as TRPCError2 } from "@trpc/server";
|
|
515
|
+
|
|
516
|
+
// src/utilities/browser.ts
|
|
517
|
+
function detectBrowser(userAgent) {
|
|
518
|
+
if (/cfnetwork|darwin/i.test(userAgent)) return "iOS App";
|
|
519
|
+
if (/iphone|ipad|ipod/i.test(userAgent) && /safari/i.test(userAgent) && !/crios|fxios|edg\//i.test(userAgent)) {
|
|
520
|
+
return "iOS Browser (Safari)";
|
|
521
|
+
}
|
|
522
|
+
if (/iphone|ipad|ipod/i.test(userAgent) && /crios/i.test(userAgent))
|
|
523
|
+
return "iOS Browser (Chrome)";
|
|
524
|
+
if (/iphone|ipad|ipod/i.test(userAgent) && /fxios/i.test(userAgent))
|
|
525
|
+
return "iOS Browser (Firefox)";
|
|
526
|
+
if (/iphone|ipad|ipod/i.test(userAgent) && /edg\//i.test(userAgent))
|
|
527
|
+
return "iOS Browser (Edge)";
|
|
528
|
+
if (/android/i.test(userAgent) && !/chrome|firefox|samsungbrowser|opr\/|edg\//i.test(userAgent)) {
|
|
529
|
+
return "Android App";
|
|
530
|
+
}
|
|
531
|
+
if (/android/i.test(userAgent) && /chrome/i.test(userAgent))
|
|
532
|
+
return "Android Browser (Chrome)";
|
|
533
|
+
if (/android/i.test(userAgent) && /firefox/i.test(userAgent))
|
|
534
|
+
return "Android Browser (Firefox)";
|
|
535
|
+
if (/android/i.test(userAgent) && /samsungbrowser/i.test(userAgent))
|
|
536
|
+
return "Android Browser (Samsung)";
|
|
537
|
+
if (/android/i.test(userAgent) && /opr\//i.test(userAgent))
|
|
538
|
+
return "Android Browser (Opera)";
|
|
539
|
+
if (/android/i.test(userAgent) && /edg\//i.test(userAgent))
|
|
540
|
+
return "Android Browser (Edge)";
|
|
541
|
+
if (/chrome|chromium/i.test(userAgent)) return "Chrome";
|
|
542
|
+
if (/firefox/i.test(userAgent)) return "Firefox";
|
|
543
|
+
if (/safari/i.test(userAgent) && !/chrome|chromium|crios/i.test(userAgent))
|
|
544
|
+
return "Safari";
|
|
545
|
+
if (/opr\//i.test(userAgent)) return "Opera";
|
|
546
|
+
if (/edg\//i.test(userAgent)) return "Edge";
|
|
547
|
+
return "Unknown";
|
|
548
|
+
}
|
|
549
|
+
function isMobileDevice(userAgent) {
|
|
550
|
+
return /android|iphone|ipad|ipod|mobile/i.test(userAgent);
|
|
551
|
+
}
|
|
552
|
+
function isNativeApp(userAgent) {
|
|
553
|
+
const browser = detectBrowser(userAgent);
|
|
554
|
+
return browser === "iOS App" || browser === "Android App";
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
// src/utilities/oauth.ts
|
|
558
|
+
import appleSignin from "apple-signin-auth";
|
|
559
|
+
import { OAuth2Client } from "google-auth-library";
|
|
560
|
+
var OAuthVerificationError = class extends Error {
|
|
561
|
+
constructor(message, statusCode = 401) {
|
|
562
|
+
super(message);
|
|
563
|
+
this.statusCode = statusCode;
|
|
564
|
+
this.name = "OAuthVerificationError";
|
|
565
|
+
}
|
|
566
|
+
};
|
|
567
|
+
function createOAuthVerifier(keys) {
|
|
568
|
+
let googleClient = null;
|
|
569
|
+
if (keys.google?.clientId) {
|
|
570
|
+
googleClient = new OAuth2Client({
|
|
571
|
+
clientId: keys.google.clientId,
|
|
572
|
+
clientSecret: keys.google.clientSecret
|
|
573
|
+
});
|
|
574
|
+
}
|
|
575
|
+
return async function verifyOAuthToken(provider, token, extra) {
|
|
576
|
+
if (provider === "GOOGLE") {
|
|
577
|
+
if (!keys.google?.clientId) {
|
|
578
|
+
throw new OAuthVerificationError(
|
|
579
|
+
"Google OAuth configuration missing",
|
|
580
|
+
500
|
|
581
|
+
);
|
|
582
|
+
}
|
|
583
|
+
if (!googleClient) {
|
|
584
|
+
throw new OAuthVerificationError(
|
|
585
|
+
"Google OAuth client not initialized",
|
|
586
|
+
500
|
|
587
|
+
);
|
|
588
|
+
}
|
|
589
|
+
const audience = [keys.google.clientId];
|
|
590
|
+
if (keys.google.iosClientId) {
|
|
591
|
+
audience.push(keys.google.iosClientId);
|
|
592
|
+
}
|
|
593
|
+
const ticket = await googleClient.verifyIdToken({
|
|
594
|
+
idToken: token,
|
|
595
|
+
audience
|
|
596
|
+
});
|
|
597
|
+
const payload = ticket.getPayload();
|
|
598
|
+
if (!payload?.sub || !payload.email) {
|
|
599
|
+
throw new OAuthVerificationError("Invalid Google token", 401);
|
|
600
|
+
}
|
|
601
|
+
return {
|
|
602
|
+
oauthId: payload.sub,
|
|
603
|
+
email: payload.email
|
|
604
|
+
};
|
|
605
|
+
}
|
|
606
|
+
if (provider === "APPLE") {
|
|
607
|
+
if (!keys.apple?.clientId) {
|
|
608
|
+
throw new OAuthVerificationError(
|
|
609
|
+
"Apple OAuth configuration missing",
|
|
610
|
+
500
|
|
611
|
+
);
|
|
612
|
+
}
|
|
613
|
+
const audience = [keys.apple.clientId];
|
|
614
|
+
if (keys.apple.iosClientId) {
|
|
615
|
+
audience.push(keys.apple.iosClientId);
|
|
616
|
+
}
|
|
617
|
+
const { sub, email } = await appleSignin.verifyIdToken(token, {
|
|
618
|
+
audience,
|
|
619
|
+
ignoreExpiration: false
|
|
620
|
+
});
|
|
621
|
+
const finalEmail = email || extra?.email;
|
|
622
|
+
if (!finalEmail || !sub) {
|
|
623
|
+
throw new OAuthVerificationError("Invalid Apple token", 401);
|
|
624
|
+
}
|
|
625
|
+
return {
|
|
626
|
+
oauthId: sub,
|
|
627
|
+
email: finalEmail
|
|
628
|
+
};
|
|
629
|
+
}
|
|
630
|
+
throw new OAuthVerificationError("Unsupported OAuth provider", 400);
|
|
631
|
+
};
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
// src/utilities/password.ts
|
|
635
|
+
import bcrypt from "bcryptjs";
|
|
636
|
+
var DEFAULT_SALT_ROUNDS = 10;
|
|
637
|
+
async function hashPassword(password, saltRounds = DEFAULT_SALT_ROUNDS) {
|
|
638
|
+
const salt = await bcrypt.genSalt(saltRounds);
|
|
639
|
+
return bcrypt.hash(password, salt);
|
|
640
|
+
}
|
|
641
|
+
async function comparePassword(password, hashedPassword) {
|
|
642
|
+
return bcrypt.compare(password, hashedPassword);
|
|
643
|
+
}
|
|
644
|
+
function validatePasswordStrength(password, minLength = 6) {
|
|
645
|
+
if (password.length < minLength) {
|
|
646
|
+
return {
|
|
647
|
+
valid: false,
|
|
648
|
+
error: `Password must be at least ${minLength} characters`
|
|
649
|
+
};
|
|
650
|
+
}
|
|
651
|
+
return { valid: true };
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
// src/utilities/totp.ts
|
|
655
|
+
import crypto from "crypto";
|
|
656
|
+
import { TOTP } from "totp-generator";
|
|
657
|
+
var BASE32_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
|
|
658
|
+
function generateTotpSecret(length = 16) {
|
|
659
|
+
let secret = "";
|
|
660
|
+
for (let i = 0; i < length; i++) {
|
|
661
|
+
const randIndex = Math.floor(Math.random() * BASE32_CHARS.length);
|
|
662
|
+
secret += BASE32_CHARS[randIndex];
|
|
663
|
+
}
|
|
664
|
+
return secret;
|
|
665
|
+
}
|
|
666
|
+
function cleanBase32String(input) {
|
|
667
|
+
return input.replace(/[^A-Z2-7]/gi, "").toUpperCase();
|
|
668
|
+
}
|
|
669
|
+
async function generateTotpCode(secret) {
|
|
670
|
+
const cleanSecret = cleanBase32String(secret);
|
|
671
|
+
const { otp } = await TOTP.generate(cleanSecret);
|
|
672
|
+
return otp;
|
|
673
|
+
}
|
|
674
|
+
async function verifyTotp(code, secret) {
|
|
675
|
+
const cleanSecret = cleanBase32String(secret);
|
|
676
|
+
const normalizedCode = code.replace(/\s/g, "");
|
|
677
|
+
const { otp } = await TOTP.generate(cleanSecret);
|
|
678
|
+
return otp === normalizedCode;
|
|
679
|
+
}
|
|
680
|
+
function generateOtp(min = 1e5, max = 999999) {
|
|
681
|
+
return Math.floor(crypto.randomInt(min, max + 1));
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
// src/procedures/base.ts
|
|
685
|
+
var BaseProcedureFactory = class {
|
|
686
|
+
constructor(config, procedure, authProcedure) {
|
|
687
|
+
this.config = config;
|
|
688
|
+
this.procedure = procedure;
|
|
689
|
+
this.authProcedure = authProcedure;
|
|
690
|
+
}
|
|
691
|
+
/** Returns all base auth procedures to be merged into the router. */
|
|
692
|
+
createBaseProcedures(schemas) {
|
|
693
|
+
return {
|
|
694
|
+
register: this.register(schemas.signup),
|
|
695
|
+
login: this.login(schemas.login),
|
|
696
|
+
logout: this.logout(),
|
|
697
|
+
refresh: this.refresh(),
|
|
698
|
+
endAllSessions: this.endAllSessions(),
|
|
699
|
+
changePassword: this.changePassword(),
|
|
700
|
+
sendPasswordResetEmail: this.sendPasswordResetEmail(),
|
|
701
|
+
checkPasswordReset: this.checkPasswordReset(),
|
|
702
|
+
resetPassword: this.resetPassword()
|
|
703
|
+
};
|
|
704
|
+
}
|
|
705
|
+
register(schema) {
|
|
706
|
+
return this.procedure.input(schema).mutation(async ({ ctx, input }) => {
|
|
707
|
+
const typedInput = input;
|
|
708
|
+
const { username, email, password } = typedInput;
|
|
709
|
+
const userAgent = ctx.headers["user-agent"];
|
|
710
|
+
if (!userAgent) {
|
|
711
|
+
throw new TRPCError2({
|
|
712
|
+
code: "BAD_REQUEST",
|
|
713
|
+
message: "User agent not found"
|
|
714
|
+
});
|
|
715
|
+
}
|
|
716
|
+
if (this.config.hooks?.beforeRegister) {
|
|
717
|
+
await this.config.hooks.beforeRegister(typedInput);
|
|
718
|
+
}
|
|
719
|
+
const usernameCheck = await this.config.prisma.user.findFirst({
|
|
720
|
+
where: { username: { equals: username, mode: "insensitive" } }
|
|
721
|
+
});
|
|
722
|
+
if (usernameCheck) {
|
|
723
|
+
throw new TRPCError2({
|
|
724
|
+
code: "CONFLICT",
|
|
725
|
+
message: "An account already exists with that username."
|
|
726
|
+
});
|
|
727
|
+
}
|
|
728
|
+
const emailCheck = await this.config.prisma.user.findFirst({
|
|
729
|
+
where: { email: { equals: email, mode: "insensitive" } },
|
|
730
|
+
select: { id: true }
|
|
731
|
+
});
|
|
732
|
+
if (emailCheck) {
|
|
733
|
+
throw new TRPCError2({
|
|
734
|
+
code: "CONFLICT",
|
|
735
|
+
message: "An account already exists with that email."
|
|
736
|
+
});
|
|
737
|
+
}
|
|
738
|
+
const hashedPassword = await hashPassword(password);
|
|
739
|
+
const user = await this.config.prisma.user.create({
|
|
740
|
+
data: {
|
|
741
|
+
username,
|
|
742
|
+
email,
|
|
743
|
+
password: hashedPassword,
|
|
744
|
+
status: "ACTIVE",
|
|
745
|
+
tag: this.config.features.biometric ? "BOT" : "HUMAN",
|
|
746
|
+
twoFaEnabled: false,
|
|
747
|
+
emailVerificationStatus: "UNVERIFIED",
|
|
748
|
+
verifiedHumanAt: null
|
|
749
|
+
}
|
|
750
|
+
});
|
|
751
|
+
if (this.config.hooks?.onUserCreated) {
|
|
752
|
+
await this.config.hooks.onUserCreated(user.id, typedInput);
|
|
753
|
+
}
|
|
754
|
+
const refreshToken = randomUUID();
|
|
755
|
+
const extraSessionData = this.config.hooks?.getSessionData ? await this.config.hooks.getSessionData(typedInput) : {};
|
|
756
|
+
const session = await this.config.prisma.session.create({
|
|
757
|
+
data: {
|
|
758
|
+
userId: user.id,
|
|
759
|
+
browserName: detectBrowser(userAgent),
|
|
760
|
+
socketId: null,
|
|
761
|
+
refreshToken,
|
|
762
|
+
...extraSessionData
|
|
763
|
+
},
|
|
764
|
+
select: { id: true, refreshToken: true, userId: true }
|
|
765
|
+
});
|
|
766
|
+
if (this.config.hooks?.onSessionCreated) {
|
|
767
|
+
await this.config.hooks.onSessionCreated(session.id, typedInput);
|
|
768
|
+
}
|
|
769
|
+
const accessToken = createAccessToken(
|
|
770
|
+
{ id: session.id, userId: session.userId, verifiedHumanAt: null },
|
|
771
|
+
{
|
|
772
|
+
secret: this.config.secrets.jwt,
|
|
773
|
+
expiresIn: this.config.tokenSettings.accessTokenExpiry
|
|
774
|
+
}
|
|
775
|
+
);
|
|
776
|
+
setAuthCookies(
|
|
777
|
+
ctx.res,
|
|
778
|
+
{ accessToken, refreshToken: session.refreshToken },
|
|
779
|
+
this.config.cookieSettings,
|
|
780
|
+
this.config.storageKeys
|
|
781
|
+
);
|
|
782
|
+
return {
|
|
783
|
+
success: true,
|
|
784
|
+
user: { id: user.id, email: user.email, username: user.username }
|
|
785
|
+
};
|
|
786
|
+
});
|
|
787
|
+
}
|
|
788
|
+
login(schema) {
|
|
789
|
+
return this.procedure.input(schema).mutation(async ({ ctx, input }) => {
|
|
790
|
+
const typedInput = input;
|
|
791
|
+
const { username, password, code } = typedInput;
|
|
792
|
+
const userAgent = ctx.headers["user-agent"];
|
|
793
|
+
if (!userAgent) {
|
|
794
|
+
throw new TRPCError2({
|
|
795
|
+
code: "BAD_REQUEST",
|
|
796
|
+
message: "User agent not found"
|
|
797
|
+
});
|
|
798
|
+
}
|
|
799
|
+
if (this.config.hooks?.beforeLogin) {
|
|
800
|
+
await this.config.hooks.beforeLogin(typedInput);
|
|
801
|
+
}
|
|
802
|
+
const user = await this.config.prisma.user.findFirst({
|
|
803
|
+
where: {
|
|
804
|
+
OR: [
|
|
805
|
+
{ email: { equals: username, mode: "insensitive" } },
|
|
806
|
+
{ username: { equals: username, mode: "insensitive" } }
|
|
807
|
+
]
|
|
808
|
+
},
|
|
809
|
+
select: {
|
|
810
|
+
id: true,
|
|
811
|
+
status: true,
|
|
812
|
+
password: true,
|
|
813
|
+
twoFaEnabled: true,
|
|
814
|
+
email: true,
|
|
815
|
+
username: true,
|
|
816
|
+
oauthProvider: true,
|
|
817
|
+
verifiedHumanAt: true
|
|
818
|
+
}
|
|
819
|
+
});
|
|
820
|
+
if (!user) {
|
|
821
|
+
throw new TRPCError2({
|
|
822
|
+
code: "FORBIDDEN",
|
|
823
|
+
message: "Invalid credentials."
|
|
824
|
+
});
|
|
825
|
+
}
|
|
826
|
+
if (user.status === "DEACTIVATED") {
|
|
827
|
+
throw new TRPCError2({
|
|
828
|
+
code: "FORBIDDEN",
|
|
829
|
+
message: "Your account has been deactivated."
|
|
830
|
+
});
|
|
831
|
+
}
|
|
832
|
+
if (user.status === "BANNED") {
|
|
833
|
+
throw new TRPCError2({
|
|
834
|
+
code: "FORBIDDEN",
|
|
835
|
+
message: "Your account has been banned."
|
|
836
|
+
});
|
|
837
|
+
}
|
|
838
|
+
if (!user.password) {
|
|
839
|
+
throw new TRPCError2({
|
|
840
|
+
code: "FORBIDDEN",
|
|
841
|
+
message: `This account uses ${user.oauthProvider?.toLowerCase() || "social login"}. Please use that method.`
|
|
842
|
+
});
|
|
843
|
+
}
|
|
844
|
+
const isMatch = await comparePassword(password, user.password);
|
|
845
|
+
if (!isMatch) {
|
|
846
|
+
throw new TRPCError2({
|
|
847
|
+
code: "FORBIDDEN",
|
|
848
|
+
message: "Invalid credentials."
|
|
849
|
+
});
|
|
850
|
+
}
|
|
851
|
+
if (user.twoFaEnabled && this.config.features?.twoFa) {
|
|
852
|
+
if (!code) {
|
|
853
|
+
throw new TRPCError2({
|
|
854
|
+
code: "FORBIDDEN",
|
|
855
|
+
message: "2FA code required."
|
|
856
|
+
});
|
|
857
|
+
}
|
|
858
|
+
let validCode = false;
|
|
859
|
+
const secrets = await this.config.prisma.session.findMany({
|
|
860
|
+
where: { userId: user.id, twoFaSecret: { not: null } },
|
|
861
|
+
select: { twoFaSecret: true }
|
|
862
|
+
});
|
|
863
|
+
for (const s of secrets) {
|
|
864
|
+
if (s.twoFaSecret && await verifyTotp(code, cleanBase32String(s.twoFaSecret))) {
|
|
865
|
+
validCode = true;
|
|
866
|
+
break;
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
if (!validCode) {
|
|
870
|
+
const checkOTP = await this.config.prisma.oTPBasedLogin.findFirst({
|
|
871
|
+
where: {
|
|
872
|
+
code: Number(code),
|
|
873
|
+
userId: user.id,
|
|
874
|
+
disabled: false,
|
|
875
|
+
createdAt: { gte: new Date(Date.now() - this.config.tokenSettings.otpValidityMs) }
|
|
876
|
+
}
|
|
877
|
+
});
|
|
878
|
+
if (checkOTP) {
|
|
879
|
+
validCode = true;
|
|
880
|
+
await this.config.prisma.oTPBasedLogin.updateMany({
|
|
881
|
+
where: { code: Number(code) },
|
|
882
|
+
data: { disabled: true }
|
|
883
|
+
});
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
if (!validCode) {
|
|
887
|
+
throw new TRPCError2({
|
|
888
|
+
code: "FORBIDDEN",
|
|
889
|
+
message: "Invalid 2FA code."
|
|
890
|
+
});
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
const refreshToken = randomUUID();
|
|
894
|
+
const extraSessionData = this.config.hooks?.getSessionData ? await this.config.hooks.getSessionData(typedInput) : {};
|
|
895
|
+
const session = await this.config.prisma.session.create({
|
|
896
|
+
data: {
|
|
897
|
+
userId: user.id,
|
|
898
|
+
browserName: detectBrowser(userAgent),
|
|
899
|
+
socketId: null,
|
|
900
|
+
refreshToken,
|
|
901
|
+
...extraSessionData
|
|
902
|
+
},
|
|
903
|
+
select: {
|
|
904
|
+
id: true,
|
|
905
|
+
refreshToken: true,
|
|
906
|
+
userId: true,
|
|
907
|
+
socketId: true,
|
|
908
|
+
browserName: true,
|
|
909
|
+
issuedAt: true,
|
|
910
|
+
lastUsed: true,
|
|
911
|
+
revokedAt: true,
|
|
912
|
+
deviceId: true,
|
|
913
|
+
twoFaSecret: true
|
|
914
|
+
}
|
|
915
|
+
});
|
|
916
|
+
if (this.config.hooks?.onUserLogin) {
|
|
917
|
+
await this.config.hooks.onUserLogin(user.id, session.id);
|
|
918
|
+
}
|
|
919
|
+
if (this.config.hooks?.onSessionCreated) {
|
|
920
|
+
await this.config.hooks.onSessionCreated(session.id, typedInput);
|
|
921
|
+
}
|
|
922
|
+
const accessToken = createAccessToken(
|
|
923
|
+
{ id: session.id, userId: session.userId, verifiedHumanAt: user.verifiedHumanAt },
|
|
924
|
+
{
|
|
925
|
+
secret: this.config.secrets.jwt,
|
|
926
|
+
expiresIn: this.config.tokenSettings.accessTokenExpiry
|
|
927
|
+
}
|
|
928
|
+
);
|
|
929
|
+
setAuthCookies(
|
|
930
|
+
ctx.res,
|
|
931
|
+
{ accessToken, refreshToken: session.refreshToken },
|
|
932
|
+
this.config.cookieSettings,
|
|
933
|
+
this.config.storageKeys
|
|
934
|
+
);
|
|
935
|
+
return {
|
|
936
|
+
success: true,
|
|
937
|
+
user: { id: user.id, email: user.email, username: user.username }
|
|
938
|
+
};
|
|
939
|
+
});
|
|
940
|
+
}
|
|
941
|
+
logout() {
|
|
942
|
+
return this.procedure.mutation(async ({ ctx }) => {
|
|
943
|
+
const { userId, sessionId } = ctx;
|
|
944
|
+
if (sessionId) {
|
|
945
|
+
await this.config.prisma.session.update({
|
|
946
|
+
where: { id: sessionId },
|
|
947
|
+
data: { revokedAt: /* @__PURE__ */ new Date() }
|
|
948
|
+
});
|
|
949
|
+
if (userId) {
|
|
950
|
+
await this.config.prisma.user.update({
|
|
951
|
+
where: { id: userId },
|
|
952
|
+
data: { isActive: false }
|
|
953
|
+
});
|
|
954
|
+
}
|
|
955
|
+
if (this.config.hooks?.afterLogout) {
|
|
956
|
+
await this.config.hooks.afterLogout(userId, sessionId, ctx.socketId);
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
clearAuthCookies(ctx.res, this.config.cookieSettings, this.config.storageKeys);
|
|
960
|
+
return { success: true };
|
|
961
|
+
});
|
|
962
|
+
}
|
|
963
|
+
refresh() {
|
|
964
|
+
return this.authProcedure.meta({ ignoreExpiration: true }).query(async ({ ctx }) => {
|
|
965
|
+
const session = await this.config.prisma.session.update({
|
|
966
|
+
where: { id: ctx.sessionId },
|
|
967
|
+
data: { refreshToken: randomUUID(), lastUsed: /* @__PURE__ */ new Date() },
|
|
968
|
+
select: {
|
|
969
|
+
id: true,
|
|
970
|
+
refreshToken: true,
|
|
971
|
+
userId: true,
|
|
972
|
+
user: { select: { verifiedHumanAt: true } }
|
|
973
|
+
}
|
|
974
|
+
});
|
|
975
|
+
if (this.config.hooks?.onRefresh) {
|
|
976
|
+
this.config.hooks.onRefresh(session.userId).catch(() => {
|
|
977
|
+
});
|
|
978
|
+
}
|
|
979
|
+
const accessToken = createAccessToken(
|
|
980
|
+
{ id: session.id, userId: session.userId, verifiedHumanAt: session.user.verifiedHumanAt },
|
|
981
|
+
{
|
|
982
|
+
secret: this.config.secrets.jwt,
|
|
983
|
+
expiresIn: this.config.tokenSettings.accessTokenExpiry
|
|
984
|
+
}
|
|
985
|
+
);
|
|
986
|
+
setAuthCookies(
|
|
987
|
+
ctx.res,
|
|
988
|
+
{ accessToken, refreshToken: session.refreshToken },
|
|
989
|
+
this.config.cookieSettings,
|
|
990
|
+
this.config.storageKeys
|
|
991
|
+
);
|
|
992
|
+
return { success: true };
|
|
993
|
+
});
|
|
994
|
+
}
|
|
995
|
+
endAllSessions() {
|
|
996
|
+
return this.authProcedure.input(endAllSessionsSchema).mutation(async ({ ctx, input }) => {
|
|
997
|
+
const { skipCurrentSession } = input;
|
|
998
|
+
const { userId, sessionId } = ctx;
|
|
999
|
+
const sessionsToRevoke = await this.config.prisma.session.findMany({
|
|
1000
|
+
where: {
|
|
1001
|
+
userId,
|
|
1002
|
+
revokedAt: null,
|
|
1003
|
+
...skipCurrentSession ? { NOT: { id: sessionId } } : {}
|
|
1004
|
+
},
|
|
1005
|
+
select: { socketId: true, id: true, userId: true }
|
|
1006
|
+
});
|
|
1007
|
+
await this.config.prisma.session.updateMany({
|
|
1008
|
+
where: {
|
|
1009
|
+
userId,
|
|
1010
|
+
revokedAt: null,
|
|
1011
|
+
...skipCurrentSession ? { NOT: { id: sessionId } } : {}
|
|
1012
|
+
},
|
|
1013
|
+
data: { revokedAt: /* @__PURE__ */ new Date() }
|
|
1014
|
+
});
|
|
1015
|
+
for (const session of sessionsToRevoke) {
|
|
1016
|
+
if (this.config.hooks?.onSessionRevoked) {
|
|
1017
|
+
await this.config.hooks.onSessionRevoked(session.id, session.socketId, "End all sessions");
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
if (!skipCurrentSession) {
|
|
1021
|
+
await this.config.prisma.user.update({
|
|
1022
|
+
where: { id: userId },
|
|
1023
|
+
data: { isActive: false }
|
|
1024
|
+
});
|
|
1025
|
+
}
|
|
1026
|
+
return { success: true, revokedCount: sessionsToRevoke.length };
|
|
1027
|
+
});
|
|
1028
|
+
}
|
|
1029
|
+
changePassword() {
|
|
1030
|
+
return this.authProcedure.input(changePasswordSchema).mutation(async ({ ctx, input }) => {
|
|
1031
|
+
const { userId, sessionId } = ctx;
|
|
1032
|
+
const { currentPassword, newPassword } = input;
|
|
1033
|
+
if (currentPassword === newPassword) {
|
|
1034
|
+
throw new TRPCError2({
|
|
1035
|
+
code: "BAD_REQUEST",
|
|
1036
|
+
message: "New password cannot be the same as current password"
|
|
1037
|
+
});
|
|
1038
|
+
}
|
|
1039
|
+
const user = await this.config.prisma.user.findUnique({
|
|
1040
|
+
where: { id: userId },
|
|
1041
|
+
select: { password: true }
|
|
1042
|
+
});
|
|
1043
|
+
if (!user) {
|
|
1044
|
+
throw new TRPCError2({ code: "NOT_FOUND", message: "User not found" });
|
|
1045
|
+
}
|
|
1046
|
+
if (!user.password) {
|
|
1047
|
+
throw new TRPCError2({
|
|
1048
|
+
code: "BAD_REQUEST",
|
|
1049
|
+
message: "This account uses social login and cannot change password."
|
|
1050
|
+
});
|
|
1051
|
+
}
|
|
1052
|
+
const isMatch = await comparePassword(currentPassword, user.password);
|
|
1053
|
+
if (!isMatch) {
|
|
1054
|
+
throw new TRPCError2({
|
|
1055
|
+
code: "FORBIDDEN",
|
|
1056
|
+
message: "Current password is incorrect"
|
|
1057
|
+
});
|
|
1058
|
+
}
|
|
1059
|
+
const hashedPassword = await hashPassword(newPassword);
|
|
1060
|
+
await this.config.prisma.user.update({
|
|
1061
|
+
where: { id: userId },
|
|
1062
|
+
data: { password: hashedPassword }
|
|
1063
|
+
});
|
|
1064
|
+
await this.config.prisma.session.updateMany({
|
|
1065
|
+
where: { userId, revokedAt: null, NOT: { id: sessionId } },
|
|
1066
|
+
data: { revokedAt: /* @__PURE__ */ new Date() }
|
|
1067
|
+
});
|
|
1068
|
+
if (this.config.hooks?.onPasswordChanged) {
|
|
1069
|
+
await this.config.hooks.onPasswordChanged(userId);
|
|
1070
|
+
}
|
|
1071
|
+
return {
|
|
1072
|
+
success: true,
|
|
1073
|
+
message: "Password changed. You will need to re-login on other devices."
|
|
1074
|
+
};
|
|
1075
|
+
});
|
|
1076
|
+
}
|
|
1077
|
+
sendPasswordResetEmail() {
|
|
1078
|
+
return this.procedure.input(requestPasswordResetSchema).mutation(async ({ input }) => {
|
|
1079
|
+
const { email } = input;
|
|
1080
|
+
const user = await this.config.prisma.user.findFirst({
|
|
1081
|
+
where: { email: { equals: email, mode: "insensitive" }, status: "ACTIVE" },
|
|
1082
|
+
select: { id: true, password: true, email: true }
|
|
1083
|
+
});
|
|
1084
|
+
if (!user) {
|
|
1085
|
+
return { message: "If an account exists with that email, a reset link has been sent." };
|
|
1086
|
+
}
|
|
1087
|
+
if (!user.password) {
|
|
1088
|
+
throw new TRPCError2({
|
|
1089
|
+
code: "BAD_REQUEST",
|
|
1090
|
+
message: "This account uses social login. Please use that method."
|
|
1091
|
+
});
|
|
1092
|
+
}
|
|
1093
|
+
await this.config.prisma.passwordReset.deleteMany({ where: { userId: user.id } });
|
|
1094
|
+
const passwordReset = await this.config.prisma.passwordReset.create({
|
|
1095
|
+
data: { userId: user.id }
|
|
1096
|
+
});
|
|
1097
|
+
if (this.config.emailService) {
|
|
1098
|
+
await this.config.emailService.sendPasswordResetEmail(user.email, String(passwordReset.id));
|
|
1099
|
+
}
|
|
1100
|
+
return { message: "Password reset email sent." };
|
|
1101
|
+
});
|
|
1102
|
+
}
|
|
1103
|
+
checkPasswordReset() {
|
|
1104
|
+
return this.procedure.input(checkPasswordResetSchema).query(async ({ input }) => {
|
|
1105
|
+
const { token } = input;
|
|
1106
|
+
const passwordReset = await this.config.prisma.passwordReset.findUnique({
|
|
1107
|
+
where: { id: token },
|
|
1108
|
+
select: { id: true, createdAt: true, userId: true }
|
|
1109
|
+
});
|
|
1110
|
+
if (!passwordReset) {
|
|
1111
|
+
throw new TRPCError2({ code: "NOT_FOUND", message: "Invalid reset token." });
|
|
1112
|
+
}
|
|
1113
|
+
if (passwordReset.createdAt.getTime() + this.config.tokenSettings.passwordResetExpiryMs < Date.now()) {
|
|
1114
|
+
await this.config.prisma.passwordReset.delete({ where: { id: token } });
|
|
1115
|
+
throw new TRPCError2({ code: "FORBIDDEN", message: "Reset token expired." });
|
|
1116
|
+
}
|
|
1117
|
+
return { valid: true };
|
|
1118
|
+
});
|
|
1119
|
+
}
|
|
1120
|
+
resetPassword() {
|
|
1121
|
+
return this.procedure.input(resetPasswordSchema).mutation(async ({ input }) => {
|
|
1122
|
+
const { token, password } = input;
|
|
1123
|
+
const passwordReset = await this.config.prisma.passwordReset.findFirst({
|
|
1124
|
+
where: { id: token },
|
|
1125
|
+
select: { id: true, createdAt: true, userId: true }
|
|
1126
|
+
});
|
|
1127
|
+
if (!passwordReset) {
|
|
1128
|
+
throw new TRPCError2({ code: "NOT_FOUND", message: "Invalid reset token." });
|
|
1129
|
+
}
|
|
1130
|
+
if (passwordReset.createdAt.getTime() + this.config.tokenSettings.passwordResetExpiryMs < Date.now()) {
|
|
1131
|
+
await this.config.prisma.passwordReset.delete({ where: { id: token } });
|
|
1132
|
+
throw new TRPCError2({ code: "FORBIDDEN", message: "Reset token expired." });
|
|
1133
|
+
}
|
|
1134
|
+
const hashedPassword = await hashPassword(password);
|
|
1135
|
+
await this.config.prisma.user.update({
|
|
1136
|
+
where: { id: passwordReset.userId },
|
|
1137
|
+
data: { password: hashedPassword }
|
|
1138
|
+
});
|
|
1139
|
+
await this.config.prisma.passwordReset.delete({ where: { id: token } });
|
|
1140
|
+
await this.config.prisma.session.updateMany({
|
|
1141
|
+
where: { userId: passwordReset.userId },
|
|
1142
|
+
data: { revokedAt: /* @__PURE__ */ new Date() }
|
|
1143
|
+
});
|
|
1144
|
+
return { message: "Password updated. Please log in with your new password." };
|
|
1145
|
+
});
|
|
1146
|
+
}
|
|
1147
|
+
};
|
|
1148
|
+
|
|
1149
|
+
// src/procedures/biometric.ts
|
|
1150
|
+
import { TRPCError as TRPCError3 } from "@trpc/server";
|
|
1151
|
+
var BiometricProcedureFactory = class {
|
|
1152
|
+
constructor(config, authProcedure) {
|
|
1153
|
+
this.config = config;
|
|
1154
|
+
this.authProcedure = authProcedure;
|
|
1155
|
+
}
|
|
1156
|
+
createBiometricProcedures() {
|
|
1157
|
+
return {
|
|
1158
|
+
verifyBiometric: this.verifyBiometric(),
|
|
1159
|
+
getBiometricStatus: this.getBiometricStatus()
|
|
1160
|
+
};
|
|
1161
|
+
}
|
|
1162
|
+
checkConfig() {
|
|
1163
|
+
if (!this.config.features.biometric) {
|
|
1164
|
+
throw new TRPCError3({ code: "NOT_FOUND" });
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
verifyBiometric() {
|
|
1168
|
+
return this.authProcedure.input(biometricVerifySchema).mutation(async ({ ctx }) => {
|
|
1169
|
+
this.checkConfig();
|
|
1170
|
+
const { userId } = ctx;
|
|
1171
|
+
await this.config.prisma.user.update({
|
|
1172
|
+
where: { id: userId },
|
|
1173
|
+
data: { verifiedHumanAt: /* @__PURE__ */ new Date(), tag: "HUMAN" }
|
|
1174
|
+
});
|
|
1175
|
+
if (this.config.hooks?.onBiometricVerified) {
|
|
1176
|
+
await this.config.hooks.onBiometricVerified(userId);
|
|
1177
|
+
}
|
|
1178
|
+
return { success: true, verifiedAt: /* @__PURE__ */ new Date() };
|
|
1179
|
+
});
|
|
1180
|
+
}
|
|
1181
|
+
getBiometricStatus() {
|
|
1182
|
+
return this.authProcedure.query(async ({ ctx }) => {
|
|
1183
|
+
this.checkConfig();
|
|
1184
|
+
const { userId } = ctx;
|
|
1185
|
+
const user = await this.config.prisma.user.findUnique({
|
|
1186
|
+
where: { id: userId },
|
|
1187
|
+
select: { verifiedHumanAt: true }
|
|
1188
|
+
});
|
|
1189
|
+
if (!user) {
|
|
1190
|
+
throw new TRPCError3({ code: "NOT_FOUND", message: "User not found" });
|
|
1191
|
+
}
|
|
1192
|
+
let timeoutMs = null;
|
|
1193
|
+
if (this.config.hooks?.getBiometricTimeout) {
|
|
1194
|
+
timeoutMs = await this.config.hooks.getBiometricTimeout();
|
|
1195
|
+
}
|
|
1196
|
+
let isExpired = false;
|
|
1197
|
+
if (user.verifiedHumanAt && timeoutMs !== null) {
|
|
1198
|
+
const expiresAt = new Date(user.verifiedHumanAt.getTime() + timeoutMs);
|
|
1199
|
+
isExpired = /* @__PURE__ */ new Date() > expiresAt;
|
|
1200
|
+
}
|
|
1201
|
+
return {
|
|
1202
|
+
verifiedHumanAt: user.verifiedHumanAt,
|
|
1203
|
+
isVerified: !!user.verifiedHumanAt && !isExpired,
|
|
1204
|
+
isExpired,
|
|
1205
|
+
requiresVerification: timeoutMs !== null
|
|
1206
|
+
};
|
|
1207
|
+
});
|
|
1208
|
+
}
|
|
1209
|
+
};
|
|
1210
|
+
|
|
1211
|
+
// src/procedures/emailVerification.ts
|
|
1212
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
1213
|
+
import { TRPCError as TRPCError4 } from "@trpc/server";
|
|
1214
|
+
var EmailVerificationProcedureFactory = class {
|
|
1215
|
+
constructor(config, authProcedure) {
|
|
1216
|
+
this.config = config;
|
|
1217
|
+
this.authProcedure = authProcedure;
|
|
1218
|
+
}
|
|
1219
|
+
createEmailVerificationProcedures() {
|
|
1220
|
+
return {
|
|
1221
|
+
sendVerificationEmail: this.sendVerificationEmail(),
|
|
1222
|
+
verifyEmail: this.verifyEmail(),
|
|
1223
|
+
getVerificationStatus: this.getVerificationStatus()
|
|
1224
|
+
};
|
|
1225
|
+
}
|
|
1226
|
+
checkConfig() {
|
|
1227
|
+
if (!this.config.features.emailVerification) {
|
|
1228
|
+
throw new TRPCError4({ code: "NOT_FOUND" });
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
sendVerificationEmail() {
|
|
1232
|
+
return this.authProcedure.mutation(async ({ ctx }) => {
|
|
1233
|
+
this.checkConfig();
|
|
1234
|
+
const { userId } = ctx;
|
|
1235
|
+
const user = await this.config.prisma.user.findUnique({
|
|
1236
|
+
where: { id: userId, status: "ACTIVE" },
|
|
1237
|
+
select: { id: true, email: true, emailVerificationStatus: true }
|
|
1238
|
+
});
|
|
1239
|
+
if (!user) {
|
|
1240
|
+
throw new TRPCError4({ code: "NOT_FOUND", message: "User not found" });
|
|
1241
|
+
}
|
|
1242
|
+
if (user.emailVerificationStatus === "VERIFIED") {
|
|
1243
|
+
return { message: "Email is already verified", emailSent: false };
|
|
1244
|
+
}
|
|
1245
|
+
const otp = randomUUID2();
|
|
1246
|
+
await this.config.prisma.user.update({
|
|
1247
|
+
where: { id: userId },
|
|
1248
|
+
data: { emailVerificationStatus: "PENDING", otpForEmailVerification: otp }
|
|
1249
|
+
});
|
|
1250
|
+
if (this.config.emailService) {
|
|
1251
|
+
try {
|
|
1252
|
+
await this.config.emailService.sendVerificationEmail(user.email, otp);
|
|
1253
|
+
return { message: "Verification email sent", emailSent: true };
|
|
1254
|
+
} catch {
|
|
1255
|
+
return { message: "Failed to send email", emailSent: false };
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
return { message: "Email service not configured", emailSent: false };
|
|
1259
|
+
});
|
|
1260
|
+
}
|
|
1261
|
+
verifyEmail() {
|
|
1262
|
+
return this.authProcedure.input(verifyEmailSchema).mutation(async ({ ctx, input }) => {
|
|
1263
|
+
this.checkConfig();
|
|
1264
|
+
const { userId } = ctx;
|
|
1265
|
+
const { code } = input;
|
|
1266
|
+
const user = await this.config.prisma.user.findUnique({
|
|
1267
|
+
where: { id: userId, status: "ACTIVE" },
|
|
1268
|
+
select: { id: true, emailVerificationStatus: true, otpForEmailVerification: true }
|
|
1269
|
+
});
|
|
1270
|
+
if (!user) {
|
|
1271
|
+
throw new TRPCError4({ code: "NOT_FOUND", message: "User not found" });
|
|
1272
|
+
}
|
|
1273
|
+
if (user.emailVerificationStatus === "VERIFIED") {
|
|
1274
|
+
return { success: true, message: "Email is already verified" };
|
|
1275
|
+
}
|
|
1276
|
+
if (code !== user.otpForEmailVerification) {
|
|
1277
|
+
throw new TRPCError4({ code: "BAD_REQUEST", message: "Invalid verification code" });
|
|
1278
|
+
}
|
|
1279
|
+
await this.config.prisma.user.update({
|
|
1280
|
+
where: { id: userId },
|
|
1281
|
+
data: { emailVerificationStatus: "VERIFIED", otpForEmailVerification: null }
|
|
1282
|
+
});
|
|
1283
|
+
if (this.config.hooks?.onEmailVerified) {
|
|
1284
|
+
await this.config.hooks.onEmailVerified(userId);
|
|
1285
|
+
}
|
|
1286
|
+
return { success: true, message: "Email verified" };
|
|
1287
|
+
});
|
|
1288
|
+
}
|
|
1289
|
+
getVerificationStatus() {
|
|
1290
|
+
return this.authProcedure.query(async ({ ctx }) => {
|
|
1291
|
+
this.checkConfig();
|
|
1292
|
+
const { userId } = ctx;
|
|
1293
|
+
const user = await this.config.prisma.user.findUnique({
|
|
1294
|
+
where: { id: userId },
|
|
1295
|
+
select: { emailVerificationStatus: true, email: true }
|
|
1296
|
+
});
|
|
1297
|
+
if (!user) {
|
|
1298
|
+
throw new TRPCError4({ code: "NOT_FOUND", message: "User not found" });
|
|
1299
|
+
}
|
|
1300
|
+
return {
|
|
1301
|
+
email: user.email,
|
|
1302
|
+
status: user.emailVerificationStatus,
|
|
1303
|
+
isVerified: user.emailVerificationStatus === "VERIFIED"
|
|
1304
|
+
};
|
|
1305
|
+
});
|
|
1306
|
+
}
|
|
1307
|
+
};
|
|
1308
|
+
|
|
1309
|
+
// src/procedures/oauth.ts
|
|
1310
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
1311
|
+
import { TRPCError as TRPCError5 } from "@trpc/server";
|
|
1312
|
+
var OAuthLoginProcedureFactory = class {
|
|
1313
|
+
constructor(config, procedure) {
|
|
1314
|
+
this.config = config;
|
|
1315
|
+
this.procedure = procedure;
|
|
1316
|
+
this.verifyOAuthToken = null;
|
|
1317
|
+
if (config.oauthKeys) {
|
|
1318
|
+
this.verifyOAuthToken = createOAuthVerifier(config.oauthKeys);
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
createOAuthLoginProcedures(schemas) {
|
|
1322
|
+
return { oAuthLogin: this.oAuthLogin(schemas.oauth) };
|
|
1323
|
+
}
|
|
1324
|
+
checkConfig() {
|
|
1325
|
+
if (!this.config.features.oauth?.google && !this.config.features.oauth?.apple) {
|
|
1326
|
+
throw new TRPCError5({ code: "NOT_FOUND" });
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
oAuthLogin(schema) {
|
|
1330
|
+
return this.procedure.input(schema).mutation(async ({ ctx, input }) => {
|
|
1331
|
+
this.checkConfig();
|
|
1332
|
+
const typedInput = input;
|
|
1333
|
+
const { idToken, user: appleUser, provider } = typedInput;
|
|
1334
|
+
const userAgent = ctx.headers["user-agent"];
|
|
1335
|
+
if (!userAgent) {
|
|
1336
|
+
throw new TRPCError5({ code: "BAD_REQUEST", message: "User agent not found" });
|
|
1337
|
+
}
|
|
1338
|
+
if (!this.verifyOAuthToken) {
|
|
1339
|
+
throw new TRPCError5({
|
|
1340
|
+
code: "INTERNAL_SERVER_ERROR",
|
|
1341
|
+
message: "OAuth not configured. Provide oauthKeys in config."
|
|
1342
|
+
});
|
|
1343
|
+
}
|
|
1344
|
+
const { email, oauthId } = await this.verifyOAuthToken(provider, idToken, appleUser);
|
|
1345
|
+
if (!email) {
|
|
1346
|
+
throw new TRPCError5({ code: "BAD_REQUEST", message: "Email not provided by OAuth provider" });
|
|
1347
|
+
}
|
|
1348
|
+
let user = await this.config.prisma.user.findFirst({
|
|
1349
|
+
where: {
|
|
1350
|
+
OR: [
|
|
1351
|
+
{ email: { equals: email, mode: "insensitive" } },
|
|
1352
|
+
{ oauthId: { equals: oauthId } }
|
|
1353
|
+
]
|
|
1354
|
+
},
|
|
1355
|
+
select: {
|
|
1356
|
+
id: true,
|
|
1357
|
+
status: true,
|
|
1358
|
+
email: true,
|
|
1359
|
+
username: true,
|
|
1360
|
+
password: true,
|
|
1361
|
+
oauthProvider: true,
|
|
1362
|
+
oauthId: true,
|
|
1363
|
+
twoFaEnabled: true,
|
|
1364
|
+
verifiedHumanAt: true,
|
|
1365
|
+
emailVerificationStatus: true
|
|
1366
|
+
}
|
|
1367
|
+
});
|
|
1368
|
+
if (user?.oauthProvider && user.oauthProvider !== provider) {
|
|
1369
|
+
throw new TRPCError5({
|
|
1370
|
+
code: "BAD_REQUEST",
|
|
1371
|
+
message: `This email uses ${user.oauthProvider.toLowerCase()} sign-in.`
|
|
1372
|
+
});
|
|
1373
|
+
}
|
|
1374
|
+
if (user && !user.oauthProvider && user.password) {
|
|
1375
|
+
throw new TRPCError5({
|
|
1376
|
+
code: "BAD_REQUEST",
|
|
1377
|
+
message: "This email uses password login. Please use email/password."
|
|
1378
|
+
});
|
|
1379
|
+
}
|
|
1380
|
+
if (!user) {
|
|
1381
|
+
const generateUsername = this.config.generateUsername ?? (() => `user_${Date.now()}`);
|
|
1382
|
+
user = await this.config.prisma.user.create({
|
|
1383
|
+
data: {
|
|
1384
|
+
username: generateUsername(),
|
|
1385
|
+
email,
|
|
1386
|
+
password: null,
|
|
1387
|
+
emailVerificationStatus: "VERIFIED",
|
|
1388
|
+
oauthProvider: provider,
|
|
1389
|
+
oauthId,
|
|
1390
|
+
status: "ACTIVE",
|
|
1391
|
+
tag: this.config.features.biometric ? "BOT" : "HUMAN",
|
|
1392
|
+
twoFaEnabled: false,
|
|
1393
|
+
verifiedHumanAt: null
|
|
1394
|
+
}
|
|
1395
|
+
});
|
|
1396
|
+
if (this.config.hooks?.onUserCreated) {
|
|
1397
|
+
await this.config.hooks.onUserCreated(user.id, typedInput);
|
|
1398
|
+
}
|
|
1399
|
+
if (this.config.hooks?.onOAuthLinked) {
|
|
1400
|
+
await this.config.hooks.onOAuthLinked(user.id, provider);
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
if (user.status === "DEACTIVATED") {
|
|
1404
|
+
throw new TRPCError5({ code: "FORBIDDEN", message: "Your account has been deactivated." });
|
|
1405
|
+
}
|
|
1406
|
+
if (user.status === "BANNED") {
|
|
1407
|
+
throw new TRPCError5({ code: "FORBIDDEN", message: "Your account has been banned." });
|
|
1408
|
+
}
|
|
1409
|
+
const refreshToken = randomUUID3();
|
|
1410
|
+
const extraSessionData = this.config.hooks?.getSessionData ? await this.config.hooks.getSessionData(typedInput) : {};
|
|
1411
|
+
const session = await this.config.prisma.session.create({
|
|
1412
|
+
data: {
|
|
1413
|
+
userId: user.id,
|
|
1414
|
+
browserName: detectBrowser(userAgent),
|
|
1415
|
+
socketId: null,
|
|
1416
|
+
refreshToken,
|
|
1417
|
+
...extraSessionData
|
|
1418
|
+
},
|
|
1419
|
+
select: {
|
|
1420
|
+
id: true,
|
|
1421
|
+
refreshToken: true,
|
|
1422
|
+
userId: true,
|
|
1423
|
+
socketId: true,
|
|
1424
|
+
browserName: true,
|
|
1425
|
+
issuedAt: true,
|
|
1426
|
+
lastUsed: true,
|
|
1427
|
+
revokedAt: true,
|
|
1428
|
+
deviceId: true,
|
|
1429
|
+
twoFaSecret: true
|
|
1430
|
+
}
|
|
1431
|
+
});
|
|
1432
|
+
if (this.config.hooks?.onUserLogin) {
|
|
1433
|
+
await this.config.hooks.onUserLogin(user.id, session.id);
|
|
1434
|
+
}
|
|
1435
|
+
if (this.config.hooks?.onSessionCreated) {
|
|
1436
|
+
await this.config.hooks.onSessionCreated(session.id, typedInput);
|
|
1437
|
+
}
|
|
1438
|
+
const accessToken = createAccessToken(
|
|
1439
|
+
{ id: session.id, userId: session.userId, verifiedHumanAt: user.verifiedHumanAt ?? null },
|
|
1440
|
+
{
|
|
1441
|
+
secret: this.config.secrets.jwt,
|
|
1442
|
+
expiresIn: this.config.tokenSettings.accessTokenExpiry
|
|
1443
|
+
}
|
|
1444
|
+
);
|
|
1445
|
+
setAuthCookies(
|
|
1446
|
+
ctx.res,
|
|
1447
|
+
{ accessToken, refreshToken: session.refreshToken },
|
|
1448
|
+
this.config.cookieSettings,
|
|
1449
|
+
this.config.storageKeys
|
|
1450
|
+
);
|
|
1451
|
+
return {
|
|
1452
|
+
success: true,
|
|
1453
|
+
user: { id: user.id, email: user.email, username: user.username }
|
|
1454
|
+
};
|
|
1455
|
+
});
|
|
1456
|
+
}
|
|
1457
|
+
};
|
|
1458
|
+
|
|
1459
|
+
// src/procedures/twoFa.ts
|
|
1460
|
+
import { TRPCError as TRPCError6 } from "@trpc/server";
|
|
1461
|
+
var TwoFaProcedureFactory = class {
|
|
1462
|
+
constructor(config, procedure, authProcedure) {
|
|
1463
|
+
this.config = config;
|
|
1464
|
+
this.procedure = procedure;
|
|
1465
|
+
this.authProcedure = authProcedure;
|
|
1466
|
+
}
|
|
1467
|
+
createTwoFaProcedures() {
|
|
1468
|
+
return {
|
|
1469
|
+
enableTwofa: this.enableTwofa(),
|
|
1470
|
+
disableTwofa: this.disableTwofa(),
|
|
1471
|
+
getTwofaSecret: this.getTwofaSecret(),
|
|
1472
|
+
twoFaReset: this.twoFaReset(),
|
|
1473
|
+
twoFaResetVerify: this.twoFaResetVerify(),
|
|
1474
|
+
registerPushToken: this.registerPushToken(),
|
|
1475
|
+
deregisterPushToken: this.deregisterPushToken()
|
|
1476
|
+
};
|
|
1477
|
+
}
|
|
1478
|
+
checkConfig() {
|
|
1479
|
+
if (!this.config.features.twoFa) {
|
|
1480
|
+
throw new TRPCError6({ code: "NOT_FOUND" });
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1483
|
+
enableTwofa() {
|
|
1484
|
+
return this.authProcedure.mutation(async ({ ctx }) => {
|
|
1485
|
+
this.checkConfig();
|
|
1486
|
+
const { userId, sessionId } = ctx;
|
|
1487
|
+
const user = await this.config.prisma.user.findFirst({
|
|
1488
|
+
where: { id: userId },
|
|
1489
|
+
select: { twoFaEnabled: true, oauthProvider: true, password: true }
|
|
1490
|
+
});
|
|
1491
|
+
if (!user) {
|
|
1492
|
+
throw new TRPCError6({ code: "NOT_FOUND", message: "User not found." });
|
|
1493
|
+
}
|
|
1494
|
+
if (user.oauthProvider) {
|
|
1495
|
+
throw new TRPCError6({
|
|
1496
|
+
code: "FORBIDDEN",
|
|
1497
|
+
message: "2FA is not available for social login accounts."
|
|
1498
|
+
});
|
|
1499
|
+
}
|
|
1500
|
+
if (user.twoFaEnabled) {
|
|
1501
|
+
throw new TRPCError6({ code: "BAD_REQUEST", message: "2FA already enabled." });
|
|
1502
|
+
}
|
|
1503
|
+
const checkSession = await this.config.prisma.session.findFirst({
|
|
1504
|
+
where: { userId, id: sessionId },
|
|
1505
|
+
select: { deviceId: true }
|
|
1506
|
+
});
|
|
1507
|
+
if (!checkSession?.deviceId) {
|
|
1508
|
+
throw new TRPCError6({
|
|
1509
|
+
code: "BAD_REQUEST",
|
|
1510
|
+
message: "You must be logged in on mobile to enable 2FA."
|
|
1511
|
+
});
|
|
1512
|
+
}
|
|
1513
|
+
await this.config.prisma.session.updateMany({
|
|
1514
|
+
where: { userId, revokedAt: null, NOT: { id: sessionId } },
|
|
1515
|
+
data: { revokedAt: /* @__PURE__ */ new Date() }
|
|
1516
|
+
});
|
|
1517
|
+
await this.config.prisma.session.updateMany({
|
|
1518
|
+
where: { userId, NOT: { id: sessionId } },
|
|
1519
|
+
data: { twoFaSecret: null }
|
|
1520
|
+
});
|
|
1521
|
+
const secret = generateTotpSecret();
|
|
1522
|
+
await this.config.prisma.user.update({
|
|
1523
|
+
where: { id: userId },
|
|
1524
|
+
data: { twoFaEnabled: true }
|
|
1525
|
+
});
|
|
1526
|
+
await this.config.prisma.session.update({
|
|
1527
|
+
where: { id: sessionId },
|
|
1528
|
+
data: { twoFaSecret: secret }
|
|
1529
|
+
});
|
|
1530
|
+
if (this.config.hooks?.onTwoFaStatusChanged) {
|
|
1531
|
+
await this.config.hooks.onTwoFaStatusChanged(userId, true);
|
|
1532
|
+
}
|
|
1533
|
+
return { secret };
|
|
1534
|
+
});
|
|
1535
|
+
}
|
|
1536
|
+
disableTwofa() {
|
|
1537
|
+
return this.authProcedure.input(disableTwofaSchema).mutation(async ({ ctx, input }) => {
|
|
1538
|
+
this.checkConfig();
|
|
1539
|
+
const { userId, sessionId } = ctx;
|
|
1540
|
+
const { password } = input;
|
|
1541
|
+
const user = await this.config.prisma.user.findFirst({
|
|
1542
|
+
where: { id: userId },
|
|
1543
|
+
select: { password: true, status: true, oauthProvider: true }
|
|
1544
|
+
});
|
|
1545
|
+
if (!user) {
|
|
1546
|
+
throw new TRPCError6({ code: "NOT_FOUND", message: "User not found." });
|
|
1547
|
+
}
|
|
1548
|
+
if (user.status !== "ACTIVE") {
|
|
1549
|
+
throw new TRPCError6({ code: "FORBIDDEN", message: "Account deactivated." });
|
|
1550
|
+
}
|
|
1551
|
+
if (user.oauthProvider) {
|
|
1552
|
+
throw new TRPCError6({
|
|
1553
|
+
code: "FORBIDDEN",
|
|
1554
|
+
message: "2FA is not available for social login accounts."
|
|
1555
|
+
});
|
|
1556
|
+
}
|
|
1557
|
+
if (!user.password) {
|
|
1558
|
+
throw new TRPCError6({
|
|
1559
|
+
code: "BAD_REQUEST",
|
|
1560
|
+
message: "Cannot verify password for social login account."
|
|
1561
|
+
});
|
|
1562
|
+
}
|
|
1563
|
+
const isMatch = await comparePassword(password, user.password);
|
|
1564
|
+
if (!isMatch) {
|
|
1565
|
+
throw new TRPCError6({ code: "FORBIDDEN", message: "Incorrect password." });
|
|
1566
|
+
}
|
|
1567
|
+
await this.config.prisma.user.update({
|
|
1568
|
+
where: { id: userId },
|
|
1569
|
+
data: { twoFaEnabled: false }
|
|
1570
|
+
});
|
|
1571
|
+
await this.config.prisma.session.update({
|
|
1572
|
+
where: { id: sessionId },
|
|
1573
|
+
data: { twoFaSecret: null }
|
|
1574
|
+
});
|
|
1575
|
+
if (this.config.hooks?.onTwoFaStatusChanged) {
|
|
1576
|
+
await this.config.hooks.onTwoFaStatusChanged(userId, false);
|
|
1577
|
+
}
|
|
1578
|
+
return { disabled: true };
|
|
1579
|
+
});
|
|
1580
|
+
}
|
|
1581
|
+
getTwofaSecret() {
|
|
1582
|
+
return this.authProcedure.input(getTwofaSecretSchema).query(async ({ ctx, input }) => {
|
|
1583
|
+
this.checkConfig();
|
|
1584
|
+
const { userId, sessionId } = ctx;
|
|
1585
|
+
const { pushCode } = input;
|
|
1586
|
+
const user = await this.config.prisma.user.findFirst({
|
|
1587
|
+
where: { id: userId },
|
|
1588
|
+
select: { twoFaEnabled: true, oauthProvider: true }
|
|
1589
|
+
});
|
|
1590
|
+
if (user?.oauthProvider) {
|
|
1591
|
+
throw new TRPCError6({
|
|
1592
|
+
code: "FORBIDDEN",
|
|
1593
|
+
message: "2FA is not available for social login accounts."
|
|
1594
|
+
});
|
|
1595
|
+
}
|
|
1596
|
+
if (!user?.twoFaEnabled) {
|
|
1597
|
+
throw new TRPCError6({ code: "BAD_REQUEST", message: "2FA not enabled." });
|
|
1598
|
+
}
|
|
1599
|
+
const session = await this.config.prisma.session.findUnique({
|
|
1600
|
+
where: { id: sessionId, userId },
|
|
1601
|
+
select: { twoFaSecret: true, device: { select: { pushToken: true } } }
|
|
1602
|
+
});
|
|
1603
|
+
if (!session?.device) {
|
|
1604
|
+
throw new TRPCError6({ code: "BAD_REQUEST", message: "Invalid request" });
|
|
1605
|
+
}
|
|
1606
|
+
const expectedCode = await verifyTotp(pushCode, cleanBase32String(session.device.pushToken));
|
|
1607
|
+
if (!expectedCode) {
|
|
1608
|
+
throw new TRPCError6({ code: "BAD_REQUEST", message: "Invalid request" });
|
|
1609
|
+
}
|
|
1610
|
+
if (session.twoFaSecret) {
|
|
1611
|
+
return { secret: session.twoFaSecret };
|
|
1612
|
+
}
|
|
1613
|
+
const secret = generateTotpSecret();
|
|
1614
|
+
await this.config.prisma.session.update({
|
|
1615
|
+
where: { id: sessionId },
|
|
1616
|
+
data: { twoFaSecret: secret }
|
|
1617
|
+
});
|
|
1618
|
+
return { secret };
|
|
1619
|
+
});
|
|
1620
|
+
}
|
|
1621
|
+
twoFaReset() {
|
|
1622
|
+
return this.procedure.input(twoFaResetSchema).mutation(async ({ input }) => {
|
|
1623
|
+
this.checkConfig();
|
|
1624
|
+
const { username, password } = input;
|
|
1625
|
+
const user = await this.config.prisma.user.findFirst({
|
|
1626
|
+
where: { username: { equals: username, mode: "insensitive" }, twoFaEnabled: true },
|
|
1627
|
+
select: { id: true, password: true, email: true }
|
|
1628
|
+
});
|
|
1629
|
+
if (!user) {
|
|
1630
|
+
throw new TRPCError6({ code: "UNAUTHORIZED", message: "Invalid credentials." });
|
|
1631
|
+
}
|
|
1632
|
+
if (!user.password) {
|
|
1633
|
+
throw new TRPCError6({
|
|
1634
|
+
code: "BAD_REQUEST",
|
|
1635
|
+
message: "Social login accounts cannot use 2FA reset."
|
|
1636
|
+
});
|
|
1637
|
+
}
|
|
1638
|
+
const isMatch = await comparePassword(password, user.password);
|
|
1639
|
+
if (!isMatch) {
|
|
1640
|
+
throw new TRPCError6({ code: "FORBIDDEN", message: "Invalid credentials." });
|
|
1641
|
+
}
|
|
1642
|
+
const otp = generateOtp();
|
|
1643
|
+
await this.config.prisma.oTPBasedLogin.create({
|
|
1644
|
+
data: { userId: user.id, code: otp }
|
|
1645
|
+
});
|
|
1646
|
+
if (this.config.emailService) {
|
|
1647
|
+
await this.config.emailService.sendOTPEmail(user.email, otp);
|
|
1648
|
+
}
|
|
1649
|
+
return { success: true };
|
|
1650
|
+
});
|
|
1651
|
+
}
|
|
1652
|
+
twoFaResetVerify() {
|
|
1653
|
+
return this.procedure.input(twoFaResetVerifySchema).mutation(async ({ input }) => {
|
|
1654
|
+
this.checkConfig();
|
|
1655
|
+
const { code, username } = input;
|
|
1656
|
+
const user = await this.config.prisma.user.findFirst({
|
|
1657
|
+
where: { username: { equals: username, mode: "insensitive" } },
|
|
1658
|
+
select: { id: true }
|
|
1659
|
+
});
|
|
1660
|
+
if (!user) {
|
|
1661
|
+
throw new TRPCError6({ code: "NOT_FOUND", message: "User not found" });
|
|
1662
|
+
}
|
|
1663
|
+
const otp = await this.config.prisma.oTPBasedLogin.findFirst({
|
|
1664
|
+
where: {
|
|
1665
|
+
userId: user.id,
|
|
1666
|
+
code,
|
|
1667
|
+
disabled: false,
|
|
1668
|
+
createdAt: { gte: new Date(Date.now() - this.config.tokenSettings.otpValidityMs) }
|
|
1669
|
+
}
|
|
1670
|
+
});
|
|
1671
|
+
if (!otp) {
|
|
1672
|
+
throw new TRPCError6({ code: "FORBIDDEN", message: "Invalid or expired OTP" });
|
|
1673
|
+
}
|
|
1674
|
+
await this.config.prisma.oTPBasedLogin.update({
|
|
1675
|
+
where: { id: otp.id },
|
|
1676
|
+
data: { disabled: true }
|
|
1677
|
+
});
|
|
1678
|
+
await this.config.prisma.user.update({
|
|
1679
|
+
where: { id: user.id },
|
|
1680
|
+
data: { twoFaEnabled: false }
|
|
1681
|
+
});
|
|
1682
|
+
await this.config.prisma.session.updateMany({
|
|
1683
|
+
where: { userId: user.id },
|
|
1684
|
+
data: { twoFaSecret: null }
|
|
1685
|
+
});
|
|
1686
|
+
return { success: true, message: "2FA has been reset." };
|
|
1687
|
+
});
|
|
1688
|
+
}
|
|
1689
|
+
registerPushToken() {
|
|
1690
|
+
return this.authProcedure.input(registerPushTokenSchema).mutation(async ({ ctx, input }) => {
|
|
1691
|
+
this.checkConfig();
|
|
1692
|
+
const { userId, sessionId } = ctx;
|
|
1693
|
+
const { pushToken } = input;
|
|
1694
|
+
await this.config.prisma.session.updateMany({
|
|
1695
|
+
where: {
|
|
1696
|
+
userId,
|
|
1697
|
+
id: { not: sessionId },
|
|
1698
|
+
revokedAt: null,
|
|
1699
|
+
device: { pushToken }
|
|
1700
|
+
},
|
|
1701
|
+
data: { revokedAt: /* @__PURE__ */ new Date() }
|
|
1702
|
+
});
|
|
1703
|
+
const checkDevice = await this.config.prisma.device.findFirst({
|
|
1704
|
+
where: {
|
|
1705
|
+
pushToken,
|
|
1706
|
+
sessions: { some: { id: sessionId } },
|
|
1707
|
+
users: { some: { id: userId } }
|
|
1708
|
+
},
|
|
1709
|
+
select: { id: true }
|
|
1710
|
+
});
|
|
1711
|
+
if (!checkDevice) {
|
|
1712
|
+
await this.config.prisma.device.upsert({
|
|
1713
|
+
where: { pushToken },
|
|
1714
|
+
create: {
|
|
1715
|
+
pushToken,
|
|
1716
|
+
sessions: { connect: { id: sessionId } },
|
|
1717
|
+
users: { connect: { id: userId } }
|
|
1718
|
+
},
|
|
1719
|
+
update: {
|
|
1720
|
+
sessions: { connect: { id: sessionId } },
|
|
1721
|
+
users: { connect: { id: userId } }
|
|
1722
|
+
}
|
|
1723
|
+
});
|
|
1724
|
+
}
|
|
1725
|
+
return { registered: true };
|
|
1726
|
+
});
|
|
1727
|
+
}
|
|
1728
|
+
deregisterPushToken() {
|
|
1729
|
+
return this.authProcedure.meta({ ignoreExpiration: true }).input(deregisterPushTokenSchema).mutation(async ({ ctx, input }) => {
|
|
1730
|
+
this.checkConfig();
|
|
1731
|
+
const { userId } = ctx;
|
|
1732
|
+
const { pushToken } = input;
|
|
1733
|
+
const device = await this.config.prisma.device.findFirst({
|
|
1734
|
+
where: {
|
|
1735
|
+
...userId !== 0 && { users: { some: { id: userId } } },
|
|
1736
|
+
pushToken
|
|
1737
|
+
},
|
|
1738
|
+
select: { id: true }
|
|
1739
|
+
});
|
|
1740
|
+
if (device) {
|
|
1741
|
+
await this.config.prisma.device.delete({
|
|
1742
|
+
where: { id: device.id }
|
|
1743
|
+
});
|
|
1744
|
+
}
|
|
1745
|
+
return { deregistered: true };
|
|
1746
|
+
});
|
|
1747
|
+
}
|
|
1748
|
+
};
|
|
1749
|
+
|
|
1750
|
+
// src/utilities/trpc.ts
|
|
1751
|
+
import { initTRPC } from "@trpc/server";
|
|
1752
|
+
import SuperJSON from "superjson";
|
|
1753
|
+
import { ZodError } from "zod";
|
|
1754
|
+
function isPrismaConnectionError(error) {
|
|
1755
|
+
if (!error || typeof error !== "object") {
|
|
1756
|
+
return false;
|
|
1757
|
+
}
|
|
1758
|
+
const errorCode = error.code;
|
|
1759
|
+
if (errorCode && typeof errorCode === "string") {
|
|
1760
|
+
const codeMatch = errorCode.match(/^P(\d+)$/);
|
|
1761
|
+
if (codeMatch) {
|
|
1762
|
+
const codeNum = parseInt(codeMatch[1], 10);
|
|
1763
|
+
if (codeNum >= 1e3 && codeNum <= 1003) {
|
|
1764
|
+
return true;
|
|
1765
|
+
}
|
|
1766
|
+
}
|
|
1767
|
+
}
|
|
1768
|
+
const constructorName = error.constructor?.name || "";
|
|
1769
|
+
if (constructorName.includes("Prisma")) {
|
|
1770
|
+
const errorMessage = error.message?.toLowerCase() || "";
|
|
1771
|
+
if (errorMessage.includes("can't reach database") || errorMessage.includes("authentication failed") || errorMessage.includes("database server") || errorMessage.includes("timeout") || errorMessage.includes("connection")) {
|
|
1772
|
+
return true;
|
|
1773
|
+
}
|
|
1774
|
+
}
|
|
1775
|
+
const cause = error.cause;
|
|
1776
|
+
if (cause) {
|
|
1777
|
+
return isPrismaConnectionError(cause);
|
|
1778
|
+
}
|
|
1779
|
+
return false;
|
|
1780
|
+
}
|
|
1781
|
+
function createTrpcBuilder(config) {
|
|
1782
|
+
return initTRPC.context().meta().create({
|
|
1783
|
+
transformer: SuperJSON,
|
|
1784
|
+
errorFormatter: (opts) => {
|
|
1785
|
+
const { shape, error } = opts;
|
|
1786
|
+
const { stack: _stack, ...safeData } = shape.data;
|
|
1787
|
+
if (error.code === "INTERNAL_SERVER_ERROR") {
|
|
1788
|
+
if (config.hooks?.logError) {
|
|
1789
|
+
const errorType = isPrismaConnectionError(error) || isPrismaConnectionError(error.cause) ? "DATABASE_ERROR" : "SERVER_ERROR";
|
|
1790
|
+
config.hooks.logError({
|
|
1791
|
+
type: errorType,
|
|
1792
|
+
description: error.message,
|
|
1793
|
+
stack: error.stack || "No stack trace",
|
|
1794
|
+
ip: opts.ctx?.ip,
|
|
1795
|
+
userId: opts.ctx?.userId ?? null
|
|
1796
|
+
}).catch(() => {
|
|
1797
|
+
});
|
|
1798
|
+
}
|
|
1799
|
+
return {
|
|
1800
|
+
...shape,
|
|
1801
|
+
message: "An unexpected error occurred. Please try again later.",
|
|
1802
|
+
data: {
|
|
1803
|
+
...safeData,
|
|
1804
|
+
zodError: error.cause instanceof ZodError ? error.cause.flatten() : null
|
|
1805
|
+
}
|
|
1806
|
+
};
|
|
1807
|
+
}
|
|
1808
|
+
return {
|
|
1809
|
+
...shape,
|
|
1810
|
+
data: {
|
|
1811
|
+
...safeData,
|
|
1812
|
+
zodError: error.cause instanceof ZodError ? error.cause.flatten() : null
|
|
1813
|
+
}
|
|
1814
|
+
};
|
|
1815
|
+
}
|
|
1816
|
+
});
|
|
1817
|
+
}
|
|
1818
|
+
function createBaseProcedure(t, authGuard) {
|
|
1819
|
+
return t.procedure.use(authGuard);
|
|
1820
|
+
}
|
|
1821
|
+
function getClientIp(req) {
|
|
1822
|
+
const forwarded = req.headers["x-forwarded-for"];
|
|
1823
|
+
if (forwarded) {
|
|
1824
|
+
return forwarded.split(",")[0]?.trim();
|
|
1825
|
+
}
|
|
1826
|
+
return req.socket.remoteAddress || void 0;
|
|
1827
|
+
}
|
|
1828
|
+
|
|
1829
|
+
// src/router.ts
|
|
1830
|
+
var createContext = ({
|
|
1831
|
+
req,
|
|
1832
|
+
res
|
|
1833
|
+
}) => ({
|
|
1834
|
+
headers: req.headers,
|
|
1835
|
+
userId: null,
|
|
1836
|
+
sessionId: null,
|
|
1837
|
+
refreshToken: null,
|
|
1838
|
+
socketId: null,
|
|
1839
|
+
ip: getClientIp(req),
|
|
1840
|
+
res
|
|
1841
|
+
});
|
|
1842
|
+
var AuthRouterFactory = class {
|
|
1843
|
+
constructor(userConfig) {
|
|
1844
|
+
this.userConfig = userConfig;
|
|
1845
|
+
this.config = createAuthConfig(this.userConfig);
|
|
1846
|
+
this.schemas = createSchemas(
|
|
1847
|
+
this.config.schemaExtensions
|
|
1848
|
+
);
|
|
1849
|
+
this.t = createTrpcBuilder(this.config);
|
|
1850
|
+
this.authGuard = createAuthGuard(this.config, this.t);
|
|
1851
|
+
this.procedure = createBaseProcedure(this.t, this.authGuard);
|
|
1852
|
+
this.authProcedure = this.procedure.meta({ authRequired: true });
|
|
1853
|
+
}
|
|
1854
|
+
createRouter() {
|
|
1855
|
+
const baseRoutes = new BaseProcedureFactory(
|
|
1856
|
+
this.config,
|
|
1857
|
+
this.procedure,
|
|
1858
|
+
this.authProcedure
|
|
1859
|
+
);
|
|
1860
|
+
const biometricRoutes = new BiometricProcedureFactory(
|
|
1861
|
+
this.config,
|
|
1862
|
+
this.authProcedure
|
|
1863
|
+
);
|
|
1864
|
+
const emailVerificationRoutes = new EmailVerificationProcedureFactory(
|
|
1865
|
+
this.config,
|
|
1866
|
+
this.authProcedure
|
|
1867
|
+
);
|
|
1868
|
+
const oAuthLoginRoutes = new OAuthLoginProcedureFactory(
|
|
1869
|
+
this.config,
|
|
1870
|
+
this.procedure
|
|
1871
|
+
);
|
|
1872
|
+
const twoFaRoutes = new TwoFaProcedureFactory(
|
|
1873
|
+
this.config,
|
|
1874
|
+
this.procedure,
|
|
1875
|
+
this.authProcedure
|
|
1876
|
+
);
|
|
1877
|
+
return this.t.router({
|
|
1878
|
+
...baseRoutes.createBaseProcedures(this.schemas),
|
|
1879
|
+
...oAuthLoginRoutes.createOAuthLoginProcedures(this.schemas),
|
|
1880
|
+
...twoFaRoutes.createTwoFaProcedures(),
|
|
1881
|
+
...biometricRoutes.createBiometricProcedures(),
|
|
1882
|
+
...emailVerificationRoutes.createEmailVerificationProcedures()
|
|
1883
|
+
});
|
|
1884
|
+
}
|
|
1885
|
+
};
|
|
1886
|
+
function createAuthRouter(config) {
|
|
1887
|
+
const factory = new AuthRouterFactory(config);
|
|
1888
|
+
const router = factory.t.router({
|
|
1889
|
+
auth: factory.createRouter()
|
|
1890
|
+
});
|
|
1891
|
+
return {
|
|
1892
|
+
router,
|
|
1893
|
+
t: factory.t,
|
|
1894
|
+
procedure: factory.procedure,
|
|
1895
|
+
authProcedure: factory.authProcedure,
|
|
1896
|
+
createContext
|
|
1897
|
+
};
|
|
1898
|
+
}
|
|
1899
|
+
export {
|
|
1900
|
+
DEFAULT_STORAGE_KEYS,
|
|
1901
|
+
OAuthVerificationError,
|
|
1902
|
+
biometricVerifySchema,
|
|
1903
|
+
changePasswordSchema,
|
|
1904
|
+
cleanBase32String,
|
|
1905
|
+
clearAuthCookies,
|
|
1906
|
+
comparePassword,
|
|
1907
|
+
createAccessToken,
|
|
1908
|
+
createAuthConfig,
|
|
1909
|
+
createAuthGuard,
|
|
1910
|
+
createAuthRouter,
|
|
1911
|
+
createConsoleEmailAdapter,
|
|
1912
|
+
createNoopEmailAdapter,
|
|
1913
|
+
createOAuthVerifier,
|
|
1914
|
+
decodeToken,
|
|
1915
|
+
defaultAuthConfig,
|
|
1916
|
+
defaultCookieSettings,
|
|
1917
|
+
defaultStorageKeys,
|
|
1918
|
+
defaultTokenSettings,
|
|
1919
|
+
detectBrowser,
|
|
1920
|
+
endAllSessionsSchema,
|
|
1921
|
+
generateOtp,
|
|
1922
|
+
generateTotpCode,
|
|
1923
|
+
generateTotpSecret,
|
|
1924
|
+
hashPassword,
|
|
1925
|
+
isMobileDevice,
|
|
1926
|
+
isNativeApp,
|
|
1927
|
+
isTokenExpiredError,
|
|
1928
|
+
isTokenInvalidError,
|
|
1929
|
+
loginSchema,
|
|
1930
|
+
logoutSchema,
|
|
1931
|
+
oAuthLoginSchema,
|
|
1932
|
+
otpLoginRequestSchema,
|
|
1933
|
+
otpLoginVerifySchema,
|
|
1934
|
+
parseAuthCookies,
|
|
1935
|
+
requestPasswordResetSchema,
|
|
1936
|
+
resetPasswordSchema,
|
|
1937
|
+
setAuthCookies,
|
|
1938
|
+
signupSchema,
|
|
1939
|
+
twoFaResetSchema,
|
|
1940
|
+
twoFaSetupSchema,
|
|
1941
|
+
twoFaVerifySchema,
|
|
1942
|
+
validatePasswordStrength,
|
|
1943
|
+
verifyAccessToken,
|
|
1944
|
+
verifyEmailSchema,
|
|
1945
|
+
verifyTotp
|
|
1946
|
+
};
|
|
1947
|
+
//# sourceMappingURL=index.mjs.map
|