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:
100
components/login/util.ts
Normal file
100
components/login/util.ts
Normal 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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user