重构Handlers结构,新增HandlePublicEndpoints方法以处理公开端点信息请求,优化URL统计缓存逻辑,更新Router以支持新路由,简化认证信息管理,移除不再使用的刷新令牌逻辑。

This commit is contained in:
wood chen 2025-06-14 20:00:01 +08:00
parent 0465adbb36
commit 5176cc85db
6 changed files with 227 additions and 94 deletions

View File

@ -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)
}

View File

@ -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)

90
middleware/auth.go Normal file
View File

@ -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
}

View File

@ -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)) {

View File

@ -86,7 +86,7 @@ async function getSystemMetrics(): Promise<SystemMetrics | null> {
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')
}

View File

@ -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<boolean> {
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)