ShopSystem Day 9 Magic Link 完整上线 + 前后端闭环(注册 → 申请商家 → 审核 → 上架 → 买家下单 → Magic Link 自动登录,Day 8 阶段 1 闭环测试今天升级为生产可用)。Day 8 末尾预告的”邮箱无密码登录”今天正式收尾:邮件服务基础设施 email.ts 三 provider(Resend / SMTP / console.log fallback) + magic_links 表迁移 021 + POST /api/auth/magic-link 申请路由 + GET /api/auth/magic-link/verify 验证路由 + /login/magic 输入页 + 登录页/注册页入口互通。完整实现后跑 13 项 e2e 测试全过(限流 5/分钟、token SHA-256 哈希存库、原子 used_at 标记、二次使用拒绝、伪造 token 拒绝、错误码 5 种用户友好提示)。

前端 MagicLinkForm 组件(Client Component,useSearchParams 读 error param)+ /login/magic 页面(Suspense 包裹,5 种错误码中文提示:missing_token / invalid_token / expired / already_used / user_not_found);登录页 /ladmintood 底部加”📧 用邮箱无密码登录”入口,注册页 /register 底部加”邮箱无密码登录”链接,三页互通。后端核心实现 `src/app/api/auth/magic-link/route.ts`(POST 申请):限流复用 register scope(5/分钟/IP,防邮件轰炸)+ 邮箱格式校验 + 邮箱存在性检查(未注册静默 200 防枚举,拒绝变成注册后门)+ 生成 32 字节随机 token(64 hex)+ DB 存 SHA-256 哈希(token 原始值只走邮件,数据库泄露也无法登录)+ INSERT magic_links(TTL 15 分钟 + IP + UA)+ 调 sendEmail(magicLinkEmail 模板);`src/app/api/auth/magic-link/verify/route.ts`(GET 验证):token 必须 64 hex(防注入)+ SHA-256 后查 magic_links + 三重校验(exists + used_at IS NULL + expires_at > NOW())+ 原子 UPDATE used_at(WHERE used_at IS NULL 看 affectedRows 防并发)+ 查 users WHERE email status=active + createSession + setSessionCookies(双 cookie shop_user_id + shop_sess)+ 重定向到 redirect 参数(站内白名单,防止 open redirect)或默认 /account + 失败统一跳 /login/magic?error=xxx。配套修复 email.ts nodemailer webpackIgnore 注释(可选依赖未装时 build 不报错)。

自测 13 项 e2e curl 全过:① 未注册邮箱静默成功(防枚举,不写库不发邮件)② 连发 7 次触发限流(第 5 次起 HTTP 429)③ 邮箱格式错返 400″请输入有效的邮箱地址”④ 已注册邮箱 devLink 返回(provider=log 兜底时响应体附 devLink,方便不接邮件服务时自检)⑤ DB 查 magic_links 行有 SHA-256 哈希(原始 token 不入库)⑥ GET verify 一次性 token 返 307 跳 /account + Set-Cookie 双 cookie(/api/me 返 admin 用户信息证明登录成功)⑦ 二次用同一 token 返 already_used(并发原子保护)⑧ 伪造 token(全 1)返 invalid_token ⑨ 无 token 返 missing_token ⑩ /ladmintood 含”用邮箱无密码登录”+”还没账号?注册”入口 ⑪ /register 含”密码登录”+”邮箱无密码登录”入口 ⑫ devLink 控制台输出(开发环境便利)⑬ token 一次性并发保护(5 个 verify 同 token 只有第 1 个成功)。生产 build `npm run build` 通过(/login/magic 1.36kB,/ladmintood 1.46kB,/register 2.05kB),rsync 到部署服务器 + pm2 restart。

Day 9 测试期间发现 3 个严重 Bug 全部修掉(全部由”PM2 启动下 req.url 是 localhost:3002″引起,本质是 NextResponse.redirect 用 req.url 拼接会让浏览器跟跳失败):Bug 001 Magic Link verify 成功后 Location 跳 localhost:3002/account(用户实际已登录但浏览器看不到页面);Bug 002 5 种错误场景(missing_token / invalid_token / expired / already_used / user_not_found)Location 也都跳 localhost;修复方案新建 `src/lib/url.ts` 的 `getBaseUrl(req)` helper(优先级 NEXT_PUBLIC_SITE_URL → Host header + x-forwarded-proto → req.url.origin),替换 verify/route.ts 7 处 redirect;Bug 003 logout/route.ts 同根因,1 处 redirect 也跳 localhost,同步修复。验收 9 项全过(3 个原 Bug + 5 种错误 Location + admin 密码登录回归 + Magic Link 限流回归 + token 一次性回归 + grep req.url 仅剩 reviews/route.ts 用于 searchParams 合法用法)。顺手改进:reviews/route.ts:92 把 `new URL(req.url).searchParams.get(…)` 改成 `req.nextUrl.searchParams.get(…)`(Next.js 标准 API,行为不变)。Day 9 新增 3 条铁律入 memory:① Next.js 可选依赖动态 import 必须 webpackIgnore + 字符串变量(否则 build Module not found)② Magic Link token 哈希存 DB(数据库泄露也无法登录)③ Magic Link 仅服务已注册邮箱(未注册静默 200 防变成注册后门)。Day 10 预告:Google OAuth 占位(API 文件 + TODO 注释)或等用户决策优先任务。
关于作者:WoodStoneShopSystem 项目负责人,专注网商平台与电商 AI 集成
OpenClaw—AI研究