env.js 2.73 KB
/**
 * voko-server 连接与基础设施配置的唯一解析点。业务运行参数由所属模块直接读取。
 */

import { devMysqlUrl } from '@drama/db'

/**
 * @typedef {{
 *   nodeEnv: string,
 *   port: number,
 *   databaseUrl: string,
 *   logLevel: 'debug'|'info'|'warn'|'error',
 *   authTokenSecret: string,
 *   publicUrl: string,
 *   email: { host: string, port: number, secure: boolean, user: string, pass: string, from: string },
 *   oauth: { googleClientId: string|null, googleClientSecret: string|null },
 * }} VokoEnv
 */

/**
 * @param {Record<string, string|undefined>} rawEnv
 * @returns {VokoEnv}
 */
export function readEnv(rawEnv) {
  const nodeEnv = rawEnv.NODE_ENV || 'development'
  const databaseUrl = rawEnv.DATABASE_URL || devMysqlUrl('voko')
  const scheme = new URL(databaseUrl).protocol
  if (scheme !== 'mysql:' && scheme !== 'mysql2:') throw new Error(`voko-server: DATABASE_URL 必须是 MySQL,收到 ${scheme}`)
  if (nodeEnv === 'production' && !rawEnv.AUTH_TOKEN_SECRET) throw new Error('voko-server: 生产环境必须设置 AUTH_TOKEN_SECRET')

  const email = {
    host: rawEnv.SMTP_HOST,
    port: Number(rawEnv.SMTP_PORT || 465),
    secure: rawEnv.SMTP_SECURE !== 'false',
    user: rawEnv.SMTP_USER,
    pass: rawEnv.SMTP_PASS,
    from: rawEnv.EMAIL_FROM,
  }
  const missing = [['SMTP_HOST', email.host], ['SMTP_USER', email.user], ['SMTP_PASS', email.pass], ['EMAIL_FROM', email.from]]
    .filter(([, value]) => !value)
    .map(([name]) => name)
  if (missing.length > 0) throw new Error(`voko-server: 邮件缺少配置,拒绝启动(无 fake 降级):${missing.join(', ')}`)

  return {
    nodeEnv,
    port: parsePort(rawEnv.PORT, 9201),
    databaseUrl,
    logLevel: parseLogLevel(rawEnv.LOG_LEVEL),
    authTokenSecret: rawEnv.AUTH_TOKEN_SECRET || 'voko-dev-token-secret',
    publicUrl: (rawEnv.PUBLIC_URL || 'http://localhost:5201').replace(/\/$/, ''),
    email: /** @type {VokoEnv['email']} */ (email),
    oauth: {
      googleClientId: rawEnv.GOOGLE_OAUTH_CLIENT_ID || null,
      googleClientSecret: rawEnv.GOOGLE_OAUTH_CLIENT_SECRET || null,
    },
  }
}

/**
 * @param {string|undefined} raw
 * @param {number} fallback
 */
function parsePort(raw, fallback) {
  if (raw === undefined || raw === '') return fallback
  const port = Number(raw)
  if (!Number.isInteger(port) || port <= 0 || port > 65535) throw new Error(`voko-server: PORT 非法:${raw}`)
  return port
}

/**
 * @param {string|undefined} raw
 * @returns {'debug'|'info'|'warn'|'error'}
 */
function parseLogLevel(raw) {
  const level = raw || 'info'
  switch (level) {
    case 'debug':
    case 'info':
    case 'warn':
    case 'error':
      return level
    default:
      throw new Error(`voko-server: LOG_LEVEL 非法:${level}`)
  }
}