auth.js 5.21 KB
/**
 * createAuth —— 唯一入口工厂,只做依赖装配 + 各能力编排。
 *
 * 纪律:本文件不碰任何密码学(全在注入 Port);缺依赖在装配时即 assert 抛错
 * (不拖到运行某行);每条业务流程在独立文件(register/login/oauth/refresh/code)。
 * verifyAccessToken / logout / revokeAllSessions 是薄方法,直接落本文件。
 */

import { AuthError } from './errors.js'
import { makeRegister } from './register.js'
import { makeLogin } from './login.js'
import { makeOauthLogin } from './oauth.js'
import { makeRefresh } from './refresh.js'
import { makeSendCode, makeVerifyCode } from './code.js'
import { makeResetPassword } from './reset-password.js'
import { makeRequestPasswordReset } from './request-password-reset.js'
import { parseRefreshToken } from './session.js'

/**
 * @param {Object} [deps]
 * @param {import('./index.js').UserRepo} [deps.userRepo]
 * @param {import('./index.js').EmailPort} [deps.email]
 * @param {import('./index.js').OauthPort} [deps.oauth]
 * @param {import('./index.js').TokenSigner} [deps.tokenSigner]
 * @param {import('./index.js').Hasher} [deps.hasher]
 * @param {Object} [opts]   可选的业务规则覆写(TTL / 阈值 / 窗口)。
 * @returns {import('./index.js').Auth}
 */
export function createAuth({ userRepo, email, oauth, tokenSigner, hasher } = {}, opts = {}) {
  assertDep('userRepo', userRepo, ['findByEmail', 'create', 'updatePasswordHash', 'saveSession', 'findSession'])
  assertDep('email', email, ['sendCode'])
  assertDep('oauth', oauth, ['exchange'])
  assertDep('tokenSigner', tokenSigner, ['sign', 'verify'])
  assertDep('hasher', hasher, ['hash', 'verify'])

  const deps = /** @type {{ userRepo: import('./index.js').UserRepo, email: import('./index.js').EmailPort, oauth: import('./index.js').OauthPort, tokenSigner: import('./index.js').TokenSigner, hasher: import('./index.js').Hasher, maxAttempts?: number, windowMs?: number, accessTtlMs?: number, refreshTtlMs?: number, ttlMs?: number, rand?: () => number }} */ (
    { userRepo, email, oauth, tokenSigner, hasher, ...opts }
  )

  const register = makeRegister(deps)
  const login = makeLogin(deps)
  const oauthLogin = makeOauthLogin(deps)
  const refresh = makeRefresh(deps)
  const sendCode = makeSendCode(deps)
  const requestPasswordReset = makeRequestPasswordReset({ userRepo: deps.userRepo, sendCode })
  const verifyCode = makeVerifyCode(deps)
  const resetPassword = makeResetPassword(deps)

  /** @type {import('./index.js').Auth['verifyAccessToken']} */
  async function verifyAccessToken(token) {
    if (!token || typeof token !== 'string') {
      throw new AuthError({ code: 'TOKEN_INVALID' })
    }
    try {
      const claims = await deps.tokenSigner.verify(token)
      await assertAccessSessionActive(claims)
      return claims
    } catch (/** @type {any} */ err) {
      if (err instanceof AuthError) throw err
      // jose 用 'ERR_JWT_EXPIRED'(code 或 name)区分过期与无效签名。
      const tag = (err && (err.code || err.name)) || ''
      const expired = /EXPIRED/i.test(String(tag))
      throw new AuthError({ code: expired ? 'TOKEN_EXPIRED' : 'TOKEN_INVALID', cause: err })
    }
  }

  /**
   * @param {Record<string, any>} claims
   */
  async function assertAccessSessionActive(claims) {
    const sid = String(claims?.sid || '')
    if (!sid) throw new AuthError({ code: 'TOKEN_INVALID' })
    const session = await deps.userRepo.findSession({ sid })
    if (!session || session.revoked) throw new AuthError({ code: 'TOKEN_INVALID' })
    if (Date.now() > session.expires_at.getTime()) {
      throw new AuthError({ code: 'TOKEN_EXPIRED' })
    }
  }

  /** @type {import('./index.js').Auth['logout']} */
  async function logout({ refresh_token }) {
    const parsed = parseRefreshToken(refresh_token)
    if (parsed) {
      const session = await deps.userRepo.findSession({ sid: parsed.sid })
      if (session) await deps.userRepo.revokeFamily({ family: session.family })
    }
    return { ok: true }
  }

  /** @type {import('./index.js').Auth['revokeAllSessions']} */
  async function revokeAllSessions({ user_id }) {
    if (!user_id) throw new AuthError({ code: 'USER_NOT_FOUND' })
    await deps.userRepo.revokeUserSessions({ user_id })
    return { ok: true }
  }

  /**
   * @param {{ user_id: string }} args
   */
  async function getUser({ user_id }) {
    if (!user_id || typeof deps.userRepo.findById !== 'function') return null
    return deps.userRepo.findById(user_id)
  }

  return {
    register,
    login,
    oauthLogin,
    refresh,
    verifyCode,
    sendCode,
    requestPasswordReset,
    resetPassword,
    getUser,
    verifyAccessToken,
    logout,
    revokeAllSessions,
  }
}

/**
 * 装配时依赖自检:缺整个依赖或缺关键方法都立刻抛 AuthError。
 * @param {string} name
 * @param {any} dep
 * @param {string[]} methods
 */
function assertDep(name, dep, methods) {
  if (!dep || typeof dep !== 'object') {
    throw new AuthError({ code: 'TOKEN_INVALID', message: `createAuth: missing dependency "${name}"` })
  }
  for (const m of methods) {
    if (typeof dep[m] !== 'function') {
      throw new AuthError({
        code: 'TOKEN_INVALID',
        message: `createAuth: dependency "${name}" missing method "${m}"`,
      })
    }
  }
}