+
+
+
+
+ |
+
+ |
+
+
+
+ 确认是你本人。
+ 您好,您正在注册 {{ if or (eq .RedirectTo "https://sm.shadow.wang") (eq .RedirectTo "https://sm.shadow.wang/") (eq .Data.product_id "shadow-mate") }}影伴 Shadow Mate{{ else if or (eq .RedirectTo "https://sc.shadow.wang") (eq .RedirectTo "https://sc.shadow.wang/") (eq .RedirectTo "https://sbc.shadow.wang") (eq .RedirectTo "https://sbc.shadow.wang/") (eq .Data.product_id "shadow-card") }}Shadow Card{{ else if or (eq .RedirectTo "https://ss.shadow.wang") (eq .RedirectTo "https://ss.shadow.wang/") (eq .Data.product_id "shadow-size") }}Shadow Size{{ else if or (eq .RedirectTo "https://flomo.shadow.wang") (eq .RedirectTo "https://flomo.shadow.wang/") (eq .Data.product_id "quick-flomo") }}Quick flomo{{ else }}Shadow Nexus{{ end }}。请输入下面的验证码完成注册。
+ Your verification code
+
+
+ | {{ .Token }} |
+ 短时间内有效 仅可使用一次 |
+
+
+
+ 验证码只能使用一次。如果邮件客户端支持按钮,也可以点击下方按钮完成注册。
+ 完成注册 ↗
+ |
+
+
+
+
+
+ |
+
+
diff --git a/supabase/templates/email_change.html b/supabase/templates/email_change.html
new file mode 100644
index 0000000..c68e447
--- /dev/null
+++ b/supabase/templates/email_change.html
@@ -0,0 +1,77 @@
+
+
+
+
+
+
+
+
+ |
+
+ |
+
+
+
+ 确认新邮箱。
+ 我们收到了把邮箱从 {{ .Email }} 改为 {{ .NewEmail }} 的请求。请输入下面的验证码完成确认。
+ Your verification code
+
+
+ 验证码只能使用一次。如果邮件客户端支持按钮,也可以点击下方按钮完成确认。
+ 确认新邮箱 ↗
+ |
+
+
+
+
+
+ |
+
+
+
+
diff --git a/supabase/templates/invite.html b/supabase/templates/invite.html
new file mode 100644
index 0000000..f690c21
--- /dev/null
+++ b/supabase/templates/invite.html
@@ -0,0 +1,77 @@
+
+
+
+
+
+
+
+
+ |
+
+ |
+
+
+
+ 欢迎加入。
+ 你被邀请加入 {{ if or (eq .RedirectTo "https://sm.shadow.wang") (eq .RedirectTo "https://sm.shadow.wang/") (eq .Data.product_id "shadow-mate") }}影伴 Shadow Mate{{ else if or (eq .RedirectTo "https://sc.shadow.wang") (eq .RedirectTo "https://sc.shadow.wang/") (eq .RedirectTo "https://sbc.shadow.wang") (eq .RedirectTo "https://sbc.shadow.wang/") (eq .Data.product_id "shadow-card") }}Shadow Card{{ else if or (eq .RedirectTo "https://ss.shadow.wang") (eq .RedirectTo "https://ss.shadow.wang/") (eq .Data.product_id "shadow-size") }}Shadow Size{{ else if or (eq .RedirectTo "https://flomo.shadow.wang") (eq .RedirectTo "https://flomo.shadow.wang/") (eq .Data.product_id "quick-flomo") }}Quick flomo{{ else }}Shadow Nexus{{ end }}。请输入下面的验证码完成注册。
+ Your verification code
+
+
+ 邀请码只能使用一次。如果邮件客户端支持按钮,也可以点击下方按钮完成注册。
+ 接受邀请 ↗
+ |
+
+
+
+
+
+ |
+
+
+
+
diff --git a/supabase/templates/magic_link.html b/supabase/templates/magic_link.html
index 98f8e7b..d085bcc 100644
--- a/supabase/templates/magic_link.html
+++ b/supabase/templates/magic_link.html
@@ -1,22 +1,77 @@
-
-
+
+
+
+
+ |
+
+ |
+
+
+
+ 确认是你本人。
+ 您好,您正在登录 {{ if or (eq .RedirectTo "https://sm.shadow.wang") (eq .RedirectTo "https://sm.shadow.wang/") (eq .Data.product_id "shadow-mate") }}影伴 Shadow Mate{{ else if or (eq .RedirectTo "https://sc.shadow.wang") (eq .RedirectTo "https://sc.shadow.wang/") (eq .RedirectTo "https://sbc.shadow.wang") (eq .RedirectTo "https://sbc.shadow.wang/") (eq .Data.product_id "shadow-card") }}Shadow Card{{ else if or (eq .RedirectTo "https://ss.shadow.wang") (eq .RedirectTo "https://ss.shadow.wang/") (eq .Data.product_id "shadow-size") }}Shadow Size{{ else if or (eq .RedirectTo "https://flomo.shadow.wang") (eq .RedirectTo "https://flomo.shadow.wang/") (eq .Data.product_id "quick-flomo") }}Quick flomo{{ else }}Shadow Nexus{{ end }}。请输入下面的验证码继续。
+ Your verification code
+
+
+ 验证码只能使用一次。如果邮件客户端支持按钮,也可以点击下方按钮完成登录。
+ 点击登录 ↗
+ |
+
+
+
+
+
+ |
+
+
diff --git a/supabase/templates/reauthentication.html b/supabase/templates/reauthentication.html
new file mode 100644
index 0000000..37f2605
--- /dev/null
+++ b/supabase/templates/reauthentication.html
@@ -0,0 +1,77 @@
+
+
+
+
+
+
+
+
+ |
+
+ |
+
+
+
+ 确认是你本人。
+ 为保护你的账号安全,这次敏感操作需要再次验证身份。请输入下面的验证码继续。
+ Your verification code
+
+
+ 验证码只能使用一次。如果邮件客户端支持按钮,也可以点击下方按钮完成验证。
+ 验证身份 ↗
+ |
+
+
+
+
+
+ |
+
+
+
+
diff --git a/supabase/templates/recovery.html b/supabase/templates/recovery.html
index c897f58..aa9cceb 100644
--- a/supabase/templates/recovery.html
+++ b/supabase/templates/recovery.html
@@ -1,19 +1,70 @@
-
-
+
+
+
+
+ |
+
+ |
+
+
+
+ 设置一个新的密码。
+ 我们收到了来自 {{ if or (eq .RedirectTo "https://sm.shadow.wang") (eq .RedirectTo "https://sm.shadow.wang/") (eq .Data.product_id "shadow-mate") }}影伴 Shadow Mate{{ else if or (eq .RedirectTo "https://sc.shadow.wang") (eq .RedirectTo "https://sc.shadow.wang/") (eq .RedirectTo "https://sbc.shadow.wang") (eq .Data.product_id "shadow-card") }}影匣 Shadow Card{{ else if or (eq .RedirectTo "https://ss.shadow.wang") (eq .RedirectTo "https://ss.shadow.wang/") (eq .Data.product_id "shadow-size") }}影裁 Shadow Size{{ else if or (eq .RedirectTo "https://flomo.shadow.wang") (eq .RedirectTo "https://flomo.shadow.wang/") (eq .Data.product_id "quick-flomo") }}Quick flomo{{ else }}Shadow Nexus{{ end }} 的密码重设请求。点击下面的按钮继续。
+ 重设密码 ↗
+
+ 此链接只能使用一次,并将在短时间后失效。完成后请使用新密码登录。
+ 如果您没有请求重设密码,请忽略此邮件。
+ |
+
+
+
+
+
+ |
+
+
diff --git a/tests/e2e/cloud.spec.js b/tests/e2e/cloud.spec.js
index bb0952d..4969731 100644
--- a/tests/e2e/cloud.spec.js
+++ b/tests/e2e/cloud.spec.js
@@ -687,6 +687,13 @@ test.describe("Shared password authentication", () => {
localStorage.clear();
sessionStorage.clear();
});
+ await page.route("**/functions/v1/check-auth-email", async (route) => {
+ await route.fulfill({
+ status: 200,
+ contentType: "application/json",
+ body: JSON.stringify({ registered: true }),
+ });
+ });
await page.route("**/auth/v1/recover**", async (route) => {
await route.fulfill({
status: 503,
@@ -805,11 +812,18 @@ test.describe("Shared password authentication", () => {
await expect(page.locator('#accountButton[data-state="online"]')).toBeVisible();
});
- test("requests a branded recovery email without revealing account existence", async ({ page }) => {
- let recoveryRequestUrl = "";
+ test("blocks password recovery for an unregistered email", async ({ page }) => {
+ let recoveryRequestCount = 0;
await page.addInitScript(() => { localStorage.clear(); sessionStorage.clear(); });
+ await page.route("**/functions/v1/check-auth-email", async (route) => {
+ await route.fulfill({
+ status: 200,
+ contentType: "application/json",
+ body: JSON.stringify({ registered: false }),
+ });
+ });
await page.route("**/auth/v1/recover**", async (route) => {
- recoveryRequestUrl = route.request().url();
+ recoveryRequestCount += 1;
await route.fulfill({ status: 200, contentType: "application/json", body: "{}" });
});
@@ -820,7 +834,7 @@ test.describe("Shared password authentication", () => {
await page.click("[data-forgot-password]");
await page.click('#passwordRecoveryForm button[type="submit"]');
- await expect.poll(() => recoveryRequestUrl).toContain("redirect_to=http%3A%2F%2F127.0.0.1");
- await expect(page.locator("#cloudPanel")).toContainText("如果该邮箱已注册,密码重设邮件已经发送");
+ await expect(page.locator("#syncToast")).toContainText("该邮箱尚未注册");
+ expect(recoveryRequestCount).toBe(0);
});
});
diff --git a/tests/unit/email-templates.test.js b/tests/unit/email-templates.test.js
index b0ffb9b..5a9f03a 100644
--- a/tests/unit/email-templates.test.js
+++ b/tests/unit/email-templates.test.js
@@ -3,8 +3,12 @@ import { resolve } from "node:path";
import { describe, expect, it } from "vitest";
const recovery = readFileSync(resolve(process.cwd(), "supabase/templates/recovery.html"), "utf8");
+const confirmation = readFileSync(resolve(process.cwd(), "supabase/templates/confirmation.html"), "utf8");
+const magicLink = readFileSync(resolve(process.cwd(), "supabase/templates/magic_link.html"), "utf8");
const config = readFileSync(resolve(process.cwd(), "supabase/config.toml"), "utf8");
+const templates = [confirmation, magicLink, recovery];
+
describe("multi-product password recovery email", () => {
it("maps every supported production product from RedirectTo", () => {
expect(recovery).toContain("https://sm.shadow.wang");
@@ -26,4 +30,66 @@ describe("multi-product password recovery email", () => {
expect(config).toContain("[auth.email.template.recovery]");
expect(config).toContain('content_path = "./supabase/templates/recovery.html"');
});
+
+ it("uses the Editorial Utility shell for every Supabase Auth email", () => {
+ for (const template of templates) {
+ expect(template).toContain('meta name="color-scheme" content="light dark"');
+ expect(template).toContain("prefers-color-scheme: dark");
+ expect(template).toContain("[data-ogsc]");
+ expect(template).toContain("Shadow Nexus");
+ expect(template).toContain("https://shadow.wang/zh");
+ expect(template).toContain("shadow_mate.svg");
+ expect(template).toContain("shadow_card_logo.png");
+ expect(template).toContain("shadow_size.png");
+ expect(template).toContain("shadow_portal_logo.png");
+ expect(template).toContain("https://shadow.wang/zh/products/shadow-mate");
+ expect(template).toContain("https://shadow.wang/zh/products/shadow-card");
+ expect(template).toContain("https://shadow.wang/zh/products/shadow-size");
+ expect(template).not.toContain("🔐");
+ }
+ });
+
+ it("preserves Supabase's auth variables for each email flow", () => {
+ expect(confirmation).toContain("{{ .Token }}");
+ expect(confirmation).toContain("{{ .TokenHash }}");
+ expect(magicLink).toContain("{{ .Token }}");
+ expect(magicLink).toContain("{{ .TokenHash }}");
+ expect(recovery).toContain("{{ .ConfirmationURL }}");
+ });
+
+ it("registers all three local email templates", () => {
+ expect(config).toContain("[auth.email.template.confirmation]");
+ expect(config).toContain('content_path = "./supabase/templates/confirmation.html"');
+ expect(config).toContain("[auth.email.template.magic_link]");
+ expect(config).toContain('content_path = "./supabase/templates/magic_link.html"');
+ });
+
+ it("uses the project name in every Auth email subject, including local development", () => {
+ const subjects = config
+ .split("\n")
+ .filter((line) => line.startsWith("subject ="));
+
+ expect(subjects).toHaveLength(6);
+
+ for (const subject of subjects) {
+ expect(subject).toContain("http://127.0.0.1:5173");
+ expect(subject).toContain("http://localhost:5173");
+ expect(subject).toContain("影伴 Shadow Mate");
+ expect(subject).toContain("影匣 Shadow Card");
+ expect(subject).toContain("影裁 Shadow Size");
+ expect(subject).toContain("Quick flomo");
+ expect(subject).toContain("Shadow Nexus");
+ }
+ });
+
+ it("keeps footer links on the same environment as the Auth redirect", () => {
+ for (const template of templates) {
+ expect(template).toContain('href="http://localhost:3000/zh/products/shadow-mate"');
+ expect(template).toContain('href="http://localhost:3000/zh"');
+ expect(template).toContain('href="https://shadow-portal.vercel.app/zh/products/shadow-mate"');
+ expect(template).toContain('href="https://shadow-portal.vercel.app/zh"');
+ expect(template).toContain('href="https://shadow.wang/zh/products/shadow-mate"');
+ expect(template).toContain('href="https://shadow.wang/zh"');
+ }
+ });
});