diff --git a/components/auth/email-action-form.tsx b/components/auth/email-action-form.tsx index 2ffe3d0..066a1ad 100644 --- a/components/auth/email-action-form.tsx +++ b/components/auth/email-action-form.tsx @@ -73,7 +73,7 @@ export function EmailActionForm({ mode, token = "", invalidToken = false, nextPa } - } diff --git a/lib/auth/server.test.ts b/lib/auth/server.test.ts index 4dcb414..95ee3d7 100644 --- a/lib/auth/server.test.ts +++ b/lib/auth/server.test.ts @@ -81,19 +81,39 @@ describe("actual administrator session boundary", () => { (await import("./server")).getAuth(); const options = mocks.auth.mock.calls.at(-1)![0]; expect(options.emailAndPassword.requireEmailVerification).toBe(true); + expect(options.emailAndPassword.resetPasswordTokenExpiresIn).toBe(3600); expect(options.emailVerification).toMatchObject({ - sendOnSignUp: false, autoSignInAfterVerification: true, + sendOnSignUp: false, autoSignInAfterVerification: true, expiresIn: 3600, }); expect(options.user.changeEmail.enabled).toBe(true); const otp = options.plugins.find((plugin: { id?: string }) => plugin.id === "email-otp"); - expect(otp.options).toMatchObject({ sendVerificationOnSignUp: true, storeOTP: "hashed" }); + expect(otp.options).toMatchObject({ sendVerificationOnSignUp: true, storeOTP: "hashed", expiresIn: 300 }); const user = { email: "reader@example.test" }; await otp.options.sendVerificationOTP({ email: user.email, otp: "123456", type: "email-verification" }); + const otpEmail = JSON.parse(String(fetchMock.mock.calls[0][1]?.body)); + expect(otpEmail.subject).toContain("รหัส OTP"); + expect(otpEmail.text).toContain("รหัส OTP: 123456"); + expect(otpEmail.text).toContain("หมดอายุใน 5 นาที"); + expect(otpEmail.text).toContain("อย่าแชร์รหัสนี้กับผู้อื่น"); + expect(otpEmail.text).toContain("กรุณาอย่าตอบกลับ"); await options.emailVerification.sendVerificationEmail({ user, url: "https://guide.example.test/verify-email?token=abc" }); await options.user.changeEmail.sendChangeEmailConfirmation({ user, newEmail: "new@example.test", url: "https://guide.example.test/verify-email?token=change" }); await options.emailAndPassword.sendResetPassword({ user, url: "https://guide.example.test/reset-password/abc" }); expect(fetchMock).toHaveBeenCalledTimes(4); + const emails = fetchMock.mock.calls.map((call) => JSON.parse(String(call[1]?.body))); + expect(emails[1].text).toContain(user.email); + expect(emails[1].text).toContain("ยืนยันอีเมลใหม่"); + expect(emails[2].text).toContain(`อีเมลปัจจุบัน: ${user.email}\nอีเมลใหม่: new@example.test`); + expect(emails[2].text).toContain("คุณต้องเปิดลิงก์ในอีเมลใหม่"); + expect(emails[3].text).toContain("รหัสผ่านของคุณจะเปลี่ยนเมื่อ"); + expect(emails[3].text).toContain("รหัสผ่านเดิมของคุณจะยังใช้งานได้"); + for (const email of emails.slice(1)) { + expect(email.text).toContain("หมดอายุใน 1 ชั่วโมง"); + expect(email.text).toContain("อย่าแชร์หรือส่งต่อลิงก์นี้"); + expect(email.text).toContain("หากคุณไม่ได้"); + expect(email.text).toContain("กรุณาอย่าตอบกลับ"); + } for (const call of fetchMock.mock.calls) { expect(call[0]).toBe("https://api.resend.com/emails"); expect(call[1]?.headers).toMatchObject({ Authorization: "Bearer test-key" }); diff --git a/lib/auth/server.ts b/lib/auth/server.ts index 23fdfc3..0abf91d 100644 --- a/lib/auth/server.ts +++ b/lib/auth/server.ts @@ -56,24 +56,50 @@ function createAuth() { requireEmailVerification: true, minPasswordLength: 6, maxPasswordLength: 128, + resetPasswordTokenExpiresIn: 3600, sendResetPassword: async ({ user, url }) => { - await sendAuthEmail(user.email, "รีเซ็ตรหัสผ่าน Buzz Guide", `เปิดลิงก์นี้เพื่อรีเซ็ตรหัสผ่าน: ${url}\n\nลิงก์นี้หมดอายุใน 1 ชั่วโมง`); + await sendAuthEmail(user.email, "ตั้งรหัสผ่านใหม่สำหรับบัญชี Buzz Guide", [ + `เราได้รับคำขอตั้งรหัสผ่านใหม่สำหรับบัญชีที่ใช้อีเมล ${user.email}`, + `เปิดลิงก์นี้เพื่อตั้งรหัสผ่านใหม่:\n${url}`, + "ลิงก์นี้หมดอายุใน 1 ชั่วโมง หากหมดอายุ ให้ขอลิงก์ใหม่จากหน้า “ลืมรหัสผ่าน”", + "รหัสผ่านของคุณจะเปลี่ยนเมื่อคุณตั้งรหัสผ่านใหม่และกดยืนยันบนเว็บไซต์เท่านั้น", + "อย่าแชร์หรือส่งต่อลิงก์นี้ให้ผู้อื่น ทีมงาน Buzz Guide จะไม่ขอรหัสผ่านของคุณ", + "หากคุณไม่ได้ขอเปลี่ยนรหัสผ่าน ไม่ต้องเปิดลิงก์นี้ รหัสผ่านเดิมของคุณจะยังใช้งานได้", + "อีเมลนี้ส่งอัตโนมัติจาก no-reply@sudloh.com กรุณาอย่าตอบกลับ", + ].join("\n\n")); }, }, user: { changeEmail: { enabled: true, sendChangeEmailConfirmation: async ({ user, newEmail, url }) => { - await sendAuthEmail(user.email, "ยืนยันการเปลี่ยนอีเมล Buzz Guide", - `เปิดลิงก์นี้เพื่ออนุมัติการเปลี่ยนอีเมลเป็น ${newEmail}: ${url}\n\nลิงก์นี้หมดอายุใน 1 ชั่วโมง`); + await sendAuthEmail(user.email, "อนุมัติคำขอเปลี่ยนอีเมลบัญชี Buzz Guide", [ + "เราได้รับคำขอเปลี่ยนอีเมลสำหรับบัญชี Buzz Guide ของคุณ", + `อีเมลปัจจุบัน: ${user.email}\nอีเมลใหม่: ${newEmail}`, + `หากคุณเป็นผู้ขอเปลี่ยน เปิดลิงก์นี้เพื่ออนุมัติ:\n${url}`, + `หลังอนุมัติ เราจะส่งลิงก์ยืนยันไปที่ ${newEmail} คุณต้องเปิดลิงก์ในอีเมลใหม่เพื่อเปลี่ยนอีเมลให้เสร็จสมบูรณ์`, + "ลิงก์อนุมัตินี้หมดอายุใน 1 ชั่วโมง หากหมดอายุ ให้ขอเปลี่ยนอีเมลอีกครั้งจากหน้าโปรไฟล์", + "อย่าแชร์หรือส่งต่อลิงก์นี้ให้ผู้อื่น", + "หากคุณไม่ได้ขอเปลี่ยนอีเมล อย่าเปิดลิงก์นี้ และตรวจสอบความปลอดภัยของบัญชีจากหน้าโปรไฟล์", + "อีเมลนี้ส่งอัตโนมัติจาก no-reply@sudloh.com กรุณาอย่าตอบกลับ", + ].join("\n\n")); }, }, }, emailVerification: { sendOnSignUp: false, autoSignInAfterVerification: true, + expiresIn: 3600, sendVerificationEmail: async ({ user, url }) => { - await sendAuthEmail(user.email, "ยืนยันอีเมล Buzz Guide", `เปิดลิงก์นี้เพื่อยืนยันอีเมล: ${url}\n\nลิงก์นี้หมดอายุใน 1 ชั่วโมง`); + await sendAuthEmail(user.email, "ยืนยันอีเมลสำหรับบัญชี Buzz Guide", [ + `ยืนยันว่า ${user.email} เป็นอีเมลที่คุณต้องการใช้กับบัญชี Buzz Guide`, + `เปิดลิงก์นี้เพื่อยืนยันอีเมล:\n${url}`, + "หากคุณกำลังเปลี่ยนอีเมลบัญชี การเปิดลิงก์นี้จะยืนยันอีเมลใหม่และทำให้การเปลี่ยนอีเมลเสร็จสมบูรณ์", + "ลิงก์นี้หมดอายุใน 1 ชั่วโมง หากหมดอายุ ให้ขอยืนยันอีเมลอีกครั้งบนเว็บไซต์", + "อย่าแชร์หรือส่งต่อลิงก์นี้ให้ผู้อื่น", + "หากคุณไม่ได้สมัครบัญชีหรือขอเปลี่ยนอีเมล อย่าเปิดลิงก์นี้ และไม่ต้องดำเนินการใด ๆ", + "อีเมลนี้ส่งอัตโนมัติจาก no-reply@sudloh.com กรุณาอย่าตอบกลับ", + ].join("\n\n")); }, }, advanced: { @@ -97,8 +123,16 @@ function createAuth() { emailOTP({ sendVerificationOnSignUp: true, storeOTP: "hashed", + expiresIn: 300, sendVerificationOTP: async ({ email, otp }) => { - await sendAuthEmail(email, "รหัสยืนยัน Buzz Guide", `รหัสยืนยันอีเมลของคุณคือ ${otp}\n\nรหัสนี้หมดอายุใน 5 นาที`); + await sendAuthEmail(email, "รหัส OTP สำหรับยืนยันอีเมล Buzz Guide", [ + "ใช้รหัสนี้เพื่อยืนยันอีเมลของคุณใน Buzz Guide", + `รหัส OTP: ${otp}`, + "รหัสนี้หมดอายุใน 5 นาที หากหมดอายุ ให้กดส่งรหัสใหม่บนหน้ายืนยันอีเมล", + "อย่าแชร์รหัสนี้กับผู้อื่น ทีมงาน Buzz Guide จะไม่ขอรหัส OTP ของคุณ", + "หากคุณไม่ได้ขอรหัสนี้ ไม่ต้องดำเนินการใด ๆ และอย่าส่งรหัสให้ใคร", + "อีเมลนี้ส่งอัตโนมัติจาก no-reply@sudloh.com กรุณาอย่าตอบกลับ", + ].join("\n\n")); }, }), ...(process.env.NODE_ENV === "development" ? [] : [