Add OAuth 2.0 authorization flow documentation and troubleshooting guide

- Created AUTH/OAUTH_FLOW.MD to detail the OAuth 2.0 authorization process for SECTL Auth, including flow diagrams, detailed steps, token management, and security recommendations.
- Added AUTH/TROUBLESHOOTING.md to provide a comprehensive troubleshooting guide for common issues encountered during the OAuth process, covering authorization, token exchange, user info retrieval, remote logout, platform management, and network/CORS issues.
- Implemented OAuthCallback and OAuthLogin components in React to handle OAuth login flow, including deep linking and token exchange.
This commit is contained in:
NanGua-QWQ
2026-03-30 19:15:57 +08:00
parent 9c3e84d912
commit 9f2bf644b5
7 changed files with 2424 additions and 0 deletions
+49
View File
@@ -0,0 +1,49 @@
import { useEffect } from "react"
import { useSearchParams, useNavigate } from "react-router-dom"
export function OAuthCallback() {
const [searchParams] = useSearchParams()
const navigate = useNavigate()
useEffect(() => {
const code = searchParams.get("code")
const error = searchParams.get("error")
const errorDescription = searchParams.get("error_description")
if (error) {
window.postMessage(
{
error: errorDescription || error,
},
window.location.origin
)
navigate("/")
return
}
if (code) {
window.postMessage(
{
code,
},
window.location.origin
)
navigate("/")
}
}, [searchParams, navigate])
return (
<div
style={{
display: "flex",
justifyContent: "center",
alignItems: "center",
height: "100vh",
background: "var(--ss-bg-color)",
color: "var(--ss-text-main)",
}}
>
<div>...</div>
</div>
)
}
+183
View File
@@ -0,0 +1,183 @@
import { Button, message, Modal, Space, Spin } from "antd"
import { useEffect, useRef, useState } from "react"
import { useTranslation } from "react-i18next"
import { open } from "@tauri-apps/plugin-shell"
import { listen, UnlistenFn } from "@tauri-apps/api/event"
interface OAuthLoginProps {
visible: boolean
onClose: () => void
onSuccess: (userInfo: {
user_id: string
email: string
name: string
github_username?: string
permission: number
}) => void
}
interface OAuthConfig {
platform_id: string
platform_secret: string
callback_url: string
}
export function OAuthLogin({ visible, onClose, onSuccess }: OAuthLoginProps) {
const { t } = useTranslation()
const [loading, setLoading] = useState(false)
const deepLinkUnlistenRef = useRef<UnlistenFn | null>(null)
const getOAuthConfig = (): OAuthConfig | null => {
const api = (window as any).api
if (!api) return null
const platformId = import.meta.env.VITE_OAUTH_PLATFORM_ID
const platformSecret = import.meta.env.VITE_OAUTH_PLATFORM_SECRET
const callbackUrl = import.meta.env.VITE_OAUTH_CALLBACK_URL || "secscore://oauth/callback"
if (!platformId || !platformSecret) {
return null
}
return {
platform_id: platformId,
platform_secret: platformSecret,
callback_url: callbackUrl,
}
}
const handleDeepLink = async (url: string) => {
const config = getOAuthConfig()
if (!config) return
try {
const urlObj = new URL(url)
const code = urlObj.searchParams.get("code")
const error = urlObj.searchParams.get("error")
if (error) {
message.error(decodeURIComponent(error))
setLoading(false)
return
}
if (code) {
const api = (window as any).api
const tokenRes = await api.oauthExchangeCode(
code,
config.platform_id,
config.platform_secret,
config.callback_url
)
if (!tokenRes.success) {
message.error(tokenRes.message || "获取访问令牌失败")
setLoading(false)
return
}
const userRes = await api.oauthGetUserInfo(tokenRes.data.access_token)
if (!userRes.success) {
message.error(userRes.message || "获取用户信息失败")
setLoading(false)
return
}
onSuccess(userRes.data)
onClose()
}
} catch (error: any) {
message.error(error.message || "登录失败")
} finally {
setLoading(false)
}
}
useEffect(() => {
const setupDeepLink = async () => {
try {
const unlisten = await listen<string>("deep-link://new-url", (event) => {
if (event.payload) {
handleDeepLink(event.payload)
}
})
deepLinkUnlistenRef.current = unlisten
} catch (error) {
console.error("Failed to setup deep link listener:", error)
}
}
setupDeepLink()
return () => {
if (deepLinkUnlistenRef.current) {
deepLinkUnlistenRef.current()
deepLinkUnlistenRef.current = null
}
}
}, [])
const handleOAuthLogin = async () => {
const config = getOAuthConfig()
if (!config) {
message.error("OAuth 配置未设置")
return
}
setLoading(true)
try {
const api = (window as any).api
const urlRes = await api.oauthGetAuthorizationUrl(config.platform_id, config.callback_url)
if (!urlRes.success) {
message.error(urlRes.message || "获取授权链接失败")
setLoading(false)
return
}
await open(urlRes.data)
} catch (error: any) {
message.error(error.message || "登录失败")
setLoading(false)
}
}
return (
<Modal
title={t("auth.oauthLogin", "SECTL Auth 登录")}
open={visible}
onCancel={onClose}
footer={null}
width={400}
centered
>
<div style={{ textAlign: "center", padding: "20px 0" }}>
{loading ? (
<Space direction="vertical" size="middle">
<Spin size="large" />
<div>{t("auth.oauthLoggingIn", "正在登录...")}</div>
<div style={{ fontSize: "12px", color: "var(--ss-text-secondary)" }}>
</div>
</Space>
) : (
<Space direction="vertical" size="large" style={{ width: "100%" }}>
<div style={{ color: "var(--ss-text-secondary)" }}>
{t("auth.oauthHint", "使用 SECTL Auth 账号登录,享受统一认证和远程退登功能")}
</div>
<Button
type="primary"
size="large"
onClick={handleOAuthLogin}
style={{ width: "100%" }}
>
{t("auth.oauthButton", "使用 SECTL Auth 登录")}
</Button>
</Space>
)}
</div>
</Modal>
)
}