Files
sso-portal/app/api/login/route.ts
zhangheng 37f10118d3 chore: initial commit of sso-mock SSO login service
Next.js + Mantine mock SSO service providing account/password, phone
verify-code, and Feishu QR login. Issues a one-time sso_code that the
external app exchanges for user info.

Includes:
- Feishu callback fix: guard against duplicate/concurrent /api/login
  submissions of the same single-use authorization code (feishuLogin
  once-guard + memoized onLoginSuccess to stop effect re-fire).
- ADAPTATION_GUIDE.md: how to switch from the in-memory code store to
  the real backend auth/index + inner_get_user_info endpoints.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 18:09:30 +08:00

126 lines
3.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { NextResponse } from 'next/server'
import CryptoJS from 'crypto-js'
import { codeStore } from '@/utils/code-store'
const CODE_CRYPTO_KEY = 'k#8Mp$2Qr&9Tz@4W'
const SHARED_API = (
process.env.NEXT_PUBLIC_SHARED_API_PATH || 'http://172.16.115.31:6610'
).replace(/\/+$/, '')
function decrypt(encrypted: string): string {
try {
const bytes = CryptoJS.AES.decrypt(encrypted, CODE_CRYPTO_KEY)
return bytes.toString(CryptoJS.enc.Utf8)
} catch {
return encrypted
}
}
function generateCode(): string {
return Math.random().toString(36).substring(2, 10) + Date.now().toString(36)
}
async function callBasisApi(path: string, body: object) {
const url = `${SHARED_API}${path}`
console.log('[sso-mock] →', url)
const res = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
})
const text = await res.text()
let data: any
try {
data = JSON.parse(text)
} catch {
throw new Error(`后端返回非 JSON [${res.status}]: ${text.slice(0, 200)}`)
}
return data
}
// ─── POST /api/login ───
// 与 uirefbase 保持一致:客户端发 JSON.stringify(encrypt(...))
// req.json() 去掉外层引号得到加密字符串decrypt 解密得到原始 JSON
export async function POST(req: Request) {
try {
const decryptData = await req.json() // JSON 字符串 → 去掉引号 → 加密原文
const body = JSON.parse(decrypt(decryptData))
const type = body?.type
console.log('[sso-mock] login | type:', type, '| body keys:', Object.keys(body || {}))
if (!type) {
return NextResponse.json({ error: '缺少 type 字段', debug_type: typeof type })
}
let res: any
if (type === 'account') {
res = await callBasisApi('/api/v1/basis/login/account', {
account: body.account,
password: body.password,
abbr: body.abbr,
})
} else if (type === 'phone') {
res = await callBasisApi('/api/v1/basis/user/phone/login', {
phone: body.phone,
verify_code: body.verify_code,
tntkey: body.tntkey,
})
} else if (type === 'feishu') {
res = await callBasisApi('/api/v1/basis/login/feishu', {
code: body.code,
redirect_uri: body.redirect_uri,
tenant_abbr: body.tenant,
})
} else {
console.error('[sso-mock] 未知 type:', type)
return NextResponse.json({ error: `不支持的登录方式: ${type}` })
}
console.log('[sso-mock] backend res.code:', res?.code)
// 后端业务码非 200 视为失败,直接返回错误,不生成 code
if (res?.code !== 200) {
const message = res?.message || res?.msg || '登录失败'
console.log('[sso-mock] backend error:', message)
return NextResponse.json({ error: message })
}
const user = res.data?.user
if (!user || !user.account) {
console.log('[sso-mock] backend returned 200 but no valid user')
return NextResponse.json({ error: '后端未返回有效用户信息' })
}
const tenant = user?.tntkey || body.abbr || 'coowa'
const user_info = user
const rbac = res?.data?.rbac || { data: [] }
const token = res.data?.access_token || ''
const responseData: any = { tenant, user_info, rbac, token }
// SSO 模式:仅当 client_id + client_redirect_uri 都存在时生成 code
const client_id = body.client_id
const client_redirect_uri = body.client_redirect_uri
if (client_id && client_redirect_uri) {
const code = generateCode()
codeStore.set(code, {
user: user_info,
expires: Date.now() + 5 * 60 * 1000,
})
responseData.code = code
console.log('[sso-mock] code generated →', code)
}
return NextResponse.json(responseData)
} catch (err: any) {
console.error('[sso-mock] login error:', err.message)
return NextResponse.json({ error: err?.message || '登录失败' })
}
}