mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-13 02:26:36 +08:00
fix(frontend): optimize
This commit is contained in:
@@ -32,12 +32,12 @@
|
|||||||
|
|
||||||
## 严格遵循事项 (Guardrails)
|
## 严格遵循事项 (Guardrails)
|
||||||
|
|
||||||
- 切勿删除 `frontend/node_modules`;如果需要刷新依赖,请使用 `pnpm install` 重新安装。
|
- 切勿删除 `frontend/node_modules`
|
||||||
- 保持 `internal/util/` 绝对纯净且不引入任何框架。禁止从 `internal/util/` 及其子包中导入 Gin、GORM、sessions 等 HTTP/Web/数据库相关框架包(例如,Web 会话选项已收敛至 `internal/apps/oauth/session.go`)。
|
- 保持 `internal/util/` 绝对纯净且不引入任何框架。禁止从 `internal/util/` 及其子包中导入 Gin、GORM、sessions 等 HTTP/Web/数据库相关框架包(例如,Web 会话选项已收敛至 `internal/apps/oauth/session.go`)。
|
||||||
- 编写测试用例时,禁止使用硬编码的相对路径(如 `"uploads/test_cache"`)在源码目录下创建临时测试目录,必须统一使用 Go 内置的 `t.TempDir()` 以避免污染源码目录。
|
- 编写测试用例时,禁止使用硬编码的相对路径(如 `"uploads/test_cache"`)在源码目录下创建临时测试目录,必须统一使用 Go 内置的 `t.TempDir()` 以避免污染源码目录。
|
||||||
- 所有 HTTP 路由仅在 `internal/router/router.go` 中注册。
|
- 所有 HTTP 路由仅在 `internal/router/router.go` 中注册。
|
||||||
- 当 API Handler 发生变化时,更新 Swagger 文档(运行 `make swagger`)。
|
- 当 API Handler 发生变化时,更新 Swagger 文档(运行 `make swagger`)。
|
||||||
- 在提交更改前运行 `make code-check`。
|
- 在完成代码开发后必须运行 `make code-check`, 并修复报错。
|
||||||
- 需要缓存或文件管理能力时,必须复用现有平台实现,禁止在业务包中自行创建缓存目录、直接管理缓存文件或重复封装存储后端。
|
- 需要缓存或文件管理能力时,必须复用现有平台实现,禁止在业务包中自行创建缓存目录、直接管理缓存文件或重复封装存储后端。
|
||||||
|
|
||||||
## 项目介绍
|
## 项目介绍
|
||||||
@@ -161,16 +161,6 @@ Handler 规范:
|
|||||||
- 不要创建物理数据库外键。改为关系字段添加显式索引。
|
- 不要创建物理数据库外键。改为关系字段添加显式索引。
|
||||||
- 数据库默认值必须与 Go 模型零值(`nil`、`0`、`false`、`""`)匹配,以避免意外的插入。
|
- 数据库默认值必须与 Go 模型零值(`nil`、`0`、`false`、`""`)匹配,以避免意外的插入。
|
||||||
|
|
||||||
严格依赖防线:
|
|
||||||
|
|
||||||
- `internal/util/` 及其子包必须保持无框架依赖。
|
|
||||||
- 不要从 `internal/util/` 中导入 `github.com/gin-gonic/gin`、`gorm.io/gorm`、`github.com/gin-contrib/sessions` 或 HTTP 中间件/框架包。
|
|
||||||
- 如果实用工具逻辑需要 web 胶水,请将纯验证/计算保留在 `internal/util/` 中,并将 Gin 中间件/响应处理放在 `internal/apps/` 中。
|
|
||||||
|
|
||||||
新增接口与模块开发工作流:
|
|
||||||
|
|
||||||
- 关于自定义业务接口(如 Admin/User/Custom 模块等)的详细包职责、文件结构和核心开发步骤,请直接阅读并严格遵循 [new-api](file:///Users/ryan/DEV/Go/Wavelet/.agent/skills/new-api/SKILL.md) 技能。
|
|
||||||
|
|
||||||
### 前端规则
|
### 前端规则
|
||||||
|
|
||||||
在进行任何 Next.js 工作之前,请在 `node_modules/next/dist/docs/` 中找到并阅读相关文档。您的训练数据已过时 —— 这些文档是唯一的真理来源。
|
在进行任何 Next.js 工作之前,请在 `node_modules/next/dist/docs/` 中找到并阅读相关文档。您的训练数据已过时 —— 这些文档是唯一的真理来源。
|
||||||
|
|||||||
+2
-1
@@ -7514,7 +7514,8 @@ const docTemplate = `{
|
|||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"id": {
|
"id": {
|
||||||
"type": "integer"
|
"type": "string",
|
||||||
|
"example": "0"
|
||||||
},
|
},
|
||||||
"is_active": {
|
"is_active": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
|
|||||||
+2
-1
@@ -7507,7 +7507,8 @@
|
|||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"id": {
|
"id": {
|
||||||
"type": "integer"
|
"type": "string",
|
||||||
|
"example": "0"
|
||||||
},
|
},
|
||||||
"is_active": {
|
"is_active": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
|
|||||||
+2
-1
@@ -1379,7 +1379,8 @@ definitions:
|
|||||||
gender:
|
gender:
|
||||||
type: string
|
type: string
|
||||||
id:
|
id:
|
||||||
type: integer
|
example: "0"
|
||||||
|
type: string
|
||||||
is_active:
|
is_active:
|
||||||
type: boolean
|
type: boolean
|
||||||
is_admin:
|
is_admin:
|
||||||
|
|||||||
@@ -19,12 +19,11 @@ limitations under the License.
|
|||||||
import {memo, useCallback, useEffect, useRef, useState} from "react"
|
import {memo, useCallback, useEffect, useRef, useState} from "react"
|
||||||
import {useVirtualizer} from "@tanstack/react-virtual"
|
import {useVirtualizer} from "@tanstack/react-virtual"
|
||||||
import {toast} from "sonner"
|
import {toast} from "sonner"
|
||||||
import {ArrowDown, ChevronUp, Loader2, Pause, Play} from "lucide-react"
|
import {ArrowDown, ChevronUp, Loader2} from "lucide-react"
|
||||||
|
|
||||||
import {AdminService} from "@/lib/services/admin"
|
import {AdminService} from "@/lib/services/admin"
|
||||||
import {ErrorInline} from "@/components/layout/error"
|
import {ErrorInline} from "@/components/layout/error"
|
||||||
import {LoadingStateWithBorder} from "@/components/layout/loading"
|
import {LoadingStateWithBorder} from "@/components/layout/loading"
|
||||||
import {Badge} from "@/components/ui/badge"
|
|
||||||
import {Button} from "@/components/ui/button"
|
import {Button} from "@/components/ui/button"
|
||||||
|
|
||||||
interface LogEntry {
|
interface LogEntry {
|
||||||
@@ -48,7 +47,7 @@ const LogLine = memo(function LogLine({data}: {data: string}) {
|
|||||||
: "text-gray-300"
|
: "text-gray-300"
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`${color} whitespace-pre-wrap break-all hover:bg-white/5`}>
|
<div className={`${color} whitespace-pre-wrap break-all px-2 py-0.5 rounded hover:bg-white/5`}>
|
||||||
{data}
|
{data}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@@ -59,7 +58,7 @@ function getApiBaseUrl(): string {
|
|||||||
// If NEXT_PUBLIC_WAVELET_BACKEND_URL is set, use it. Otherwise, use origin.
|
// If NEXT_PUBLIC_WAVELET_BACKEND_URL is set, use it. Otherwise, use origin.
|
||||||
const base = process.env.NEXT_PUBLIC_WAVELET_BACKEND_URL || ""
|
const base = process.env.NEXT_PUBLIC_WAVELET_BACKEND_URL || ""
|
||||||
if (base.startsWith("http")) return base
|
if (base.startsWith("http")) return base
|
||||||
|
|
||||||
// Relative URL fallback
|
// Relative URL fallback
|
||||||
const proto = window.location.protocol
|
const proto = window.location.protocol
|
||||||
const host = window.location.host
|
const host = window.location.host
|
||||||
@@ -92,19 +91,14 @@ export function AppLogs() {
|
|||||||
const [nextCursor, setNextCursor] = useState(0)
|
const [nextCursor, setNextCursor] = useState(0)
|
||||||
const [loadingMore, setLoadingMore] = useState(false)
|
const [loadingMore, setLoadingMore] = useState(false)
|
||||||
|
|
||||||
const [connected, setConnected] = useState(false)
|
|
||||||
const [paused, setPaused] = useState(false)
|
|
||||||
|
|
||||||
// autoScroll = true → new logs auto-scroll to bottom
|
// autoScroll = true → new logs auto-scroll to bottom
|
||||||
const [autoScroll, setAutoScroll] = useState(true)
|
const [autoScroll, setAutoScroll] = useState(true)
|
||||||
|
|
||||||
const containerRef = useRef<HTMLDivElement>(null)
|
const containerRef = useRef<HTMLDivElement>(null)
|
||||||
const wsRef = useRef<WebSocket | null>(null)
|
const wsRef = useRef<WebSocket | null>(null)
|
||||||
const pausedRef = useRef(paused)
|
|
||||||
const autoScrollRef = useRef(autoScroll)
|
const autoScrollRef = useRef(autoScroll)
|
||||||
const isUserScrolling = useRef(false)
|
const isUserScrolling = useRef(false)
|
||||||
|
|
||||||
useEffect(() => { pausedRef.current = paused }, [paused])
|
|
||||||
useEffect(() => { autoScrollRef.current = autoScroll }, [autoScroll])
|
useEffect(() => { autoScrollRef.current = autoScroll }, [autoScroll])
|
||||||
|
|
||||||
const rowVirtualizer = useVirtualizer({
|
const rowVirtualizer = useVirtualizer({
|
||||||
@@ -213,10 +207,7 @@ export function AppLogs() {
|
|||||||
const ws = new WebSocket(buildWsUrl())
|
const ws = new WebSocket(buildWsUrl())
|
||||||
wsRef.current = ws
|
wsRef.current = ws
|
||||||
|
|
||||||
ws.onopen = () => { setConnected(true) }
|
|
||||||
|
|
||||||
ws.onmessage = (event) => {
|
ws.onmessage = (event) => {
|
||||||
if (pausedRef.current) return
|
|
||||||
try {
|
try {
|
||||||
const msg = JSON.parse(event.data)
|
const msg = JSON.parse(event.data)
|
||||||
if (msg.type === "log" && msg.data) {
|
if (msg.type === "log" && msg.data) {
|
||||||
@@ -229,8 +220,7 @@ export function AppLogs() {
|
|||||||
} catch { /* ignore */ }
|
} catch { /* ignore */ }
|
||||||
}
|
}
|
||||||
|
|
||||||
ws.onclose = () => { setConnected(false); wsRef.current = null }
|
ws.onclose = () => { wsRef.current = null }
|
||||||
ws.onerror = () => { setConnected(false) }
|
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
// ---- Initialize ------------------------------------------------------
|
// ---- Initialize ------------------------------------------------------
|
||||||
@@ -253,8 +243,6 @@ export function AppLogs() {
|
|||||||
})
|
})
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const togglePause = useCallback(() => setPaused(p => !p), [])
|
|
||||||
const reconnect = useCallback(() => connectWs(), [connectWs])
|
|
||||||
const handleLoadMore = useCallback(() => {
|
const handleLoadMore = useCallback(() => {
|
||||||
if (nextCursor > 0) loadHistory(nextCursor)
|
if (nextCursor > 0) loadHistory(nextCursor)
|
||||||
}, [nextCursor, loadHistory])
|
}, [nextCursor, loadHistory])
|
||||||
@@ -265,38 +253,13 @@ export function AppLogs() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-full relative">
|
<div className="flex flex-col h-full relative">
|
||||||
{/* Sub Header / Control Bar */}
|
|
||||||
<div className="flex items-center justify-between pb-3 border-b border-border/40 mb-3">
|
|
||||||
<div className="text-sm text-muted-foreground font-medium">
|
|
||||||
系统后台实时输出的运行日志 (最多缓存 2000 行)
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Badge variant={connected ? "secondary" : "destructive"} className="h-6">
|
|
||||||
{connected ? "已连接" : "断开连接"}
|
|
||||||
</Badge>
|
|
||||||
{connected && (
|
|
||||||
<Button variant="outline" size="sm" onClick={togglePause} className="h-8">
|
|
||||||
{paused
|
|
||||||
? <><Play className="size-3.5 mr-1.5" />恢复</>
|
|
||||||
: <><Pause className="size-3.5 mr-1.5" />暂停</>
|
|
||||||
}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
{!connected && (
|
|
||||||
<Button variant="outline" size="sm" onClick={reconnect} className="h-8">
|
|
||||||
重新连接
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Log viewer — fixed height, scrollable */}
|
{/* Log viewer — fixed height, scrollable */}
|
||||||
<div
|
<div
|
||||||
ref={containerRef}
|
ref={containerRef}
|
||||||
onScroll={handleScroll}
|
onScroll={handleScroll}
|
||||||
onWheel={handleWheel}
|
onWheel={handleWheel}
|
||||||
onTouchStart={handleTouchStart}
|
onTouchStart={handleTouchStart}
|
||||||
className="h-[calc(100vh-270px)] overflow-y-auto overflow-x-hidden rounded-md border border-border/50 bg-[#0d1117] font-mono text-[13px] leading-5 relative"
|
className="h-[calc(100vh-270px)] overflow-y-auto overflow-x-hidden rounded-lg border border-border/40 bg-[#0d1117] font-mono text-[13px] leading-5 relative"
|
||||||
>
|
>
|
||||||
{/* Load older logs */}
|
{/* Load older logs */}
|
||||||
{hasMore && (
|
{hasMore && (
|
||||||
@@ -317,10 +280,10 @@ export function AppLogs() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{logs.length === 0 ? (
|
{logs.length === 0 ? (
|
||||||
<div className="px-3 py-8 text-center text-gray-500">暂无日志</div>
|
<div className="px-4 py-8 text-center text-gray-500">暂无日志</div>
|
||||||
) : (
|
) : (
|
||||||
<div
|
<div
|
||||||
className="relative px-3 py-2"
|
className="relative px-3 py-3"
|
||||||
style={{height: `${rowVirtualizer.getTotalSize()}px`}}
|
style={{height: `${rowVirtualizer.getTotalSize()}px`}}
|
||||||
>
|
>
|
||||||
{rowVirtualizer.getVirtualItems().map((virtualRow) => {
|
{rowVirtualizer.getVirtualItems().map((virtualRow) => {
|
||||||
|
|||||||
@@ -4,15 +4,19 @@
|
|||||||
// Package auth_source 提供认证源管理功能
|
// Package auth_source 提供认证源管理功能
|
||||||
package auth_source
|
package auth_source
|
||||||
|
|
||||||
import ("errors"
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/Rain-kl/Wavelet/internal/apps/admin"
|
"github.com/Rain-kl/Wavelet/internal/apps/admin"
|
||||||
|
"github.com/Rain-kl/Wavelet/internal/apps/oauth"
|
||||||
"github.com/Rain-kl/Wavelet/internal/model"
|
"github.com/Rain-kl/Wavelet/internal/model"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
|
||||||
"github.com/Rain-kl/Wavelet/internal/common/response")
|
"github.com/Rain-kl/Wavelet/internal/common/response"
|
||||||
|
)
|
||||||
|
|
||||||
// AuthSourceRequest 创建或更新认证源的请求参数
|
// AuthSourceRequest 创建或更新认证源的请求参数
|
||||||
type AuthSourceRequest struct {
|
type AuthSourceRequest struct {
|
||||||
@@ -119,6 +123,9 @@ func UpdateAuthSource(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 记录更新前的 Discovery URL,以便更新成功后清除旧缓存条目。
|
||||||
|
existing, _ := model.GetAuthSourceByID(c.Request.Context(), id)
|
||||||
|
|
||||||
source := model.AuthSource{
|
source := model.AuthSource{
|
||||||
ID: id,
|
ID: id,
|
||||||
Name: req.Name,
|
Name: req.Name,
|
||||||
@@ -136,6 +143,14 @@ func UpdateAuthSource(c *gin.Context) {
|
|||||||
c.JSON(http.StatusBadRequest, response.Err(err.Error()))
|
c.JSON(http.StatusBadRequest, response.Err(err.Error()))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Discovery URL 可能已变更,清除旧、新 issuer 的 provider 缓存,
|
||||||
|
// 确保下次登录时重新拉取最新 OIDC 元数据。
|
||||||
|
if existing != nil {
|
||||||
|
oauth.InvalidateOIDCProviderCache(normalizeIssuer(existing.OpenIDDiscoveryURL))
|
||||||
|
}
|
||||||
|
oauth.InvalidateOIDCProviderCache(normalizeIssuer(req.OpenIDDiscoveryURL))
|
||||||
|
|
||||||
updated, err := model.GetAuthSourceByID(c.Request.Context(), id)
|
updated, err := model.GetAuthSourceByID(c.Request.Context(), id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, response.Err(err.Error()))
|
c.JSON(http.StatusInternalServerError, response.Err(err.Error()))
|
||||||
@@ -219,3 +234,12 @@ func parseSourceID(c *gin.Context) (uint64, error) {
|
|||||||
}
|
}
|
||||||
return id, nil
|
return id, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// normalizeIssuer 将 Discovery URL 规范化为 issuer 基础 URL,
|
||||||
|
// 与 oauth.buildOAuthConfig 中的规范化逻辑保持一致。
|
||||||
|
func normalizeIssuer(discoveryURL string) string {
|
||||||
|
issuer := strings.TrimSuffix(strings.TrimSpace(discoveryURL), "/")
|
||||||
|
issuer = strings.TrimSuffix(issuer, "/.well-known/openid-configuration")
|
||||||
|
issuer = strings.TrimSuffix(issuer, "/.well-known/oauth-authorization-server")
|
||||||
|
return issuer
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,90 @@
|
|||||||
|
// Copyright 2026 Arctel.net
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package oauth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/coreos/go-oidc/v3/oidc"
|
||||||
|
"golang.org/x/sync/singleflight"
|
||||||
|
)
|
||||||
|
|
||||||
|
// oidcProviderCache 进程级 OIDC provider 缓存。
|
||||||
|
//
|
||||||
|
// oidc.NewProvider 每次调用都会向远端 issuer 的
|
||||||
|
// /.well-known/openid-configuration 发起 HTTP 请求拉取元数据。
|
||||||
|
// 由于 provider 元数据极少变动,将其缓存后可消除登录发起与回调时的
|
||||||
|
// 重复外部 HTTP 往返。
|
||||||
|
//
|
||||||
|
// 并发安全性:
|
||||||
|
// - mu + entries 防止并发读写 map。
|
||||||
|
// - sfGroup 保证同一 issuer 同时只有一次在途的 NewProvider 调用
|
||||||
|
// (singleflight),后续等待者复用同一结果,彻底消除 thundering herd。
|
||||||
|
type oidcProviderCache struct {
|
||||||
|
mu sync.RWMutex
|
||||||
|
entries map[string]*oidc.Provider // key: normalized issuer URL
|
||||||
|
sfGroup singleflight.Group
|
||||||
|
}
|
||||||
|
|
||||||
|
// globalOIDCProviderCache 是包级单例缓存,与进程同生命周期。
|
||||||
|
var globalOIDCProviderCache = &oidcProviderCache{
|
||||||
|
entries: make(map[string]*oidc.Provider),
|
||||||
|
}
|
||||||
|
|
||||||
|
// get 返回缓存的 provider;若无则通过 oidc.NewProvider 获取并写入缓存。
|
||||||
|
// 同一 issuer 并发调用时,singleflight 保证只有一次实际 HTTP 请求。
|
||||||
|
//
|
||||||
|
// 注意:接受 _ 形参以与调用方类型一致,内部有意使用 context.Background() 而非传入的请求 ctx,
|
||||||
|
// 以防止请求被提前取消时导致缓存写入失败。
|
||||||
|
func (c *oidcProviderCache) get(_ context.Context, issuer string) (*oidc.Provider, error) { //nolint:contextcheck // intentional: use Background to avoid request cancellation affecting cache write
|
||||||
|
// 快路径:已有缓存则直接返回。
|
||||||
|
c.mu.RLock()
|
||||||
|
if p, ok := c.entries[issuer]; ok {
|
||||||
|
c.mu.RUnlock()
|
||||||
|
return p, nil
|
||||||
|
}
|
||||||
|
c.mu.RUnlock()
|
||||||
|
|
||||||
|
// 慢路径:通过 singleflight 合并并发的首次请求。
|
||||||
|
// 闭包内有意使用 context.Background() 而非请求 ctx,防止请求取消导致缓存写入失败。
|
||||||
|
v, err, _ := c.sfGroup.Do(issuer, func() (any, error) { //nolint:contextcheck // intentional: Background ctx prevents cache write failure on request cancellation
|
||||||
|
// 双检:singleflight 内再次检查,前一个并发组可能已写入缓存。
|
||||||
|
c.mu.RLock()
|
||||||
|
if p, ok := c.entries[issuer]; ok {
|
||||||
|
c.mu.RUnlock()
|
||||||
|
return p, nil
|
||||||
|
}
|
||||||
|
c.mu.RUnlock()
|
||||||
|
|
||||||
|
p, err := oidc.NewProvider(context.Background(), issuer)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
c.mu.Lock()
|
||||||
|
c.entries[issuer] = p
|
||||||
|
c.mu.Unlock()
|
||||||
|
return p, nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return v.(*oidc.Provider), nil //nolint:forcetypeassert // singleflight value 由同函数写入,类型确定
|
||||||
|
}
|
||||||
|
|
||||||
|
// invalidate 从缓存中移除指定 issuer 对应的 provider。
|
||||||
|
// 在认证源的 Discovery URL 被修改时调用,强制下次请求重新拉取元数据。
|
||||||
|
func (c *oidcProviderCache) invalidate(issuer string) {
|
||||||
|
c.mu.Lock()
|
||||||
|
delete(c.entries, issuer)
|
||||||
|
c.mu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
// InvalidateOIDCProviderCache 从进程级缓存中清除指定 issuer 的 provider 条目。
|
||||||
|
// 当管理员更新认证源的 Discovery URL 后调用,以确保下次登录时重新拉取最新元数据。
|
||||||
|
// issuer 值应为去掉 /.well-known/openid-configuration 后缀的规范化 URL。
|
||||||
|
func InvalidateOIDCProviderCache(issuer string) {
|
||||||
|
globalOIDCProviderCache.invalidate(issuer)
|
||||||
|
}
|
||||||
@@ -3,7 +3,8 @@
|
|||||||
|
|
||||||
package oauth
|
package oauth
|
||||||
|
|
||||||
import ("context"
|
import (
|
||||||
|
"context"
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"errors"
|
"errors"
|
||||||
@@ -15,6 +16,7 @@ import ("context"
|
|||||||
|
|
||||||
"github.com/Rain-kl/Wavelet/internal/apps/admin/push/custom_events"
|
"github.com/Rain-kl/Wavelet/internal/apps/admin/push/custom_events"
|
||||||
"github.com/Rain-kl/Wavelet/internal/common"
|
"github.com/Rain-kl/Wavelet/internal/common"
|
||||||
|
"github.com/Rain-kl/Wavelet/internal/common/response"
|
||||||
"github.com/Rain-kl/Wavelet/internal/config"
|
"github.com/Rain-kl/Wavelet/internal/config"
|
||||||
"github.com/Rain-kl/Wavelet/internal/db"
|
"github.com/Rain-kl/Wavelet/internal/db"
|
||||||
"github.com/Rain-kl/Wavelet/internal/model"
|
"github.com/Rain-kl/Wavelet/internal/model"
|
||||||
@@ -25,7 +27,6 @@ import ("context"
|
|||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"golang.org/x/oauth2"
|
"golang.org/x/oauth2"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
"github.com/Rain-kl/Wavelet/internal/common/response"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// AuthSourceView 登录源展示信息
|
// AuthSourceView 登录源展示信息
|
||||||
@@ -161,7 +162,9 @@ func buildOAuthConfig(ctx context.Context, source *model.AuthSource, redirectURL
|
|||||||
issuer = strings.TrimSuffix(issuer, "/.well-known/openid-configuration")
|
issuer = strings.TrimSuffix(issuer, "/.well-known/openid-configuration")
|
||||||
issuer = strings.TrimSuffix(issuer, "/.well-known/oauth-authorization-server")
|
issuer = strings.TrimSuffix(issuer, "/.well-known/oauth-authorization-server")
|
||||||
|
|
||||||
provider, err := oidc.NewProvider(ctx, issuer)
|
// 使用进程级缓存获取 provider,避免每次调用都向 issuer 发起
|
||||||
|
// /.well-known/openid-configuration HTTP 请求。
|
||||||
|
provider, err := globalOIDCProviderCache.get(ctx, issuer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user