oauth.js 2.92 KB
/**
 * OAuth 登录:经注入 oauth Port 换码 + 验签取邮箱 → 首登建号 / 已存在合并 → 签发会话。
 *
 * 红线:包内不自写 JWKS 拉取 / RS256 验签 / id_token 解码——全在 oauth.exchange 内完成
 * (Google 走 google-auth-library 校验 id_token、Apple 走其 JWKS)。验签失败统一
 * OAUTH_VERIFY_FAILED。本文件只做 provider 校验 + 建号/合并的账户状态机。
 */

import { AuthError } from './errors.js'
import { issueSession } from './session.js'

const SUPPORTED_PROVIDERS = new Set(['google', 'apple'])

/**
 * @param {Object} deps
 * @param {import('./index.js').UserRepo} deps.userRepo
 * @param {import('./index.js').OauthPort} deps.oauth
 * @param {import('./index.js').TokenSigner} deps.tokenSigner
 * @param {import('./index.js').Hasher} deps.hasher
 * @param {number} [deps.accessTtlMs]
 * @param {number} [deps.refreshTtlMs]
 * @returns {(args: { provider: string, code: string, oauth_flow?: 'access_token'|'authorization_code', redirect_uri?: string, device_id?: string }) => Promise<import('./index.js').AuthTokens & { is_new_user: boolean }>}
 */
export function makeOauthLogin(deps) {
  const { userRepo, oauth } = deps

  return async function oauthLogin({ provider, code, oauth_flow = 'access_token', redirect_uri }) {
    if (!SUPPORTED_PROVIDERS.has(provider)) {
      throw new AuthError({ code: 'UNSUPPORTED_PROVIDER', message: `provider: ${provider}` })
    }

    let identity
    try {
      identity = await oauth.exchange({
        provider: /** @type {'google'|'apple'} */ (provider),
        code,
        oauth_flow,
        redirect_uri,
      })
    } catch (err) {
      throw new AuthError({ code: 'OAUTH_VERIFY_FAILED', cause: err })
    }
    if (!identity || !identity.email || !identity.provider_user_id) {
      throw new AuthError({ code: 'OAUTH_VERIFY_FAILED' })
    }

    const email = String(identity.email).trim().toLowerCase()

    let user = await userRepo.findByEmail(email)
    let is_new_user = false
    if (!user) {
      // 首登:自动建号并绑定 provider 身份。
      user = await userRepo.create({
        email,
        oauth_providers: [{ provider, provider_user_id: identity.provider_user_id }],
        ...(identity.email_verified ? { email_verified_at: new Date() } : {}),
      })
      is_new_user = true
    } else {
      // 已存在:合并到同一 user(绑定该 provider,若未绑)。
      const linked = (user.oauth_providers ?? []).some(
        (p) => p.provider === provider && p.provider_user_id === identity.provider_user_id,
      )
      if (!linked) {
        user = await userRepo.linkOauth({
          user_id: user.id,
          provider,
          provider_user_id: identity.provider_user_id,
        })
      }
    }

    const issued = await issueSession(deps, { user_id: user.id })
    return {
      user,
      access_token: issued.access_token,
      refresh_token: issued.refresh_token,
      is_new_user,
    }
  }
}