ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Wasp 邮箱认证自定义 UI 实战:用 `wasp/client/auth` 从零构建登录、注册、邮箱验证与密码重置页面

Wasp 邮箱认证自定义 UI 实战:用 `wasp/client/auth` 从零构建登录、注册、邮箱验证与密码重置页面 Wasp 邮箱认证自定义 UI 实战用wasp/client/auth从零构建登录、注册、邮箱验证与密码重置页面【免费下载链接】waspThe batteries-included full-stack framework for the AI era. Develop JS/TS web apps (React, Node.js, and Prisma) using declarative code that abstracts away complex full-stack features like auth, background jobs, RPC, email sending, end-to-end type safety, single-command deployment, and more.项目地址: https://gitcode.com/GitHub_Trending/wa/wasp本篇技术指南聚焦 Wasp 全栈框架中邮箱Email认证的自定义 UI 方案当你不再满足于框架预置的 Auth UI希望完全掌控登录、注册、邮箱验证、忘记密码等页面的外观与交互时可以直接在自己的 React 代码中调用wasp/client/auth暴露的login、signup、verifyEmail、requestPasswordReset、resetPassword五个认证动作像 Auth UI 内部所做的那样手动驱动整套认证流程。读完本文你将掌握这五个动作的完整签名、token 在 URL 中的传递约定、自定义认证页面的前后端配置以及如何在此基础上扩展注册字段与邮件内容。为什么需要自定义认证 UIWasp 为邮箱认证提供了开箱即用的 Auth UI 组件LoginForm、SignupForm、VerifyEmailForm、ForgotPasswordForm、ResetPasswordForm几行代码就能铺好完整的登录/注册页面。但真实业务中经常出现以下诉求登录表单需要与产品视觉体系完全一致而不是受限于预制组件的样式注册流程需要额外的业务字段如地址、昵称或自定义校验提示邮箱验证、密码重置页面需要特殊的落地文案与交互需要在登录/注册动作前后插入自定义逻辑埋点、跳转、二次确认。此时可以放弃预制组件改为在客户端代码中自行构建 UI并直接调用 Wasp 生成并暴露的认证动作。Wasp 的服务端实现、邮件模板与安全逻辑验证邮箱后才能登录、密码重置、防邮箱泄露等完全保留你只需要负责“壳”。提示在使用邮箱认证时Wasp 默认有一套邮箱与密码校验规则自定义 UI 同样遵循这些规则详见 认证总览中的默认校验规则。前置条件在main.wasp中启用邮箱认证自定义 UI 不改变服务端配置方式。要在项目中使用邮箱认证并让上述动作可用需要先在main.wasp中完成如下声明以 0.19 版本文档 email.md 中的配置为准app myApp { wasp: { version: ^0.19.0 }, title: My App, auth: { // 1. 指定用户实体需在 schema.prisma 中定义 User userEntity: User, methods: { // 2. 启用邮箱认证 email: { // 3. 指定发件人信息 fromField: { name: My App Postman, email: helloitsme.com }, // 4. 指定邮箱验证与密码重置要跳转的客户端路由 emailVerification: { clientRoute: EmailVerificationRoute, }, passwordReset: { clientRoute: PasswordResetRoute, }, }, }, onAuthFailedRedirectTo: /login, onAuthSucceededRedirectTo: / }, // 5. 配置邮件发送器Dummy 只把邮件打到控制台方便本地开发 emailSender: { provider: Dummy, } }其中clientRoute字段指向你在main.wasp中定义的路由这些路由将承载我们稍后自建的认证页面route LoginRoute { path: /login, to: LoginPage } page LoginPage { component: import { Login } from src/pages/auth } route SignupRoute { path: /signup, to: SignupPage } page SignupPage { component: import { Signup } from src/pages/auth } route RequestPasswordResetRoute { path: /request-password-reset, to: RequestPasswordResetPage } page RequestPasswordResetPage { component: import { RequestPasswordReset } from src/pages/auth, } route PasswordResetRoute { path: /password-reset, to: PasswordResetPage } page PasswordResetPage { component: import { PasswordReset } from src/pages/auth, } route EmailVerificationRoute { path: /email-verification, to: EmailVerificationPage } page EmailVerificationPage { component: import { EmailVerification } from src/pages/auth, }完成上述声明后执行wasp db migrate-dev再运行wasp start即可在本地跑通整套邮箱认证流程。开发期间若不想每次注册都走邮箱验证可在.env.server中设置SKIP_EMAIL_VERIFICATION_IN_DEVtrue跳过验证步骤。完整示例自建五个认证页面下面的代码覆盖登录、注册、邮箱验证、请求重置密码、重置密码五个完整流程是官方文档提供的自建 UI 起点。所有认证动作均从wasp/client/auth导入你可以自由调整每个组件的样式与行为但务必保持动作调用参数的正确性。JavaScript 版本src/pages/auth.jsximport { login, requestPasswordReset, resetPassword, signup, verifyEmail, } from wasp/client/auth import { useState } from react import { useNavigate } from react-router-dom // 用户登录页面 export function Login() { const [email, setEmail] useState() const [password, setPassword] useState() const [error, setError] useState(null) const navigate useNavigate() async function handleSubmit(event) { event.preventDefault() setError(null) try { await login({ email, password }) navigate(/) } catch (error) { setError(error) } } return ( form onSubmit{handleSubmit} {error pError: {error.message}/p} input typeemail value{email} onChange{(e) setEmail(e.target.value)} placeholderEmail / input typepassword value{password} onChange{(e) setPassword(e.target.value)} placeholderPassword / button typesubmitLog In/button /form ) } // 用户注册页面 export function Signup() { const [email, setEmail] useState() const [password, setPassword] useState() const [error, setError] useState(null) const [needsConfirmation, setNeedsConfirmation] useState(false) async function handleSubmit(event) { event.preventDefault() setError(null) try { await signup({ email, password }) setNeedsConfirmation(true) } catch (error) { console.error(Error during signup:, error) setError(error) } } if (needsConfirmation) { return ( p Check your email for the confirmation link. If you dont see it, check spam/junk folder. /p ) } return ( form onSubmit{handleSubmit} {error pError: {error.message}/p} input typeemail value{email} onChange{(e) setEmail(e.target.value)} placeholderEmail / input typepassword value{password} onChange{(e) setPassword(e.target.value)} placeholderPassword / button typesubmitSign Up/button /form ) } // 用户点击邮件中的验证链接后展示的页面 export function EmailVerification() { const [error, setError] useState(null) const navigate useNavigate() async function handleClick() { setError(null) try { // token 以 URL 查询参数的形式传入 const token new URLSearchParams(window.location.search).get(token) if (!token) throw new Error(Token not found in URL) await verifyEmail({ token }) navigate(/) } catch (error) { console.error(Error during email verification:, error) setError(error) } } return ( {error pError: {error.message}/p} button onClick{handleClick}Verify email/button / ) } // 用户请求重置密码页面 export function RequestPasswordReset() { const [email, setEmail] useState() const [error, setError] useState(null) const [needsConfirmation, setNeedsConfirmation] useState(false) async function handleSubmit(event) { event.preventDefault() setError(null) try { await requestPasswordReset({ email }) setNeedsConfirmation(true) } catch (error) { console.error(Error during requesting reset:, error) setError(error) } } if (needsConfirmation) { return ( p Check your email for the confirmation link. If you dont see it, check spam/junk folder. /p ) } return ( form onSubmit{handleSubmit} {error pError: {error.message}/p} input typeemail value{email} onChange{(e) setEmail(e.target.value)} placeholderEmail / button typesubmitSend password reset/button /form ) } // 用户点击邮件中的重置链接后展示的页面 export function PasswordReset() { const [error, setError] useState(null) const [newPassword, setNewPassword] useState() const navigate useNavigate() async function handleSubmit(event) { event.preventDefault() setError(null) try { // token 以 URL 查询参数的形式传入 const token new URLSearchParams(window.location.search).get(token) if (!token) throw new Error(Token not found in URL) await resetPassword({ token, password: newPassword }) navigate(/) } catch (error) { console.error(Error during password reset:, error) setError(error) } } return ( form onSubmit{handleSubmit} {error pError: {error.message}/p} input typepassword autoCompletenew-password value{newPassword} onChange{(e) setNewPassword(e.target.value)} placeholderNew password / button typesubmitReset password/button /form ) }TypeScript 版本src/pages/auth.tsximport { login, requestPasswordReset, resetPassword, signup, verifyEmail, } from wasp/client/auth import { useState } from react import { useNavigate } from react-router-dom // 用户登录页面 export function Login() { const [email, setEmail] useState() const [password, setPassword] useState() const [error, setError] useStateError | null(null) const navigate useNavigate() async function handleSubmit(event: React.FormEventHTMLFormElement) { event.preventDefault() setError(null) try { await login({ email, password }) navigate(/) } catch (error: unknown) { setError(error as Error) } } return ( form onSubmit{handleSubmit} {error pError: {error.message}/p} input typeemail value{email} onChange{(e) setEmail(e.target.value)} placeholderEmail / input typepassword value{password} onChange{(e) setPassword(e.target.value)} placeholderPassword / button typesubmitLog In/button /form ) } // 用户注册页面 export function Signup() { const [email, setEmail] useState() const [password, setPassword] useState() const [error, setError] useStateError | null(null) const [needsConfirmation, setNeedsConfirmation] useState(false) async function handleSubmit(event: React.FormEventHTMLFormElement) { event.preventDefault() setError(null) try { await signup({ email, password }) setNeedsConfirmation(true) } catch (error: unknown) { console.error(Error during signup:, error) setError(error as Error) } } if (needsConfirmation) { return ( p Check your email for the confirmation link. If you dont see it, check spam/junk folder. /p ) } return ( form onSubmit{handleSubmit} {error pError: {error.message}/p} input typeemail value{email} onChange{(e) setEmail(e.target.value)} placeholderEmail / input typepassword value{password} onChange{(e) setPassword(e.target.value)} placeholderPassword / button typesubmitSign Up/button /form ) } // 用户点击邮件中的验证链接后展示的页面 export function EmailVerification() { const [error, setError] useStateError | null(null) const navigate useNavigate() async function handleClick() { setError(null) try { // token 以 URL 查询参数的形式传入 const token new URLSearchParams(window.location.search).get(token) if (!token) throw new Error(Token not found in URL) await verifyEmail({ token }) navigate(/) } catch (error: unknown) { console.error(Error during email verification:, error) setError(error as Error) } } return ( {error pError: {error.message}/p} button onClick{handleClick}Verify email/button / ) } // 用户请求重置密码页面 export function RequestPasswordReset() { const [email, setEmail] useState() const [error, setError] useStateError | null(null) const [needsConfirmation, setNeedsConfirmation] useState(false) async function handleSubmit(event: React.FormEventHTMLFormElement) { event.preventDefault() setError(null) try { await requestPasswordReset({ email }) setNeedsConfirmation(true) } catch (error: unknown) { console.error(Error during requesting reset:, error) setError(error as Error) } } if (needsConfirmation) { return ( p Check your email for the confirmation link. If you dont see it, check spam/junk folder. /p ) } return ( form onSubmit{handleSubmit} {error pError: {error.message}/p} input typeemail value{email} onChange{(e) setEmail(e.target.value)} placeholderEmail / button typesubmitSend password reset/button /form ) } // 用户点击邮件中的重置链接后展示的页面 export function PasswordReset() { const [error, setError] useStateError | null(null) const [newPassword, setNewPassword] useState() const navigate useNavigate() async function handleSubmit(event: React.FormEventHTMLFormElement) { event.preventDefault() setError(null) try { // token 以 URL 查询参数的形式传入 const token new URLSearchParams(window.location.search).get(token) if (!token) throw new Error(Token not found in URL) await resetPassword({ token, password: newPassword }) navigate(/) } catch (error: unknown) { console.error(Error during password reset:, error) setError(error as Error) } } return ( form onSubmit{handleSubmit} {error pError: {error.message}/p} input typepassword autoCompletenew-password value{newPassword} onChange{(e) setNewPassword(e.target.value)} placeholderNew password / button typesubmitReset password/button /form ) }五个认证动作逐一拆解从wasp/client/auth可以导入五个认证动作它们的共同点是都返回 Promise失败时抛出异常示例代码统一用 try/catch 捕获并渲染error.message成功后的页面跳转navigate(...)必须由你自行处理。login({ email, password })登录动作。唯一参数data对象包含两个必填字段字段类型必填说明emailstring是用户注册时使用的邮箱passwordstring是用户密码成功后 Wasp 会建立会话此时需要主动重定向示例中跳转到主页/。注意如果邮箱尚未验证登录会被拒绝这与 Wasp“默认要求邮箱验证后才能登录”的策略一致。signup({ email, password })注册动作同时触发邮箱验证流程。参数同样为{ email, password }两个必填字段。与直觉不同注册成功后用户并不会处于登录状态因为账户还未验证仍需用户点击验证邮件中的链接。示例代码在成功后切换为“请查收邮件”的确认提示needsConfirmation状态。Wasp 默认只保存email和password两个字段。若需在注册时收集额外字段如昵称、地址需要结合userSignupFields自定义注册流程详见 认证总览的注册流程定制。verifyEmail({ token })邮箱验证动作。参数data包含一个必填字段token: string。这个 token 是注册时由服务端生成的并会被放入验证邮件链接的 URL 查询参数中参数名固定为token因此客户端需要从window.location.search解析出来再传给动作。验证成功后账户被激活示例代码随后跳转回主页。requestPasswordReset({ email })请求密码重置动作。它只负责“发送重置邮件”并不会立即重置密码。参数为必填的email: string。发送成功后同样建议展示“请查收邮件”之类的确认文案。出于防泄露考虑即使邮箱不存在服务端也会返回与成功一致的响应。resetPassword({ token, password })确认密码重置并设置新密码。参数包含两个必填字段字段类型必填说明tokenstring是请求重置时生成、放在邮件链接 URL 查询参数token中的令牌passwordstring是用户设置的新密码成功后应重定向到登录页让用户用新密码登录。从仓库示例看两种实现路线官方仓库提供了两套风格迥异的认证页面实现可作为自建 UI 的参考样板路线一基于预制组件的“轻定制”。仓库 examples/kitchen-sink/src/features/auth/pages/Login.tsx 与 Signup.tsx 仍然使用LoginForm、SignupForm等组件但通过customisationProps注入自定义 props并在表单下方用Link自由编排“去注册/找回密码”的入口注册表单甚至通过additionalFields追加了地址字段与自定义校验提示展示了“不重写全部代码”的折中方案。路线二完全手动调用动作。仓库 examples/kitchen-sink/src/features/auth/pages/ManualSignupPage.tsx 展示了不使用任何预制表单组件的写法用react-hook-form接管表单状态wasp/client/auth只导入signup动作与FormInput、FormLabel、FormError、FormItemGroup等底层表单构件提交时直接await signup(data)并按返回结果区分成功/失败提示const onSubmit handleSubmit(async (data) { try { const result await signup(data); if (result.success) { setMessage({ type: success, text: Youve signed up successfully! Check your email for the confirmation link., }); } else { setMessage({ type: error, text: Signup failed. Please try again. }); } } catch (error) { const { message, data } error.data; setMessage({ type: error, text: ${message}: ${data.message} }); } });同样的模式也体现在 examples/kitchen-sink/src/features/auth/pages/EmailVerification.tsx、RequestPasswordReset.tsx 与 PasswordReset.tsx 中这几个页面仍以VerifyEmailForm、ForgotPasswordForm、ResetPasswordForm为骨架但配合自建的布局容器与“去登录”链接完成了对默认体验的定制。此外examples/tutorials/TodoApp/src/LoginPage.jsx 给出了最精简的预制组件接入方式适合快速对比理解。进阶让自建 UI 与框架能力完全对齐自建 UI 只是替换了“表现层”Wasp 服务端的认证能力与安全默认行为全部保留理解这些行为有助于你写出体验一致的页面注册限流服务端对每个邮箱地址的注册请求限流为 1 次/分钟防止垃圾注册防邮箱泄露用已存在且已验证的邮箱注册时服务端会“假装”创建成功而不是提示账户已存在请求未知邮箱重置密码时也返回同样的成功响应未验证邮箱可重复注册若邮箱存在但未验证允许再次注册防止恶意用户用他人邮箱抢占注册密码校验默认密码强度规则由认证总览统一定义可覆盖参见 认证总览。定制注册字段若signup()需要携带额外字段先在main.wasp中声明auth.methods.email.userSignupFields指向src/auth下的导出再在src/auth/signup.{js,ts}中用defineUserSignupFields定义每个字段的取值函数详见 认证总览的注册字段定制。定制邮件内容验证邮件与重置邮件的文案/HTML 可通过emailVerification.getEmailContentFn与passwordReset.getEmailContentFn自定义。两个函数分别接收{ verificationLink }与{ passwordResetLink }返回{ subject, text, html }完整的配置与代码示例见 email.md 的邮箱验证流程。小结自建邮箱认证 UI 的关键是把wasp/client/auth的五个动作当作“提交按钮背后的黑盒”你负责表单、校验提示、错误展示与跳转Wasp 负责密码散列、token 生成、邮件发送与账户激活等安全细节。结合本文的完整示例代码、API 参数表与仓库中的两套参考实现你可以快速打造一套既贴合产品设计、又不丢失框架安全能力的认证体验。【免费下载链接】waspThe batteries-included full-stack framework for the AI era. Develop JS/TS web apps (React, Node.js, and Prisma) using declarative code that abstracts away complex full-stack features like auth, background jobs, RPC, email sending, end-to-end type safety, single-command deployment, and more.项目地址: https://gitcode.com/GitHub_Trending/wa/wasp创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表