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:
zhangheng
2026-07-06 18:09:30 +08:00
commit 37f10118d3
33 changed files with 3058 additions and 0 deletions

100
components/login/util.ts Normal file
View File

@@ -0,0 +1,100 @@
import CryptoJS from 'crypto-js'
const CODE_CRYPTO_KEY = 'k#8Mp$2Qr&9Tz@4W'
export const APP_ID = 'cli_a56577acb2f3d00c' // 飞书应用 ID与 uirefbase 相同)
export const TENANT = 'coowa'
export const validatePhoneNumber = (str: string) => {
const reg = /^[1][3,4,5,6,7,8,9][0-9]{9}$/
return reg.test(str)
}
/** 获取 url 中的某个参数值 */
export const getQueryVariable = (variable: string) => {
if (typeof window === 'undefined') return false
const query = window.location.search.substring(1)
const vars = query.split('&')
for (let i = 0; i < vars.length; i++) {
const pair = vars[i].split('=')
if (pair[0] === variable) {
return pair[1]
}
}
return false
}
/** 加载飞书二维码 SDK */
export function qr_load() {
return new Promise<void>((resolve, reject) => {
// 如果已加载则跳过
if ((window as any).QRLogin) {
resolve()
return
}
const script = document.createElement('script')
script.type = 'text/javascript'
script.src =
'https://sf3-cn.feishucdn.com/obj/feishu-static/lark/passport/qrcode/LarkSSOSDKWebQRCode-1.0.2.js'
script.onerror = reject
script.onload = () => resolve()
document.head.appendChild(script)
})
}
/** 初始化飞书二维码 */
export async function qr_login(
appId: string,
redirect_url: string,
state: string
) {
const gotoUrl = `https://passport.feishu.cn/suite/passport/oauth/authorize?client_id=${appId}&redirect_uri=${encodeURIComponent(
redirect_url
)}&response_type=code&state=${encodeURIComponent(state)}`
const QRLogin = (window as any).QRLogin
const QRLoginObj = QRLogin({
id: 'login_container',
goto: gotoUrl,
style: `width: 270px;height: 270px;border: 0;border-radius:12px;
background-image:
radial-gradient(at 47% 33%, hsl(212.37, 72%, 59%) 0, transparent 59%),
radial-gradient(at 82% 65%, hsl(198.00, 100%, 50%) 0, transparent 55%);`,
})
const handleMessage = function (event: any) {
const origin = event.origin
if (
QRLoginObj.matchOrigin(origin) &&
window.location.href.indexOf('login') > -1
) {
const loginTmpCode = event.data
window.location.href = `${gotoUrl}&tmp_code=${loginTmpCode}`
}
}
if (typeof window.addEventListener !== 'undefined') {
window.addEventListener('message', handleMessage, false)
} else if (typeof (window as any).attachEvent !== 'undefined') {
;(window as any).attachEvent('onmessage', handleMessage)
}
}
/** 加密 — 与 uirefbase 客户端保持一致 */
export const encrypt = (data: string) => {
try {
return CryptoJS.AES.encrypt(data, CODE_CRYPTO_KEY).toString()
} catch (error) {
return data
}
}
/** 解密 */
export const decrypt = (data: string) => {
try {
const bytes = CryptoJS.AES.decrypt(data, CODE_CRYPTO_KEY)
return bytes.toString(CryptoJS.enc.Utf8)
} catch (error) {
return data
}
}