refactor(util): move response helper to common/response and session logic to oauth

- Relocated generic HTTP response helpers (Response, OK, Err, etc.) from internal/util/ to a dedicated internal/common/response/ package.
- Renamed ResponseAny to Any to resolve revive stuttering warnings.
- Moved session building options and cookie headers logic from internal/util/ to internal/apps/oauth/.
- Removed all direct imports of Gin/Sessions/HTTP frameworks from internal/util/ to keep general utilities 100% pure.
- Regenerated Swagger API documentation via make swagger.
- All tests and make code-check compile and pass with 0 issues.
This commit is contained in:
ryan
2026-06-15 16:38:44 +08:00
parent b3ed94342c
commit 953af7d8db
43 changed files with 1833 additions and 1853 deletions
+30 -30
View File
@@ -5,8 +5,7 @@
// Package user 提供用户认证与帐户管理功能
package user
import (
"net/http"
import ("net/http"
"strconv"
"strings"
@@ -15,7 +14,8 @@ import (
"github.com/Rain-kl/Wavelet/internal/model"
"github.com/Rain-kl/Wavelet/internal/util"
"github.com/gin-gonic/gin"
)
"github.com/Rain-kl/Wavelet/internal/common/response")
type createTokenRequest struct {
Name string `json:"name"`
@@ -33,8 +33,8 @@ type tokenResponse struct {
// @Tags user
// @Produce json
// @Security SessionCookie
// @Success 200 {object} util.ResponseAny{data=[]model.AccessToken} "令牌列表"
// @Failure 401 {object} util.ResponseAny "未登录"
// @Success 200 {object} response.Any{data=[]model.AccessToken} "令牌列表"
// @Failure 401 {object} response.Any "未登录"
// @Router /api/v1/user/access-tokens [get]
// ListAccessTokens 获取当前用户的 AccessToken 列表
func ListAccessTokens(c *gin.Context) {
@@ -43,11 +43,11 @@ func ListAccessTokens(c *gin.Context) {
var tokens []model.AccessToken
if err := db.DB(ctx).Where("user_id = ?", currUser.ID).Order("created_at desc").Find(&tokens).Error; err != nil {
c.JSON(http.StatusOK, util.Err(err.Error()))
c.JSON(http.StatusOK, response.Err(err.Error()))
return
}
c.JSON(http.StatusOK, util.OK(tokens))
c.JSON(http.StatusOK, response.OK(tokens))
}
// CreateAccessToken 创建一个新的 AccessToken
@@ -58,8 +58,8 @@ func ListAccessTokens(c *gin.Context) {
// @Produce json
// @Param request body user.createTokenRequest true "令牌名称"
// @Security SessionCookie
// @Success 200 {object} util.ResponseAny{data=user.tokenResponse} "新建令牌成功"
// @Failure 400 {object} util.ResponseAny "参数错误或超限"
// @Success 200 {object} response.Any{data=user.tokenResponse} "新建令牌成功"
// @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)
@@ -67,19 +67,19 @@ func CreateAccessToken(c *gin.Context) {
var req createTokenRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusOK, util.Err(errBindParamsFailed))
c.JSON(http.StatusOK, response.Err(errBindParamsFailed))
return
}
req.Name = strings.TrimSpace(req.Name)
if req.Name == "" {
c.JSON(http.StatusOK, util.Err(errTokenNameRequired))
c.JSON(http.StatusOK, response.Err(errTokenNameRequired))
return
}
// 只有管理员才能创建具有管理员权限的令牌
if req.IsAdmin && !currUser.IsAdmin {
c.JSON(http.StatusOK, util.Err(errAdminTokenRequiresAdmin))
c.JSON(http.StatusOK, response.Err(errAdminTokenRequiresAdmin))
return
}
@@ -91,19 +91,19 @@ func CreateAccessToken(c *gin.Context) {
var count int64
if err := db.DB(ctx).Model(&model.AccessToken{}).Where("user_id = ?", currUser.ID).Count(&count).Error; err != nil {
c.JSON(http.StatusOK, util.Err(err.Error()))
c.JSON(http.StatusOK, response.Err(err.Error()))
return
}
if int(count) >= maxLimit {
c.JSON(http.StatusOK, util.Err(errAccessTokenLimitReached))
c.JSON(http.StatusOK, response.Err(errAccessTokenLimitReached))
return
}
// 生成 Token
tokenStr, err := model.GenerateTokenString()
if err != nil {
c.JSON(http.StatusOK, util.Err(errGenerateTokenFailed))
c.JSON(http.StatusOK, response.Err(errGenerateTokenFailed))
return
}
@@ -119,11 +119,11 @@ func CreateAccessToken(c *gin.Context) {
}
if err := db.DB(ctx).Create(&tokenRecord).Error; err != nil {
c.JSON(http.StatusOK, util.Err(err.Error()))
c.JSON(http.StatusOK, response.Err(err.Error()))
return
}
c.JSON(http.StatusOK, util.OK(tokenResponse{
c.JSON(http.StatusOK, response.OK(tokenResponse{
Token: tokenStr,
Record: tokenRecord,
}))
@@ -136,8 +136,8 @@ func CreateAccessToken(c *gin.Context) {
// @Produce json
// @Param id path string true "令牌ID"
// @Security SessionCookie
// @Success 200 {object} util.ResponseAny{data=string} "删除成功"
// @Failure 400 {object} util.ResponseAny "参数错误"
// @Success 200 {object} response.Any{data=string} "删除成功"
// @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)
@@ -146,22 +146,22 @@ func DeleteAccessToken(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 64)
if err != nil {
c.JSON(http.StatusOK, util.Err(errInvalidTokenID))
c.JSON(http.StatusOK, response.Err(errInvalidTokenID))
return
}
tx := db.DB(ctx).Where("id = ? AND user_id = ?", id, currUser.ID).Delete(&model.AccessToken{})
if tx.Error != nil {
c.JSON(http.StatusOK, util.Err(tx.Error.Error()))
c.JSON(http.StatusOK, response.Err(tx.Error.Error()))
return
}
if tx.RowsAffected == 0 {
c.JSON(http.StatusOK, util.Err(errTokenNotFoundOrForbidden))
c.JSON(http.StatusOK, response.Err(errTokenNotFoundOrForbidden))
return
}
c.JSON(http.StatusOK, util.OK("删除成功"))
c.JSON(http.StatusOK, response.OK("删除成功"))
}
// RotateAccessToken 轮换一个 AccessToken
@@ -171,8 +171,8 @@ func DeleteAccessToken(c *gin.Context) {
// @Produce json
// @Param id path string true "令牌ID"
// @Security SessionCookie
// @Success 200 {object} util.ResponseAny{data=user.tokenResponse} "令牌轮换成功"
// @Failure 400 {object} util.ResponseAny "参数错误"
// @Success 200 {object} response.Any{data=user.tokenResponse} "令牌轮换成功"
// @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)
@@ -181,20 +181,20 @@ func RotateAccessToken(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 64)
if err != nil {
c.JSON(http.StatusOK, util.Err(errInvalidTokenID))
c.JSON(http.StatusOK, response.Err(errInvalidTokenID))
return
}
var tokenRecord model.AccessToken
if err := db.DB(ctx).Where("id = ? AND user_id = ?", id, currUser.ID).First(&tokenRecord).Error; err != nil {
c.JSON(http.StatusOK, util.Err(errTokenNotFoundOrForbidden))
c.JSON(http.StatusOK, response.Err(errTokenNotFoundOrForbidden))
return
}
// 生成新的 Token
newTokenStr, err := model.GenerateTokenString()
if err != nil {
c.JSON(http.StatusOK, util.Err(errGenerateTokenFailed))
c.JSON(http.StatusOK, response.Err(errGenerateTokenFailed))
return
}
@@ -205,11 +205,11 @@ func RotateAccessToken(c *gin.Context) {
tokenRecord.MaskedToken = newMaskedToken
if err := db.DB(ctx).Save(&tokenRecord).Error; err != nil {
c.JSON(http.StatusOK, util.Err(err.Error()))
c.JSON(http.StatusOK, response.Err(err.Error()))
return
}
c.JSON(http.StatusOK, util.OK(tokenResponse{
c.JSON(http.StatusOK, response.OK(tokenResponse{
Token: newTokenStr,
Record: tokenRecord,
}))
+29 -29
View File
@@ -3,8 +3,7 @@
package user
import (
"context"
import ("context"
"crypto/rand"
"encoding/json"
"errors"
@@ -22,7 +21,8 @@ import (
"github.com/Rain-kl/Wavelet/internal/util"
pkgu "github.com/Rain-kl/Wavelet/pkg/util"
"github.com/gin-gonic/gin"
)
"github.com/Rain-kl/Wavelet/internal/common/response")
type sendEmailCodeRequest struct {
Email string `json:"email" binding:"required,email"`
@@ -139,7 +139,7 @@ func verifyEmailCode(ctx context.Context, email, scene, code string) bool {
func handleLoginEmailVerification(ctx context.Context, c *gin.Context, req *loginRequest, user *model.User) error {
if req.Code != "" {
if !verifyEmailCode(ctx, user.Email, "login", req.Code) {
c.JSON(http.StatusOK, util.Err(errEmailCodeInvalidOrExpired))
c.JSON(http.StatusOK, response.Err(errEmailCodeInvalidOrExpired))
return errors.New("handled")
}
return nil
@@ -149,7 +149,7 @@ func handleLoginEmailVerification(ctx context.Context, c *gin.Context, req *logi
if !isSMTPConfigured(ctx) || user.Email == "" {
codeKey := getEmailCodeKey("login", user.Email)
if err := db.SetJSON(ctx, codeKey, "888888", emailCodeExpiry); err != nil {
c.JSON(http.StatusOK, util.Err(errGenerateEmailCodeFailed))
c.JSON(http.StatusOK, response.Err(errGenerateEmailCodeFailed))
return errors.New("handled")
}
var msg string
@@ -158,7 +158,7 @@ func handleLoginEmailVerification(ctx context.Context, c *gin.Context, req *logi
} else {
msg = errSMTPInvalidUseTempCodePrefix + "该账号未绑定邮箱,使用临时码登录"
}
c.JSON(http.StatusOK, util.Err(msg))
c.JSON(http.StatusOK, response.Err(msg))
return errors.New("handled")
}
@@ -167,13 +167,13 @@ func handleLoginEmailVerification(ctx context.Context, c *gin.Context, req *logi
err := db.GetJSON(ctx, cooldownKey, &temp)
if err != nil {
if err := sendEmailVerificationCode(ctx, user.Email, "login", "login_email"); err != nil {
c.JSON(http.StatusOK, util.Err(err.Error()))
c.JSON(http.StatusOK, response.Err(err.Error()))
return errors.New("handled")
}
}
maskedEmail := pkgu.MaskEmail(user.Email)
c.JSON(http.StatusOK, util.Err(errNeedEmailCodePrefix+maskedEmail))
c.JSON(http.StatusOK, response.Err(errNeedEmailCodePrefix+maskedEmail))
return errors.New("handled")
}
@@ -184,24 +184,24 @@ func handleLoginEmailVerification(ctx context.Context, c *gin.Context, req *logi
// @Accept json
// @Produce json
// @Param request body user.sendEmailCodeRequest true "发送验证码请求参数"
// @Success 200 {object} util.ResponseAny "发送成功"
// @Failure 400 {object} util.ResponseAny "参数错误"
// @Success 200 {object} response.Any "发送成功"
// @Failure 400 {object} response.Any "参数错误"
// @Router /api/v1/user/send-email-code [post]
func SendEmailCode(c *gin.Context) {
var req sendEmailCodeRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, util.Err(err.Error()))
c.JSON(http.StatusBadRequest, response.Err(err.Error()))
return
}
req.Email = strings.TrimSpace(req.Email)
if req.Email == "" {
c.JSON(http.StatusOK, util.Err(errEmailRequired))
c.JSON(http.StatusOK, response.Err(errEmailRequired))
return
}
if req.Scene != "register" {
c.JSON(http.StatusOK, util.Err(errUnsupportedEmailScene))
c.JSON(http.StatusOK, response.Err(errUnsupportedEmailScene))
return
}
@@ -209,11 +209,11 @@ func SendEmailCode(c *gin.Context) {
var count int64
if err := db.DB(ctx).Model(&model.User{}).Where("email = ?", req.Email).Count(&count).Error; err != nil {
c.JSON(http.StatusOK, util.Err(err.Error()))
c.JSON(http.StatusOK, response.Err(err.Error()))
return
}
if count > 0 {
c.JSON(http.StatusOK, util.Err(errEmailAlreadyRegistered))
c.JSON(http.StatusOK, response.Err(errEmailAlreadyRegistered))
return
}
@@ -221,16 +221,16 @@ func SendEmailCode(c *gin.Context) {
var temp string
err := db.GetJSON(ctx, cooldownKey, &temp)
if err == nil {
c.JSON(http.StatusOK, util.Err(errEmailCodeCooldown))
c.JSON(http.StatusOK, response.Err(errEmailCodeCooldown))
return
}
if err := sendEmailVerificationCode(ctx, req.Email, "register", "register_email"); err != nil {
c.JSON(http.StatusOK, util.Err(err.Error()))
c.JSON(http.StatusOK, response.Err(err.Error()))
return
}
c.JSON(http.StatusOK, util.OKNil())
c.JSON(http.StatusOK, response.OKNil())
}
func validateRegisterEmailVerification(ctx context.Context, req *registerRequest) error {
@@ -264,44 +264,44 @@ type updateProfileRequest struct {
// @Accept json
// @Produce json
// @Param request body user.updateProfileRequest true "更新请求参数"
// @Success 200 {object} util.ResponseAny{data=oauth.BasicUserInfo} "修改成功,返回更新后的用户信息"
// @Failure 400 {object} util.ResponseAny "邮箱已被占用或参数错误"
// @Failure 401 {object} util.ResponseAny "未登录"
// @Success 200 {object} response.Any{data=oauth.BasicUserInfo} "修改成功,返回更新后的用户信息"
// @Failure 400 {object} response.Any "邮箱已被占用或参数错误"
// @Failure 401 {object} response.Any "未登录"
// @Router /api/v1/user/profile [put]
func UpdateProfile(c *gin.Context) {
var req updateProfileRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, util.Err(err.Error()))
c.JSON(http.StatusBadRequest, response.Err(err.Error()))
return
}
userObj, _ := util.GetFromContext[*model.User](c, oauth.UserObjKey)
if userObj == nil {
c.JSON(http.StatusUnauthorized, util.Err(errLoginRequired))
c.JSON(http.StatusUnauthorized, response.Err(errLoginRequired))
return
}
ctx := c.Request.Context()
var dbUser model.User
if err := db.DB(ctx).Where("id = ?", userObj.ID).First(&dbUser).Error; err != nil {
c.JSON(http.StatusOK, util.Err(errUserNotFound))
c.JSON(http.StatusOK, response.Err(errUserNotFound))
return
}
req.Email = strings.TrimSpace(req.Email)
if req.Email != "" && req.Email != dbUser.Email {
if !strings.Contains(req.Email, "@") || !strings.Contains(req.Email, ".") {
c.JSON(http.StatusOK, util.Err(errEmailFormatInvalid))
c.JSON(http.StatusOK, response.Err(errEmailFormatInvalid))
return
}
var count int64
if err := db.DB(ctx).Model(&model.User{}).Where("email = ? AND id != ?", req.Email, dbUser.ID).Count(&count).Error; err != nil {
c.JSON(http.StatusOK, util.Err(err.Error()))
c.JSON(http.StatusOK, response.Err(err.Error()))
return
}
if count > 0 {
c.JSON(http.StatusOK, util.Err(errEmailAlreadyBound))
c.JSON(http.StatusOK, response.Err(errEmailAlreadyBound))
return
}
}
@@ -319,12 +319,12 @@ func UpdateProfile(c *gin.Context) {
dbUser.Location = strings.TrimSpace(req.Location)
if err := db.DB(ctx).Save(&dbUser).Error; err != nil {
c.JSON(http.StatusOK, util.Err(err.Error()))
c.JSON(http.StatusOK, response.Err(err.Error()))
return
}
session := sessions.Default(c)
needChange := session.Get("need_change_password") == true
c.JSON(http.StatusOK, util.OK(oauth.BuildBasicUserInfo(&dbUser, needChange)))
c.JSON(http.StatusOK, response.OK(oauth.BuildBasicUserInfo(&dbUser, needChange)))
}
+47 -47
View File
@@ -3,8 +3,7 @@
package user
import (
"context"
import ("context"
"net/http"
"strings"
"time"
@@ -20,6 +19,7 @@ import (
"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 {
@@ -83,14 +83,14 @@ func setLoginSession(ctx context.Context, c *gin.Context, user *model.User) erro
isSessionCookie = true
}
}
session.Options(util.GetSessionOptions(maxAge))
session.Options(oauth.GetSessionOptions(maxAge))
if err := session.Save(); err != nil {
return err
}
if isSessionCookie {
util.StripCookieMaxAgeAndExpires(c.Writer.Header(), config.Config.App.SessionCookieName)
oauth.StripCookieMaxAgeAndExpires(c.Writer.Header(), config.Config.App.SessionCookieName)
}
return nil
@@ -103,23 +103,23 @@ func setLoginSession(ctx context.Context, c *gin.Context, user *model.User) erro
// @Accept json
// @Produce json
// @Param request body user.loginRequest true "登录请求参数"
// @Success 200 {object} util.ResponseAny{data=oauth.BasicUserInfo} "登录成功,返回用户信息"
// @Failure 400 {object} util.ResponseAny "用户名或密码错误、帐号已禁用等"
// @Failure 500 {object} util.ResponseAny "服务内部错误"
// @Success 200 {object} response.Any{data=oauth.BasicUserInfo} "登录成功,返回用户信息"
// @Failure 400 {object} response.Any "用户名或密码错误、帐号已禁用等"
// @Failure 500 {object} response.Any "服务内部错误"
// @Router /api/v1/user/login [post]
func Login(c *gin.Context) {
if !isPasswordLoginEnabled() {
c.JSON(http.StatusOK, util.Err(errPasswordLoginDisabled))
c.JSON(http.StatusOK, response.Err(errPasswordLoginDisabled))
return
}
var req loginRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, util.Err(err.Error()))
c.JSON(http.StatusBadRequest, response.Err(err.Error()))
return
}
req.Username = strings.TrimSpace(req.Username)
if req.Username == "" || req.Password == "" {
c.JSON(http.StatusOK, util.Err(errInvalidParams))
c.JSON(http.StatusOK, response.Err(errInvalidParams))
return
}
@@ -127,12 +127,12 @@ func Login(c *gin.Context) {
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())
c.JSON(http.StatusOK, util.Err(errUsernameOrPasswordWrong))
c.JSON(http.StatusOK, response.Err(errUsernameOrPasswordWrong))
return
}
if !user.IsActive {
logger.WarnF(ctx, "[LoginAudit] banned user login attempt for username: %s, ID: %d, IP: %s", user.Username, user.ID, c.ClientIP())
c.JSON(http.StatusOK, util.Err(common.BannedAccount))
c.JSON(http.StatusOK, response.Err(common.BannedAccount))
return
}
@@ -141,7 +141,7 @@ func Login(c *gin.Context) {
if !user.CheckPassword(req.Password) {
logger.WarnF(ctx, "[LoginAudit] failed login attempt (incorrect password) for username: %s, ID: %d, IP: %s", user.Username, user.ID, c.ClientIP())
c.JSON(http.StatusOK, util.Err(errUsernameOrPasswordWrong))
c.JSON(http.StatusOK, response.Err(errUsernameOrPasswordWrong))
return
}
@@ -162,11 +162,11 @@ func Login(c *gin.Context) {
user.LastLoginAt = time.Now()
if err := db.DB(ctx).Model(&user).Update("last_login_at", user.LastLoginAt).Error; err != nil {
c.JSON(http.StatusOK, util.Err(err.Error()))
c.JSON(http.StatusOK, response.Err(err.Error()))
return
}
if err := setLoginSession(ctx, c, &user); err != nil {
c.JSON(http.StatusOK, util.Err(errSaveSessionFailed))
c.JSON(http.StatusOK, response.Err(errSaveSessionFailed))
return
}
@@ -174,7 +174,7 @@ func Login(c *gin.Context) {
custom_events.TriggerAdminLoginEvent(ctx, &user, c.ClientIP())
c.JSON(http.StatusOK, util.OK(oauth.BuildBasicUserInfo(&user, needChangePassword)))
c.JSON(http.StatusOK, response.OK(oauth.BuildBasicUserInfo(&user, needChangePassword)))
}
// Register 用户注册
@@ -184,19 +184,19 @@ func Login(c *gin.Context) {
// @Accept json
// @Produce json
// @Param request body user.registerRequest true "注册请求参数"
// @Success 200 {object} util.ResponseAny{data=oauth.BasicUserInfo} "注册并登录成功,返回用户信息"
// @Failure 400 {object} util.ResponseAny "参数错误、用户名已存在或注册已关闭"
// @Failure 500 {object} util.ResponseAny "服务内部错误"
// @Success 200 {object} response.Any{data=oauth.BasicUserInfo} "注册并登录成功,返回用户信息"
// @Failure 400 {object} response.Any "参数错误、用户名已存在或注册已关闭"
// @Failure 500 {object} response.Any "服务内部错误"
// @Router /api/v1/user/register [post]
func Register(c *gin.Context) {
if !isRegistrationEnabled() || !isPasswordRegisterEnabled() {
c.JSON(http.StatusOK, util.Err(errRegistrationDisabled))
c.JSON(http.StatusOK, response.Err(errRegistrationDisabled))
return
}
var req registerRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, util.Err(err.Error()))
c.JSON(http.StatusBadRequest, response.Err(err.Error()))
return
}
@@ -208,15 +208,15 @@ func Register(c *gin.Context) {
req.Code = strings.TrimSpace(req.Code)
if req.Username == "" || req.Password == "" {
c.JSON(http.StatusOK, util.Err(errInvalidParams))
c.JSON(http.StatusOK, response.Err(errInvalidParams))
return
}
if req.Email == "" {
c.JSON(http.StatusOK, util.Err(errEmailRequired))
c.JSON(http.StatusOK, response.Err(errEmailRequired))
return
}
if len(req.Password) < minPasswordLength {
c.JSON(http.StatusOK, util.Err(errPasswordTooShort))
c.JSON(http.StatusOK, response.Err(errPasswordTooShort))
return
}
@@ -224,7 +224,7 @@ func Register(c *gin.Context) {
// 邮箱注册验证校验
if err := validateRegisterEmailVerification(ctx, &req); err != nil {
c.JSON(http.StatusOK, util.Err(err.Error()))
c.JSON(http.StatusOK, response.Err(err.Error()))
return
}
@@ -245,21 +245,21 @@ func Register(c *gin.Context) {
user.Nickname = req.Username
}
if err := user.SetEncryptedPassword(req.Password); err != nil {
c.JSON(http.StatusOK, util.Err(err.Error()))
c.JSON(http.StatusOK, response.Err(err.Error()))
return
}
if err := user.RegisterUser(ctx, db.DB(ctx)); err != nil {
c.JSON(http.StatusOK, util.Err(err.Error()))
c.JSON(http.StatusOK, response.Err(err.Error()))
return
}
if err := setLoginSession(ctx, c, &user); err != nil {
c.JSON(http.StatusOK, util.Err(errSaveSessionFailed))
c.JSON(http.StatusOK, response.Err(errSaveSessionFailed))
return
}
c.JSON(http.StatusOK, util.OK(oauth.BuildBasicUserInfo(&user, false)))
c.JSON(http.StatusOK, response.OK(oauth.BuildBasicUserInfo(&user, false)))
}
// Logout 用户退出登录
@@ -268,8 +268,8 @@ func Register(c *gin.Context) {
// @Tags user
// @Produce json
// @Security SessionCookie
// @Success 200 {object} util.ResponseAny{data=string} "退出成功"
// @Failure 500 {object} util.ResponseAny "Session 清除失败"
// @Success 200 {object} response.Any{data=string} "退出成功"
// @Failure 500 {object} response.Any "Session 清除失败"
// @Router /api/v1/user/logout [get]
func Logout(c *gin.Context) {
session := sessions.Default(c)
@@ -278,13 +278,13 @@ func Logout(c *gin.Context) {
if userID != nil {
logger.InfoF(c.Request.Context(), "[LoginAudit] user logged out: %v, ID: %v, IP: %s", username, userID, c.ClientIP())
}
session.Options(util.GetSessionOptions(-1))
session.Options(oauth.GetSessionOptions(-1))
session.Clear()
if err := session.Save(); err != nil {
c.JSON(http.StatusOK, util.Err(err.Error()))
c.JSON(http.StatusOK, response.Err(err.Error()))
return
}
c.JSON(http.StatusOK, util.OK(""))
c.JSON(http.StatusOK, response.OK(""))
}
type changePasswordRequest struct {
@@ -299,14 +299,14 @@ type changePasswordRequest struct {
// @Accept json
// @Produce json
// @Param request body user.changePasswordRequest true "修改密码请求参数"
// @Success 200 {object} util.ResponseAny{data=string} "修改密码成功"
// @Failure 400 {object} util.ResponseAny "原密码错误或新密码不符合要求"
// @Failure 401 {object} util.ResponseAny "请先登录"
// @Success 200 {object} response.Any{data=string} "修改密码成功"
// @Failure 400 {object} response.Any "原密码错误或新密码不符合要求"
// @Failure 401 {object} response.Any "请先登录"
// @Router /api/v1/user/change-password [post]
func ChangePassword(c *gin.Context) {
var req changePasswordRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, util.Err(err.Error()))
c.JSON(http.StatusBadRequest, response.Err(err.Error()))
return
}
@@ -314,47 +314,47 @@ func ChangePassword(c *gin.Context) {
req.NewPassword = strings.TrimSpace(req.NewPassword)
if req.OldPassword == "" || req.NewPassword == "" {
c.JSON(http.StatusOK, util.Err(errInvalidParams))
c.JSON(http.StatusOK, response.Err(errInvalidParams))
return
}
if len(req.NewPassword) < minPasswordLength {
c.JSON(http.StatusOK, util.Err(errNewPasswordTooShort))
c.JSON(http.StatusOK, response.Err(errNewPasswordTooShort))
return
}
userObj, _ := util.GetFromContext[*model.User](c, oauth.UserObjKey)
if userObj == nil {
c.JSON(http.StatusUnauthorized, util.Err(errLoginRequired))
c.JSON(http.StatusUnauthorized, response.Err(errLoginRequired))
return
}
ctx := c.Request.Context()
var dbUser model.User
if err := db.DB(ctx).Where("id = ?", userObj.ID).First(&dbUser).Error; err != nil {
c.JSON(http.StatusOK, util.Err(errUserNotFound))
c.JSON(http.StatusOK, response.Err(errUserNotFound))
return
}
// 校验旧密码
if !dbUser.CheckPassword(req.OldPassword) {
c.JSON(http.StatusOK, util.Err(errOldPasswordIncorrect))
c.JSON(http.StatusOK, response.Err(errOldPasswordIncorrect))
return
}
// 加密并更新为新密码
if err := dbUser.SetEncryptedPassword(req.NewPassword); err != nil {
c.JSON(http.StatusOK, util.Err(errPasswordEncryptFailed))
c.JSON(http.StatusOK, response.Err(errPasswordEncryptFailed))
return
}
if err := db.DB(ctx).Model(&dbUser).Update("password", dbUser.Password).Error; err != nil {
c.JSON(http.StatusOK, util.Err(err.Error()))
c.JSON(http.StatusOK, response.Err(err.Error()))
return
}
// 吊销该用户所有的 Access Token
if err := db.DB(ctx).Where("user_id = ?", dbUser.ID).Delete(&model.AccessToken{}).Error; err != nil {
c.JSON(http.StatusOK, util.Err("吊销 Access Token 失败: "+err.Error()))
c.JSON(http.StatusOK, response.Err("吊销 Access Token 失败: "+err.Error()))
return
}
@@ -363,5 +363,5 @@ func ChangePassword(c *gin.Context) {
session.Clear()
_ = session.Save()
c.JSON(http.StatusOK, util.OK("密码修改成功"))
c.JSON(http.StatusOK, response.OK("密码修改成功"))
}
+5 -6
View File
@@ -3,8 +3,7 @@
package user
import (
"bytes"
import ("bytes"
"context"
"encoding/json"
"net/http"
@@ -18,11 +17,11 @@ import (
"github.com/Rain-kl/Wavelet/internal/db"
"github.com/Rain-kl/Wavelet/internal/model"
"github.com/Rain-kl/Wavelet/internal/testhelper"
"github.com/Rain-kl/Wavelet/internal/util"
"github.com/gin-contrib/sessions"
"github.com/gin-contrib/sessions/cookie"
"github.com/gin-gonic/gin"
)
"github.com/Rain-kl/Wavelet/internal/common/response")
func setupUserTestRouter(t *testing.T) *gin.Engine {
t.Helper()
@@ -49,7 +48,7 @@ func setupUserTestRouter(t *testing.T) *gin.Engine {
gin.SetMode(gin.TestMode)
r := gin.New()
store := cookie.NewStore([]byte(config.Config.App.SessionSecret))
store.Options(util.GetSessionOptions(3600))
store.Options(oauth.GetSessionOptions(3600))
r.Use(sessions.Sessions(config.Config.App.SessionCookieName, store))
api := r.Group("/api/v1")
@@ -92,7 +91,7 @@ func sessionCookieFromResponse(t *testing.T, w *httptest.ResponseRecorder) *http
func basicUserInfoFromResponse(t *testing.T, w *httptest.ResponseRecorder) oauth.BasicUserInfo {
t.Helper()
var resp util.ResponseAny
var resp response.Any
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
t.Fatalf("basicUserInfoFromResponse() decode response failed: %v", err)
}