@activescott/auth-adapter-反应-路由器
反应路由器适配器(v7和v8) @activescott/auth组合框架不可知性 Auth 处理器中读取/写入标准的类 Request 财务报告和审定财务报表 Response 对象 – 精确的 React 路由器加载器和动作返回什么 .
适配器没有从 react-router; 它只会说抓取 Request编号 :Response,所以同样的构建在两个主要版本上. 这对管理员的页面来说也是一样 Link。 。 。 .
用于生产 frumblefeed.com (英语) 财务报告和审定财务报表 (原始内容存档于2018-10-21). dinkerbellbot.com。 。 。 .
安装
npm install @activescott/auth @activescott/auth-provider-email @activescott/auth-adapter-react-router
使用量
// app/lib/auth.server.ts
import { Auth } from "@activescott/auth"
import { EmailProvider } from "@activescott/auth-provider-email"
import { createAuthHandlers } from "@activescott/auth-adapter-react-router"
export const auth = new Auth({/* ...session, stores, providers... */})
export const { handleAuth, getSession, requireAuth, optionalAuth, logout } =
createAuthHandlers(auth, {
successRedirect: "/",
errorRedirect: "/login",
loginUrl: "/login",
})
errorRedirect 作为字符串附件 ?error=<code> 该路径将丢弃提交表单的查询。 如果您的登录页面在查询中保持状态—— a ?via=sms 选项卡选择,例如——在错误的标签上会回答一个失败的代码。 函数表单接收失败的请求, 所以核心的 buildReturnUrl 可以将浏览器发回它从 :
import { buildReturnUrl } from "@activescott/auth"
errorRedirect: (error, request) =>
buildReturnUrl(request, { error: error.code })
然后在 app/routes/auth.$provider.$action.tsx 处理每个提供者的 HTTP 端点 :
import { handleAuth } from "~/lib/auth.server"
import type { Route } from "./+types/auth.$provider.$action"
export const loader = ({ request }: Route.LoaderArgs) => handleAuth({ request })
export const action = ({ request }: Route.ActionArgs) => handleAuth({ request })
这个文件覆盖 /auth/<provider>/<action> 每个注册提供者,例如:. POST /auth/email/initiate (您在此登录表单), GET /auth/email/verify?... (魔法链接确认页), POST /auth/email/verify (链接救赎或代码条目), /auth/sms/..., (中文(简体) ), /auth/passkey/...等 (中文(简体) ). handleAuth 发送到右侧提供者, 运行 verify 或者说 initiate,设置/清除会话 cookie,并返回方向调整——或通过提供者制作的一页(如魔法链接确认页面).
该路线没有组件,使它成为一个资源路线,反应-路由器运行它 Origin 只检查文档和单向动作。 这么说吧 handleAuth 运行检查本身:带有 Origin 这不是这个应用程序的400, 一个请求没有 Origin 过关(如反应 -- -- 路由器所做的那样),而GET则未受影响。 登录路由是资源路由 出于同样的原因,所以 logout 接受请求并运行相同的检查 :
import { logout } from "~/lib/auth.server"
import type { Route } from "./+types/logout"
export function action({ request }: Route.ActionArgs) {
return logout(request, "/")
}
比较 request.url,因此在终止 TLS 的代理之后,应用程序仍然必须信任代理(app.set("trust proxy", 1) ),或者浏览器的每个帖子看起来都是跨出源的.
保护任何装入器 requireAuth(request)数字 :
export async function loader({ request }: Route.LoaderArgs) {
const user = await requireAuth(request) // redirects to /login if no session
return { user }
}
按要求进行的检查
Apps通常有一个规则,规定除了"饼干核实"之外,谁可以举行一个会话:只批准账户,而不是你今天上午所封的账户. onSessionVerified 每个会话运行 getSession, (中文(简体) ), requireAuth 财务报告和审定财务报表 optionalAuth 核实, 之后 mapUser返回或投掷 Response 请求将随它而反弹。不返回任何结果,会话将持续到您的装载器上:
export const { requireAuth, optionalAuth, clearSessionCookie, logout } =
createAuthHandlers<AppUser>(auth, {
mapUser,
onSessionVerified: ({ user }) => {
if (user.status === "BLOCKED") {
return redirect("/login", {
headers: { "Set-Cookie": auth.destroySessionCookie() },
})
}
if (user.status === "PENDING") return redirect("/waitlist")
},
})
代替包装软件 requireAuth 重新读取用户并按他们找到的东西行事,因为它运行在 optionalAuth 财务报告和审定财务报表 getSession 也可以免费获得同样的规则。 设定 session: { cacheTtlMs: 0 } 编辑 Auth 与它相接:钩子 user 不管怎么样 verifySession 返回,默认时间为两分钟.
它不运行 renewSessionCookie 或者说 refreshSessionCookie,它重新为会话发行饼干,一个已经接受的加载器.
clearSessionCookie() 返回时 Set-Cookie 过期会话 cookie 的值,因为您需要标题而不是方向 logout 把它包起来.
滚动会议
届会结束 maxAge 在发布后,无论访问者是否仍在使用该应用程序。 设定 session.renewAfter 打电话 renewSessionCookie 由您的根加载器在饼干过后再发行
export const { renewSessionCookie, ...handlers } = createAuthHandlers(auth, {
session: { renewAfter: "7d" }, // well inside the "30d" maxAge
})
// app/root.tsx
export async function loader({ request }: Route.LoaderArgs) {
const user = await optionalAuth(request)
const loaderData = { user }
const cookie = user && (await renewSessionCookie(request, user))
return cookie
? data(loaderData, { headers: { "Set-Cookie": cookie } })
: loaderData
}
renewSessionCookie 当会话仍然新鲜时返回为无效,所以常见的大小写没有标题。 如果你不配置,它就会扔出去 session.renewAfter。要更新会话,是因为用户自己的数据发生了变化(如一个新的手柄),而不是因为它是旧的,使用 refreshSessionCookie,无条件重发.
API 密码
| 导出 | 目的 |
|---|---|
createAuthHandlers | 回返 { handleAuth, getSession, requireAuth, optionalAuth, renewSessionCookie, refreshSessionCookie, clearSessionCookie, logout }。 。 。 . |
createAuthPageLoaders | 回返 { signInLoader, profileAuthLoader, listSignInMethods }。见 签名和简介页面。 。 。 . |
钩子(来自 ./client(中文(简体) ) | useTurnstile, (中文(简体) ), usePasskeySignIn, (中文(简体) ), useRegisterPasskey, (中文(简体) ), useOtpAutoSubmit, (中文(简体) ), usePreservedInput。 。 。 . |
createAdminHandlers (从) ./admin(中文(简体) ) | 回返 { requireAdmin, adminUsersLoader, adminConfigLoader }。 。 。 . |
AdminUsersPage, (中文(简体) ), AdminConfigPage | 管理页面,从 ./admin。 。 。 . |
ProfilePage (从) ./profile(中文(简体) ) | 简介页,以及 AccountSummary, (中文(简体) ), SignInMethods, (中文(简体) ), Passkeys 分别。 见 简介页。 。 。 . |
登录页面不需要自己的动作:将电子邮件表直接寄到 /auth/email/initiate (提供方以 ?sent=1)和代码表 /auth/email/verify。 。 。 .
createAuthHandlers<TUser> 在应用程序的用户类型上通用。 通过 a mapUser 键入 requireAuth<TUser> 编号 : optionalAuth<TUser> 而不是赤裸的 AuthUser。 。 。 .
签名和简介页面
适配器将一个登录页和配置页的登录方法部分的逻辑传递给您,然后将标记留给您. 加载器运行在服务器上,并读取供应商在回程时放入查询字符串的内容;钩子运行在浏览器中. 简介页的标记船也作为组件:见 简介页。 。 。 .
// app/lib/auth.server.ts
import { createAuthPageLoaders } from "@activescott/auth-adapter-react-router"
import { listPasskeys } from "@activescott/auth-provider-passkey" // only if you use passkeys
export const { signInLoader, profileAuthLoader } = createAuthPageLoaders(auth, {
turnstileSiteKey: process.env.TURNSTILE_SITE_KEY,
listPasskeys,
errorMessages: { blocked: "Your account has been blocked." },
})
signInLoader(request) 返回时 { via, sent, error, errorCode, redirectTo, formToken, turnstileSiteKey }: 哪些提供者的表格可以显示, 信件是否出出, 信件用于 ?error=,同一来源 redirectTo 或无效,签名的表格表示滥用检查的预期值 FORM_TOKEN_FIELD。 。 。 . profileAuthLoader(userId, request) 返回时 { identities, passkeys, linkFlow, removal }时 linkFlow 是添加- a- sign- in- 方法流: 打开的窗体(?add=, (中文(简体) ), sent, (中文(简体) ), linked, (中文(简体) ), merged,以及 conflict,其 link-merge 赎回合并票 IDENTITY_CONFLICT。它从 ?provider=,则 ?add=,所以将 ?add= 在验证步骤中 redirectTo。 。 。 . removal 是删除按钮的结果:提供者(?removed=,或 ?provider= 与 ?removeError=),是否通过,和拒绝的信息. errorMessages 添加库所不知道的代码,如起动门's,并重写任何它所做的;两个加载器也接受每个调用.
钩子在那边 ./client 子路径和需要 React 而非 React 路由器 :
| 钩子 | 对于 |
|---|---|
useTurnstile(siteKey) | 将旋转部件渲染成 containerRef 和报告 ready; 禁用提交到那时。 报告 "failed" 20秒后,没有标志. |
usePasskeySignIn({ client, redirectTo }) | signIn 密码按钮; 全页负载 redirectTo 在成功方面. autofill: true 添加有条件的UI. |
useRegisterPasskey({ client, onRegistered }) | register 用于“添加密码”;通过 useRevalidator().revalidate 作为 onRegistered。 。 。 . |
useOtpAutoSubmit(length) | inputProps 用于在最后一个数字降落时提交表单的代码字段(包含自动填充属性). |
usePreservedInput(key) | 在会话Storage中,保持打入的地址或号码,通过认证路线进行往返. |
钥匙钩拿 createPasskeyClient() 从 @activescott/auth-provider-passkey/browser 作为 client,因此没有密码的应用程序从不安装WebAuthn库. 上山时,他们叫客户的 prepareSignIn 或者说 prepareRegistrationSafari拒绝在等待取回后开始的密码仪式。 一个Turnstile标志比它看起来更重要:一个没有一张的表格被屏蔽. 核心发布方式 BOT_CHECK_INCOMPLETE 以可重试错误回答; 年龄较长 @activescott/auth 仍然回答它像一个成功的发送, 和所有其他bot检查拒绝保持不透明 无论是哪种方式。 门提交于 ready。 。 。 .
examples/react-router 使用其中的全部: app/routes/login.tsx 财务报告和审定财务报表 app/routes/dashboard.tsx。 。 。 .
简介页
每个应用程序都有一个简介页,所以适配器在 ./profile 子路径. ProfilePage 制作账户摘要、登录方法和密码 profileAuthLoader 返回.
// app/routes/profile.tsx
import { Link, useRevalidator } from "react-router"
import { ProfilePage } from "@activescott/auth-adapter-react-router/profile"
import { profileAuthLoader, requireAuth } from "~/lib/auth.server"
import { passkeys } from "~/lib/passkeys"
export async function loader({ request }: Route.LoaderArgs) {
const user = await requireAuth(request)
return {
email: user.email,
memberSince: user.createdAt.toISOString(),
...(await profileAuthLoader(user.id, request)),
}
}
export default function Profile({ loaderData }: Route.ComponentProps) {
const revalidator = useRevalidator()
return (
<ProfilePage
{...loaderData}
addMethods={[
{ provider: "email" },
{ provider: "sms", callingCode: "+1" },
]}
passkeyClient={passkeys}
onPasskeyRegistered={revalidator.revalidate}
linkComponent={Link}
/>
)
}
addMethods 是页面提供要添加的内容, 按链接显示的顺序; 它默认不提供任何东西, 除非条目设定, 并且将账户已经签名的提供者丢入 allowMultiple。 。 。 . callingCode 使球场采用国家编号并提交完整的E.164 1. allowMerge 转动 IDENTITY_CONFLICT 输入一个合并两个账户的提议,而不是一个错误,所以它只设定在您 UserStore.onMerge 合并. allowRemove 在每个登录方式和密码上设置一个删除按钮;它会张贴到核心的 /auth/{provider}/unlink (单位:千美元)@activescott/auth 第5.9.0或以后),拒绝用户与用户签入的方法和最后一个可以收回账户的方法。 过 removal 从加载器上,所以页面报告结果.
带有自己的部分的申请书使区块按任何顺序和区块之间的任何顺序直接:
<AccountSummary email={user.email} entries={[{ label: "Handle", value: user.handle }]} />
<HandleSection handle={user.handle} />
<SignInMethods identities={identities} linkFlow={linkFlow} addMethods={addMethods} linkComponent={Link} />
<Passkeys passkeys={passkeys} client={passkeyClient} onRegistered={revalidator.revalidate} />
样式
每个街区 classNames,每个槽一个类 : card, (中文(简体) ), cardBody, (中文(简体) ), cardTitle, (中文(简体) ), table, (中文(简体) ), th, (中文(简体) ), td, (中文(简体) ), field, (中文(简体) ), label, (中文(简体) ), input, (中文(简体) ), submitButton, (中文(简体) ), success 和其余部分 ProfileClassNames。一个槽,你的名字会得到你的班级,没有内置的样式,因为内置的样式比它原本会与之竞争的级别要高;槽,你不会说保持平地内置的外观 includeDefaultStyles={false} 到处都是.
这意味着"半名地图"是"半作"一页",有34个档. 在"靴子"上,通过地图 包船代替:
import {
BOOTSTRAP_PROFILE_CLASS_NAMES,
ProfilePage,
} from "@activescott/auth-adapter-react-router/profile"
;<ProfilePage {...loaderData} classNames={BOOTSTRAP_PROFILE_CLASS_NAMES} />
张开以更改一个位置并保留其余部分: { ...BOOTSTRAP_PROFILE_CLASS_NAMES, title: "fw-bold text-primary mb-4" }输入 Required<ProfileClassNames>,所以一个后加的插槽必须在软件包构建之前给出一个类.
对于另一个框架,你自己写地图. 空字符串是您拥有的插槽, 但不想要上课, 即Bootstrap's的方式 .table 到达自有的单元格:它不添加属性就放弃了内置样式.
通知在加价中而不是在 classNames,因为一个班级不能增加一个班级:失败或谨慎带有 role="alert",确认 role="status",关于您输入的地址或代码的信息将绑定在该字段上 aria-invalid 财务报告和审定财务报表 aria-describedby。 。 。 .
区块导入回放,但没有从 react-router因此,链接通过可选 linkComponent;没有它,它们就是平地锚, 导航整个文件。 表格是简单的 <form> 元素在目的上: 添加- a- sign- in- method 流的每个步骤都是一个文件 POST , 认证路由用重定向回答, 也就是让流的状态活在 URL 中.
行政仪表板
仅读用户页面和配置页面,位于 ./admin 子路径所以不使用它们的应用程序从不装入反应 :
// app/lib/auth.server.ts
import { createAdminHandlers } from "@activescott/auth-adapter-react-router/admin"
export const { adminUsersLoader, adminConfigLoader } = createAdminHandlers(
auth,
{
requireAuth,
admins: process.env.AUTH_ADMIN_IDENTIFIERS,
},
)
// app/routes/admin.users.tsx
import { Link } from "react-router"
import { AdminUsersPage } from "@activescott/auth-adapter-react-router/admin"
export const loader = ({ request }: Route.LoaderArgs) =>
adminUsersLoader({ request })
export default function AdminUsers({ loaderData }: Route.ComponentProps) {
return <AdminUsersPage data={loaderData} linkComponent={Link} />
}
用户页面需要 UserStore.listUsers,该选项是可选的,由应用程序执行;将您想要的任何内容作为额外的一列输入每个用户的 metadata 并用 metadataColumns。访问是电子邮件地址和电话号码的允许列表(AUTH_ADMIN_IDENTIFIERS (默认情况下),当未设置时,不会接纳任何人;非管理员得到404分.
完全通过 : 行政仪表板。 。 。 .
E2e 代码回读
createCaptureReadbackLoader时 ./testing subpath,是一个资源路由加载器,它把最后缴获的登录电子邮件或短消息交给您的e2e测试,这样他们就可以在没有收件箱或手机的情况下签名. 把运输工具包起来 CaptureEmailTransport 编号 : CaptureSmsTransport (来自每个供应商的) /testing 子路径)在同一试验模式旗下:
// app/routes/e2e.otp-code.tsx
import { createCaptureReadbackLoader } from "@activescott/auth-adapter-react-router/testing"
export const loader = createCaptureReadbackLoader({
transports: { email: captureEmailTransport, sms: captureSmsTransport },
enabled: process.env.E2E_TEST_MODE === "true",
secret: process.env.E2E_MAGIC_LINK_SECRET,
})
测试呼叫 GET /e2e/otp-code?email=... 或者说 ?phone=... 与秘密在一个 x-e2e-secret 头曰. 捕捉到的代码是现场记录: 路线回答一个赤裸的404,除非 enabled 没错 true 和密制的火柴 enabled 但没有秘密。 从不设定生产旗.
文档实例( E)
Full Docs 和一个带有 Playwright 测试的可运行的 React Router 框架- mode 实例在 monrepo 中直播 :
→ https://github.com/活塞斯科特/奥斯** 联合国
许可证
麻省理工学院