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>
This commit is contained in:
125
app/api/login/route.ts
Normal file
125
app/api/login/route.ts
Normal file
@@ -0,0 +1,125 @@
|
||||
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 || '登录失败' })
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user