登录与用户在 Func 里实现登录

在 Func 里实现登录

ctx.auth.login 给已有用户签发会话,用来做 SDK 表达不了的免密码验证码登录,或者在登录时跑封禁、实名、多租户这类自己的规则。含完整代码、一条不能写错的规则,以及每次登录的审计在哪看。

站点的登录默认用 useAuth().login() 就够了。要用 Func 实现登录只有两种情况: 做免密码的验证码登录(SDK 表达不了),或者登录时要跑你自己的服务端规则——封禁名单、 必须先完成实名、多租户按域名限制谁能登进哪个租户。

核心是 ctx.auth.login(ref):给一个已有用户签发会话。它不接收密码也不接收验证码, 「凭什么允许登录」由你的代码决定。

先记住一条规则

这条写在最前面,因为它是这个能力唯一会出大事的地方:

ctx.auth.login 的参数必须来自服务端刚刚验证过的事实。 先用 ctx.users.findctx.users.checkPassword 拿到用户,再把 user.id 交给 login。直接写 ctx.auth.login({ email: input.email }) 意味着「浏览器说自己是谁就是谁」——那不是登录, 是让任何人以任何账号身份进来。

为什么单独强调它:改密码写错了,真实用户会被锁在门外从而察觉;冒名登录悄无声息, 没有任何人会发现。

验证码登录(免密码)

需要项目已连接邮件集成,见 使用集成发送邮件与验证码

// /backend/func/auth/login.ts
import type { TalizenFuncContext } from 'talizen/func-runtime'

const SCENE = 'login'

// 第一步:要验证码。与找回密码同一套写法——先查再发,两条分支返回值一样
export function requestCode(input: { email?: string }, ctx: TalizenFuncContext) {
  const email = (input.email ?? '').trim()
  if (!email) throw new Error('请填写邮箱')

  const user = ctx.users.find({ email })
  if (user) {
    try {
      ctx.email.sendCode({ to: email, scene: SCENE })
    } catch (err) {
      console.error('send login code failed', String(err))
    }
  }
  return { ok: true }
}

// 第二步:验码并登录
export function withCode(
  input: { email?: string; code?: string },
  ctx: TalizenFuncContext,
) {
  const email = (input.email ?? '').trim()
  const code = (input.code ?? '').trim()
  if (!email || !code) throw new Error('请填写邮箱和验证码')

  const user = ctx.users.find({ email })
  if (!user) throw new Error('验证码错误或已过期')

  if (!ctx.email.verifyCode({ to: email, scene: SCENE, code }))
    throw new Error('验证码错误或已过期')

  // 用 find 回来的 user.id,不是 input.email
  return ctx.auth.login({ userId: user.id })
}

返回的是登录后的用户对象;会话 cookie 由平台写在这次响应上,页面拿到返回值就等于已登录。 Func 代码看不到会话 token,也没有办法自己造一个。

密码登录 + 你自己的规则

export function withPassword(
  input: { account: string; password: string },
  ctx: TalizenFuncContext,
) {
  // 失败次数与平台的 /auth/login 共用同一份额度:错 5 次锁 1 小时,
  // 所以这条路不能被拿来绕过登录锁定爆破密码
  if (!ctx.users.checkPassword({ account: input.account, password: input.password }))
    throw new Error('账号或密码错误')

  const user = ctx.users.find({ account: input.account })
  if (!user) throw new Error('账号或密码错误')

  // 这里才是用 Func 做登录的理由:平台管不了的规则
  if (ctx.db.get('banned_users', user.id)) throw new Error('该账号已被封禁')
  if (!user.profile?.onboarded) return { ok: false, next: '/onboarding' }

  return ctx.auth.login({ userId: user.id })
}

连续验错太多次时 checkPassword429(不是返回 false)。 给它单独的提示文案「尝试次数过多,请稍后再试」,否则用户会一直重试到锁死。

页面代码

import { invoke } from 'talizen/func'
import { useAuth } from 'talizen/auth'

async function loginWithCode(email: string, code: string) {
  await invoke('auth/login.withCode', { email, code })
  // 会话 cookie 已经随这次响应下发,刷新一下 useAuth() 的状态即可
  await useAuth().refresh()
  window.location.href = '/account'
}

登出仍然用 useAuth().logout(),不需要写 Func。

每一次登录都有记录

平台会记下每一次会话签发:时间、来源、IP、设备。来源分四种——直连登录OAuth(带 provider)、注册、以及 Func(带是哪个 Func 文件签的)。

在编辑器 后端 → 用户 里,某个用户那一行的菜单 → 登录记录 就能看到。

这不只是给你看登录历史用的。Func 里的登录是唯一一条「站点代码说谁登录就谁登录」的路, 所以哪个文件签发了哪个账号的会话必须能查到——上面那条规则万一写错了,这里是唯一能发现的地方。

其它要知道的

情况行为
浏览器直连 /auth/login照旧可用。没有「把登录收口到 Func」的开关,两者并存;你可以只给需要的入口写 Func
用户不存在login 抛 404。别把它原样回给浏览器——统一回「账号或密码错误」「验证码错误」这类不区分的文案
账号被停用抛 403,与直连登录一致
指人的三个字段email / account / userId 只能填一个,填两个报 400
只用第三方登录的账号login 照样能签发会话(它不看有没有密码);checkPassword 对这类账号返回 false
OAuth 登录仍由平台处理,不要在 Func 里写回调或换 token

相关:实现找回密码与修改密码注册时验证邮箱使用集成发送邮件与验证码

Render diagnostics