mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-05 15:26:36 +08:00
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:
@@ -5,17 +5,19 @@
|
||||
// Package user 提供用户认证与帐户管理功能
|
||||
package user
|
||||
|
||||
import ("net/http"
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/Rain-kl/Wavelet/internal/apps/oauth"
|
||||
"github.com/Rain-kl/Wavelet/internal/db"
|
||||
"github.com/Rain-kl/Wavelet/internal/model"
|
||||
"github.com/Rain-kl/Wavelet/internal/util"
|
||||
"github.com/Rain-kl/Wavelet/internal/repository"
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"github.com/Rain-kl/Wavelet/internal/common/response")
|
||||
"github.com/Rain-kl/Wavelet/internal/common/response"
|
||||
)
|
||||
|
||||
type createTokenRequest struct {
|
||||
Name string `json:"name"`
|
||||
@@ -38,7 +40,7 @@ type tokenResponse struct {
|
||||
// @Router /api/v1/user/access-tokens [get]
|
||||
// ListAccessTokens 获取当前用户的 AccessToken 列表
|
||||
func ListAccessTokens(c *gin.Context) {
|
||||
currUser, _ := util.GetFromContext[*model.User](c, oauth.UserObjKey)
|
||||
currUser, _ := oauth.GetFromContext[*model.User](c, oauth.UserObjKey)
|
||||
ctx := c.Request.Context()
|
||||
|
||||
var tokens []model.AccessToken
|
||||
@@ -62,7 +64,7 @@ func ListAccessTokens(c *gin.Context) {
|
||||
// @Failure 400 {object} response.Any "参数错误或超限"
|
||||
// @Router /api/v1/user/access-tokens [post]
|
||||
func CreateAccessToken(c *gin.Context) {
|
||||
currUser, _ := util.GetFromContext[*model.User](c, oauth.UserObjKey)
|
||||
currUser, _ := oauth.GetFromContext[*model.User](c, oauth.UserObjKey)
|
||||
ctx := c.Request.Context()
|
||||
|
||||
var req createTokenRequest
|
||||
@@ -85,7 +87,7 @@ func CreateAccessToken(c *gin.Context) {
|
||||
|
||||
// 检查最大限制(基于 ConfigKeyMaxAPIKeysPerUser 配置,默认值为 5)
|
||||
maxLimit := 5
|
||||
if val, err := model.GetIntByKey(ctx, model.ConfigKeyMaxAPIKeysPerUser); err == nil {
|
||||
if val, err := repository.GetIntByKey(ctx, model.ConfigKeyMaxAPIKeysPerUser); err == nil {
|
||||
maxLimit = val
|
||||
}
|
||||
|
||||
@@ -140,7 +142,7 @@ func CreateAccessToken(c *gin.Context) {
|
||||
// @Failure 400 {object} response.Any "参数错误"
|
||||
// @Router /api/v1/user/access-tokens/{id} [delete]
|
||||
func DeleteAccessToken(c *gin.Context) {
|
||||
currUser, _ := util.GetFromContext[*model.User](c, oauth.UserObjKey)
|
||||
currUser, _ := oauth.GetFromContext[*model.User](c, oauth.UserObjKey)
|
||||
ctx := c.Request.Context()
|
||||
|
||||
idStr := c.Param("id")
|
||||
@@ -175,7 +177,7 @@ func DeleteAccessToken(c *gin.Context) {
|
||||
// @Failure 400 {object} response.Any "参数错误"
|
||||
// @Router /api/v1/user/access-tokens/{id}/rotate [post]
|
||||
func RotateAccessToken(c *gin.Context) {
|
||||
currUser, _ := util.GetFromContext[*model.User](c, oauth.UserObjKey)
|
||||
currUser, _ := oauth.GetFromContext[*model.User](c, oauth.UserObjKey)
|
||||
ctx := c.Request.Context()
|
||||
|
||||
idStr := c.Param("id")
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
|
||||
"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/task"
|
||||
pkgu "github.com/Rain-kl/Wavelet/pkg/util"
|
||||
)
|
||||
@@ -47,8 +48,32 @@ type updateProfileInput struct {
|
||||
Location string
|
||||
}
|
||||
|
||||
func isPasswordLoginEnabled(ctx context.Context) bool {
|
||||
enabled, err := repository.GetBoolByKey(ctx, model.ConfigKeyPasswordLoginEnabled)
|
||||
if err != nil {
|
||||
return true
|
||||
}
|
||||
return enabled
|
||||
}
|
||||
|
||||
func isPasswordRegisterEnabled(ctx context.Context) bool {
|
||||
enabled, err := repository.GetBoolByKey(ctx, model.ConfigKeyPasswordRegisterEnabled)
|
||||
if err != nil {
|
||||
return true
|
||||
}
|
||||
return enabled
|
||||
}
|
||||
|
||||
func isRegistrationEnabled(ctx context.Context) bool {
|
||||
enabled, err := repository.GetBoolByKey(ctx, model.ConfigKeyRegistrationEnabled)
|
||||
if err != nil {
|
||||
return true
|
||||
}
|
||||
return enabled
|
||||
}
|
||||
|
||||
func isEmailLoginVerificationEnabled(ctx context.Context) bool {
|
||||
enabled, err := model.GetBoolByKey(ctx, model.ConfigKeyEmailLoginVerificationEnabled)
|
||||
enabled, err := repository.GetBoolByKey(ctx, model.ConfigKeyEmailLoginVerificationEnabled)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
@@ -56,7 +81,7 @@ func isEmailLoginVerificationEnabled(ctx context.Context) bool {
|
||||
}
|
||||
|
||||
func isEmailRegisterVerificationEnabled(ctx context.Context) bool {
|
||||
enabled, err := model.GetBoolByKey(ctx, model.ConfigKeyEmailRegisterVerificationEnabled)
|
||||
enabled, err := repository.GetBoolByKey(ctx, model.ConfigKeyEmailRegisterVerificationEnabled)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
@@ -64,26 +89,14 @@ func isEmailRegisterVerificationEnabled(ctx context.Context) bool {
|
||||
}
|
||||
|
||||
func isSMTPConfigured(ctx context.Context) bool {
|
||||
var host, port, username, password string
|
||||
|
||||
var scHost model.SystemConfig
|
||||
if err := scHost.GetByKey(ctx, model.ConfigKeySMTPHost); err == nil {
|
||||
host = scHost.Value
|
||||
scHost, errHost := repository.GetSystemConfigByKey(ctx, model.ConfigKeySMTPHost)
|
||||
scPort, errPort := repository.GetSystemConfigByKey(ctx, model.ConfigKeySMTPPort)
|
||||
scUser, errUser := repository.GetSystemConfigByKey(ctx, model.ConfigKeySMTPUsername)
|
||||
scPass, errPass := repository.GetSystemConfigByKey(ctx, model.ConfigKeySMTPPassword)
|
||||
if errHost != nil || errPort != nil || errUser != nil || errPass != nil {
|
||||
return false
|
||||
}
|
||||
var scPort model.SystemConfig
|
||||
if err := scPort.GetByKey(ctx, model.ConfigKeySMTPPort); err == nil {
|
||||
port = scPort.Value
|
||||
}
|
||||
var scUser model.SystemConfig
|
||||
if err := scUser.GetByKey(ctx, model.ConfigKeySMTPUsername); err == nil {
|
||||
username = scUser.Value
|
||||
}
|
||||
var scPass model.SystemConfig
|
||||
if err := scPass.GetByKey(ctx, model.ConfigKeySMTPPassword); err == nil {
|
||||
password = scPass.Value
|
||||
}
|
||||
|
||||
return host != "" && port != "" && username != "" && password != ""
|
||||
return scHost.Value != "" && scPort.Value != "" && scUser.Value != "" && scPass.Value != ""
|
||||
}
|
||||
|
||||
func generateVerificationCode() (string, error) {
|
||||
@@ -114,11 +127,11 @@ func sendEmailVerificationCode(ctx context.Context, email, scene, templateName s
|
||||
codeKey := getEmailCodeKey(scene, email)
|
||||
cooldownKey := getEmailCooldownKey(scene, email)
|
||||
|
||||
emailSubject, emailBody, err := model.RenderTemplate(
|
||||
ctx,
|
||||
templateName,
|
||||
map[string]any{"Code": code},
|
||||
)
|
||||
tmpl, err := repository.GetTemplateByKey(ctx, templateName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("模板 %s 不存在或不可用: %w", templateName, err)
|
||||
}
|
||||
emailSubject, emailBody, err := tmpl.Render(map[string]any{"Code": code})
|
||||
if err != nil {
|
||||
return fmt.Errorf(errRenderEmailTemplateFailed, err)
|
||||
}
|
||||
@@ -271,4 +284,4 @@ func updateUserProfile(ctx context.Context, userID uint64, input updateProfileIn
|
||||
return nil, err
|
||||
}
|
||||
return &dbUser, nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
|
||||
"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"
|
||||
)
|
||||
|
||||
@@ -39,7 +40,7 @@ func TestProcessLoginEmailVerificationSMTPFallback(t *testing.T) {
|
||||
Update("value", "").Error; err != nil {
|
||||
t.Fatalf("clear SMTP host failed: %v", err)
|
||||
}
|
||||
if err := db.Redis.Del(context.Background(), db.PrefixedKey(model.SystemConfigRedisHashKey)).Err(); err != nil {
|
||||
if err := db.Redis.Del(context.Background(), db.PrefixedKey(repository.SystemConfigRedisHashKey)).Err(); err != nil {
|
||||
t.Fatalf("invalidate system config cache failed: %v", err)
|
||||
}
|
||||
|
||||
@@ -104,7 +105,7 @@ func TestProcessLoginEmailVerificationEmptyEmailFallback(t *testing.T) {
|
||||
t.Fatalf("set %s failed: %v", cfg.key, err)
|
||||
}
|
||||
}
|
||||
if err := db.Redis.Del(context.Background(), db.PrefixedKey(model.SystemConfigRedisHashKey)).Err(); err != nil {
|
||||
if err := db.Redis.Del(context.Background(), db.PrefixedKey(repository.SystemConfigRedisHashKey)).Err(); err != nil {
|
||||
t.Fatalf("invalidate system config cache failed: %v", err)
|
||||
}
|
||||
|
||||
@@ -148,4 +149,4 @@ func TestProcessLoginEmailVerificationInvalidCode(t *testing.T) {
|
||||
if result.Status != LoginEmailVerificationRejected || result.Message != errEmailCodeInvalidOrExpired {
|
||||
t.Fatalf("processLoginEmailVerification() = %+v, want rejected invalid code", result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,23 +3,24 @@
|
||||
|
||||
package user
|
||||
|
||||
import ("context"
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/Rain-kl/Wavelet/internal/apps/oauth"
|
||||
"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/db"
|
||||
"github.com/Rain-kl/Wavelet/internal/db/idgen"
|
||||
"github.com/Rain-kl/Wavelet/internal/listener"
|
||||
"github.com/Rain-kl/Wavelet/internal/model"
|
||||
"github.com/Rain-kl/Wavelet/internal/util"
|
||||
"github.com/Rain-kl/Wavelet/internal/repository"
|
||||
"github.com/Rain-kl/Wavelet/pkg/logger"
|
||||
"github.com/gin-contrib/sessions"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/Rain-kl/Wavelet/internal/common/response"
|
||||
)
|
||||
|
||||
type loginRequest struct {
|
||||
@@ -53,30 +54,6 @@ type updateProfileRequest struct {
|
||||
Location string `json:"location"`
|
||||
}
|
||||
|
||||
func isPasswordLoginEnabled() bool {
|
||||
enabled, err := model.GetBoolByKey(context.Background(), model.ConfigKeyPasswordLoginEnabled)
|
||||
if err != nil {
|
||||
return true
|
||||
}
|
||||
return enabled
|
||||
}
|
||||
|
||||
func isPasswordRegisterEnabled() bool {
|
||||
enabled, err := model.GetBoolByKey(context.Background(), model.ConfigKeyPasswordRegisterEnabled)
|
||||
if err != nil {
|
||||
return true
|
||||
}
|
||||
return enabled
|
||||
}
|
||||
|
||||
func isRegistrationEnabled() bool {
|
||||
enabled, err := model.GetBoolByKey(context.Background(), model.ConfigKeyRegistrationEnabled)
|
||||
if err != nil {
|
||||
return true
|
||||
}
|
||||
return enabled
|
||||
}
|
||||
|
||||
func setLoginSession(ctx context.Context, c *gin.Context, user *model.User) error {
|
||||
session := sessions.Default(c)
|
||||
session.Set(oauth.UserIDKey, user.ID)
|
||||
@@ -87,7 +64,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:
|
||||
@@ -124,7 +101,8 @@ func setLoginSession(ctx context.Context, c *gin.Context, user *model.User) erro
|
||||
// @Failure 500 {object} response.Any "服务内部错误"
|
||||
// @Router /api/v1/user/login [post]
|
||||
func Login(c *gin.Context) {
|
||||
if !isPasswordLoginEnabled() {
|
||||
ctx := c.Request.Context()
|
||||
if !isPasswordLoginEnabled(ctx) {
|
||||
response.AbortBadRequest(c, errPasswordLoginDisabled)
|
||||
return
|
||||
}
|
||||
@@ -140,7 +118,6 @@ func Login(c *gin.Context) {
|
||||
}
|
||||
|
||||
var user model.User
|
||||
ctx := c.Request.Context()
|
||||
if err := db.DB(ctx).Where("username = ? OR email = ?", req.Username, req.Username).First(&user).Error; err != nil {
|
||||
logger.WarnF(ctx, "[LoginAudit] failed login attempt (username not found) for input: %s, IP: %s", req.Username, c.ClientIP())
|
||||
response.AbortBadRequest(c, errUsernameOrPasswordWrong)
|
||||
@@ -211,7 +188,8 @@ func Login(c *gin.Context) {
|
||||
// @Failure 500 {object} response.Any "服务内部错误"
|
||||
// @Router /api/v1/user/register [post]
|
||||
func Register(c *gin.Context) {
|
||||
if !isRegistrationEnabled() || !isPasswordRegisterEnabled() {
|
||||
ctx := c.Request.Context()
|
||||
if !isRegistrationEnabled(ctx) || !isPasswordRegisterEnabled(ctx) {
|
||||
response.AbortBadRequest(c, errRegistrationDisabled)
|
||||
return
|
||||
}
|
||||
@@ -242,8 +220,6 @@ func Register(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
ctx := c.Request.Context()
|
||||
|
||||
// 邮箱注册验证校验
|
||||
if err := validateRegisterEmailVerification(ctx, req.Email, req.Code); err != nil {
|
||||
response.AbortBadRequest(c, err.Error())
|
||||
@@ -344,7 +320,7 @@ func ChangePassword(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
userObj, _ := util.GetFromContext[*model.User](c, oauth.UserObjKey)
|
||||
userObj, _ := oauth.GetFromContext[*model.User](c, oauth.UserObjKey)
|
||||
if userObj == nil {
|
||||
response.AbortUnauthorized(c, errLoginRequired)
|
||||
return
|
||||
@@ -443,7 +419,7 @@ func UpdateProfile(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
userObj, _ := util.GetFromContext[*model.User](c, oauth.UserObjKey)
|
||||
userObj, _ := oauth.GetFromContext[*model.User](c, oauth.UserObjKey)
|
||||
if userObj == nil {
|
||||
response.AbortUnauthorized(c, errLoginRequired)
|
||||
return
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
|
||||
package user
|
||||
|
||||
import ("bytes"
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
@@ -16,12 +17,14 @@ import ("bytes"
|
||||
"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/gin-contrib/sessions"
|
||||
"github.com/gin-contrib/sessions/cookie"
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"github.com/Rain-kl/Wavelet/internal/common/response")
|
||||
"github.com/Rain-kl/Wavelet/internal/common/response"
|
||||
)
|
||||
|
||||
func setupUserTestRouter(t *testing.T) *gin.Engine {
|
||||
t.Helper()
|
||||
@@ -281,7 +284,7 @@ func TestLoginEmailVerificationFallbackWhenSMTPUnconfigured(t *testing.T) {
|
||||
}
|
||||
|
||||
// 2.5 Invalidate the system config cache in Redis
|
||||
if err := db.Redis.Del(context.Background(), db.PrefixedKey(model.SystemConfigRedisHashKey)).Err(); err != nil {
|
||||
if err := db.Redis.Del(context.Background(), db.PrefixedKey(repository.SystemConfigRedisHashKey)).Err(); err != nil {
|
||||
t.Fatalf("invalidate system config cache failed: %v", err)
|
||||
}
|
||||
|
||||
@@ -387,7 +390,7 @@ func TestLoginEmailVerificationFallbackForEmptyEmail(t *testing.T) {
|
||||
}
|
||||
|
||||
// Invalidate the system config cache in Redis
|
||||
if err := db.Redis.Del(context.Background(), db.PrefixedKey(model.SystemConfigRedisHashKey)).Err(); err != nil {
|
||||
if err := db.Redis.Del(context.Background(), db.PrefixedKey(repository.SystemConfigRedisHashKey)).Err(); err != nil {
|
||||
t.Fatalf("invalidate system config cache failed: %v", err)
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/Rain-kl/Wavelet/internal/model"
|
||||
"github.com/Rain-kl/Wavelet/internal/repository"
|
||||
"github.com/Rain-kl/Wavelet/internal/task"
|
||||
"github.com/Rain-kl/Wavelet/pkg/mail"
|
||||
)
|
||||
@@ -111,17 +112,16 @@ func (h *SendEmailHandler) Execute(ctx context.Context, payload []byte) (*task.T
|
||||
var smtpUsername string
|
||||
var smtpPassword string
|
||||
|
||||
var sc model.SystemConfig
|
||||
if err := sc.GetByKey(ctx, model.ConfigKeySMTPHost); err == nil {
|
||||
if sc, err := repository.GetSystemConfigByKey(ctx, model.ConfigKeySMTPHost); err == nil {
|
||||
smtpHost = sc.Value
|
||||
}
|
||||
if err := sc.GetByKey(ctx, model.ConfigKeySMTPPort); err == nil {
|
||||
if sc, err := repository.GetSystemConfigByKey(ctx, model.ConfigKeySMTPPort); err == nil {
|
||||
smtpPortVal = sc.Value
|
||||
}
|
||||
if err := sc.GetByKey(ctx, model.ConfigKeySMTPUsername); err == nil {
|
||||
if sc, err := repository.GetSystemConfigByKey(ctx, model.ConfigKeySMTPUsername); err == nil {
|
||||
smtpUsername = sc.Value
|
||||
}
|
||||
if err := sc.GetByKey(ctx, model.ConfigKeySMTPPassword); err == nil {
|
||||
if sc, err := repository.GetSystemConfigByKey(ctx, model.ConfigKeySMTPPassword); err == nil {
|
||||
smtpPassword = sc.Value
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user