@syncello/auth 2.5.1 → 3.2.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/README.md +1 -6
- package/dist/index.cjs +546 -69
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1455 -112
- package/dist/index.d.ts +1455 -112
- package/dist/index.js +545 -69
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
package/dist/index.cjs
CHANGED
|
@@ -32,6 +32,7 @@ var src_exports = {};
|
|
|
32
32
|
__export(src_exports, {
|
|
33
33
|
AUTH_DEFAULTS: () => AUTH_DEFAULTS,
|
|
34
34
|
CHALLENGE_TTL_MS: () => CHALLENGE_TTL_MS,
|
|
35
|
+
CloudflareEmailAdapter: () => CloudflareEmailAdapter,
|
|
35
36
|
EmailService: () => EmailService,
|
|
36
37
|
MAX_CHALLENGE_ATTEMPTS: () => MAX_CHALLENGE_ATTEMPTS,
|
|
37
38
|
ProblemTypes: () => ProblemTypes,
|
|
@@ -898,16 +899,16 @@ function createDeviceToken() {
|
|
|
898
899
|
};
|
|
899
900
|
}
|
|
900
901
|
async function validateTrustedDevice(db, userTrustedDevicesTable, userId, tokenHash) {
|
|
901
|
-
const { eq:
|
|
902
|
+
const { eq: eq33, and: and14, gt: gt6 } = await import("drizzle-orm");
|
|
902
903
|
const [device] = await db.select().from(userTrustedDevicesTable).where(
|
|
903
904
|
and14(
|
|
904
|
-
|
|
905
|
-
|
|
905
|
+
eq33(userTrustedDevicesTable.userId, userId),
|
|
906
|
+
eq33(userTrustedDevicesTable.tokenHash, tokenHash),
|
|
906
907
|
gt6(userTrustedDevicesTable.expiresAt, Date.now())
|
|
907
908
|
)
|
|
908
909
|
).limit(1);
|
|
909
910
|
if (device) {
|
|
910
|
-
await db.update(userTrustedDevicesTable).set({ lastUsedAt: Date.now() }).where(
|
|
911
|
+
await db.update(userTrustedDevicesTable).set({ lastUsedAt: Date.now() }).where(eq33(userTrustedDevicesTable.id, device.id));
|
|
911
912
|
return true;
|
|
912
913
|
}
|
|
913
914
|
return false;
|
|
@@ -1829,7 +1830,7 @@ var requireVerifiedEmail = (0, import_factory5.createMiddleware)(
|
|
|
1829
1830
|
);
|
|
1830
1831
|
|
|
1831
1832
|
// src/routes/index.ts
|
|
1832
|
-
var
|
|
1833
|
+
var import_zod_openapi42 = require("@hono/zod-openapi");
|
|
1833
1834
|
|
|
1834
1835
|
// src/routes/signup.ts
|
|
1835
1836
|
var import_zod_openapi2 = require("@hono/zod-openapi");
|
|
@@ -2572,13 +2573,92 @@ var ResendAdapter = class {
|
|
|
2572
2573
|
}
|
|
2573
2574
|
};
|
|
2574
2575
|
|
|
2576
|
+
// src/lib/email/adapters/cloudflare-adapter.ts
|
|
2577
|
+
var CloudflareEmailAdapter = class {
|
|
2578
|
+
constructor(binding, options) {
|
|
2579
|
+
this.binding = binding;
|
|
2580
|
+
this.options = options;
|
|
2581
|
+
}
|
|
2582
|
+
binding;
|
|
2583
|
+
options;
|
|
2584
|
+
providerName = "cloudflare";
|
|
2585
|
+
async send(options) {
|
|
2586
|
+
if (this.options.dryRun) {
|
|
2587
|
+
logger_default.info("Cloudflare: Dry-run, email not sent", {
|
|
2588
|
+
provider: this.providerName,
|
|
2589
|
+
to: options.to,
|
|
2590
|
+
from: options.from,
|
|
2591
|
+
subject: options.subject
|
|
2592
|
+
});
|
|
2593
|
+
return { success: true, emailId: "dry-run" };
|
|
2594
|
+
}
|
|
2595
|
+
try {
|
|
2596
|
+
logger_default.info("Cloudflare: Sending email", {
|
|
2597
|
+
provider: this.providerName,
|
|
2598
|
+
to: options.to,
|
|
2599
|
+
subject: options.subject
|
|
2600
|
+
});
|
|
2601
|
+
const result = await this.binding.send({
|
|
2602
|
+
to: options.to,
|
|
2603
|
+
from: options.from,
|
|
2604
|
+
subject: options.subject,
|
|
2605
|
+
html: options.html,
|
|
2606
|
+
text: options.text,
|
|
2607
|
+
headers: options.tags ? Object.fromEntries(
|
|
2608
|
+
Object.entries(options.tags).map(([name, value]) => [`X-Tag-${name}`, value])
|
|
2609
|
+
) : void 0
|
|
2610
|
+
});
|
|
2611
|
+
logger_default.info("Cloudflare: Email sent successfully", {
|
|
2612
|
+
provider: this.providerName,
|
|
2613
|
+
emailId: result.messageId,
|
|
2614
|
+
to: options.to
|
|
2615
|
+
});
|
|
2616
|
+
return { success: true, emailId: result.messageId };
|
|
2617
|
+
} catch (error) {
|
|
2618
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
2619
|
+
const code = error instanceof Error && "code" in error && typeof error.code === "string" ? error.code : void 0;
|
|
2620
|
+
const errorText = code ? `${code}: ${message}` : message;
|
|
2621
|
+
if (code === "E_RECIPIENT_SUPPRESSED") {
|
|
2622
|
+
logger_default.warn("Cloudflare: Recipient is on the suppression list", {
|
|
2623
|
+
provider: this.providerName,
|
|
2624
|
+
to: options.to
|
|
2625
|
+
});
|
|
2626
|
+
} else {
|
|
2627
|
+
logger_default.error("Cloudflare send error", {
|
|
2628
|
+
provider: this.providerName,
|
|
2629
|
+
error: errorText,
|
|
2630
|
+
to: options.to,
|
|
2631
|
+
subject: options.subject
|
|
2632
|
+
});
|
|
2633
|
+
}
|
|
2634
|
+
return { success: false, error: errorText };
|
|
2635
|
+
}
|
|
2636
|
+
}
|
|
2637
|
+
};
|
|
2638
|
+
|
|
2575
2639
|
// src/lib/email/adapters/factory.ts
|
|
2576
2640
|
function createEmailAdapter(env) {
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2641
|
+
const provider = env.EMAIL_PROVIDER ?? "resend";
|
|
2642
|
+
logger_default.info("Creating email adapter", { provider });
|
|
2643
|
+
switch (provider) {
|
|
2644
|
+
case "resend":
|
|
2645
|
+
if (!env.RESEND_API_KEY) {
|
|
2646
|
+
throw new Error("RESEND_API_KEY is required");
|
|
2647
|
+
}
|
|
2648
|
+
return new ResendAdapter(env.RESEND_API_KEY);
|
|
2649
|
+
case "cloudflare": {
|
|
2650
|
+
if (!env.EMAIL) {
|
|
2651
|
+
throw new Error(
|
|
2652
|
+
'EMAIL send_email binding is required when EMAIL_PROVIDER=cloudflare - add [[send_email]] name = "EMAIL" to wrangler.toml'
|
|
2653
|
+
);
|
|
2654
|
+
}
|
|
2655
|
+
const environment = env.ENVIRONMENT?.toLowerCase();
|
|
2656
|
+
const dryRun = environment !== "production" && environment !== "staging";
|
|
2657
|
+
return new CloudflareEmailAdapter(env.EMAIL, { dryRun });
|
|
2658
|
+
}
|
|
2659
|
+
default:
|
|
2660
|
+
throw new Error(`Unknown EMAIL_PROVIDER: ${String(provider)}`);
|
|
2580
2661
|
}
|
|
2581
|
-
return new ResendAdapter(env.RESEND_API_KEY);
|
|
2582
2662
|
}
|
|
2583
2663
|
|
|
2584
2664
|
// src/lib/email/email-service.ts
|
|
@@ -2586,6 +2666,9 @@ var EmailService = class {
|
|
|
2586
2666
|
adapter;
|
|
2587
2667
|
env;
|
|
2588
2668
|
get fromAddress() {
|
|
2669
|
+
if (this.env.EMAIL_FROM) {
|
|
2670
|
+
return this.env.EMAIL_FROM;
|
|
2671
|
+
}
|
|
2589
2672
|
const appName = this.env.APP_NAME ?? "Your App";
|
|
2590
2673
|
const appUrl = this.env.APP_URL ?? "";
|
|
2591
2674
|
let domain = "example.com";
|
|
@@ -3207,19 +3290,19 @@ var signupRequestSchema = import_zod_openapi.z.object({
|
|
|
3207
3290
|
var loginRequestSchema = import_zod_openapi.z.object({
|
|
3208
3291
|
email: import_zod_openapi.z.string().email().openapi({ example: "user@example.com" }),
|
|
3209
3292
|
password: import_zod_openapi.z.string().min(1).openapi({ example: "SecurePass123!" }),
|
|
3210
|
-
turnstileToken: import_zod_openapi.z.string().
|
|
3293
|
+
turnstileToken: import_zod_openapi.z.string().optional().openapi({ example: "XXXX.DUMMY.TOKEN.XXXX" })
|
|
3211
3294
|
}).openapi("LoginRequest");
|
|
3212
3295
|
var verifyEmailRequestSchema = import_zod_openapi.z.object({
|
|
3213
3296
|
token: import_zod_openapi.z.string().min(1).openapi({ example: "verification-token-abc123" })
|
|
3214
3297
|
}).openapi("VerifyEmailRequest");
|
|
3215
3298
|
var forgotPasswordRequestSchema = import_zod_openapi.z.object({
|
|
3216
3299
|
email: import_zod_openapi.z.string().email().openapi({ example: "user@example.com" }),
|
|
3217
|
-
turnstileToken: import_zod_openapi.z.string().
|
|
3300
|
+
turnstileToken: import_zod_openapi.z.string().optional().openapi({ example: "XXXX.DUMMY.TOKEN.XXXX" })
|
|
3218
3301
|
}).openapi("ForgotPasswordRequest");
|
|
3219
3302
|
var resetPasswordRequestSchema = import_zod_openapi.z.object({
|
|
3220
3303
|
token: import_zod_openapi.z.string().min(1).openapi({ example: "reset-token-xyz789" }),
|
|
3221
3304
|
password: import_zod_openapi.z.string().min(8).openapi({ example: "NewSecurePass456!" }),
|
|
3222
|
-
turnstileToken: import_zod_openapi.z.string().
|
|
3305
|
+
turnstileToken: import_zod_openapi.z.string().optional().openapi({ example: "XXXX.DUMMY.TOKEN.XXXX" })
|
|
3223
3306
|
}).openapi("ResetPasswordRequest");
|
|
3224
3307
|
var changePasswordRequestSchema = import_zod_openapi.z.object({
|
|
3225
3308
|
currentPassword: import_zod_openapi.z.string().min(1).openapi({ example: "OldPassword123!" }),
|
|
@@ -3442,7 +3525,8 @@ var signupHandler = async (c) => {
|
|
|
3442
3525
|
verificationUrl,
|
|
3443
3526
|
firstName: newUser.name || void 0
|
|
3444
3527
|
},
|
|
3445
|
-
database
|
|
3528
|
+
database,
|
|
3529
|
+
schema.users
|
|
3446
3530
|
);
|
|
3447
3531
|
const fingerprint = await generateFingerprint(c.req.raw);
|
|
3448
3532
|
const ipAddress = getClientIp(c.req.raw);
|
|
@@ -3916,7 +4000,7 @@ var forgotPasswordRoute = (0, import_zod_openapi7.createRoute)({
|
|
|
3916
4000
|
});
|
|
3917
4001
|
var forgotPasswordHandler = async (c) => {
|
|
3918
4002
|
const { email, turnstileToken } = c.req.valid("json");
|
|
3919
|
-
const database = c
|
|
4003
|
+
const { db: database, schema } = getAuthContext(c);
|
|
3920
4004
|
const env = c.env;
|
|
3921
4005
|
const turnstileValid = await verifyTurnstileToken(
|
|
3922
4006
|
turnstileToken,
|
|
@@ -3927,7 +4011,11 @@ var forgotPasswordHandler = async (c) => {
|
|
|
3927
4011
|
if (!turnstileValid) {
|
|
3928
4012
|
return problems.badRequest(c, "Invalid captcha");
|
|
3929
4013
|
}
|
|
3930
|
-
const resetResult = await createPasswordResetToken(
|
|
4014
|
+
const resetResult = await createPasswordResetToken(
|
|
4015
|
+
database,
|
|
4016
|
+
{ users: schema.users, passwordResetTokens: schema.passwordResetTokens },
|
|
4017
|
+
email
|
|
4018
|
+
);
|
|
3931
4019
|
if (!resetResult) {
|
|
3932
4020
|
return c.json({ success: true });
|
|
3933
4021
|
}
|
|
@@ -3939,7 +4027,8 @@ var forgotPasswordHandler = async (c) => {
|
|
|
3939
4027
|
token: resetResult.token,
|
|
3940
4028
|
resetUrl
|
|
3941
4029
|
},
|
|
3942
|
-
database
|
|
4030
|
+
database,
|
|
4031
|
+
schema.users
|
|
3943
4032
|
);
|
|
3944
4033
|
logSecurityEvent("password_reset_requested", "medium", { userId: resetResult.userId });
|
|
3945
4034
|
return c.json({ success: true });
|
|
@@ -4081,7 +4170,7 @@ var changePasswordHandler = async (c) => {
|
|
|
4081
4170
|
const userId = c.get("userId");
|
|
4082
4171
|
if (!userId) return problems.unauthorized(c, "Not authenticated");
|
|
4083
4172
|
const { currentPassword, newPassword } = c.req.valid("json");
|
|
4084
|
-
const database = c
|
|
4173
|
+
const { db: database, schema } = getAuthContext(c);
|
|
4085
4174
|
const env = c.env;
|
|
4086
4175
|
const passwordValidation = validatePassword(newPassword);
|
|
4087
4176
|
if (!passwordValidation.valid) {
|
|
@@ -4098,7 +4187,8 @@ var changePasswordHandler = async (c) => {
|
|
|
4098
4187
|
newPassword,
|
|
4099
4188
|
currentSessionId,
|
|
4100
4189
|
pepper: env.PASSWORD_PEPPER_V1,
|
|
4101
|
-
db: database
|
|
4190
|
+
db: database,
|
|
4191
|
+
tables: { users: schema.users, sessions: schema.sessions }
|
|
4102
4192
|
});
|
|
4103
4193
|
if (!result.success) {
|
|
4104
4194
|
return problems.badRequest(c, result.error || "Failed to change password");
|
|
@@ -4177,7 +4267,7 @@ var changeEmailHandler = async (c) => {
|
|
|
4177
4267
|
const userId = c.get("userId");
|
|
4178
4268
|
if (!userId) return problems.unauthorized(c, "Not authenticated");
|
|
4179
4269
|
const { password, newEmail } = c.req.valid("json");
|
|
4180
|
-
const database = c
|
|
4270
|
+
const { db: database, schema } = getAuthContext(c);
|
|
4181
4271
|
const env = c.env;
|
|
4182
4272
|
const appUrl = env.APP_URL || "http://localhost:5173";
|
|
4183
4273
|
const result = await requestEmailChange({
|
|
@@ -4185,7 +4275,8 @@ var changeEmailHandler = async (c) => {
|
|
|
4185
4275
|
password,
|
|
4186
4276
|
newEmail,
|
|
4187
4277
|
pepper: env.PASSWORD_PEPPER_V1,
|
|
4188
|
-
db: database
|
|
4278
|
+
db: database,
|
|
4279
|
+
tables: { users: schema.users, emailChangeTokens: schema.emailChangeTokens }
|
|
4189
4280
|
});
|
|
4190
4281
|
if (!result.success) {
|
|
4191
4282
|
if (result.error === "Incorrect password") {
|
|
@@ -4197,7 +4288,8 @@ var changeEmailHandler = async (c) => {
|
|
|
4197
4288
|
const confirmUrl = `${appUrl}/confirm-email-change?token=${result.confirmToken}`;
|
|
4198
4289
|
const confirmResult = await emailService.sendEmailChangeConfirmation(
|
|
4199
4290
|
{ newEmail, confirmUrl },
|
|
4200
|
-
database
|
|
4291
|
+
database,
|
|
4292
|
+
schema.users
|
|
4201
4293
|
);
|
|
4202
4294
|
if (!confirmResult.success) {
|
|
4203
4295
|
return problems.badRequest(
|
|
@@ -4208,7 +4300,8 @@ var changeEmailHandler = async (c) => {
|
|
|
4208
4300
|
const cancelUrl = `${appUrl}/cancel-email-change?token=${result.cancelToken}`;
|
|
4209
4301
|
await emailService.sendEmailChangeNotification(
|
|
4210
4302
|
{ oldEmail: result.oldEmail, newEmail, cancelUrl },
|
|
4211
|
-
database
|
|
4303
|
+
database,
|
|
4304
|
+
schema.users
|
|
4212
4305
|
);
|
|
4213
4306
|
logSecurityEvent("email_change_requested", "medium", { userId });
|
|
4214
4307
|
return c.json({ success: true });
|
|
@@ -4249,8 +4342,12 @@ var confirmEmailChangeRoute = (0, import_zod_openapi13.createRoute)({
|
|
|
4249
4342
|
});
|
|
4250
4343
|
var confirmEmailChangeHandler = async (c) => {
|
|
4251
4344
|
const { token } = c.req.valid("json");
|
|
4252
|
-
const database = c
|
|
4253
|
-
const result = await confirmEmailChange({
|
|
4345
|
+
const { db: database, schema } = getAuthContext(c);
|
|
4346
|
+
const result = await confirmEmailChange({
|
|
4347
|
+
token,
|
|
4348
|
+
db: database,
|
|
4349
|
+
tables: { users: schema.users, emailChangeTokens: schema.emailChangeTokens }
|
|
4350
|
+
});
|
|
4254
4351
|
if (!result.success) {
|
|
4255
4352
|
return problems.badRequest(c, result.error || "Invalid or expired token");
|
|
4256
4353
|
}
|
|
@@ -4291,8 +4388,12 @@ var cancelEmailChangeRoute = (0, import_zod_openapi15.createRoute)({
|
|
|
4291
4388
|
});
|
|
4292
4389
|
var cancelEmailChangeHandler = async (c) => {
|
|
4293
4390
|
const { token } = c.req.valid("json");
|
|
4294
|
-
const database = c
|
|
4295
|
-
const result = await cancelEmailChange({
|
|
4391
|
+
const { db: database, schema } = getAuthContext(c);
|
|
4392
|
+
const result = await cancelEmailChange({
|
|
4393
|
+
token,
|
|
4394
|
+
db: database,
|
|
4395
|
+
tables: { emailChangeTokens: schema.emailChangeTokens }
|
|
4396
|
+
});
|
|
4296
4397
|
if (!result.success) {
|
|
4297
4398
|
return problems.badRequest(c, result.error || "Invalid or expired token");
|
|
4298
4399
|
}
|
|
@@ -4431,7 +4532,7 @@ var refreshHandler = async (c) => {
|
|
|
4431
4532
|
if (!session) {
|
|
4432
4533
|
return problems.unauthorized(c, "Invalid or expired refresh token");
|
|
4433
4534
|
}
|
|
4434
|
-
await deleteSession(db, session.id);
|
|
4535
|
+
await deleteSession(db, { sessions: schema.sessions }, session.id);
|
|
4435
4536
|
const newSessionId = await createSession(
|
|
4436
4537
|
db,
|
|
4437
4538
|
{ sessions: schema.sessions },
|
|
@@ -4522,7 +4623,8 @@ var resendVerificationHandler = async (c) => {
|
|
|
4522
4623
|
verificationUrl,
|
|
4523
4624
|
firstName: user.name || void 0
|
|
4524
4625
|
},
|
|
4525
|
-
db
|
|
4626
|
+
db,
|
|
4627
|
+
schema.users
|
|
4526
4628
|
);
|
|
4527
4629
|
logger_default.info("Verification email resent", { userId: user.id, email: user.email });
|
|
4528
4630
|
return c.json({
|
|
@@ -4848,7 +4950,8 @@ var totpVerifyHandler = async (c) => {
|
|
|
4848
4950
|
const firstName = user.name?.split(" ")[0] || null;
|
|
4849
4951
|
await emailService.send2faEnabledEmail(
|
|
4850
4952
|
{ email: user.email, firstName, method: "totp" },
|
|
4851
|
-
db
|
|
4953
|
+
db,
|
|
4954
|
+
schema.users
|
|
4852
4955
|
);
|
|
4853
4956
|
}
|
|
4854
4957
|
logSecurityEvent("2fa_totp_enrolled", "high", { userId });
|
|
@@ -4905,12 +5008,12 @@ var totpDisableHandler = async (c) => {
|
|
|
4905
5008
|
if (remaining.length === 0) {
|
|
4906
5009
|
await db.delete(schema.userBackupCodes).where((0, import_drizzle_orm23.eq)(schema.userBackupCodes.userId, userId));
|
|
4907
5010
|
await db.delete(schema.userTrustedDevices).where((0, import_drizzle_orm23.eq)(schema.userTrustedDevices.userId, userId));
|
|
4908
|
-
await invalidateAllUserSessions(db, userId);
|
|
5011
|
+
await invalidateAllUserSessions(db, { sessions: schema.sessions, users: schema.users }, userId);
|
|
4909
5012
|
const [user] = await db.select({ email: schema.users.email, name: schema.users.name }).from(schema.users).where((0, import_drizzle_orm23.eq)(schema.users.id, userId)).limit(1);
|
|
4910
5013
|
if (user) {
|
|
4911
5014
|
const emailService = new EmailService(env);
|
|
4912
5015
|
const firstName = user.name?.split(" ")[0] || null;
|
|
4913
|
-
await emailService.send2faDisabledEmail({ email: user.email, firstName }, db);
|
|
5016
|
+
await emailService.send2faDisabledEmail({ email: user.email, firstName }, db, schema.users);
|
|
4914
5017
|
}
|
|
4915
5018
|
logSecurityEvent("2fa_disabled", "critical", { userId, lastMethod: "totp" });
|
|
4916
5019
|
return c.json({ success: true, sessionInvalidated: true });
|
|
@@ -4966,7 +5069,7 @@ var emailSetupHandler = async (c) => {
|
|
|
4966
5069
|
await env.OAUTH_STATES.put(`email_2fa_setup:${userId}`, codeHash, { expirationTtl: 300 });
|
|
4967
5070
|
const emailService = new EmailService(env);
|
|
4968
5071
|
const firstName = user.name?.split(" ")[0] || null;
|
|
4969
|
-
await emailService.send2faCodeEmail({ email: user.email, firstName, code }, db);
|
|
5072
|
+
await emailService.send2faCodeEmail({ email: user.email, firstName, code }, db, schema.users);
|
|
4970
5073
|
logSecurityEvent("2fa_email_setup_initiated", "low", { userId });
|
|
4971
5074
|
return c.json({ success: true });
|
|
4972
5075
|
};
|
|
@@ -5047,7 +5150,8 @@ var emailVerifyHandler = async (c) => {
|
|
|
5047
5150
|
const firstName = user.name?.split(" ")[0] || null;
|
|
5048
5151
|
await emailService.send2faEnabledEmail(
|
|
5049
5152
|
{ email: user.email, firstName, method: "email" },
|
|
5050
|
-
db
|
|
5153
|
+
db,
|
|
5154
|
+
schema.users
|
|
5051
5155
|
);
|
|
5052
5156
|
}
|
|
5053
5157
|
logSecurityEvent("2fa_email_enrolled", "high", { userId });
|
|
@@ -5101,7 +5205,8 @@ var emailSendCodeHandler = async (c) => {
|
|
|
5101
5205
|
const firstName = user.name?.split(" ")[0] || null;
|
|
5102
5206
|
const emailResult = await emailService.send2faCodeEmail(
|
|
5103
5207
|
{ email: user.email, firstName, code },
|
|
5104
|
-
db
|
|
5208
|
+
db,
|
|
5209
|
+
schema.users
|
|
5105
5210
|
);
|
|
5106
5211
|
if (!emailResult.success) {
|
|
5107
5212
|
return problems.badRequest(
|
|
@@ -5163,12 +5268,12 @@ var emailDisableHandler = async (c) => {
|
|
|
5163
5268
|
if (remaining.length === 0) {
|
|
5164
5269
|
await db.delete(schema.userBackupCodes).where((0, import_drizzle_orm27.eq)(schema.userBackupCodes.userId, userId));
|
|
5165
5270
|
await db.delete(schema.userTrustedDevices).where((0, import_drizzle_orm27.eq)(schema.userTrustedDevices.userId, userId));
|
|
5166
|
-
await invalidateAllUserSessions(db, userId);
|
|
5271
|
+
await invalidateAllUserSessions(db, { sessions: schema.sessions, users: schema.users }, userId);
|
|
5167
5272
|
const [user] = await db.select({ email: schema.users.email, name: schema.users.name }).from(schema.users).where((0, import_drizzle_orm27.eq)(schema.users.id, userId)).limit(1);
|
|
5168
5273
|
if (user) {
|
|
5169
5274
|
const emailService = new EmailService(env);
|
|
5170
5275
|
const firstName = user.name?.split(" ")[0] || null;
|
|
5171
|
-
await emailService.send2faDisabledEmail({ email: user.email, firstName }, db);
|
|
5276
|
+
await emailService.send2faDisabledEmail({ email: user.email, firstName }, db, schema.users);
|
|
5172
5277
|
}
|
|
5173
5278
|
logSecurityEvent("2fa_disabled", "critical", { userId, lastMethod: "email" });
|
|
5174
5279
|
return c.json({ success: true, sessionInvalidated: true });
|
|
@@ -5489,7 +5594,7 @@ var challengeResendHandler = async (c) => {
|
|
|
5489
5594
|
});
|
|
5490
5595
|
const emailService = new EmailService(c.env);
|
|
5491
5596
|
const firstName = user.name?.split(" ")[0] || null;
|
|
5492
|
-
await emailService.send2faCodeEmail({ email: user.email, firstName, code }, db);
|
|
5597
|
+
await emailService.send2faCodeEmail({ email: user.email, firstName, code }, db, schema.users);
|
|
5493
5598
|
logger_default.info("Email 2FA code resent", { userId: payload.userId });
|
|
5494
5599
|
return c.json({ success: true });
|
|
5495
5600
|
};
|
|
@@ -5498,11 +5603,8 @@ var challengeResendHandler = async (c) => {
|
|
|
5498
5603
|
var twoFa = new import_zod_openapi36.OpenAPIHono();
|
|
5499
5604
|
twoFa.use("*", csrf);
|
|
5500
5605
|
twoFa.use("/status", requireAuth);
|
|
5501
|
-
twoFa.openapi(statusRoute, statusHandler);
|
|
5502
5606
|
twoFa.use("/totp/setup", requireAuth);
|
|
5503
|
-
twoFa.openapi(totpSetupRoute, totpSetupHandler);
|
|
5504
5607
|
twoFa.use("/totp/verify", requireAuth);
|
|
5505
|
-
twoFa.openapi(totpVerifyRoute, totpVerifyHandler);
|
|
5506
5608
|
twoFa.use(
|
|
5507
5609
|
"/totp/disable",
|
|
5508
5610
|
requireAuth,
|
|
@@ -5514,11 +5616,8 @@ twoFa.use(
|
|
|
5514
5616
|
// 5 attempts per 15 minutes
|
|
5515
5617
|
})
|
|
5516
5618
|
);
|
|
5517
|
-
twoFa.openapi(totpDisableRoute, totpDisableHandler);
|
|
5518
5619
|
twoFa.use("/email/setup", requireAuth);
|
|
5519
|
-
twoFa.openapi(emailSetupRoute, emailSetupHandler);
|
|
5520
5620
|
twoFa.use("/email/verify", requireAuth);
|
|
5521
|
-
twoFa.openapi(emailVerifyRoute, emailVerifyHandler);
|
|
5522
5621
|
twoFa.use(
|
|
5523
5622
|
"/email/send-code",
|
|
5524
5623
|
requireAuth,
|
|
@@ -5530,7 +5629,6 @@ twoFa.use(
|
|
|
5530
5629
|
// 3 per 5 minutes
|
|
5531
5630
|
})
|
|
5532
5631
|
);
|
|
5533
|
-
twoFa.openapi(emailSendCodeRoute, emailSendCodeHandler);
|
|
5534
5632
|
twoFa.use(
|
|
5535
5633
|
"/email/disable",
|
|
5536
5634
|
requireAuth,
|
|
@@ -5542,11 +5640,8 @@ twoFa.use(
|
|
|
5542
5640
|
// 5 attempts per 15 minutes
|
|
5543
5641
|
})
|
|
5544
5642
|
);
|
|
5545
|
-
twoFa.openapi(emailDisableRoute, emailDisableHandler);
|
|
5546
5643
|
twoFa.use("/trusted-devices", requireAuth);
|
|
5547
|
-
twoFa.openapi(trustedDevicesGetRoute, trustedDevicesGetHandler);
|
|
5548
5644
|
twoFa.use("/trusted-devices/:id", requireAuth);
|
|
5549
|
-
twoFa.openapi(trustedDevicesDeleteRoute, trustedDevicesDeleteHandler);
|
|
5550
5645
|
twoFa.use(
|
|
5551
5646
|
"/backup-codes/regenerate",
|
|
5552
5647
|
requireAuth,
|
|
@@ -5558,7 +5653,6 @@ twoFa.use(
|
|
|
5558
5653
|
// 3 per hour per user
|
|
5559
5654
|
})
|
|
5560
5655
|
);
|
|
5561
|
-
twoFa.openapi(backupCodesRegenerateRoute, backupCodesRegenerateHandler);
|
|
5562
5656
|
twoFa.use(
|
|
5563
5657
|
"/challenge",
|
|
5564
5658
|
rateLimit({
|
|
@@ -5569,7 +5663,6 @@ twoFa.use(
|
|
|
5569
5663
|
// 10 per 5 min per IP
|
|
5570
5664
|
})
|
|
5571
5665
|
);
|
|
5572
|
-
twoFa.openapi(challengeRoute, challengeHandler);
|
|
5573
5666
|
twoFa.use(
|
|
5574
5667
|
"/challenge/resend",
|
|
5575
5668
|
rateLimit({
|
|
@@ -5580,40 +5673,423 @@ twoFa.use(
|
|
|
5580
5673
|
// 3 per 5 min per IP
|
|
5581
5674
|
})
|
|
5582
5675
|
);
|
|
5583
|
-
twoFa.openapi(challengeResendRoute, challengeResendHandler);
|
|
5584
|
-
var fa_default =
|
|
5676
|
+
var twoFaRoutes = twoFa.openapi(statusRoute, statusHandler).openapi(totpSetupRoute, totpSetupHandler).openapi(totpVerifyRoute, totpVerifyHandler).openapi(totpDisableRoute, totpDisableHandler).openapi(emailSetupRoute, emailSetupHandler).openapi(emailVerifyRoute, emailVerifyHandler).openapi(emailSendCodeRoute, emailSendCodeHandler).openapi(emailDisableRoute, emailDisableHandler).openapi(trustedDevicesGetRoute, trustedDevicesGetHandler).openapi(trustedDevicesDeleteRoute, trustedDevicesDeleteHandler).openapi(backupCodesRegenerateRoute, backupCodesRegenerateHandler).openapi(challengeRoute, challengeHandler).openapi(challengeResendRoute, challengeResendHandler);
|
|
5677
|
+
var fa_default = twoFaRoutes;
|
|
5678
|
+
|
|
5679
|
+
// src/routes/oauth/index.ts
|
|
5680
|
+
var import_zod_openapi41 = require("@hono/zod-openapi");
|
|
5681
|
+
|
|
5682
|
+
// src/routes/oauth/authorize.ts
|
|
5683
|
+
var import_zod_openapi38 = require("@hono/zod-openapi");
|
|
5684
|
+
|
|
5685
|
+
// src/routes/oauth/providers.ts
|
|
5686
|
+
var PROVIDERS = {
|
|
5687
|
+
google: {
|
|
5688
|
+
authorizeUrl: "https://accounts.google.com/o/oauth2/v2/auth",
|
|
5689
|
+
tokenUrl: "https://oauth2.googleapis.com/token",
|
|
5690
|
+
userinfoUrl: "https://www.googleapis.com/oauth2/v2/userinfo",
|
|
5691
|
+
scopes: ["email"],
|
|
5692
|
+
emailExtractor: (data) => data.email || null,
|
|
5693
|
+
userIdExtractor: (data) => data.id || null
|
|
5694
|
+
},
|
|
5695
|
+
microsoft: {
|
|
5696
|
+
authorizeUrl: "https://login.microsoftonline.com/common/oauth2/v2.0/authorize",
|
|
5697
|
+
tokenUrl: "https://login.microsoftonline.com/common/oauth2/v2.0/token",
|
|
5698
|
+
userinfoUrl: "https://graph.microsoft.com/v1.0/me",
|
|
5699
|
+
scopes: ["openid", "email", "User.Read"],
|
|
5700
|
+
emailExtractor: (data) => data.mail || data.userPrincipalName || null,
|
|
5701
|
+
userIdExtractor: (data) => data.id || null
|
|
5702
|
+
}
|
|
5703
|
+
};
|
|
5704
|
+
function getProviderCredentials(provider, env) {
|
|
5705
|
+
if (provider === "google" && env.GOOGLE_CLIENT_ID && env.GOOGLE_CLIENT_SECRET) {
|
|
5706
|
+
return { clientId: env.GOOGLE_CLIENT_ID, clientSecret: env.GOOGLE_CLIENT_SECRET };
|
|
5707
|
+
}
|
|
5708
|
+
if (provider === "microsoft" && env.MICROSOFT_CLIENT_ID && env.MICROSOFT_CLIENT_SECRET) {
|
|
5709
|
+
return { clientId: env.MICROSOFT_CLIENT_ID, clientSecret: env.MICROSOFT_CLIENT_SECRET };
|
|
5710
|
+
}
|
|
5711
|
+
return null;
|
|
5712
|
+
}
|
|
5713
|
+
|
|
5714
|
+
// src/routes/oauth/schemas.ts
|
|
5715
|
+
var import_zod_openapi37 = require("@hono/zod-openapi");
|
|
5716
|
+
var oauthProviderParamSchema = import_zod_openapi37.z.object({
|
|
5717
|
+
provider: import_zod_openapi37.z.enum(["google", "microsoft"]).openapi({ example: "google" })
|
|
5718
|
+
});
|
|
5719
|
+
var oauthAuthorizeQuerySchema = import_zod_openapi37.z.object({
|
|
5720
|
+
redirect: import_zod_openapi37.z.string().optional().openapi({ example: "/dashboard" }),
|
|
5721
|
+
invitationToken: import_zod_openapi37.z.string().optional().openapi({ example: "token123" }),
|
|
5722
|
+
code_challenge: import_zod_openapi37.z.string().min(43).max(128).optional().openapi({ example: "E9Mrozoa2owUednMY..." }),
|
|
5723
|
+
code_challenge_method: import_zod_openapi37.z.enum(["S256"]).optional().openapi({ example: "S256" }),
|
|
5724
|
+
redirect_uri: import_zod_openapi37.z.string().url().optional().openapi({ example: "myapp://oauth-callback" })
|
|
5725
|
+
}).openapi("OAuthAuthorizeQuery");
|
|
5726
|
+
var oauthCallbackQuerySchema = import_zod_openapi37.z.object({
|
|
5727
|
+
code: import_zod_openapi37.z.string().optional(),
|
|
5728
|
+
state: import_zod_openapi37.z.string().optional(),
|
|
5729
|
+
error: import_zod_openapi37.z.string().optional()
|
|
5730
|
+
});
|
|
5731
|
+
var oauthCompleteRequestSchema = import_zod_openapi37.z.object({
|
|
5732
|
+
code: import_zod_openapi37.z.string().uuid("Invalid code format").openapi({ example: "a1b2c3d4-..." })
|
|
5733
|
+
}).openapi("OAuthCompleteRequest");
|
|
5734
|
+
var oauthCompleteResponseSchema = import_zod_openapi37.z.object({
|
|
5735
|
+
message: import_zod_openapi37.z.string().openapi({ example: "Login successful" }),
|
|
5736
|
+
redirect: import_zod_openapi37.z.string().openapi({ example: "/" }),
|
|
5737
|
+
user: import_zod_openapi37.z.object({
|
|
5738
|
+
id: import_zod_openapi37.z.string().uuid(),
|
|
5739
|
+
email: import_zod_openapi37.z.string().email(),
|
|
5740
|
+
emailVerified: import_zod_openapi37.z.boolean()
|
|
5741
|
+
})
|
|
5742
|
+
}).openapi("OAuthCompleteResponse");
|
|
5743
|
+
var errorResponseSchema4 = import_zod_openapi37.z.object({
|
|
5744
|
+
type: import_zod_openapi37.z.string().url(),
|
|
5745
|
+
title: import_zod_openapi37.z.string(),
|
|
5746
|
+
status: import_zod_openapi37.z.number().int(),
|
|
5747
|
+
detail: import_zod_openapi37.z.string().optional()
|
|
5748
|
+
}).openapi("ErrorResponse");
|
|
5749
|
+
|
|
5750
|
+
// src/routes/oauth/authorize.ts
|
|
5751
|
+
var authorizeRoute = (0, import_zod_openapi38.createRoute)({
|
|
5752
|
+
method: "get",
|
|
5753
|
+
path: "/{provider}/authorize",
|
|
5754
|
+
tags: ["OAuth"],
|
|
5755
|
+
summary: "Start OAuth login flow",
|
|
5756
|
+
description: "Generates a CSRF state token and redirects the user to the OAuth provider.",
|
|
5757
|
+
request: {
|
|
5758
|
+
params: oauthProviderParamSchema,
|
|
5759
|
+
query: oauthAuthorizeQuerySchema
|
|
5760
|
+
},
|
|
5761
|
+
responses: {
|
|
5762
|
+
302: { description: "Redirect to OAuth provider or error page" }
|
|
5763
|
+
}
|
|
5764
|
+
});
|
|
5765
|
+
var authorizeHandler = async (c) => {
|
|
5766
|
+
const { provider } = c.req.valid("param");
|
|
5767
|
+
const {
|
|
5768
|
+
redirect: loginRedirect,
|
|
5769
|
+
invitationToken,
|
|
5770
|
+
code_challenge,
|
|
5771
|
+
code_challenge_method,
|
|
5772
|
+
redirect_uri: mobileRedirectUri
|
|
5773
|
+
} = c.req.valid("query");
|
|
5774
|
+
const providerConfig = PROVIDERS[provider];
|
|
5775
|
+
if (!providerConfig) {
|
|
5776
|
+
return c.redirect(`${c.env.APP_URL}/login?error=oauth_invalid_provider`);
|
|
5777
|
+
}
|
|
5778
|
+
const credentials = getProviderCredentials(provider, c.env);
|
|
5779
|
+
if (!credentials) {
|
|
5780
|
+
logError(new Error(`OAuth credentials not configured for ${provider}`), {
|
|
5781
|
+
context: "oauth_authorize",
|
|
5782
|
+
provider
|
|
5783
|
+
});
|
|
5784
|
+
return c.redirect(`${c.env.APP_URL}/login?error=oauth_not_configured`);
|
|
5785
|
+
}
|
|
5786
|
+
if (code_challenge && !code_challenge_method || !code_challenge && code_challenge_method) {
|
|
5787
|
+
return c.redirect(`${c.env.APP_URL}/login?error=oauth_invalid_pkce`);
|
|
5788
|
+
}
|
|
5789
|
+
const state = crypto.randomUUID();
|
|
5790
|
+
await c.env.OAUTH_STATES.put(
|
|
5791
|
+
`oauth:state:${state}`,
|
|
5792
|
+
JSON.stringify({
|
|
5793
|
+
provider,
|
|
5794
|
+
invitationToken: invitationToken || null,
|
|
5795
|
+
redirect: loginRedirect || null,
|
|
5796
|
+
codeChallenge: code_challenge || null,
|
|
5797
|
+
codeChallengeMethod: code_challenge_method || null,
|
|
5798
|
+
mobileRedirectUri: mobileRedirectUri || null
|
|
5799
|
+
}),
|
|
5800
|
+
{ expirationTtl: 600 }
|
|
5801
|
+
// 10 minutes
|
|
5802
|
+
);
|
|
5803
|
+
const redirectUri = new URL(`/v1/auth/oauth/${provider}/callback`, c.req.url).toString();
|
|
5804
|
+
const authUrl = new URL(providerConfig.authorizeUrl);
|
|
5805
|
+
authUrl.searchParams.set("client_id", credentials.clientId);
|
|
5806
|
+
authUrl.searchParams.set("redirect_uri", redirectUri);
|
|
5807
|
+
authUrl.searchParams.set("response_type", "code");
|
|
5808
|
+
if (providerConfig.scopes.length > 0) {
|
|
5809
|
+
authUrl.searchParams.set("scope", providerConfig.scopes.join(" "));
|
|
5810
|
+
}
|
|
5811
|
+
authUrl.searchParams.set("state", state);
|
|
5812
|
+
if (provider === "google") {
|
|
5813
|
+
authUrl.searchParams.set("prompt", "select_account");
|
|
5814
|
+
}
|
|
5815
|
+
logger_default.info("OAuth authorize initiated", { provider, hasPkce: !!code_challenge });
|
|
5816
|
+
return c.redirect(authUrl.toString());
|
|
5817
|
+
};
|
|
5818
|
+
|
|
5819
|
+
// src/routes/oauth/callback.ts
|
|
5820
|
+
var import_zod_openapi39 = require("@hono/zod-openapi");
|
|
5821
|
+
var import_drizzle_orm32 = require("drizzle-orm");
|
|
5822
|
+
var callbackRoute = (0, import_zod_openapi39.createRoute)({
|
|
5823
|
+
method: "get",
|
|
5824
|
+
path: "/{provider}/callback",
|
|
5825
|
+
tags: ["OAuth"],
|
|
5826
|
+
summary: "OAuth provider callback",
|
|
5827
|
+
description: "Handles the redirect from an OAuth provider. Validates CSRF state, exchanges the code for a token, and either starts a login or signup flow.",
|
|
5828
|
+
request: {
|
|
5829
|
+
params: oauthProviderParamSchema,
|
|
5830
|
+
query: oauthCallbackQuerySchema
|
|
5831
|
+
},
|
|
5832
|
+
responses: {
|
|
5833
|
+
302: { description: "Redirect to app with login code or signup token" }
|
|
5834
|
+
}
|
|
5835
|
+
});
|
|
5836
|
+
var callbackHandler = async (c) => {
|
|
5837
|
+
const { provider } = c.req.valid("param");
|
|
5838
|
+
const { code, state, error } = c.req.valid("query");
|
|
5839
|
+
const appUrl = c.env.APP_URL;
|
|
5840
|
+
if (error) {
|
|
5841
|
+
logger_default.info("OAuth provider returned error", { provider, error });
|
|
5842
|
+
const errorType = error === "access_denied" || error === "consent_required" ? "oauth_cancelled" : "oauth_failed";
|
|
5843
|
+
return c.redirect(`${appUrl}/login?error=${errorType}`);
|
|
5844
|
+
}
|
|
5845
|
+
if (!code || !state) {
|
|
5846
|
+
logger_default.warn("Missing code or state in OAuth callback", { provider });
|
|
5847
|
+
return c.redirect(`${appUrl}/login?error=oauth_failed`);
|
|
5848
|
+
}
|
|
5849
|
+
const storedState = await c.env.OAUTH_STATES.get(`oauth:state:${state}`);
|
|
5850
|
+
if (!storedState) {
|
|
5851
|
+
logSecurityEvent("oauth_csrf_attempt", "high", { state, provider });
|
|
5852
|
+
return c.redirect(`${appUrl}/login?error=oauth_expired`);
|
|
5853
|
+
}
|
|
5854
|
+
let stateData;
|
|
5855
|
+
try {
|
|
5856
|
+
stateData = JSON.parse(storedState);
|
|
5857
|
+
} catch (err) {
|
|
5858
|
+
logError(err, { context: "oauth_state_parse", provider });
|
|
5859
|
+
return c.redirect(`${appUrl}/login?error=oauth_failed`);
|
|
5860
|
+
}
|
|
5861
|
+
if (stateData.provider !== provider) {
|
|
5862
|
+
logSecurityEvent("oauth_csrf_attempt", "high", { state, provider });
|
|
5863
|
+
return c.redirect(`${appUrl}/login?error=oauth_expired`);
|
|
5864
|
+
}
|
|
5865
|
+
await c.env.OAUTH_STATES.delete(`oauth:state:${state}`);
|
|
5866
|
+
const {
|
|
5867
|
+
provider: _stateProvider,
|
|
5868
|
+
invitationToken,
|
|
5869
|
+
redirect: loginRedirect,
|
|
5870
|
+
codeChallenge,
|
|
5871
|
+
codeChallengeMethod,
|
|
5872
|
+
mobileRedirectUri
|
|
5873
|
+
} = stateData;
|
|
5874
|
+
const codeVerifier = c.req.query("code_verifier");
|
|
5875
|
+
if (codeChallenge && codeChallengeMethod === "S256") {
|
|
5876
|
+
if (!codeVerifier) {
|
|
5877
|
+
logger_default.warn("OAuth PKCE: missing code_verifier", { provider });
|
|
5878
|
+
return c.redirect(`${appUrl}/login?error=oauth_pkce_missing`);
|
|
5879
|
+
}
|
|
5880
|
+
const pkceValid = await verifyCodeChallenge(codeVerifier, codeChallenge);
|
|
5881
|
+
if (!pkceValid) {
|
|
5882
|
+
logger_default.warn("OAuth PKCE: invalid code_verifier", { provider });
|
|
5883
|
+
return c.redirect(`${appUrl}/login?error=oauth_pkce_invalid`);
|
|
5884
|
+
}
|
|
5885
|
+
}
|
|
5886
|
+
const providerConfig = PROVIDERS[provider];
|
|
5887
|
+
if (!providerConfig) {
|
|
5888
|
+
return c.redirect(`${appUrl}/login?error=oauth_failed`);
|
|
5889
|
+
}
|
|
5890
|
+
const credentials = getProviderCredentials(provider, c.env);
|
|
5891
|
+
if (!credentials) {
|
|
5892
|
+
return c.redirect(`${appUrl}/login?error=oauth_not_configured`);
|
|
5893
|
+
}
|
|
5894
|
+
const redirectUri = new URL(`/v1/auth/oauth/${provider}/callback`, c.req.url).toString();
|
|
5895
|
+
try {
|
|
5896
|
+
const tokenResponse = await fetch(providerConfig.tokenUrl, {
|
|
5897
|
+
method: "POST",
|
|
5898
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
5899
|
+
body: new URLSearchParams({
|
|
5900
|
+
grant_type: "authorization_code",
|
|
5901
|
+
code,
|
|
5902
|
+
redirect_uri: redirectUri,
|
|
5903
|
+
client_id: credentials.clientId,
|
|
5904
|
+
client_secret: credentials.clientSecret
|
|
5905
|
+
}),
|
|
5906
|
+
signal: AbortSignal.timeout(1e4)
|
|
5907
|
+
});
|
|
5908
|
+
if (!tokenResponse.ok) {
|
|
5909
|
+
const body = await tokenResponse.text();
|
|
5910
|
+
logError(new Error(`OAuth token exchange failed: ${tokenResponse.status} ${body}`), {
|
|
5911
|
+
context: "oauth_token_exchange",
|
|
5912
|
+
provider
|
|
5913
|
+
});
|
|
5914
|
+
return c.redirect(`${appUrl}/login?error=oauth_failed`);
|
|
5915
|
+
}
|
|
5916
|
+
const tokenData = await tokenResponse.json();
|
|
5917
|
+
if (!tokenData.access_token) {
|
|
5918
|
+
logError(new Error("No access_token in OAuth response"), {
|
|
5919
|
+
context: "oauth_token_exchange",
|
|
5920
|
+
provider
|
|
5921
|
+
});
|
|
5922
|
+
return c.redirect(`${appUrl}/login?error=oauth_failed`);
|
|
5923
|
+
}
|
|
5924
|
+
const userinfoResponse = await fetch(providerConfig.userinfoUrl, {
|
|
5925
|
+
headers: { Authorization: `Bearer ${tokenData.access_token}` },
|
|
5926
|
+
signal: AbortSignal.timeout(1e4)
|
|
5927
|
+
});
|
|
5928
|
+
if (!userinfoResponse.ok) {
|
|
5929
|
+
logError(new Error(`OAuth userinfo failed: ${userinfoResponse.status}`), {
|
|
5930
|
+
context: "oauth_userinfo",
|
|
5931
|
+
provider
|
|
5932
|
+
});
|
|
5933
|
+
return c.redirect(`${appUrl}/login?error=oauth_failed`);
|
|
5934
|
+
}
|
|
5935
|
+
const userInfo = await userinfoResponse.json();
|
|
5936
|
+
const email = providerConfig.emailExtractor(userInfo)?.toLowerCase();
|
|
5937
|
+
const providerUserId = providerConfig.userIdExtractor(userInfo);
|
|
5938
|
+
if (!email) {
|
|
5939
|
+
logger_default.warn("No email returned from OAuth provider", { provider });
|
|
5940
|
+
return c.redirect(`${appUrl}/login?error=oauth_no_email`);
|
|
5941
|
+
}
|
|
5942
|
+
if (!providerUserId) {
|
|
5943
|
+
logger_default.warn("No user ID returned from OAuth provider", { provider });
|
|
5944
|
+
return c.redirect(`${appUrl}/login?error=oauth_failed`);
|
|
5945
|
+
}
|
|
5946
|
+
const { db, schema } = getAuthContext(c);
|
|
5947
|
+
const [existingUser] = await db.select({ id: schema.users.id, email: schema.users.email }).from(schema.users).where((0, import_drizzle_orm32.eq)(schema.users.email, email)).limit(1);
|
|
5948
|
+
if (existingUser) {
|
|
5949
|
+
const loginCode = crypto.randomUUID();
|
|
5950
|
+
await c.env.OAUTH_STATES.put(
|
|
5951
|
+
`oauth:login:${loginCode}`,
|
|
5952
|
+
JSON.stringify({
|
|
5953
|
+
userId: existingUser.id,
|
|
5954
|
+
email: existingUser.email,
|
|
5955
|
+
provider,
|
|
5956
|
+
providerUserId,
|
|
5957
|
+
redirect: loginRedirect,
|
|
5958
|
+
invitationToken
|
|
5959
|
+
}),
|
|
5960
|
+
{ expirationTtl: 60 }
|
|
5961
|
+
);
|
|
5962
|
+
logSecurityEvent("oauth_login_initiated", "low", {
|
|
5963
|
+
userId: existingUser.id,
|
|
5964
|
+
email: existingUser.email,
|
|
5965
|
+
provider
|
|
5966
|
+
});
|
|
5967
|
+
if (mobileRedirectUri && isDeepLinkUri(mobileRedirectUri)) {
|
|
5968
|
+
const mobileUrl = new URL(mobileRedirectUri);
|
|
5969
|
+
mobileUrl.searchParams.set("code", loginCode);
|
|
5970
|
+
mobileUrl.searchParams.set("action", "login");
|
|
5971
|
+
return c.redirect(mobileUrl.toString());
|
|
5972
|
+
}
|
|
5973
|
+
return c.redirect(`${appUrl}/auth/callback?code=${loginCode}&action=login`);
|
|
5974
|
+
} else {
|
|
5975
|
+
const signupToken = crypto.randomUUID();
|
|
5976
|
+
await c.env.OAUTH_STATES.put(
|
|
5977
|
+
`oauth:signup:${signupToken}`,
|
|
5978
|
+
JSON.stringify({ email, provider, providerUserId, invitationToken }),
|
|
5979
|
+
{ expirationTtl: 600 }
|
|
5980
|
+
);
|
|
5981
|
+
logger_default.info("OAuth signup initiated", { provider, email });
|
|
5982
|
+
if (mobileRedirectUri && isDeepLinkUri(mobileRedirectUri)) {
|
|
5983
|
+
const mobileUrl = new URL(mobileRedirectUri);
|
|
5984
|
+
mobileUrl.searchParams.set("oauth_token", signupToken);
|
|
5985
|
+
mobileUrl.searchParams.set("email", email);
|
|
5986
|
+
if (invitationToken) {
|
|
5987
|
+
mobileUrl.searchParams.set("invitation", invitationToken);
|
|
5988
|
+
}
|
|
5989
|
+
return c.redirect(mobileUrl.toString());
|
|
5990
|
+
}
|
|
5991
|
+
let signupUrl = `${appUrl}/signup?oauth_token=${signupToken}&email=${encodeURIComponent(email)}`;
|
|
5992
|
+
if (invitationToken) signupUrl += `&invitation=${invitationToken}`;
|
|
5993
|
+
return c.redirect(signupUrl);
|
|
5994
|
+
}
|
|
5995
|
+
} catch (err) {
|
|
5996
|
+
logError(err, { context: "oauth_callback", provider });
|
|
5997
|
+
return c.redirect(`${appUrl}/login?error=oauth_failed`);
|
|
5998
|
+
}
|
|
5999
|
+
};
|
|
6000
|
+
|
|
6001
|
+
// src/routes/oauth/complete.ts
|
|
6002
|
+
var import_zod_openapi40 = require("@hono/zod-openapi");
|
|
6003
|
+
var import_drizzle_orm33 = require("drizzle-orm");
|
|
6004
|
+
var completeRoute = (0, import_zod_openapi40.createRoute)({
|
|
6005
|
+
method: "post",
|
|
6006
|
+
path: "/complete",
|
|
6007
|
+
tags: ["OAuth"],
|
|
6008
|
+
summary: "Complete OAuth login",
|
|
6009
|
+
description: "Exchanges a one-time login code (from the OAuth callback) for a session cookie. Called by the /auth/callback frontend page.",
|
|
6010
|
+
request: {
|
|
6011
|
+
body: {
|
|
6012
|
+
content: { "application/json": { schema: oauthCompleteRequestSchema } },
|
|
6013
|
+
required: true
|
|
6014
|
+
}
|
|
6015
|
+
},
|
|
6016
|
+
responses: {
|
|
6017
|
+
200: {
|
|
6018
|
+
description: "Session created successfully",
|
|
6019
|
+
content: { "application/json": { schema: oauthCompleteResponseSchema } }
|
|
6020
|
+
},
|
|
6021
|
+
401: {
|
|
6022
|
+
description: "Invalid or expired login code",
|
|
6023
|
+
content: { "application/json": { schema: errorResponseSchema4 } }
|
|
6024
|
+
}
|
|
6025
|
+
}
|
|
6026
|
+
});
|
|
6027
|
+
var completeMiddleware = [
|
|
6028
|
+
rateLimit({
|
|
6029
|
+
identifier: (c) => c.req.header("cf-connecting-ip") || "unknown",
|
|
6030
|
+
action: "oauth_complete",
|
|
6031
|
+
maxAttempts: 10,
|
|
6032
|
+
windowMs: 9e5
|
|
6033
|
+
// 15 minutes
|
|
6034
|
+
})
|
|
6035
|
+
];
|
|
6036
|
+
var completeHandler = async (c) => {
|
|
6037
|
+
const { code } = c.req.valid("json");
|
|
6038
|
+
const stored = await c.env.OAUTH_STATES.get(`oauth:login:${code}`);
|
|
6039
|
+
if (!stored) {
|
|
6040
|
+
return problems.unauthorized(c, "Invalid or expired login code");
|
|
6041
|
+
}
|
|
6042
|
+
await c.env.OAUTH_STATES.delete(`oauth:login:${code}`);
|
|
6043
|
+
const {
|
|
6044
|
+
userId,
|
|
6045
|
+
email,
|
|
6046
|
+
provider,
|
|
6047
|
+
providerUserId,
|
|
6048
|
+
redirect: storedRedirect
|
|
6049
|
+
} = JSON.parse(stored);
|
|
6050
|
+
const { db, schema } = getAuthContext(c);
|
|
6051
|
+
await db.update(schema.users).set({ emailVerified: true }).where((0, import_drizzle_orm33.eq)(schema.users.id, userId));
|
|
6052
|
+
await db.insert(schema.oauthAccounts).values({ userId, provider, providerUserId, email }).onConflictDoNothing();
|
|
6053
|
+
const fingerprint = await generateFingerprint(c.req.raw);
|
|
6054
|
+
const ipAddress = getClientIp(c.req.raw);
|
|
6055
|
+
const sessionId = await createSession(db, { sessions: schema.sessions }, userId, fingerprint, ipAddress);
|
|
6056
|
+
const redirect = storedRedirect?.startsWith("/") && !storedRedirect.startsWith("//") ? storedRedirect : "/";
|
|
6057
|
+
logger_default.info("OAuth login completed", { userId, provider, sessionId: sessionId.slice(0, 8) });
|
|
6058
|
+
logSecurityEvent("oauth_login_success", "low", {
|
|
6059
|
+
userId,
|
|
6060
|
+
email,
|
|
6061
|
+
provider,
|
|
6062
|
+
ip: c.req.header("cf-connecting-ip")
|
|
6063
|
+
});
|
|
6064
|
+
return c.json(
|
|
6065
|
+
{ message: "Login successful", redirect, user: { id: userId, email, emailVerified: true } },
|
|
6066
|
+
200,
|
|
6067
|
+
{ "Set-Cookie": setSessionCookie(sessionId, c.env) }
|
|
6068
|
+
);
|
|
6069
|
+
};
|
|
6070
|
+
|
|
6071
|
+
// src/routes/oauth/index.ts
|
|
6072
|
+
var oauth = new import_zod_openapi41.OpenAPIHono();
|
|
6073
|
+
oauth.use("/complete", ...completeMiddleware);
|
|
6074
|
+
var oauthRoutes = oauth.openapi(authorizeRoute, authorizeHandler).openapi(callbackRoute, callbackHandler).openapi(completeRoute, completeHandler);
|
|
6075
|
+
var oauth_default = oauthRoutes;
|
|
5585
6076
|
|
|
5586
6077
|
// src/routes/index.ts
|
|
5587
|
-
var auth = new
|
|
6078
|
+
var auth = new import_zod_openapi42.OpenAPIHono();
|
|
5588
6079
|
auth.use("*", csrf);
|
|
5589
6080
|
auth.use("/signup", ...signupMiddleware);
|
|
5590
|
-
auth.openapi(signupRoute, signupHandler);
|
|
5591
6081
|
auth.use("/login", ...loginMiddleware);
|
|
5592
|
-
auth.openapi(loginRoute, loginHandler);
|
|
5593
|
-
auth.openapi(logoutRoute, logoutHandler);
|
|
5594
6082
|
auth.use("/me", ...meMiddleware);
|
|
5595
|
-
auth.openapi(meRoute, meHandler);
|
|
5596
|
-
auth.openapi(verifyEmailRoute, verifyEmailHandler);
|
|
5597
6083
|
auth.use("/forgot-password", ...forgotPasswordMiddleware);
|
|
5598
|
-
auth.openapi(forgotPasswordRoute, forgotPasswordHandler);
|
|
5599
6084
|
auth.use("/reset-password", ...resetPasswordMiddleware);
|
|
5600
|
-
auth.openapi(resetPasswordRoute, resetPasswordHandler);
|
|
5601
6085
|
auth.use("/change-password", ...changePasswordMiddleware);
|
|
5602
|
-
auth.openapi(changePasswordRoute, changePasswordHandler);
|
|
5603
6086
|
auth.use("/heartbeat", ...heartbeatMiddleware);
|
|
5604
|
-
auth.openapi(heartbeatRoute, heartbeatHandler);
|
|
5605
6087
|
auth.use("/change-email", ...changeEmailMiddleware);
|
|
5606
|
-
auth.openapi(changeEmailRoute, changeEmailHandler);
|
|
5607
|
-
auth.openapi(confirmEmailChangeRoute, confirmEmailChangeHandler);
|
|
5608
|
-
auth.openapi(cancelEmailChangeRoute, cancelEmailChangeHandler);
|
|
5609
6088
|
auth.use("/account", ...deleteAccountMiddleware);
|
|
5610
|
-
auth.openapi(deleteAccountRoute, deleteAccountHandler);
|
|
5611
6089
|
auth.use("/refresh", ...refreshMiddleware);
|
|
5612
|
-
auth.openapi(refreshRoute, refreshHandler);
|
|
5613
6090
|
auth.use("/resend-verification", ...resendVerificationMiddleware);
|
|
5614
|
-
auth.openapi(resendVerificationRoute, resendVerificationHandler);
|
|
5615
|
-
|
|
5616
|
-
var routes_default = auth;
|
|
6091
|
+
var authRoutes = auth.openapi(signupRoute, signupHandler).openapi(loginRoute, loginHandler).openapi(logoutRoute, logoutHandler).openapi(meRoute, meHandler).openapi(verifyEmailRoute, verifyEmailHandler).openapi(forgotPasswordRoute, forgotPasswordHandler).openapi(resetPasswordRoute, resetPasswordHandler).openapi(changePasswordRoute, changePasswordHandler).openapi(heartbeatRoute, heartbeatHandler).openapi(changeEmailRoute, changeEmailHandler).openapi(confirmEmailChangeRoute, confirmEmailChangeHandler).openapi(cancelEmailChangeRoute, cancelEmailChangeHandler).openapi(deleteAccountRoute, deleteAccountHandler).openapi(refreshRoute, refreshHandler).openapi(resendVerificationRoute, resendVerificationHandler).route("/2fa", fa_default).route("/oauth", oauth_default);
|
|
6092
|
+
var routes_default = authRoutes;
|
|
5617
6093
|
|
|
5618
6094
|
// src/lib/email/webhook-verifier.ts
|
|
5619
6095
|
var import_svix = require("svix");
|
|
@@ -5642,6 +6118,7 @@ function verifyWebhookSignature(payload, signature, secret) {
|
|
|
5642
6118
|
0 && (module.exports = {
|
|
5643
6119
|
AUTH_DEFAULTS,
|
|
5644
6120
|
CHALLENGE_TTL_MS,
|
|
6121
|
+
CloudflareEmailAdapter,
|
|
5645
6122
|
EmailService,
|
|
5646
6123
|
MAX_CHALLENGE_ATTEMPTS,
|
|
5647
6124
|
ProblemTypes,
|