Skip to content

密码重置(未登录恢复) ​

auth.reset 命名空间提供未登录用户的三段式密码恢复:

  1. auth.reset.sendCode({ email }) — 创建 ory recovery flow + 发邮件
  2. auth.reset.verifyCode({ code }) — 提交验证码把 recovery flow 推到 privileged session(recovery code 一次性、成功后作废)
  3. auth.reset.submitNewPassword({ password }) — 在已提权的 session 内走 settings flow 改密码

已登录用户改密码请用 auth.settings

本 namespace 设计给「忘记密码 / 未登录」场景。已登录用户走 auth.reset 会导致原 session 失效、用户被登出。已登录场景请用 auth.settings 已登录改密码(3 段流程、session 保持)。

底层依次走 ory createBrowserRecoveryFlow → updateRecoveryFlow(method: 'code') → createBrowserSettingsFlow → updateSettingsFlow(method: 'password'),flow 链本身不变,只是拆到了 verifyCode(前两步 recovery 提权)与 submitNewPassword(后两步 settings 改密)两个方法里,提权状态靠浏览器 session cookie 携带。SDK 内部维护「当前活跃的 recovery 会话」串起前两步。

为什么拆三段

原来 code + password 一次性提交,验证码错和密码错(如「新密码不能与旧密码相同」)都从同一次调用抛出、都是 FLOW 类型,UI 无法区分,密码错会被错误地显示在验证码输入步。拆成 verifyCode(验证码错在此抛)+ submitNewPassword(密码错在此抛)后,两类错误各自在对应步骤抛出,UI 可分屏展示、互不串屏——与 auth.settings 已登录改密码的三段式(sendVerifyCode / verifyCode / updatePassword)是同一套「两屏 UX」模式。

最短链路 ​

ts
import { TripoAuth, TripoAuthError } from "@tripo3d/auth";

const auth = new TripoAuth({
  basePath: "https://auth.tripo3d.ai",
});

// 1. 触发 ory 下发恢复邮件
await auth.reset.sendCode({ email: "user@example.com" });

// 2. 用户拿到邮件里的验证码,提交完成提权
await auth.reset.verifyCode({ code: "123456" });

// 3. 提权 session 内提交新密码
await auth.reset.submitNewPassword({
  password: "new-strong-password",
});

// 4. 成功后 SDK 不写 signal cookie;业务侧通常引导用户去登录页重新登录

设计要点 ​

1. sendCode → verifyCode → submitNewPassword 是有状态序列 ​

sendCode 创建 recovery flow 后把 { flow, csrf_token } 写入实例级 RecoverySessionHolder;verifyCode 复用同一份 flow id 提交验证码 + csrf 完成提权,成功后清掉 recovery flow ref(recovery code 一次性、防重放)并置位内部 privileged 标志;submitNewPassword 在已提权的 session 内新建 settings flow 改密。

[sendCode]
  ory createBrowserRecoveryFlow → flow.id + ui.nodes
  ory updateRecoveryFlow(method='code', email)  → 邮件下发
  holder.set({ flow, csrf_token })
[verifyCode]
  holder.get()  ─►  cached
  ory updateRecoveryFlow(method='code', code, csrf)         → privileged session
  holder 清 recovery flow ref(code 一次性、防重放)+ 置位 privileged 标志
[submitNewPassword]
  ory createBrowserSettingsFlow → settings.id + 新 csrf      (settings flow 有自己一份 csrf)
  ory updateSettingsFlow(method='password', password, csrf)  → 密码更新
  holder.clear()

要点:

  • 三个方法必须在同一个 TripoAuth 实例上顺序调用,跨实例 holder / 提权标志不通
  • 多实例 / SSR 并发场景下 holder 互不串扰
  • verifyCode / submitNewPassword 都不再要求传 email——recovery flow 已经绑定 identity,邮箱在 sendCode 阶段就已定型

密码重置成功后 SDK 会调 writeSignal({ provider: 'password' })。原因:

  • recovery 验证码校验通过时 ory 已把当前会话认证为登录态(此后再走 submitPassword 会撞 "you are already logged in")
  • 因此重置密码即完成一次登录,与 submitPassword / submitCode / oidc 成功后一致写信号
  • 容器据信号收尾:dialog useConsumeSignal 关窗 + emitLogin;独立登录页 useReadSignal 跳 returnTo——用户改完密码直接进入登录态,无需重新登录
ts
await auth.reset.submitNewPassword({ password });
// 无需再手动 redirect 到登录页:容器会因登录信号自动收尾进入登录态
toast.success("密码修改成功");

3. 邮箱变更要重新 sendCode ​

业务 UI 改了邮箱之后必须重新 sendCode——SDK 不会替业务侧检测;旧 flow 拿来 verifyCode 会被 ory 拒绝。建议 UI 上一旦邮箱输入框值变更,就清掉「已发码」状态:

ts
import { ref, watch } from "vue";

const email = ref("");
const codeSent = ref(false);

watch(email, () => {
  codeSent.value = false;
});

4. 缺前置调用(乱序) ​

三段有严格前后依赖,跳步调用抛 VALIDATION:

ts
// 没先 sendCode 就 verifyCode
await auth.reset.verifyCode({ code });
// TripoAuthError(name: 'VALIDATION', message: 'reset.verifyCode requires a prior reset.sendCode call in the same session')

// 没先 verifyCode 就 submitNewPassword
await auth.reset.submitNewPassword({ password });
// TripoAuthError(name: 'VALIDATION', message: 'reset.submitNewPassword requires a prior reset.verifyCode call in the same session')

业务 UI 应通过分屏(验证码步 → 密码步)+ disable 提交按钮卡住这些错误路径。

重试与重发 ​

验证码错 ​

验证码无效 / 过期在 verifyCode 抛 TripoAuthError(name: 'FLOW')。此时 holder 的 pending 状态已清,需要重新 sendCode 下发新验证码后再 verifyCode:

ts
try {
  await auth.reset.verifyCode({ code: input });
} catch (error) {
  if (error instanceof TripoAuthError && error.name === "FLOW") {
    toast.error(error.message);
    // 重发码(sendCode)后重试 verifyCode
  }
}

密码强度错 / 新旧相同 ​

ory 在 settings flow 阶段校验密码(如「password too short」「password breached」「新密码不能与旧密码相同」);submitNewPassword 抛 FLOW,业务侧直接 toast error.message。此时提权标志保留、不清 holder——用户换一个合规密码原地再调一次 submitNewPassword({ password }) 即可,无需重新 verifyCode 验码。仍建议 UI 层用 zod / yup 做基本强度校验,把 ory 拒绝当兜底。

提权过期 ​

verifyCode 与 submitNewPassword 之间停留过久,ory 会判定提权已过期(回 session_refresh_required)。此时 submitNewPassword 抛 TripoAuthError(name: 'PRIVILEGED_SESSION_EXPIRED'),SDK 清掉内部提权标志。业务应把 UI 切回验证码步,引导用户重新 sendCode + verifyCode:

ts
try {
  await auth.reset.submitNewPassword({ password });
} catch (error) {
  if (
    error instanceof TripoAuthError &&
    error.name === "PRIVILEGED_SESSION_EXPIRED"
  ) {
    toast.warning("验证已过期,请重新发送验证码");
    backToVerifyStep();
  }
}

验证码过期 / flow 过期 ​

Ory recovery flow 默认 5 分钟过期。过期后 verifyCode 抛 TripoAuthError(name: 'FLOW'),业务需要重新 sendCode:

ts
async function onResend() {
  await auth.reset.sendCode({ email });
  startCountdown(60);
}

端到端示例(Vue) ​

分两屏:第一屏收邮箱 + 验证码,verifyCode 成功后进入第二屏收新密码 submitNewPassword。错误分流:验证码错留在验证码步,PRIVILEGED_SESSION_EXPIRED 退回验证码步,其它密码错留在密码步原地重试。

vue
<script setup lang="ts">
import { ref } from "vue";
import { TripoAuthError } from "@tripo3d/auth";
import { auth } from "~/plugins/auth";

type Step = "email" | "password";

const step = ref<Step>("email");
const email = ref("");
const code = ref("");
const password = ref("");
const codeSent = ref(false);
const cooldown = ref(0);
const verifying = ref(false);
const submitting = ref(false);

async function onSendCode() {
  try {
    await auth.reset.sendCode({ email: email.value });
    codeSent.value = true;
    startCooldown(60);
  } catch (err) {
    handleAuthError(err);
  }
}

// 第一屏:提交验证码完成提权,进入密码步
async function onVerifyCode() {
  if (!codeSent.value) return;
  verifying.value = true;
  try {
    await auth.reset.verifyCode({ code: code.value });
    step.value = "password";
  } catch (err) {
    // 验证码错 / 过期留在验证码步;引导重发码
    handleAuthError(err);
  } finally {
    verifying.value = false;
  }
}

// 第二屏:提权 session 内提交新密码
async function onSubmit() {
  if (password.value.length < 8) {
    toast.error("密码至少 8 位");
    return;
  }
  submitting.value = true;
  try {
    await auth.reset.submitNewPassword({ password: password.value });
    toast.success("密码修改成功,请用新密码登录");
    router.push("/login");
  } catch (err) {
    if (
      err instanceof TripoAuthError &&
      err.name === "PRIVILEGED_SESSION_EXPIRED"
    ) {
      // 提权过期:退回验证码步重发码
      toast.warning("验证已过期,请重新发送验证码");
      step.value = "email";
      return;
    }
    // 其它(密码强度错 / 新旧相同)留在密码步,原地改密码重试即可
    handleAuthError(err);
  } finally {
    submitting.value = false;
  }
}

function handleAuthError(err: unknown) {
  if (err instanceof TripoAuthError) {
    toast.error(err.message);
    return;
  }
  toast.error("网络异常,请稍后重试");
}

function startCooldown(seconds: number) {
  cooldown.value = seconds;
  const t = setInterval(() => {
    cooldown.value -= 1;
    if (cooldown.value <= 0) clearInterval(t);
  }, 1000);
}
</script>

<template>
  <!-- 第一屏:邮箱 + 验证码 -->
  <form v-if="step === 'email'" @submit.prevent="onVerifyCode">
    <input v-model="email" type="email" placeholder="邮箱" required />
    <div>
      <input
        v-model="code"
        type="text"
        inputmode="numeric"
        placeholder="验证码"
        maxlength="6"
      />
      <button
        type="button"
        :disabled="cooldown > 0 || !email"
        @click="onSendCode"
      >
        {{ cooldown > 0 ? `${cooldown}s` : "发送验证码" }}
      </button>
    </div>
    <button type="submit" :disabled="!codeSent || verifying">下一步</button>
  </form>

  <!-- 第二屏:新密码 -->
  <form v-else @submit.prevent="onSubmit">
    <input
      v-model="password"
      type="password"
      placeholder="新密码"
      minlength="8"
      required
    />
    <button type="submit" :disabled="submitting">重置密码</button>
  </form>
</template>

API 参考 ​

方法签名说明
auth.reset.sendCode(args: { email }) => Promise<void>创建 ory recovery flow + 触发邮件下发;flow id + csrf 写入实例级 holder
auth.reset.verifyCode(args: { code }) => Promise<void>提交验证码到 recovery flow 完成提权 → 当前 session 被 ory 标记为 privileged;成功后清 recovery flow ref(一次性、防重放)+ 置位 privileged 标志
auth.reset.submitNewPassword(args: { password }) => Promise<void>在已提权的 session 内走 settings flow 改密码;成功后清 holder 全部状态

常见错误 ​

完整错误分类见 错误处理:

  • FLOW — 验证码错 / 过期(verifyCode)/ 新密码强度不达标或新旧相同(submitNewPassword)(业务直接 toast error.message)
  • NETWORK — ory 调用 fetch 抛错
  • VALIDATION — verifyCode 在没先 sendCode、或 submitNewPassword 在没先 verifyCode 的情况下被调用
  • PRIVILEGED_SESSION_EXPIRED — submitNewPassword 时提权已过期(verifyCode 与 submitNewPassword 间隔过久);引导用户回验证码步重发码
  • INTERNAL — flow ui 节点取不到 csrf_token(理论上 ory 不应出现)

下一步 ​

基于 MIT 协议发布(内部使用)