2026-06-02 13:14:10 +08:00
const {
bcrypt ,
requireAuth ,
login ,
getUserContextById ,
clearUserSession ,
generateUniqueEnterpriseCode ,
crypto ,
pool ,
withTransaction ,
SMS _PURPOSES ,
SMS _CODE _TTL _MINUTES ,
SMS _CODE _COOLDOWN _SECONDS ,
validateUsername ,
validatePassword ,
normalizePhone ,
validatePhone ,
normalizeEmail ,
validateEmail ,
hashSmsCode ,
generateSmsCode ,
sendSmsCode ,
createLoginResultForUserId ,
generateUniqueUsername ,
consumeSmsCode ,
getWechatLoginConfig ,
exchangeWechatCode ,
findOrCreateWechatUser ,
validateEnterpriseName ,
buildOssPublicUrl ,
normalizeAvatarOssKey ,
normalizeProfileMediaUrl ,
2026-06-04 18:58:45 +08:00
EMAIL _PURPOSES ,
EMAIL _CODE _TTL _MINUTES ,
EMAIL _CODE _COOLDOWN _SECONDS ,
EMAIL _CODE _MAX _ATTEMPTS ,
hashEmailCode ,
sendEmailCode ,
consumeEmailCode ,
2026-06-02 13:14:10 +08:00
} = require ( "./context" ) ;
const {
checkBetaInviteCodeForRegistration ,
consumeBetaInviteCode ,
findEnterpriseBetaAccountByInviteCode ,
getBetaInviteCodeFromBody ,
} = require ( "../betaInviteCodes" ) ;
async function ensureRegistrationInvite ( req , res , client = pool ) {
const result = await checkBetaInviteCodeForRegistration ( getBetaInviteCodeFromBody ( req . body ) , client ) ;
if ( result . ok ) return result ;
res . status ( result . status || 403 ) . json ( { error : result . error || "内测码无效或缺失" } ) ;
return null ;
}
async function ensureBetaInviteCode ( req , res , client = pool ) {
const result = await ensureRegistrationInvite ( req , res , client ) ;
return result ? result . code : null ;
}
function createBetaInviteCodeError ( result ) {
const error = new Error ( result . error || "内测码无效或缺失" ) ;
error . status = result . status || 403 ;
return error ;
}
async function consumeBetaInviteCodeForUser ( client , code , userId ) {
const result = await consumeBetaInviteCode ( code , userId , client ) ;
if ( ! result . ok ) throw createBetaInviteCodeError ( result ) ;
}
function hashRegistrationInviteCode ( code ) {
const normalized = String ( code || "" )
. trim ( )
. replace ( /[\s-]/g , "" )
. toUpperCase ( ) ;
return crypto . createHash ( "sha256" ) . update ( normalized ) . digest ( "hex" ) ;
}
async function resolveEnterpriseBetaRegistrationTarget ( invite , client ) {
const account =
invite ? . account || findEnterpriseBetaAccountByInviteCode ( invite ? . code || invite ) ;
if ( ! account ) return { enterpriseId : null , isEnterpriseBeta : false } ;
const { rows } = await client . query (
"SELECT id, enabled FROM enterprises WHERE enterprise_code = $1 LIMIT 1" ,
[ account . enterpriseId ] ,
) ;
if ( rows . length === 0 || ! rows [ 0 ] . enabled ) {
const error = new Error ( "企业内测账号尚未初始化,请先运行服务端数据库初始化" ) ;
error . status = 503 ;
throw error ;
}
return {
enterpriseId : rows [ 0 ] . id ,
isEnterpriseBeta : true ,
account ,
} ;
}
async function consumeRegistrationInviteForUser ( client , invite , userId , enterpriseTarget ) {
if ( enterpriseTarget && enterpriseTarget . isEnterpriseBeta ) {
await client . query (
`
INSERT INTO enterprise_members (enterprise_id, user_id, role)
VALUES ( $ 1, $ 2, 'employee')
ON CONFLICT (enterprise_id, user_id) DO UPDATE SET role = EXCLUDED.role
` ,
[ enterpriseTarget . enterpriseId , userId ] ,
) ;
await client . query (
`
UPDATE enterprise_invites
SET used_at = COALESCE(used_at, NOW())
WHERE enterprise_id = $ 1 AND code_hash = $ 2
` ,
[ enterpriseTarget . enterpriseId , hashRegistrationInviteCode ( invite . code ) ] ,
) ;
return ;
}
await consumeBetaInviteCodeForUser ( client , invite . code , userId ) ;
}
function sendAuthRouteError ( res , error , fallback ) {
const status = Number . isInteger ( error ? . status ) ? error . status : 500 ;
if ( status >= 400 && status < 500 ) {
res . status ( status ) . json ( { error : error . message || fallback } ) ;
return ;
}
res . status ( 500 ) . json ( { error : fallback } ) ;
}
function registerAuthRoutes ( router ) {
// ── Auth ─────────────────────────────────────────────────────────────
router . post ( "/auth/login" , async ( req , res ) => {
const { username , password } = req . body ;
if ( ! username || ! password ) return res . status ( 400 ) . json ( { error : "缺少用户名或密码" } ) ;
try {
const result = await login ( username , password , req . headers [ "user-agent" ] ) ;
if ( ! result ) return res . status ( 401 ) . json ( { error : "用户名或密码错误" } ) ;
res . json ( result ) ;
} catch ( err ) {
console . error ( "[auth/login] failed" , err ) ;
res . status ( 500 ) . json ( { error : "登录失败" } ) ;
}
} ) ;
router . post ( "/auth/login-email" , async ( req , res ) => {
const email = normalizeEmail ( req . body ? . email ) ;
const password = String ( req . body ? . password || "" ) ;
const emailError = validateEmail ( email ) ;
if ( emailError ) return res . status ( 400 ) . json ( { error : emailError } ) ;
if ( ! password ) return res . status ( 400 ) . json ( { error : "缺少密码" } ) ;
try {
const { rows } = await pool . query (
"SELECT username FROM users WHERE LOWER(email) = LOWER($1) AND enabled = 1 LIMIT 1" ,
[ email ] ,
) ;
if ( rows . length === 0 ) return res . status ( 401 ) . json ( { error : "邮箱或密码错误" } ) ;
const result = await login ( rows [ 0 ] . username , password , req . headers [ "user-agent" ] ) ;
if ( ! result ) return res . status ( 401 ) . json ( { error : "邮箱或密码错误" } ) ;
res . json ( result ) ;
} catch ( err ) {
console . error ( "[auth/login-email] failed" , err ) ;
res . status ( 500 ) . json ( { error : "邮箱登录失败" } ) ;
}
} ) ;
router . post ( "/auth/register" , async ( req , res ) => {
const { username , password } = req . body ;
const usernameError = validateUsername ( username ) ;
if ( usernameError ) return res . status ( 400 ) . json ( { error : usernameError } ) ;
const passwordError = validatePassword ( password ) ;
if ( passwordError ) return res . status ( 400 ) . json ( { error : passwordError } ) ;
const registrationInvite = await ensureRegistrationInvite ( req , res ) ;
if ( ! registrationInvite ) return ;
const { rows : existing } = await pool . query ( "SELECT id FROM users WHERE username = $1" , [
username ,
] ) ;
if ( existing . length > 0 ) return res . status ( 409 ) . json ( { error : "用户名已被注册" } ) ;
try {
const hash = await bcrypt . hash ( password , 10 ) ;
await withTransaction ( async ( client ) => {
const enterpriseTarget = await resolveEnterpriseBetaRegistrationTarget (
registrationInvite ,
client ,
) ;
const { rows } = await client . query (
`
INSERT INTO users (username, password_hash, role, max_concurrency, enterprise_id, is_enterprise_admin, balance_cents)
VALUES ( $ 1, $ 2, $ 3, $ 4, $ 5, $ 6, $ 7)
RETURNING id
` ,
[ username , hash , "user" , 30 , enterpriseTarget . enterpriseId , 0 , 0 ] ,
) ;
await consumeRegistrationInviteForUser ( client , registrationInvite , rows [ 0 ] . id , enterpriseTarget ) ;
} ) ;
const loginResult = await login ( username , password , req . headers [ "user-agent" ] ) ;
res . json ( loginResult ) ;
} catch ( error ) {
console . error ( "[auth/register] failed" , error ) ;
sendAuthRouteError ( res , error , "Register failed" ) ;
}
} ) ;
router . post ( "/auth/register-email" , async ( req , res ) => {
const email = normalizeEmail ( req . body ? . email ) ;
const usernameInput = String ( req . body ? . username || "" ) . trim ( ) ;
const password = String ( req . body ? . password || "" ) ;
2026-06-04 18:58:45 +08:00
const code = String ( req . body ? . code || "" ) . trim ( ) ;
2026-06-02 13:14:10 +08:00
const emailError = validateEmail ( email ) ;
if ( emailError ) return res . status ( 400 ) . json ( { error : emailError } ) ;
2026-06-04 18:58:45 +08:00
if ( ! code ) return res . status ( 400 ) . json ( { error : "缺少验证码" } ) ;
2026-06-02 13:14:10 +08:00
const passwordError = validatePassword ( password ) ;
if ( passwordError ) return res . status ( 400 ) . json ( { error : passwordError } ) ;
const registrationInvite = await ensureRegistrationInvite ( req , res ) ;
if ( ! registrationInvite ) return ;
try {
2026-06-04 18:58:45 +08:00
const verified = await consumeEmailCode ( email , code , "register" ) ;
if ( ! verified ) return res . status ( 400 ) . json ( { error : "验证码错误或已过期" } ) ;
2026-06-02 13:14:10 +08:00
const { rows : existingEmail } = await pool . query (
"SELECT id FROM users WHERE LOWER(email) = LOWER($1) LIMIT 1" ,
[ email ] ,
) ;
if ( existingEmail . length > 0 ) return res . status ( 409 ) . json ( { error : "该邮箱已注册" } ) ;
let username = usernameInput ;
if ( username ) {
const usernameError = validateUsername ( username ) ;
if ( usernameError ) return res . status ( 400 ) . json ( { error : usernameError } ) ;
const { rows : existingUsername } = await pool . query ( "SELECT id FROM users WHERE username = $1" , [
username ,
] ) ;
if ( existingUsername . length > 0 ) return res . status ( 409 ) . json ( { error : "用户名已被注册" } ) ;
} else {
username = await generateUniqueUsername ( email . split ( "@" ) [ 0 ] , "email" ) ;
}
const hash = await bcrypt . hash ( password , 10 ) ;
const { rows } = await withTransaction ( async ( client ) => {
const enterpriseTarget = await resolveEnterpriseBetaRegistrationTarget (
registrationInvite ,
client ,
) ;
const insertResult = await client . query (
`
INSERT INTO users (username, password_hash, email, email_verified, auth_provider, role, max_concurrency, enterprise_id, is_enterprise_admin, balance_cents)
VALUES ( $ 1, $ 2, $ 3, 0, 'email', 'user', 30, $ 4, 0, 0)
RETURNING id
` ,
[ username , hash , email , enterpriseTarget . enterpriseId ] ,
) ;
await consumeRegistrationInviteForUser (
client ,
registrationInvite ,
insertResult . rows [ 0 ] . id ,
enterpriseTarget ,
) ;
return insertResult ;
} ) ;
const loginResult = await createLoginResultForUserId ( rows [ 0 ] . id , req ) ;
res . json ( loginResult ) ;
} catch ( error ) {
console . error ( "[auth/register-email] failed" , error ) ;
sendAuthRouteError ( res , error , "Email register failed" ) ;
}
} ) ;
router . post ( "/auth/sms/send" , async ( req , res ) => {
const phone = normalizePhone ( req . body ? . phone ) ;
const purpose = String ( req . body ? . purpose || "register" ) ;
const phoneError = validatePhone ( phone ) ;
if ( phoneError ) return res . status ( 400 ) . json ( { error : phoneError } ) ;
if ( ! SMS _PURPOSES . has ( purpose ) ) return res . status ( 400 ) . json ( { error : "验证码用途无效" } ) ;
if ( purpose === "register" && ! ( await ensureBetaInviteCode ( req , res ) ) ) return ;
try {
const { rows : existingUsers } = await pool . query (
"SELECT id FROM users WHERE phone = $1 LIMIT 1" ,
[ phone ] ,
) ;
if ( purpose === "register" && existingUsers . length > 0 ) {
return res . status ( 409 ) . json ( { error : "该手机号已注册" } ) ;
}
if ( purpose === "login" && existingUsers . length === 0 ) {
return res . status ( 404 ) . json ( { error : "该手机号尚未注册" } ) ;
}
const { rows : recentCodes } = await pool . query (
`
SELECT created_at
FROM sms_verification_codes
WHERE phone = $ 1 AND purpose = $ 2 AND created_at > NOW() - ( $ 3::text || ' seconds')::interval
ORDER BY created_at DESC
LIMIT 1
` ,
[ phone , purpose , SMS _CODE _COOLDOWN _SECONDS ] ,
) ;
if ( recentCodes . length > 0 ) {
return res
. status ( 429 )
. json ( { error : ` 验证码发送太频繁,请 ${ SMS _CODE _COOLDOWN _SECONDS } 秒后再试 ` } ) ;
}
const code = generateSmsCode ( ) ;
const codeHash = hashSmsCode ( phone , code ) ;
await pool . query (
`
INSERT INTO sms_verification_codes (phone, purpose, code_hash, expires_at)
VALUES ( $ 1, $ 2, $ 3, NOW() + ( $ 4::text || ' minutes')::interval)
` ,
[ phone , purpose , codeHash , SMS _CODE _TTL _MINUTES ] ,
) ;
const sendResult = await sendSmsCode ( phone , code , purpose ) ;
res . json ( {
success : true ,
provider : sendResult . provider ,
ttlSeconds : SMS _CODE _TTL _MINUTES * 60 ,
cooldownSeconds : SMS _CODE _COOLDOWN _SECONDS ,
... ( sendResult . devCode ? { devCode : sendResult . devCode } : { } ) ,
} ) ;
} catch ( error ) {
console . error ( "[auth/sms/send] failed" , error ) ;
res . status ( 500 ) . json ( { error : "验证码发送失败" } ) ;
}
} ) ;
router . post ( "/auth/register-phone" , async ( req , res ) => {
const phone = normalizePhone ( req . body ? . phone ) ;
const code = String ( req . body ? . code || "" ) . trim ( ) ;
const password = String ( req . body ? . password || "" ) ;
const phoneError = validatePhone ( phone ) ;
if ( phoneError ) return res . status ( 400 ) . json ( { error : phoneError } ) ;
if ( ! code ) return res . status ( 400 ) . json ( { error : "缺少验证码" } ) ;
const passwordError = validatePassword ( password ) ;
if ( passwordError ) return res . status ( 400 ) . json ( { error : passwordError } ) ;
const registrationInvite = await ensureRegistrationInvite ( req , res ) ;
if ( ! registrationInvite ) return ;
try {
const { rows : existing } = await pool . query ( "SELECT id FROM users WHERE phone = $1 LIMIT 1" , [
phone ,
] ) ;
if ( existing . length > 0 ) return res . status ( 409 ) . json ( { error : "该手机号已注册" } ) ;
const verified = await consumeSmsCode ( phone , code , "register" ) ;
if ( ! verified ) return res . status ( 400 ) . json ( { error : "验证码错误或已过期" } ) ;
const username = await generateUniqueUsername ( ` u ${ phone . slice ( - 4 ) } ` , "phone" ) ;
const hash = await bcrypt . hash ( password , 10 ) ;
const { rows } = await withTransaction ( async ( client ) => {
const enterpriseTarget = await resolveEnterpriseBetaRegistrationTarget (
registrationInvite ,
client ,
) ;
const insertResult = await client . query (
`
INSERT INTO users (username, password_hash, phone, auth_provider, role, max_concurrency, enterprise_id, is_enterprise_admin, balance_cents)
VALUES ( $ 1, $ 2, $ 3, 'phone', 'user', 30, $ 4, 0, 0)
RETURNING id
` ,
[ username , hash , phone , enterpriseTarget . enterpriseId ] ,
) ;
await consumeRegistrationInviteForUser (
client ,
registrationInvite ,
insertResult . rows [ 0 ] . id ,
enterpriseTarget ,
) ;
return insertResult ;
} ) ;
const loginResult = await createLoginResultForUserId ( rows [ 0 ] . id , req ) ;
res . json ( loginResult ) ;
} catch ( error ) {
console . error ( "[auth/register-phone] failed" , error ) ;
sendAuthRouteError ( res , error , "Phone register failed" ) ;
}
} ) ;
router . post ( "/auth/login-phone" , async ( req , res ) => {
const phone = normalizePhone ( req . body ? . phone ) ;
const code = String ( req . body ? . code || "" ) . trim ( ) ;
const phoneError = validatePhone ( phone ) ;
if ( phoneError ) return res . status ( 400 ) . json ( { error : phoneError } ) ;
if ( ! code ) return res . status ( 400 ) . json ( { error : "缺少验证码" } ) ;
try {
const verified = await consumeSmsCode ( phone , code , "login" ) ;
if ( ! verified ) return res . status ( 400 ) . json ( { error : "验证码错误或已过期" } ) ;
const { rows } = await pool . query ( "SELECT id, enabled FROM users WHERE phone = $1 LIMIT 1" , [
phone ,
] ) ;
if ( rows . length === 0 ) return res . status ( 404 ) . json ( { error : "该手机号尚未注册" } ) ;
if ( ! rows [ 0 ] . enabled ) return res . status ( 403 ) . json ( { error : "账号已禁用" } ) ;
const loginResult = await createLoginResultForUserId ( rows [ 0 ] . id , req ) ;
res . json ( loginResult ) ;
} catch ( error ) {
console . error ( "[auth/login-phone] failed" , error ) ;
res . status ( 500 ) . json ( { error : "手机号登录失败" } ) ;
}
} ) ;
router . get ( "/auth/wechat/login-url" , async ( req , res ) => {
const { appId , redirectUri } = getWechatLoginConfig ( ) ;
if ( ! appId || ! redirectUri ) {
return res . json ( {
configured : false ,
message : "微信开放平台 AppID 或回调地址未配置" ,
} ) ;
}
const state = String ( req . query . state || crypto . randomBytes ( 8 ) . toString ( "hex" ) ) ;
try {
await pool . query (
`
INSERT INTO wechat_login_sessions (state, status, expires_at)
VALUES ( $ 1, 'pending', NOW() + INTERVAL '10 minutes')
ON CONFLICT (state) DO UPDATE
SET status = 'pending',
user_id = NULL,
error = NULL,
consumed_at = NULL,
expires_at = NOW() + INTERVAL '10 minutes',
updated_at = NOW()
` ,
[ state ] ,
) ;
} catch ( error ) {
console . error ( "[auth/wechat/login-url] failed to create session" , error ) ;
return res . status ( 500 ) . json ( { error : "微信登录会话创建失败" } ) ;
}
const url = new URL ( "https://open.weixin.qq.com/connect/qrconnect" ) ;
url . searchParams . set ( "appid" , appId ) ;
url . searchParams . set ( "redirect_uri" , redirectUri ) ;
url . searchParams . set ( "response_type" , "code" ) ;
url . searchParams . set ( "scope" , "snsapi_login" ) ;
url . searchParams . set ( "state" , state ) ;
res . json ( {
configured : true ,
url : ` ${ url . toString ( ) } #wechat_redirect ` ,
state ,
} ) ;
} ) ;
router . get ( "/auth/wechat/callback" , async ( req , res ) => {
const code = String ( req . query ? . code || "" ) . trim ( ) ;
const state = String ( req . query ? . state || "" ) . trim ( ) ;
if ( ! code || ! state ) {
return res . status ( 400 ) . send ( '<meta charset="utf-8"><h3>微信授权参数缺失</h3>' ) ;
}
try {
const { rows : sessions } = await pool . query (
`
SELECT state
FROM wechat_login_sessions
WHERE state = $ 1 AND expires_at > NOW() AND consumed_at IS NULL
LIMIT 1
` ,
[ state ] ,
) ;
if ( sessions . length === 0 ) {
return res
. status ( 400 )
. send ( '<meta charset="utf-8"><h3>微信登录会话已过期,请回到应用重新扫码</h3>' ) ;
}
const wechatUser = await exchangeWechatCode ( code ) ;
const userId = await findOrCreateWechatUser ( wechatUser ) ;
await pool . query (
`
UPDATE wechat_login_sessions
SET status = 'completed', user_id = $ 2, error = NULL, updated_at = NOW()
WHERE state = $ 1
` ,
[ state , userId ] ,
) ;
res . send ( '<meta charset="utf-8"><h3>微信登录成功</h3><p>请回到 OmniAI 应用继续。</p>' ) ;
} catch ( error ) {
console . error ( "[auth/wechat/callback] failed" , error ) ;
await pool
. query (
`
UPDATE wechat_login_sessions
SET status = 'failed', error = $ 2, updated_at = NOW()
WHERE state = $ 1
` ,
[ state , error instanceof Error ? error . message : "微信登录失败" ] ,
)
. catch ( ( ) => { } ) ;
res . status ( 500 ) . send ( '<meta charset="utf-8"><h3>微信登录失败</h3><p>请回到应用重试。</p>' ) ;
}
} ) ;
router . get ( "/auth/wechat/session" , async ( req , res ) => {
const state = String ( req . query ? . state || "" ) . trim ( ) ;
if ( ! state ) return res . status ( 400 ) . json ( { error : "缺少微信登录 state" } ) ;
try {
const { rows } = await pool . query (
`
SELECT state, status, user_id, error, consumed_at, expires_at
FROM wechat_login_sessions
WHERE state = $ 1
LIMIT 1
` ,
[ state ] ,
) ;
const session = rows [ 0 ] ;
if ( ! session ) return res . status ( 404 ) . json ( { status : "missing" } ) ;
if ( session . consumed _at ) return res . status ( 409 ) . json ( { status : "consumed" } ) ;
if ( new Date ( session . expires _at ) . getTime ( ) < Date . now ( ) )
return res . status ( 410 ) . json ( { status : "expired" } ) ;
if ( session . status === "failed" )
return res . status ( 400 ) . json ( { status : "failed" , error : session . error || "微信登录失败" } ) ;
if ( session . status !== "completed" ) return res . json ( { status : "pending" } ) ;
const loginResult = await createLoginResultForUserId ( session . user _id , req ) ;
if ( ! loginResult ) return res . status ( 403 ) . json ( { status : "failed" , error : "账号不可用" } ) ;
await pool . query (
"UPDATE wechat_login_sessions SET consumed_at = NOW(), updated_at = NOW() WHERE state = $1" ,
[ state ] ,
) ;
res . json ( { status : "completed" , ... loginResult } ) ;
} catch ( error ) {
console . error ( "[auth/wechat/session] failed" , error ) ;
res . status ( 500 ) . json ( { error : "微信登录状态查询失败" } ) ;
}
} ) ;
router . post ( "/auth/wechat/login" , async ( req , res ) => {
const code = String ( req . body ? . code || "" ) . trim ( ) ;
if ( ! code ) return res . status ( 400 ) . json ( { error : "缺少微信授权 code" } ) ;
try {
const wechatUser = await exchangeWechatCode ( code ) ;
const userId = await findOrCreateWechatUser ( wechatUser ) ;
const loginResult = await createLoginResultForUserId ( userId , req ) ;
res . json ( loginResult ) ;
} catch ( error ) {
console . error ( "[auth/wechat/login] failed" , error ) ;
const status = typeof error ? . status === "number" ? error . status : 500 ;
res . status ( status ) . json ( { error : "微信登录失败" } ) ;
}
} ) ;
router . post ( "/auth/register-enterprise" , async ( req , res ) => {
const {
companyName ,
contactName = "" ,
contactPhone = "" ,
taxId = "" ,
legalPersonName = "" ,
legalPersonPhone = "" ,
username ,
password ,
} = req . body ;
const enterpriseError = validateEnterpriseName ( companyName ) ;
if ( enterpriseError ) return res . status ( 400 ) . json ( { error : enterpriseError } ) ;
const usernameError = validateUsername ( username ) ;
if ( usernameError ) return res . status ( 400 ) . json ( { error : usernameError } ) ;
const passwordError = validatePassword ( password ) ;
if ( passwordError ) return res . status ( 400 ) . json ( { error : passwordError } ) ;
const betaInviteCode = await ensureBetaInviteCode ( req , res ) ;
if ( ! betaInviteCode ) return ;
const { rows : existing } = await pool . query ( "SELECT id FROM users WHERE username = $1" , [
username ,
] ) ;
if ( existing . length > 0 ) return res . status ( 409 ) . json ( { error : "用户名已被注册" } ) ;
try {
const enterpriseCode = await generateUniqueEnterpriseCode ( ) ;
const hash = await bcrypt . hash ( password , 10 ) ;
await withTransaction ( async ( client ) => {
const eResult = await client . query (
`
INSERT INTO enterprises (name, contact_name, contact_phone, tax_id, legal_person_name, legal_person_phone, enterprise_code)
VALUES ( $ 1, $ 2, $ 3, $ 4, $ 5, $ 6, $ 7)
RETURNING id
` ,
[
companyName . trim ( ) ,
String ( contactName || "" ) . trim ( ) ,
String ( contactPhone || "" ) . trim ( ) ,
String ( taxId || "" ) . trim ( ) || null ,
String ( legalPersonName || "" ) . trim ( ) || null ,
String ( legalPersonPhone || "" ) . trim ( ) || null ,
enterpriseCode ,
] ,
) ;
const enterpriseId = eResult . rows [ 0 ] . id ;
const userResult = await client . query (
`
INSERT INTO users (username, password_hash, role, max_concurrency, enterprise_id, is_enterprise_admin, balance_cents)
VALUES ( $ 1, $ 2, 'user', $ 3, $ 4, 1, 0)
RETURNING id
` ,
[ username , hash , 30 , enterpriseId ] ,
) ;
await consumeBetaInviteCodeForUser ( client , betaInviteCode , userResult . rows [ 0 ] . id ) ;
} ) ;
const loginResult = await login ( username , password , req . headers [ "user-agent" ] ) ;
res . json ( loginResult ) ;
} catch ( error ) {
console . error ( "[auth/register-enterprise] failed" , error ) ;
sendAuthRouteError ( res , error , "Enterprise register failed" ) ;
}
} ) ;
router . post ( "/auth/register-employee" , async ( req , res ) => {
const { enterpriseCode , username , password } = req . body ;
if ( ! enterpriseCode ) return res . status ( 400 ) . json ( { error : "缺少企业ID" } ) ;
const usernameError = validateUsername ( username ) ;
if ( usernameError ) return res . status ( 400 ) . json ( { error : usernameError } ) ;
const passwordError = validatePassword ( password ) ;
if ( passwordError ) return res . status ( 400 ) . json ( { error : passwordError } ) ;
const betaInviteCode = await ensureBetaInviteCode ( req , res ) ;
if ( ! betaInviteCode ) return ;
const { rows : entRows } = await pool . query (
"SELECT id, name, enabled FROM enterprises WHERE enterprise_code = $1" ,
[ enterpriseCode ] ,
) ;
if ( entRows . length === 0 ) return res . status ( 404 ) . json ( { error : "企业ID不存在" } ) ;
if ( ! entRows [ 0 ] . enabled ) return res . status ( 403 ) . json ( { error : "该企业已被禁用" } ) ;
const enterpriseId = entRows [ 0 ] . id ;
const { rows : existing } = await pool . query ( "SELECT id FROM users WHERE username = $1" , [
username ,
] ) ;
if ( existing . length > 0 ) return res . status ( 409 ) . json ( { error : "用户名已被注册" } ) ;
try {
const hash = await bcrypt . hash ( password , 10 ) ;
await withTransaction ( async ( client ) => {
const { rows } = await client . query (
`
INSERT INTO users (username, password_hash, role, max_concurrency, enterprise_id, is_enterprise_admin, balance_cents)
VALUES ( $ 1, $ 2, 'user', $ 3, $ 4, 0, 0)
RETURNING id
` ,
[ username , hash , 30 , enterpriseId ] ,
) ;
await consumeBetaInviteCodeForUser ( client , betaInviteCode , rows [ 0 ] . id ) ;
} ) ;
const loginResult = await login ( username , password , req . headers [ "user-agent" ] ) ;
res . json ( loginResult ) ;
} catch ( error ) {
console . error ( "[auth/register-employee] failed" , error ) ;
sendAuthRouteError ( res , error , "Employee register failed" ) ;
}
} ) ;
router . get ( "/auth/enterprise-lookup" , async ( req , res ) => {
const { code } = req . query ;
if ( ! code ) return res . status ( 400 ) . json ( { error : "缺少企业ID" } ) ;
const { rows } = await pool . query (
"SELECT id, name, enabled FROM enterprises WHERE enterprise_code = $1" ,
[ code ] ,
) ;
if ( rows . length === 0 || ! rows [ 0 ] . enabled ) return res . json ( { valid : false } ) ;
res . json ( { valid : true , enterpriseName : rows [ 0 ] . name } ) ;
} ) ;
router . get ( "/auth/me" , requireAuth , ( req , res ) => {
res . json ( { user : req . user } ) ;
} ) ;
router . post ( "/auth/logout" , requireAuth , async ( req , res ) => {
try {
await clearUserSession ( req . user . id , req . auth ? . sessionId ) ;
res . json ( { success : true } ) ;
} catch ( error ) {
console . error ( "[auth/logout] failed" , error ) ;
res . status ( 500 ) . json ( { error : "退出登录失败" } ) ;
}
} ) ;
router . put ( "/auth/profile" , requireAuth , async ( req , res ) => {
try {
const normalizedAvatar = normalizeAvatarOssKey ( req . body ? . avatarOssKey , req . user . id ) ;
if ( normalizedAvatar . error ) {
return res . status ( 400 ) . json ( { error : normalizedAvatar . error } ) ;
}
const hasAvatarUrl = req . body && Object . prototype . hasOwnProperty . call ( req . body , "avatarUrl" ) ;
const avatarUrlInput =
normalizedAvatar . value !== undefined
? { value : normalizedAvatar . value ? ` ${ buildOssPublicUrl ( normalizedAvatar . value ) } ?v= ${ Date . now ( ) } ` : null }
: hasAvatarUrl
? normalizeProfileMediaUrl ( req . body ? . avatarUrl )
: { value : undefined } ;
if ( avatarUrlInput . error ) {
return res . status ( 400 ) . json ( { error : avatarUrlInput . error } ) ;
}
const backgroundUrlInput = normalizeProfileMediaUrl ( req . body ? . profileBackgroundUrl ) ;
if ( backgroundUrlInput . error ) {
return res . status ( 400 ) . json ( { error : backgroundUrlInput . error } ) ;
}
const fields = [ ] ;
const values = [ ] ;
if ( avatarUrlInput . value !== undefined ) {
values . push ( avatarUrlInput . value ) ;
fields . push ( ` avatar_url = $ ${ values . length } ` ) ;
}
if ( req . body && Object . prototype . hasOwnProperty . call ( req . body , "bio" ) ) {
const bio = String ( req . body . bio || "" ) . trim ( ) . slice ( 0 , 160 ) || null ;
values . push ( bio ) ;
fields . push ( ` bio = $ ${ values . length } ` ) ;
}
if ( backgroundUrlInput . value !== undefined ) {
values . push ( backgroundUrlInput . value ) ;
fields . push ( ` profile_background_url = $ ${ values . length } ` ) ;
}
if ( fields . length > 0 ) {
values . push ( req . user . id ) ;
await pool . query (
` UPDATE users SET ${ fields . join ( ", " ) } , updated_at = NOW() WHERE id = $ ${ values . length } ` ,
values ,
) ;
}
const user = await getUserContextById ( req . user . id ) ;
res . json ( { user } ) ;
} catch ( err ) {
console . error ( "[auth/profile] update failed" , err ) ;
res . status ( 500 ) . json ( { error : "更新个人资料失败" } ) ;
}
} ) ;
2026-06-04 18:58:45 +08:00
// ============================================================
// Email verification routes
// ============================================================
router . post ( "/auth/email/send-code" , async ( req , res ) => {
const email = normalizeEmail ( req . body ? . email ) ;
const purpose = String ( req . body ? . purpose || "register" ) ;
const emailError = validateEmail ( email ) ;
if ( emailError ) return res . status ( 400 ) . json ( { error : emailError } ) ;
if ( ! EMAIL _PURPOSES . has ( purpose ) ) return res . status ( 400 ) . json ( { error : "验证码用途无效" } ) ;
if ( purpose === "register" ) {
const inviteOk = await ensureBetaInviteCode ( req , res ) ;
if ( ! inviteOk ) return ;
}
try {
const { rows : recentCodes } = await pool . query (
"SELECT created_at FROM email_verification_codes WHERE email = $1 AND purpose = $2 AND created_at > NOW() - ($3::text || ' seconds')::interval ORDER BY created_at DESC LIMIT 1" ,
[ email , purpose , EMAIL _CODE _COOLDOWN _SECONDS ]
) ;
if ( recentCodes . length > 0 ) {
return res . status ( 429 ) . json ( { error : "验证码发送太频繁,请 " + EMAIL _CODE _COOLDOWN _SECONDS + " 秒后再试" } ) ;
}
if ( purpose === "register" ) {
const { rows : existing } = await pool . query ( "SELECT id FROM users WHERE LOWER(email) = LOWER($1) LIMIT 1" , [ email ] ) ;
if ( existing . length > 0 ) return res . status ( 409 ) . json ( { error : "该邮箱已注册" } ) ;
}
if ( purpose === "login" || purpose === "reset" ) {
const { rows : existing } = await pool . query ( "SELECT id FROM users WHERE LOWER(email) = LOWER($1) AND enabled = 1 LIMIT 1" , [ email ] ) ;
if ( existing . length === 0 ) return res . status ( 404 ) . json ( { error : "该邮箱尚未注册" } ) ;
}
const code = generateSmsCode ( ) ;
const codeHash = hashEmailCode ( email , code ) ;
await pool . query (
"INSERT INTO email_verification_codes (email, purpose, code_hash, expires_at) VALUES ($1, $2, $3, NOW() + ($4::text || ' minutes')::interval)" ,
[ email , purpose , codeHash , EMAIL _CODE _TTL _MINUTES ]
) ;
const sendResult = await sendEmailCode ( email , code , purpose ) ;
res . json ( {
success : true ,
provider : sendResult . provider ,
ttlSeconds : EMAIL _CODE _TTL _MINUTES * 60 ,
cooldownSeconds : EMAIL _CODE _COOLDOWN _SECONDS ,
... ( sendResult . devCode ? { devCode : sendResult . devCode } : { } ) ,
} ) ;
} catch ( error ) {
console . error ( "[auth/email/send-code] failed" , error ) ;
res . status ( 500 ) . json ( { error : "验证码发送失败" } ) ;
}
} ) ;
router . post ( "/auth/email/verify" , async ( req , res ) => {
const email = normalizeEmail ( req . body ? . email ) ;
const code = String ( req . body ? . code || "" ) . trim ( ) ;
const purpose = String ( req . body ? . purpose || "register" ) ;
const emailError = validateEmail ( email ) ;
if ( emailError ) return res . status ( 400 ) . json ( { error : emailError } ) ;
if ( ! code ) return res . status ( 400 ) . json ( { error : "缺少验证码" } ) ;
if ( ! EMAIL _PURPOSES . has ( purpose ) ) return res . status ( 400 ) . json ( { error : "验证码用途无效" } ) ;
try {
const verified = await consumeEmailCode ( email , code , purpose ) ;
if ( ! verified ) return res . status ( 400 ) . json ( { error : "验证码错误或已过期" } ) ;
if ( purpose === "register" || purpose === "login" ) {
await pool . query ( "UPDATE users SET email_verified = 1 WHERE LOWER(email) = LOWER($1)" , [ email ] ) ;
}
res . json ( { success : true } ) ;
} catch ( error ) {
console . error ( "[auth/email/verify] failed" , error ) ;
res . status ( 500 ) . json ( { error : "验证失败" } ) ;
}
} ) ;
router . post ( "/auth/forgot-password" , async ( req , res ) => {
const email = normalizeEmail ( req . body ? . email ) ;
const emailError = validateEmail ( email ) ;
if ( emailError ) return res . status ( 400 ) . json ( { error : emailError } ) ;
try {
const { rows } = await pool . query ( "SELECT id FROM users WHERE LOWER(email) = LOWER($1) AND enabled = 1 LIMIT 1" , [ email ] ) ;
if ( rows . length === 0 ) {
return res . json ( { success : true , message : "如果该邮箱已注册,重置链接已发送" } ) ;
}
const { rows : recentCodes } = await pool . query (
"SELECT created_at FROM email_verification_codes WHERE email = $1 AND purpose = 'reset' AND created_at > NOW() - ($2::text || ' seconds')::interval ORDER BY created_at DESC LIMIT 1" ,
[ email , EMAIL _CODE _COOLDOWN _SECONDS ]
) ;
if ( recentCodes . length > 0 ) {
return res . status ( 429 ) . json ( { error : "发送太频繁,请 " + EMAIL _CODE _COOLDOWN _SECONDS + " 秒后再试" } ) ;
}
const code = generateSmsCode ( ) ;
const codeHash = hashEmailCode ( email , code ) ;
await pool . query (
"INSERT INTO email_verification_codes (email, purpose, code_hash, expires_at) VALUES ($1, 'reset', $2, NOW() + ($3::text || ' minutes')::interval)" ,
[ email , codeHash , EMAIL _CODE _TTL _MINUTES ]
) ;
await sendEmailCode ( email , code , "reset" ) ;
res . json ( { success : true , message : "重置验证码已发送到您的邮箱" } ) ;
} catch ( error ) {
console . error ( "[auth/forgot-password] failed" , error ) ;
res . status ( 500 ) . json ( { error : "发送失败" } ) ;
}
} ) ;
router . post ( "/auth/reset-password" , async ( req , res ) => {
const email = normalizeEmail ( req . body ? . email ) ;
const code = String ( req . body ? . code || "" ) . trim ( ) ;
const newPassword = String ( req . body ? . newPassword || "" ) ;
const emailError = validateEmail ( email ) ;
if ( emailError ) return res . status ( 400 ) . json ( { error : emailError } ) ;
if ( ! code ) return res . status ( 400 ) . json ( { error : "缺少验证码" } ) ;
const passwordError = validatePassword ( newPassword ) ;
if ( passwordError ) return res . status ( 400 ) . json ( { error : passwordError } ) ;
try {
const verified = await consumeEmailCode ( email , code , "reset" ) ;
if ( ! verified ) return res . status ( 400 ) . json ( { error : "验证码错误或已过期" } ) ;
const hash = await bcrypt . hash ( newPassword , 10 ) ;
await pool . query ( "UPDATE users SET password_hash = $1 WHERE LOWER(email) = LOWER($2)" , [ hash , email ] ) ;
res . json ( { success : true , message : "密码重置成功,请重新登录" } ) ;
} catch ( error ) {
console . error ( "[auth/reset-password] failed" , error ) ;
res . status ( 500 ) . json ( { error : "密码重置失败" } ) ;
}
} ) ;
2026-06-02 13:14:10 +08:00
}
module . exports = {
registerAuthRoutes ,
} ;