refactor(api): extract repository layer and thin HTTP handlers

Introduce internal/repository for data access and cache-backed system
config reads. Move business logic into logics.go across admin push,
user, template, cache, system_config, and upload/handler packages.

Remove Gin from internal/util by relocating request-scoped helpers to
oauth/gin_context.go. Propagate request context for config lookups in
user flows. Slim model entities and delete model-level DB/cache helpers.

Wire handlers to logics/repository so targeted packages no longer call
db.DB directly. Update admin router tests to use ErrorHandlerMiddleware.
This commit is contained in:
ryan
2026-06-18 12:12:49 +08:00
parent e5b3a60f73
commit 1b2e083aec
77 changed files with 2370 additions and 1783 deletions
+5 -4
View File
@@ -9,12 +9,13 @@ import (
"strings"
"github.com/Rain-kl/Wavelet/internal/model"
"github.com/Rain-kl/Wavelet/internal/repository"
"github.com/coreos/go-oidc/v3/oidc"
"golang.org/x/oauth2"
)
func isOIDCLoginEnabled(ctx context.Context) bool {
enabled, err := model.GetBoolByKey(ctx, model.ConfigKeyOIDCLoginEnabled)
enabled, err := repository.GetBoolByKey(ctx, model.ConfigKeyOIDCLoginEnabled)
if err != nil {
return true
}
@@ -37,7 +38,7 @@ func resolveAuthSource(ctx context.Context, sourceName string) (*model.AuthSourc
}
func activeLoginSources(ctx context.Context) []AuthSourceView {
enabled, err := model.GetBoolByKey(ctx, model.ConfigKeyOIDCLoginEnabled)
enabled, err := repository.GetBoolByKey(ctx, model.ConfigKeyOIDCLoginEnabled)
if err == nil && !enabled {
return nil
}
@@ -62,8 +63,8 @@ func activeLoginSources(ctx context.Context) []AuthSourceView {
}
func getFrontendLoginRedirectURL(ctx context.Context) (string, error) {
var sc model.SystemConfig
if err := sc.GetByKey(ctx, model.ConfigKeyServerAddress); err != nil || strings.TrimSpace(sc.Value) == "" {
sc, err := repository.GetSystemConfigByKey(ctx, model.ConfigKeyServerAddress)
if err != nil || strings.TrimSpace(sc.Value) == "" {
return "", errors.New(errServerAddressMissing)
}
return strings.TrimRight(sc.Value, "/") + "/login", nil
+22
View File
@@ -0,0 +1,22 @@
// Copyright 2026 Arctel.net
// SPDX-License-Identifier: Apache-2.0
package oauth
import "github.com/gin-gonic/gin"
// GetFromContext 从 Gin 请求上下文获取指定类型的值。
func GetFromContext[T any](c *gin.Context, key string) (T, bool) {
value, exists := c.Get(key)
if !exists {
var zero T
return zero, false
}
typed, ok := value.(T)
return typed, ok
}
// SetToContext 设置值到 Gin 请求上下文。
func SetToContext[T any](c *gin.Context, key string, value T) {
c.Set(key, value)
}
+3 -2
View File
@@ -15,6 +15,7 @@ import (
"github.com/Rain-kl/Wavelet/internal/db"
"github.com/Rain-kl/Wavelet/internal/listener"
"github.com/Rain-kl/Wavelet/internal/model"
"github.com/Rain-kl/Wavelet/internal/repository"
"github.com/Rain-kl/Wavelet/pkg/logger"
"github.com/gin-contrib/sessions"
"github.com/gin-gonic/gin"
@@ -188,7 +189,7 @@ func handleCallbackLogin(ctx context.Context, c *gin.Context, source *model.Auth
// handleCallbackRegister 处理 OAuth 回调中的自动注册流程
// 若注册被禁用则保存 pending 信息并返回 false;若注册成功则返回新用户;若出错则返回 false
func handleCallbackRegister(ctx context.Context, c *gin.Context, source *model.AuthSource, userInfo *model.OAuthUserInfo) (model.User, bool) {
registrationEnabled, regErr := model.GetBoolByKey(ctx, model.ConfigKeyRegistrationEnabled)
registrationEnabled, regErr := repository.GetBoolByKey(ctx, model.ConfigKeyRegistrationEnabled)
if regErr != nil {
registrationEnabled = true
}
@@ -223,4 +224,4 @@ func handleCallbackRegister(ctx context.Context, c *gin.Context, source *model.A
logger.InfoF(ctx, "[LoginAudit] successful OAuth registration via source: %s, external ID: %s, user: %s, ID: %d, IP: %s", source.Name, userInfo.Sub, user.Username, user.ID, c.ClientIP())
return user, true
}
}
+7 -7
View File
@@ -12,7 +12,7 @@ import (
"github.com/Rain-kl/Wavelet/internal/common/response"
"github.com/Rain-kl/Wavelet/internal/db"
"github.com/Rain-kl/Wavelet/internal/model"
"github.com/Rain-kl/Wavelet/internal/util"
otel_trace "github.com/Rain-kl/Wavelet/pkg/trace"
"github.com/gin-contrib/sessions"
"github.com/gin-gonic/gin"
@@ -62,8 +62,8 @@ func GetUserFromRequest(c *gin.Context) (*model.User, error) {
if user.Username == "system" {
return nil, errors.New("system user is not allowed to login")
}
util.SetToContext(c, TokenAuthKey, true)
util.SetToContext(c, TokenAdminKey, tokenRecord.IsAdmin)
SetToContext(c, TokenAuthKey, true)
SetToContext(c, TokenAdminKey, tokenRecord.IsAdmin)
return user, nil
}
}
@@ -91,8 +91,8 @@ func GetUserFromRequest(c *gin.Context) (*model.User, error) {
}
// set keys in context for session auth
util.SetToContext(c, TokenAuthKey, false)
util.SetToContext(c, TokenAdminKey, false)
SetToContext(c, TokenAuthKey, false)
SetToContext(c, TokenAdminKey, false)
// 强行阻止 system 用户任何会话/Token 鉴权通过
if user.Username == "system" {
@@ -119,7 +119,7 @@ func LoginRequired() gin.HandlerFunc {
LogForAudit(ctx, user, c)
// set user info
util.SetToContext(c, UserObjKey, user)
SetToContext(c, UserObjKey, user)
// next
c.Next()
@@ -129,7 +129,7 @@ func LoginRequired() gin.HandlerFunc {
// DisallowTokenAuth 拒绝使用 Access Token 进行身份验证的请求访问该端点
func DisallowTokenAuth() gin.HandlerFunc {
return func(c *gin.Context) {
if tokenAuth, _ := util.GetFromContext[bool](c, TokenAuthKey); tokenAuth {
if tokenAuth, _ := GetFromContext[bool](c, TokenAuthKey); tokenAuth {
response.AbortForbidden(c, ErrTokenAuthNotAllowed)
return
}
+7 -6
View File
@@ -35,6 +35,7 @@ import (
"github.com/Rain-kl/Wavelet/internal/config"
"github.com/Rain-kl/Wavelet/internal/db"
"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"
)
@@ -1094,7 +1095,7 @@ func TestOIDCPolicyEnforcement(t *testing.T) {
Key: model.ConfigKeyOIDCLoginEnabled,
Value: "false",
})
mockRedis.Del(context.Background(), db.PrefixedKey(model.SystemConfigRedisHashKey)+":"+model.ConfigKeyOIDCLoginEnabled)
mockRedis.Del(context.Background(), db.PrefixedKey(repository.SystemConfigRedisHashKey)+":"+model.ConfigKeyOIDCLoginEnabled)
wLoginDisabled := performRequest(router, http.MethodGet, "/api/v1/oauth/login?source="+testSourceName, nil, nil, nil)
if wLoginDisabled.Code != http.StatusBadRequest {
t.Errorf("expected 400 when OIDC globally disabled, got %d", wLoginDisabled.Code)
@@ -1102,7 +1103,7 @@ func TestOIDCPolicyEnforcement(t *testing.T) {
// Re-enable globally, but deactivate source
dbConn.Model(&model.SystemConfig{}).Where("key = ?", model.ConfigKeyOIDCLoginEnabled).Update("value", "true")
mockRedis.Del(context.Background(), db.PrefixedKey(model.SystemConfigRedisHashKey)+":"+model.ConfigKeyOIDCLoginEnabled)
mockRedis.Del(context.Background(), db.PrefixedKey(repository.SystemConfigRedisHashKey)+":"+model.ConfigKeyOIDCLoginEnabled)
dbConn.Model(&model.AuthSource{}).Where("name = ?", testSourceName).Update("is_active", false)
wSourceInactive := performRequest(router, http.MethodGet, "/api/v1/oauth/login?source="+testSourceName, nil, nil, nil)
@@ -1113,7 +1114,7 @@ func TestOIDCPolicyEnforcement(t *testing.T) {
// --- 2. Test Authorize enforcement ---
// Deactivate globally again
dbConn.Model(&model.SystemConfig{}).Where("key = ?", model.ConfigKeyOIDCLoginEnabled).Update("value", "false")
mockRedis.Del(context.Background(), db.PrefixedKey(model.SystemConfigRedisHashKey)+":"+model.ConfigKeyOIDCLoginEnabled)
mockRedis.Del(context.Background(), db.PrefixedKey(repository.SystemConfigRedisHashKey)+":"+model.ConfigKeyOIDCLoginEnabled)
dbConn.Model(&model.AuthSource{}).Where("name = ?", testSourceName).Update("is_active", true)
wAuthDisabled := performRequest(router, http.MethodGet, "/api/v1/oauth/"+testSourceName+"/authorize", nil, nil, nil)
@@ -1124,7 +1125,7 @@ func TestOIDCPolicyEnforcement(t *testing.T) {
// --- 3. Test Callback enforcement ---
// Set up a valid state beforehand (when enabled)
dbConn.Model(&model.SystemConfig{}).Where("key = ?", model.ConfigKeyOIDCLoginEnabled).Update("value", "true")
mockRedis.Del(context.Background(), db.PrefixedKey(model.SystemConfigRedisHashKey)+":"+model.ConfigKeyOIDCLoginEnabled)
mockRedis.Del(context.Background(), db.PrefixedKey(repository.SystemConfigRedisHashKey)+":"+model.ConfigKeyOIDCLoginEnabled)
dbConn.Model(&model.AuthSource{}).Where("name = ?", testSourceName).Update("is_active", true)
wLogin := performRequest(router, http.MethodGet, "/api/v1/oauth/login?source="+testSourceName, nil, nil, nil)
@@ -1149,7 +1150,7 @@ func TestOIDCPolicyEnforcement(t *testing.T) {
// Now disable OIDC globally and attempt callback
dbConn.Model(&model.SystemConfig{}).Where("key = ?", model.ConfigKeyOIDCLoginEnabled).Update("value", "false")
mockRedis.Del(context.Background(), db.PrefixedKey(model.SystemConfigRedisHashKey)+":"+model.ConfigKeyOIDCLoginEnabled)
mockRedis.Del(context.Background(), db.PrefixedKey(repository.SystemConfigRedisHashKey)+":"+model.ConfigKeyOIDCLoginEnabled)
reqBody := fmt.Sprintf(`{"state":"%s","code":"test_auth_code"}`, state)
wCallbackDisabled := performRequest(router, http.MethodPost, "/api/v1/oauth/callback", []byte(reqBody), map[string]string{
"Content-Type": "application/json",
@@ -1160,7 +1161,7 @@ func TestOIDCPolicyEnforcement(t *testing.T) {
// Enable globally but deactivate source and attempt callback
dbConn.Model(&model.SystemConfig{}).Where("key = ?", model.ConfigKeyOIDCLoginEnabled).Update("value", "true")
mockRedis.Del(context.Background(), db.PrefixedKey(model.SystemConfigRedisHashKey)+":"+model.ConfigKeyOIDCLoginEnabled)
mockRedis.Del(context.Background(), db.PrefixedKey(repository.SystemConfigRedisHashKey)+":"+model.ConfigKeyOIDCLoginEnabled)
dbConn.Model(&model.AuthSource{}).Where("name = ?", testSourceName).Update("is_active", false)
// Since callback deletes state, we need to generate state again
+2 -2
View File
@@ -10,7 +10,7 @@ import ("net/http"
"github.com/Rain-kl/Wavelet/pkg/logger"
"github.com/gin-contrib/sessions"
"github.com/gin-gonic/gin"
"github.com/Rain-kl/Wavelet/internal/util"
"github.com/Rain-kl/Wavelet/internal/common/response")
@@ -60,7 +60,7 @@ func BuildBasicUserInfo(user *model.User, needChange bool) BasicUserInfo {
// @Router /api/v1/user-info [get]
// @Router /api/v1/user/self [get]
func UserInfo(c *gin.Context) {
user, _ := util.GetFromContext[*model.User](c, UserObjKey)
user, _ := GetFromContext[*model.User](c, UserObjKey)
session := sessions.Default(c)
needChange := session.Get("need_change_password") == true
+2 -1
View File
@@ -10,6 +10,7 @@ import (
"github.com/Rain-kl/Wavelet/internal/config"
"github.com/Rain-kl/Wavelet/internal/model"
"github.com/Rain-kl/Wavelet/internal/repository"
"github.com/gin-contrib/sessions"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
@@ -56,7 +57,7 @@ func setLoginSession(ctx context.Context, c *gin.Context, user *model.User) erro
maxAge := config.Config.App.SessionAge
isSessionCookie := false
ttlHours, err := model.GetIntByKey(ctx, model.ConfigKeyLoginSessionTTLHours)
ttlHours, err := repository.GetIntByKey(ctx, model.ConfigKeyLoginSessionTTLHours)
if err == nil {
switch {
case ttlHours == -1: