From 5176cc85db2783e1bffd4fc6acd601d8763fd28c Mon Sep 17 00:00:00 2001 From: wood chen Date: Sat, 14 Jun 2025 20:00:01 +0800 Subject: [PATCH] =?UTF-8?q?=E9=87=8D=E6=9E=84Handlers=E7=BB=93=E6=9E=84?= =?UTF-8?q?=EF=BC=8C=E6=96=B0=E5=A2=9EHandlePublicEndpoints=E6=96=B9?= =?UTF-8?q?=E6=B3=95=E4=BB=A5=E5=A4=84=E7=90=86=E5=85=AC=E5=BC=80=E7=AB=AF?= =?UTF-8?q?=E7=82=B9=E4=BF=A1=E6=81=AF=E8=AF=B7=E6=B1=82=EF=BC=8C=E4=BC=98?= =?UTF-8?q?=E5=8C=96URL=E7=BB=9F=E8=AE=A1=E7=BC=93=E5=AD=98=E9=80=BB?= =?UTF-8?q?=E8=BE=91=EF=BC=8C=E6=9B=B4=E6=96=B0Router=E4=BB=A5=E6=94=AF?= =?UTF-8?q?=E6=8C=81=E6=96=B0=E8=B7=AF=E7=94=B1=EF=BC=8C=E7=AE=80=E5=8C=96?= =?UTF-8?q?=E8=AE=A4=E8=AF=81=E4=BF=A1=E6=81=AF=E7=AE=A1=E7=90=86=EF=BC=8C?= =?UTF-8?q?=E7=A7=BB=E9=99=A4=E4=B8=8D=E5=86=8D=E4=BD=BF=E7=94=A8=E7=9A=84?= =?UTF-8?q?=E5=88=B7=E6=96=B0=E4=BB=A4=E7=89=8C=E9=80=BB=E8=BE=91=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- handlers/handlers.go | 97 +++++++++++++++++++++++++++++++++++++++++++- main.go | 4 +- middleware/auth.go | 90 ++++++++++++++++++++++++++++++++++++++++ router/router.go | 62 +++++++++++++++------------- web/app/page.tsx | 2 +- web/lib/auth.ts | 66 +++--------------------------- 6 files changed, 227 insertions(+), 94 deletions(-) create mode 100644 middleware/auth.go diff --git a/handlers/handlers.go b/handlers/handlers.go index 2236e80..f6d4ce4 100644 --- a/handlers/handlers.go +++ b/handlers/handlers.go @@ -12,6 +12,7 @@ import ( "random-api-go/stats" "random-api-go/utils" "strings" + "sync" "time" ) @@ -22,6 +23,19 @@ type Router interface { type Handlers struct { Stats *stats.StatsManager endpointService *services.EndpointService + urlStatsCache map[string]struct { + TotalURLs int `json:"total_urls"` + } + urlStatsCacheTime time.Time + urlStatsMutex sync.RWMutex + cacheDuration time.Duration +} + +func NewHandlers(statsManager *stats.StatsManager) *Handlers { + return &Handlers{ + Stats: statsManager, + cacheDuration: 5 * time.Minute, // 缓存5分钟 + } } func (h *Handlers) HandleAPIRequest(w http.ResponseWriter, r *http.Request) { @@ -116,10 +130,82 @@ func (h *Handlers) HandleStats(w http.ResponseWriter, r *http.Request) { } } +func (h *Handlers) HandlePublicEndpoints(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + + w.Header().Set("Content-Type", "application/json") + + // 使用端点服务获取端点信息 + if h.endpointService == nil { + h.endpointService = services.GetEndpointService() + } + + endpoints, err := h.endpointService.ListEndpoints() + if err != nil { + http.Error(w, "Error getting endpoints", http.StatusInternalServerError) + return + } + + // 只返回公开信息,不包含数据源配置 + type PublicEndpoint struct { + ID uint `json:"id"` + Name string `json:"name"` + URL string `json:"url"` + Description string `json:"description"` + IsActive bool `json:"is_active"` + ShowOnHomepage bool `json:"show_on_homepage"` + SortOrder int `json:"sort_order"` + CreatedAt string `json:"created_at"` + UpdatedAt string `json:"updated_at"` + } + + var publicEndpoints []PublicEndpoint + for _, endpoint := range endpoints { + publicEndpoints = append(publicEndpoints, PublicEndpoint{ + ID: endpoint.ID, + Name: endpoint.Name, + URL: endpoint.URL, + Description: endpoint.Description, + IsActive: endpoint.IsActive, + ShowOnHomepage: endpoint.ShowOnHomepage, + SortOrder: endpoint.SortOrder, + CreatedAt: endpoint.CreatedAt.Format(time.RFC3339), + UpdatedAt: endpoint.UpdatedAt.Format(time.RFC3339), + }) + } + + response := map[string]interface{}{ + "success": true, + "data": publicEndpoints, + } + + if err := json.NewEncoder(w).Encode(response); err != nil { + http.Error(w, "Error encoding response", http.StatusInternalServerError) + return + } +} + func (h *Handlers) HandleURLStats(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") - // 使用新的端点服务获取统计信息 + // 检查缓存是否有效 + h.urlStatsMutex.RLock() + if h.urlStatsCache != nil && time.Since(h.urlStatsCacheTime) < h.cacheDuration { + // 使用缓存数据 + cache := h.urlStatsCache + h.urlStatsMutex.RUnlock() + + if err := json.NewEncoder(w).Encode(cache); err != nil { + http.Error(w, "Error encoding response", http.StatusInternalServerError) + } + return + } + h.urlStatsMutex.RUnlock() + + // 缓存过期或不存在,重新计算 if h.endpointService == nil { h.endpointService = services.GetEndpointService() } @@ -166,6 +252,12 @@ func (h *Handlers) HandleURLStats(w http.ResponseWriter, r *http.Request) { } } + // 更新缓存 + h.urlStatsMutex.Lock() + h.urlStatsCache = response + h.urlStatsCacheTime = time.Now() + h.urlStatsMutex.Unlock() + if err := json.NewEncoder(w).Encode(response); err != nil { http.Error(w, "Error encoding response", http.StatusInternalServerError) return @@ -186,4 +278,7 @@ func (h *Handlers) Setup(r *router.Router) { r.HandleFunc("/api/stats", h.HandleStats) r.HandleFunc("/api/urlstats", h.HandleURLStats) r.HandleFunc("/api/metrics", h.HandleMetrics) + + // 公开的端点信息接口 + r.HandleFunc("/api/endpoints", h.HandlePublicEndpoints) } diff --git a/main.go b/main.go index 15edad7..d5f7c74 100644 --- a/main.go +++ b/main.go @@ -81,9 +81,7 @@ func (a *App) Initialize() error { } // 创建 handlers - handlers := &handlers.Handlers{ - Stats: a.Stats, - } + handlers := handlers.NewHandlers(a.Stats) // 设置路由 a.router.Setup(handlers) diff --git a/middleware/auth.go b/middleware/auth.go new file mode 100644 index 0000000..46ca66a --- /dev/null +++ b/middleware/auth.go @@ -0,0 +1,90 @@ +package middleware + +import ( + "encoding/json" + "fmt" + "log" + "net/http" + "strings" +) + +// UserInfo OAuth用户信息结构 +type UserInfo struct { + ID int `json:"id"` + Username string `json:"username"` + Nickname string `json:"nickname"` + Email string `json:"email"` + Avatar string `json:"avatar"` +} + +// AuthMiddleware 认证中间件 +type AuthMiddleware struct{} + +// NewAuthMiddleware 创建新的认证中间件 +func NewAuthMiddleware() *AuthMiddleware { + return &AuthMiddleware{} +} + +// RequireAuth 认证中间件,验证 OAuth 令牌 +func (am *AuthMiddleware) RequireAuth(next http.HandlerFunc) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + // 从 Authorization header 获取令牌 + authHeader := r.Header.Get("Authorization") + if authHeader == "" { + http.Error(w, "Authorization header required", http.StatusUnauthorized) + return + } + + // 检查 Bearer 前缀 + if !strings.HasPrefix(authHeader, "Bearer ") { + http.Error(w, "Invalid authorization header format", http.StatusUnauthorized) + return + } + + token := strings.TrimPrefix(authHeader, "Bearer ") + if token == "" { + http.Error(w, "Token required", http.StatusUnauthorized) + return + } + + // 验证令牌(通过调用用户信息接口) + userInfo, err := am.getUserInfo(token) + if err != nil { + log.Printf("Token validation failed: %v", err) + http.Error(w, "Invalid token", http.StatusUnauthorized) + return + } + + // 令牌有效,继续处理请求 + log.Printf("Authenticated user: %s (%s)", userInfo.Username, userInfo.Email) + next(w, r) + } +} + +// getUserInfo 通过访问令牌获取用户信息 +func (am *AuthMiddleware) getUserInfo(accessToken string) (*UserInfo, error) { + req, err := http.NewRequest("GET", "https://connect.czl.net/api/oauth2/userinfo", nil) + if err != nil { + return nil, fmt.Errorf("failed to create request: %w", err) + } + + req.Header.Set("Authorization", "Bearer "+accessToken) + + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + return nil, fmt.Errorf("failed to get user info: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("failed to get user info, status: %d", resp.StatusCode) + } + + var userInfo UserInfo + if err := json.NewDecoder(resp.Body).Decode(&userInfo); err != nil { + return nil, fmt.Errorf("failed to decode user info: %w", err) + } + + return &userInfo, nil +} diff --git a/router/router.go b/router/router.go index a1433f3..2f09677 100644 --- a/router/router.go +++ b/router/router.go @@ -2,14 +2,17 @@ package router import ( "net/http" + "random-api-go/middleware" "strings" ) type Router struct { - mux *http.ServeMux - staticHandler StaticHandler + mux *http.ServeMux + staticHandler StaticHandler + authMiddleware *middleware.AuthMiddleware } +// Handler 接口定义处理器需要的方法 type Handler interface { Setup(r *Router) } @@ -54,7 +57,8 @@ type AdminHandler interface { func New() *Router { return &Router{ - mux: http.NewServeMux(), + mux: http.NewServeMux(), + authMiddleware: middleware.NewAuthMiddleware(), } } @@ -70,44 +74,44 @@ func (r *Router) SetupStaticRoutes(staticHandler StaticHandler) { // SetupAdminRoutes 设置管理后台路由 func (r *Router) SetupAdminRoutes(adminHandler AdminHandler) { - // OAuth配置API(前端需要获取client_id等信息) + // OAuth配置API(前端需要获取client_id等信息)- 不需要认证 r.HandleFunc("/api/admin/oauth-config", adminHandler.GetOAuthConfig) - // OAuth令牌验证API(保留,以防需要) + // OAuth令牌验证API(保留,以防需要)- 不需要认证 r.HandleFunc("/api/admin/oauth-verify", adminHandler.VerifyOAuthToken) - // OAuth回调处理(使用API前缀以便区分前后端) + // OAuth回调处理(使用API前缀以便区分前后端)- 不需要认证 r.HandleFunc("/api/admin/oauth/callback", adminHandler.HandleOAuthCallback) - // 管理后台API路由 - r.HandleFunc("/api/admin/endpoints", adminHandler.HandleEndpoints) + // 管理后台API路由 - 需要认证 + r.HandleFunc("/api/admin/endpoints", r.authMiddleware.RequireAuth(adminHandler.HandleEndpoints)) - // 端点排序路由 - r.HandleFunc("/api/admin/endpoints/sort-order", adminHandler.UpdateEndpointSortOrder) + // 端点排序路由 - 需要认证 + r.HandleFunc("/api/admin/endpoints/sort-order", r.authMiddleware.RequireAuth(adminHandler.UpdateEndpointSortOrder)) - // 数据源路由 - 需要在端点路由之前注册,因为路径更具体 - r.HandleFunc("/api/admin/data-sources", adminHandler.CreateDataSource) + // 数据源路由 - 需要认证 + r.HandleFunc("/api/admin/data-sources", r.authMiddleware.RequireAuth(adminHandler.CreateDataSource)) - // 端点相关路由 - 使用通配符处理所有端点相关请求 - r.HandleFunc("/api/admin/endpoints/", func(w http.ResponseWriter, r *http.Request) { + // 端点相关路由 - 需要认证 + r.HandleFunc("/api/admin/endpoints/", r.authMiddleware.RequireAuth(func(w http.ResponseWriter, r *http.Request) { path := r.URL.Path if strings.Contains(path, "/data-sources") { adminHandler.HandleEndpointDataSources(w, r) } else { adminHandler.HandleEndpointByID(w, r) } - }) + })) - // 数据源操作路由 - 使用通配符处理所有数据源相关请求 - r.HandleFunc("/api/admin/data-sources/", func(w http.ResponseWriter, r *http.Request) { + // 数据源操作路由 - 需要认证 + r.HandleFunc("/api/admin/data-sources/", r.authMiddleware.RequireAuth(func(w http.ResponseWriter, r *http.Request) { path := r.URL.Path if strings.Contains(path, "/sync") { adminHandler.SyncDataSource(w, r) } else { adminHandler.HandleDataSourceByID(w, r) } - }) + })) - // URL替换规则路由 - r.HandleFunc("/api/admin/url-replace-rules", func(w http.ResponseWriter, r *http.Request) { + // URL替换规则路由 - 需要认证 + r.HandleFunc("/api/admin/url-replace-rules", r.authMiddleware.RequireAuth(func(w http.ResponseWriter, r *http.Request) { if r.Method == http.MethodGet { adminHandler.ListURLReplaceRules(w, r) } else if r.Method == http.MethodPost { @@ -115,27 +119,27 @@ func (r *Router) SetupAdminRoutes(adminHandler AdminHandler) { } else { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) } - }) - r.HandleFunc("/api/admin/url-replace-rules/", adminHandler.HandleURLReplaceRuleByID) + })) + r.HandleFunc("/api/admin/url-replace-rules/", r.authMiddleware.RequireAuth(adminHandler.HandleURLReplaceRuleByID)) - // 首页配置路由 - r.HandleFunc("/api/admin/home-config", func(w http.ResponseWriter, r *http.Request) { + // 首页配置路由 - 需要认证 + r.HandleFunc("/api/admin/home-config", r.authMiddleware.RequireAuth(func(w http.ResponseWriter, r *http.Request) { if r.Method == http.MethodGet { adminHandler.GetHomePageConfig(w, r) } else { adminHandler.UpdateHomePageConfig(w, r) } - }) + })) - // 通用配置管理路由 - r.HandleFunc("/api/admin/configs", adminHandler.ListConfigs) - r.HandleFunc("/api/admin/configs/", func(w http.ResponseWriter, r *http.Request) { + // 通用配置管理路由 - 需要认证 + r.HandleFunc("/api/admin/configs", r.authMiddleware.RequireAuth(adminHandler.ListConfigs)) + r.HandleFunc("/api/admin/configs/", r.authMiddleware.RequireAuth(func(w http.ResponseWriter, r *http.Request) { if r.Method == http.MethodDelete { adminHandler.DeleteConfigByKey(w, r) } else { adminHandler.CreateOrUpdateConfig(w, r) } - }) + })) } func (r *Router) HandleFunc(pattern string, handler func(http.ResponseWriter, *http.Request)) { diff --git a/web/app/page.tsx b/web/app/page.tsx index d158928..5332eb5 100644 --- a/web/app/page.tsx +++ b/web/app/page.tsx @@ -86,7 +86,7 @@ async function getSystemMetrics(): Promise { async function getEndpoints() { try { - const res = await apiFetch('/api/admin/endpoints') + const res = await apiFetch('/api/endpoints') if (!res.ok) { throw new Error('Failed to fetch endpoints') } diff --git a/web/lib/auth.ts b/web/lib/auth.ts index d003498..509dadb 100644 --- a/web/lib/auth.ts +++ b/web/lib/auth.ts @@ -1,7 +1,6 @@ import Cookies from 'js-cookie' const TOKEN_COOKIE_NAME = 'admin_token' -const REFRESH_TOKEN_COOKIE_NAME = 'admin_refresh_token' const USER_INFO_COOKIE_NAME = 'admin_user' // Cookie配置 @@ -19,13 +18,9 @@ export interface AuthUser { } // 保存认证信息 -export function saveAuthInfo(token: string, user: AuthUser, refreshToken?: string) { +export function saveAuthInfo(token: string, user: AuthUser) { Cookies.set(TOKEN_COOKIE_NAME, token, COOKIE_OPTIONS) Cookies.set(USER_INFO_COOKIE_NAME, JSON.stringify(user), COOKIE_OPTIONS) - - if (refreshToken) { - Cookies.set(REFRESH_TOKEN_COOKIE_NAME, refreshToken, COOKIE_OPTIONS) - } } // 获取访问令牌 @@ -33,11 +28,6 @@ export function getAccessToken(): string | null { return Cookies.get(TOKEN_COOKIE_NAME) || null } -// 获取刷新令牌 -export function getRefreshToken(): string | null { - return Cookies.get(REFRESH_TOKEN_COOKIE_NAME) || null -} - // 获取用户信息 export function getUserInfo(): AuthUser | null { const userStr = Cookies.get(USER_INFO_COOKIE_NAME) @@ -53,7 +43,6 @@ export function getUserInfo(): AuthUser | null { // 清除认证信息 export function clearAuthInfo() { Cookies.remove(TOKEN_COOKIE_NAME, { path: '/' }) - Cookies.remove(REFRESH_TOKEN_COOKIE_NAME, { path: '/' }) Cookies.remove(USER_INFO_COOKIE_NAME, { path: '/' }) } @@ -78,63 +67,20 @@ export async function authenticatedFetch(url: string, options: RequestInit = {}) const response = await fetch(url, { ...options, headers, - credentials: 'include', // 包含cookie }) - // 如果token过期,尝试刷新 + // 如果token过期或无效,清除认证信息并重定向到登录 if (response.status === 401) { - const refreshed = await refreshAccessToken() - if (refreshed) { - // 重新发送请求 - const newToken = getAccessToken() - if (newToken) { - headers['Authorization'] = `Bearer ${newToken}` - return fetch(url, { - ...options, - headers, - credentials: 'include', - }) - } - } - // 刷新失败,清除认证信息 clearAuthInfo() + // 可以选择重定向到登录页面或显示登录提示 + if (typeof window !== 'undefined') { + window.location.href = '/admin' + } } return response } -// 刷新访问令牌 -async function refreshAccessToken(): Promise { - const refreshToken = getRefreshToken() - if (!refreshToken) return false - - try { - const response = await fetch('/api/admin/auth/refresh', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ refresh_token: refreshToken }), - credentials: 'include', - }) - - if (response.ok) { - const data = await response.json() - if (data.success && data.data.access_token) { - const user = getUserInfo() - if (user) { - saveAuthInfo(data.data.access_token, user, data.data.refresh_token) - return true - } - } - } - } catch (error) { - console.error('Failed to refresh token:', error) - } - - return false -} - // OAuth状态管理 export function saveOAuthState(state: string) { sessionStorage.setItem('oauth_state', state)