Files
OpenFlare/internal/apps/user/controllers.go
T
ryan 474e3da3b7 代码优化
界面优化
2026-06-08 20:37:52 +08:00

311 lines
9.1 KiB
Go

package user
import (
"context"
"net/http"
"strings"
"time"
"github.com/gin-contrib/sessions"
"github.com/gin-gonic/gin"
"github.com/linux-do/credit/internal/apps/oauth"
"github.com/linux-do/credit/internal/common"
"github.com/linux-do/credit/internal/db"
"github.com/linux-do/credit/internal/model"
"github.com/linux-do/credit/internal/util"
)
type loginRequest struct {
Username string `json:"username"`
Password string `json:"password"`
}
type registerRequest struct {
Username string `json:"username"`
Password string `json:"password"`
Nickname string `json:"nickname"`
DisplayName string `json:"display_name"`
}
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(c *gin.Context, user *model.User) error {
session := sessions.Default(c)
session.Set(oauth.UserIDKey, user.ID)
session.Set(oauth.UserNameKey, user.Username)
if err := session.Save(); err != nil {
return err
}
return nil
}
// Login 用户密码登录
// @Summary 用户密码登录
// @Description 使用用户名和密码登录,登录成功后建立 Session。若管理员已关闭密码登录功能则返回错误。
// @Tags user
// @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 "服务内部错误"
// @Router /api/v1/user/login [post]
func Login(c *gin.Context) {
if !isPasswordLoginEnabled() {
c.JSON(http.StatusOK, util.Err("管理员关闭了密码登录"))
return
}
var req loginRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, util.Err(err.Error()))
return
}
req.Username = strings.TrimSpace(req.Username)
if req.Username == "" || req.Password == "" {
c.JSON(http.StatusOK, util.Err("无效的参数"))
return
}
var user model.User
ctx := c.Request.Context()
if err := db.DB(ctx).Where("username = ?", req.Username).First(&user).Error; err != nil {
c.JSON(http.StatusOK, util.Err("用户名或密码错误"))
return
}
if !user.IsActive {
c.JSON(http.StatusOK, util.Err(common.BannedAccount))
return
}
// 判定是否是明文密码存储
isPlaintext := !(strings.HasPrefix(user.Password, "$2a$") || strings.HasPrefix(user.Password, "$2b$") || strings.HasPrefix(user.Password, "$2y$"))
if !user.CheckPassword(req.Password) {
c.JSON(http.StatusOK, util.Err("用户名或密码错误"))
return
}
session := sessions.Default(c)
needChangePassword := false
// 如果是以明文密码登录,在数据库中置换为加密密码
if isPlaintext {
if err := user.SetEncryptedPassword(req.Password); err == nil {
if err := db.DB(ctx).Model(&user).Update("password", user.Password).Error; err != nil {
c.JSON(http.StatusOK, util.Err("升级密码安全算法失败,请重试"))
return
}
needChangePassword = true
session.Set("need_change_password", true)
_ = session.Save()
}
}
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()))
return
}
if err := setLoginSession(c, &user); err != nil {
c.JSON(http.StatusOK, util.Err("无法保存会话信息,请重试"))
return
}
c.JSON(http.StatusOK, util.OK(oauth.BuildBasicUserInfo(&user, needChangePassword)))
}
// Register 用户注册
// @Summary 用户注册
// @Description 使用用户名和密码注册新账号,注册成功后自动登录并建立 Session。密码长度不能少于 8 位。
// @Tags user
// @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 "服务内部错误"
// @Router /api/v1/user/register [post]
func Register(c *gin.Context) {
if !isRegistrationEnabled() || !isPasswordRegisterEnabled() {
c.JSON(http.StatusOK, util.Err("管理员关闭了注册"))
return
}
var req registerRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, util.Err(err.Error()))
return
}
req.Username = strings.TrimSpace(req.Username)
req.Password = strings.TrimSpace(req.Password)
req.Nickname = strings.TrimSpace(req.Nickname)
req.DisplayName = strings.TrimSpace(req.DisplayName)
if req.Username == "" || req.Password == "" {
c.JSON(http.StatusOK, util.Err("无效的参数"))
return
}
if len(req.Password) < 8 {
c.JSON(http.StatusOK, util.Err("密码长度不能少于 8 位"))
return
}
ctx := c.Request.Context()
var count int64
if err := db.DB(ctx).Model(&model.User{}).Where("username = ?", req.Username).Count(&count).Error; err != nil {
c.JSON(http.StatusOK, util.Err(err.Error()))
return
}
if count > 0 {
c.JSON(http.StatusOK, util.Err("用户名已存在"))
return
}
user := model.User{
Username: req.Username,
Nickname: req.Nickname,
AvatarUrl: "",
IsActive: true,
IsAdmin: false,
LastLoginAt: time.Now(),
}
if user.Nickname == "" {
user.Nickname = req.DisplayName
}
if user.Nickname == "" {
user.Nickname = req.Username
}
if err := user.SetPassword(req.Password); err != nil {
c.JSON(http.StatusOK, util.Err(err.Error()))
return
}
if err := db.DB(ctx).Create(&user).Error; err != nil {
c.JSON(http.StatusOK, util.Err(err.Error()))
return
}
if err := setLoginSession(c, &user); err != nil {
c.JSON(http.StatusOK, util.Err("无法保存会话信息,请重试"))
return
}
c.JSON(http.StatusOK, util.OK(oauth.BuildBasicUserInfo(&user, false)))
}
// Logout 用户退出登录
// @Summary 用户退出登录
// @Description 清除用户登录 Session,完成退出
// @Tags user
// @Produce json
// @Security SessionCookie
// @Success 200 {object} util.ResponseAny{data=string} "退出成功"
// @Failure 500 {object} util.ResponseAny "Session 清除失败"
// @Router /api/v1/user/logout [get]
func Logout(c *gin.Context) {
session := sessions.Default(c)
session.Options(util.GetSessionOptions(-1))
session.Clear()
if err := session.Save(); err != nil {
c.JSON(http.StatusOK, util.Err(err.Error()))
return
}
c.JSON(http.StatusOK, util.OK(""))
}
type changePasswordRequest struct {
OldPassword string `json:"old_password"`
NewPassword string `json:"new_password"`
}
// ChangePassword 修改用户密码
// @Summary 修改用户密码
// @Description 修改当前登录用户的密码。修改成功后,如果是首次明文登录的升级提示,则清除修改密码的提示状态。
// @Tags user
// @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 "请先登录"
// @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()))
return
}
req.OldPassword = strings.TrimSpace(req.OldPassword)
req.NewPassword = strings.TrimSpace(req.NewPassword)
if req.OldPassword == "" || req.NewPassword == "" {
c.JSON(http.StatusOK, util.Err("无效的参数"))
return
}
if len(req.NewPassword) < 8 {
c.JSON(http.StatusOK, util.Err("新密码长度不能少于 8 位"))
return
}
userObj, _ := util.GetFromContext[*model.User](c, oauth.UserObjKey)
if userObj == nil {
c.JSON(http.StatusUnauthorized, util.Err("请先登录"))
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("未找到该用户"))
return
}
// 校验旧密码
if !dbUser.CheckPassword(req.OldPassword) {
c.JSON(http.StatusOK, util.Err("原密码不正确"))
return
}
// 加密并更新为新密码
if err := dbUser.SetEncryptedPassword(req.NewPassword); err != nil {
c.JSON(http.StatusOK, util.Err("密码加密失败,请重试"))
return
}
if err := db.DB(ctx).Model(&dbUser).Update("password", dbUser.Password).Error; err != nil {
c.JSON(http.StatusOK, util.Err(err.Error()))
return
}
// 清除 Session 中修改密码提示状态
session := sessions.Default(c)
session.Delete("need_change_password")
_ = session.Save()
c.JSON(http.StatusOK, util.OK("密码修改成功"))
}