diff --git a/AGENTS.md b/AGENTS.md index a4e13aae..36c0d838 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -51,7 +51,7 @@ ## 严格遵循事项 (Guardrails) - 切勿删除 `frontend/node_modules` -- 保持 `internal/util/` 绝对纯净且不引入任何框架。禁止从 `internal/util/` 及其子包中导入 Gin、GORM、sessions 等 HTTP/Web/数据库相关框架包(例如,Web 会话选项已收敛至 `internal/apps/oauth/session.go`)。 +- 保持 `pkg/util/` 绝对纯净且不引入任何框架。禁止从 `pkg/util/` 及其子包中导入 Gin、GORM、sessions 等 HTTP/Web/数据库相关框架包(例如,Web 会话选项已收敛至 `internal/apps/oauth/session.go`)。 - 编写测试用例时,禁止使用硬编码的相对路径(如 `"uploads/test_cache"`)在源码目录下创建临时测试目录,必须统一使用 Go 内置的 `t.TempDir()` 以避免污染源码目录。 - 所有 HTTP 路由仅在 `internal/router/router.go` 中注册。 - 当 API Handler 发生变化时,更新 Swagger 文档(运行 `make swagger`)。 @@ -121,7 +121,7 @@ - `internal/infra/objectstore/`:S3 兼容对象存储适配,提供对象上传、读取、删除、CDN/代理读取及远端对象本地缓存。 - `internal/infra/task/`:Asynq 任务框架;参见 `new-async-task` 了解变更。 - `internal/shared/`:共享的通用模型及响应(如 `internal/shared/response`)、绑定(bind)、常量以及通用错误。 -- `internal/util/`:纯底层工具包,无任何 HTTP/数据库框架依赖。 +- `pkg/util/`:纯底层无副作用的系统工具(Crypto/Password/UUID、格式化、网络、版本比较等)。 - `internal/listener/`:域事件分发层。核心域(auth、user 等)在此定义并发射事件(如 `EmitAdminLoggedIn`);运维模块(push、webhook 等)在 bootstrap 阶段订阅,实现跨模块解耦。 - `internal/otel_trace/`:链路追踪(tracing)助手。 - `internal/testhelper/`:后端测试共享辅助能力。 @@ -135,7 +135,6 @@ - `pkg/push/`:推送渠道客户端集成(Lark/Telegram/Email)。 - `pkg/mail/`:邮件发送客户端。 - `pkg/trace/`:OpenTelemetry 链路追踪配置。 -- `pkg/util/`:纯底层无副作用的系统工具(Crypto/Password/UUID、格式化、网络、版本比较等)。 前端目录: diff --git a/frontend/lib/cap-solver.ts b/frontend/lib/cap-solver.ts index 7ca45473..6fac3a49 100644 --- a/frontend/lib/cap-solver.ts +++ b/frontend/lib/cap-solver.ts @@ -1,6 +1,6 @@ /** * Cap PoW (Proof-of-Work) 人机验证前端实现 - * 与后端 internal/util/cap 算法完全对应 + * 与后端 pkg/cap 算法完全对应 * * 求解在 Web Worker 中执行,不阻塞主线程 UI。 */ @@ -24,7 +24,7 @@ export interface RedeemResponse { // ——— Worker 代码(内联 Blob,避免独立文件的打包配置问题)——— // -// 算法与 Go 后端 internal/util/cap/cap.go + prng.go 完全对应: +// 算法与 Go 后端 pkg/cap/cap.go + prng.go 完全对应: // · FNV-1a 32-bit (Math.imul 保证 32-bit 截断) // · xorshift32 PRNG → hex 字符串 // · SubtleCrypto SHA-256 校验答案 diff --git a/internal/apps/oauth/oauth_test.go b/internal/apps/oauth/oauth_test.go index 5f05a700..1ae83bb0 100644 --- a/internal/apps/oauth/oauth_test.go +++ b/internal/apps/oauth/oauth_test.go @@ -37,7 +37,6 @@ import ( "github.com/Rain-kl/Wavelet/internal/model" "github.com/Rain-kl/Wavelet/internal/repository" "github.com/Rain-kl/Wavelet/internal/testhelper" - "github.com/Rain-kl/Wavelet/internal/util" ) // ----------------------------------------------------------------------------- @@ -453,7 +452,6 @@ func TestGetLoginSources(t *testing.T) { }, }, } - util.SetHTTPClient(httpMock) router := setupTestRouter(dbConn, mockRedis, httpMock) // Inject OIDC login enabled config @@ -527,7 +525,6 @@ func TestGetLoginURL(t *testing.T) { }, }, } - util.SetHTTPClient(httpMock) router := setupTestRouter(dbConn, mockRedis, httpMock) // Case 1: Default Login URL @@ -616,7 +613,6 @@ func TestAuthorize(t *testing.T) { }, }, } - util.SetHTTPClient(httpMock) router := setupTestRouter(dbConn, mockRedis, httpMock) // Case 1a: Active Source Authorize with purpose=bind without login -> 401 @@ -694,7 +690,6 @@ func TestCallbackLoginAndUserInfo(t *testing.T) { // 1. Mock the outgoing HTTP client for token exchange and user info fetching httpMock := newMockOIDCClient(testIssuerURL, testClientID, &state, "88888", "test_oauth_user", "oauth@linux.do", "Oauth Test User") - util.SetHTTPClient(httpMock) router := setupTestRouter(dbConn, mockRedis, httpMock) // Get Login URL first to initialize the session and generate the state @@ -774,7 +769,6 @@ func TestCallbackLoginAndUserInfo(t *testing.T) { var state2 string // Callback with same username but different external ID (99999) httpMock2 := newMockOIDCClient(testIssuerURL, testClientID, &state2, "99999", "test_oauth_user", "another@linux.do", "Another User") - util.SetHTTPClient(httpMock2) // Create another router for this mock client router2 := setupTestRouter(dbConn, mockRedis, httpMock2) @@ -826,7 +820,6 @@ func TestCallbackLoginAndUserInfo(t *testing.T) { var state4 string httpMock4 := newMockOIDCClient(testIssuerURL, testClientID, &state4, "77777", "need_bind_user", "needbind@linux.do", "Need Bind User") - util.SetHTTPClient(httpMock4) router4 := setupTestRouter(dbConn, mockRedis, httpMock4) wLogin4 := performRequest(router4, http.MethodGet, "/api/v1/oauth/login?source="+testSourceName, nil, nil, nil) @@ -898,7 +891,6 @@ func TestCallbackBind(t *testing.T) { var state string // Mock OIDC discovery, JWKS, and Token exchange for custom source (GitHub) httpMock := newMockOIDCClient("https://github.com", "gh_client", &state, "github_user_123", "github_tester", "tester@github.com", "GitHub Tester") - util.SetHTTPClient(httpMock) router := setupTestRouter(dbConn, mockRedis, httpMock) // Set up login helper @@ -1017,7 +1009,6 @@ func TestCallbackBind(t *testing.T) { var state3 string // Re-sign token for new state (since state serves as OIDC Nonce) httpMock3 := newMockOIDCClient("https://github.com", "gh_client", &state3, "github_user_123", "github_tester", "tester@github.com", "GitHub Tester") - util.SetHTTPClient(httpMock3) router3 := setupTestRouter(dbConn, mockRedis, httpMock3) // Generate state3 and SessionHash using activeCookie2 @@ -1131,7 +1122,6 @@ func TestOIDCPolicyEnforcement(t *testing.T) { // Set up mock client & router var state string httpMock := newMockOIDCClient(testIssuerURL, testClientID, &state, "88888", "test_oauth_user", "oauth@linux.do", "Oauth Test User") - util.SetHTTPClient(httpMock) router := setupTestRouter(dbConn, mockRedis, httpMock) // --- 1. Test GetLoginURL enforcement --- diff --git a/internal/util/custom_types.go b/internal/util/custom_types.go deleted file mode 100644 index f7dd38ff..00000000 --- a/internal/util/custom_types.go +++ /dev/null @@ -1,29 +0,0 @@ -// Copyright 2025 linux.do -// Copyright 2026 Arctel.net -// SPDX-License-Identifier: Apache-2.0 - -// Package util provides framework-agnostic helper types and HTTP utilities. -package util - -import ( - "database/sql/driver" - "encoding/json" - "fmt" -) - -// StringArray custom type for handling JSON arrays -type StringArray []string - -// Scan 实现 sql.Scanner 接口,从数据库读取 JSON 数组 -func (sa *StringArray) Scan(value interface{}) error { - bytesValue, ok := value.([]byte) - if !ok { - return fmt.Errorf(errInvalidCustomValue, value) - } - return json.Unmarshal(bytesValue, sa) -} - -// Value 实现 driver.Valuer 接口,将 JSON 数组序列化为数据库存储值 -func (sa StringArray) Value() (driver.Value, error) { - return json.Marshal(sa) -} diff --git a/internal/util/errs.go b/internal/util/errs.go deleted file mode 100644 index ed878b14..00000000 --- a/internal/util/errs.go +++ /dev/null @@ -1,10 +0,0 @@ -// Copyright 2026 Arctel.net -// SPDX-License-Identifier: Apache-2.0 - -package util - -const ( - errCreateHTTPRequestFailed = "创建HTTP请求失败: %w" - errHTTPRequestFailed = "请求%s接口失败: %w" - errInvalidCustomValue = "invalid value: %v" -) diff --git a/internal/util/http_clients.go b/internal/util/http_clients.go deleted file mode 100644 index a370e405..00000000 --- a/internal/util/http_clients.go +++ /dev/null @@ -1,68 +0,0 @@ -// Copyright 2025 linux.do -// Copyright 2026 Arctel.net -// SPDX-License-Identifier: Apache-2.0 - -package util - -import ( - "context" - "fmt" - "io" - "net/http" - "net/url" - "time" - - "github.com/Rain-kl/Wavelet/pkg/httppool" -) - -// IsLocalhost 检查 URL 是否为 localhost -func IsLocalhost(urlStr string) bool { - u, err := url.Parse(urlStr) - if err != nil { - return false - } - hostname := u.Hostname() - return hostname == "localhost" || hostname == "127.0.0.1" || hostname == "::1" -} - -// HTTP 客户端配置常量 -const ( - httpClientTimeout = 10 // HTTP 客户端超时时间(秒) - httpMaxIdleConns = 100 - httpMaxIdleConnsPerHost = 20 - httpIdleConnTimeout = 60 // 空闲连接超时(秒) -) - -// 配置HTTP客户端 使用 otelhttp 自动注入 trace span -var httpClient = &http.Client{ - Timeout: httpClientTimeout * time.Second, - Transport: httppool.DefaultTransport(), -} - -// SetHTTPClient 替换全局 HTTP 客户端实例 -func SetHTTPClient(c *http.Client) { - httpClient = c -} - -// Request 发送 HTTP 请求,支持自定义 Headers 和 Cookies -func Request(ctx context.Context, method, url string, body io.Reader, headers, cookies map[string]string) (*http.Response, error) { - req, err := http.NewRequestWithContext(ctx, method, url, body) - if err != nil { - return nil, fmt.Errorf(errCreateHTTPRequestFailed, err) - } - - for key, value := range cookies { - req.AddCookie(&http.Cookie{Name: key, Value: value}) //nolint:gosec // client-side cookies do not require server attributes (Secure/HttpOnly) - } - - for key, value := range headers { - req.Header.Set(key, value) - } - - resp, err := httpClient.Do(req) - if err != nil { - return nil, fmt.Errorf(errHTTPRequestFailed, url, err) - } - - return resp, nil -}