管理员后台: - User 新增 IsAdmin,第一个注册的用户自动成为管理员; 老库升级时自动把最早注册的用户提升为管理员 - 新增 AdminMiddleware 与 /api/v1/admin/config 读写接口 - 可改开放注册、检查间隔、规则重试时长、日志保留条数,保存即生效 - 检查间隔改动后自动重排定时任务,为此把 cron 抽成 handler/scheduler - 监听地址、数据库、会话密钥等启动项不开放,接口只返回只读展示 - 设置页新增"系统配置"区块,仅管理员可见 PostgreSQL: - 新增 database_driver / database_dsn 配置,默认仍为 sqlite - postgres 用独立连接池(20/5/1h),sqlite 保持单连接避免写锁 - 启动时先 Ping,配置写错在启动阶段就报错而不是等到首次查询 - 支持 GOODBABY_DB_DRIVER / GOODBABY_DB_DSN 环境变量, 只给 DSN 时自动按 postgres 处理 文档: 新增 docs/database.md(含 compose 示例与切换说明),README 补充管理员与数据库章节 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
249 lines
6.0 KiB
Go
249 lines
6.0 KiB
Go
package user
|
|
|
|
import (
|
|
"errors"
|
|
"net/http"
|
|
|
|
"github.com/gin-contrib/sessions"
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/ssdomei232/goodBaby/api/response"
|
|
"github.com/ssdomei232/goodBaby/configs"
|
|
"github.com/ssdomei232/goodBaby/handler/db"
|
|
"github.com/ssdomei232/goodBaby/model"
|
|
)
|
|
|
|
// HandleRegistry 处理注册请求,配置文件中可设置禁用注册
|
|
//
|
|
// 系统内还没有任何用户时始终允许注册,否则全新部署将无法创建第一个账号。
|
|
func HandleRegistry(c *gin.Context) {
|
|
config, err := configs.GetConfig()
|
|
if err != nil {
|
|
response.ServerError(c, "获取配置文件失败")
|
|
return
|
|
}
|
|
|
|
userCount, err := CountUsers()
|
|
if err != nil {
|
|
response.ServerError(c, "服务器内部错误")
|
|
return
|
|
}
|
|
|
|
if !config.EnableRegistry && userCount > 0 {
|
|
response.Fail(c, http.StatusForbidden, "注册功能已关闭")
|
|
return
|
|
}
|
|
|
|
var registryRequest model.UserRegistryReuest
|
|
if err := c.ShouldBindJSON(®istryRequest); err != nil {
|
|
response.BadRequest(c, "输入错误")
|
|
return
|
|
}
|
|
|
|
user := model.User{
|
|
Username: registryRequest.Username,
|
|
Password: registryRequest.Password,
|
|
// 第一个注册的用户是管理员
|
|
IsAdmin: userCount == 0,
|
|
}
|
|
|
|
if err := user.IsValid(); err != nil {
|
|
response.BadRequest(c, err.Error())
|
|
return
|
|
}
|
|
|
|
taken, err := IsUsernameTaken(user.Username)
|
|
if err != nil {
|
|
response.ServerError(c, "服务器内部错误")
|
|
return
|
|
}
|
|
if taken {
|
|
response.BadRequest(c, "用户名已存在")
|
|
return
|
|
}
|
|
|
|
if err := createUser(&user); err != nil {
|
|
response.ServerError(c, "注册失败")
|
|
return
|
|
}
|
|
|
|
if err := setSession(c, &user); err != nil {
|
|
response.ServerError(c, "写入会话失败")
|
|
return
|
|
}
|
|
|
|
response.OK(c, "注册成功")
|
|
}
|
|
|
|
// HandleLogin 处理登录请求
|
|
func HandleLogin(c *gin.Context) {
|
|
var loginRequest model.UserRegistryReuest
|
|
if err := c.ShouldBindJSON(&loginRequest); err != nil {
|
|
response.BadRequest(c, "输入错误")
|
|
return
|
|
}
|
|
|
|
if loginRequest.Username == "" || loginRequest.Password == "" {
|
|
response.BadRequest(c, "用户名或密码不能为空")
|
|
return
|
|
}
|
|
|
|
user, err := verifyUser(loginRequest.Username, loginRequest.Password)
|
|
switch {
|
|
case errors.Is(err, ErrUserNotFound), errors.Is(err, ErrWrongPassword):
|
|
// 不区分“用户不存在”和“密码错误”,避免泄露用户名是否存在
|
|
response.Unauthorized(c, "用户名或密码错误")
|
|
return
|
|
case err != nil:
|
|
response.ServerError(c, "登录失败")
|
|
return
|
|
}
|
|
|
|
if err := setSession(c, user); err != nil {
|
|
response.ServerError(c, "写入会话失败")
|
|
return
|
|
}
|
|
|
|
response.OK(c, "登录成功")
|
|
}
|
|
|
|
// HandleLogout 退出登录
|
|
func HandleLogout(c *gin.Context) {
|
|
session := sessions.Default(c)
|
|
session.Clear()
|
|
session.Options(sessions.Options{Path: "/", MaxAge: -1})
|
|
if err := session.Save(); err != nil {
|
|
response.ServerError(c, "退出登录失败")
|
|
return
|
|
}
|
|
response.OK(c, "已退出登录")
|
|
}
|
|
|
|
// HandleGetUserInfo 获取当前登录用户信息
|
|
func HandleGetUserInfo(c *gin.Context) {
|
|
userInfo, err := GetUserInfoByGinCtx(c)
|
|
if err != nil {
|
|
response.Unauthorized(c, "获取用户信息失败")
|
|
return
|
|
}
|
|
response.OK(c, userInfo)
|
|
}
|
|
|
|
// HandleChangePassword 修改密码
|
|
func HandleChangePassword(c *gin.Context) {
|
|
userInfo, err := GetUserInfoByGinCtx(c)
|
|
if err != nil {
|
|
response.Unauthorized(c, "获取用户信息失败")
|
|
return
|
|
}
|
|
|
|
var req model.ChangePasswordRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
response.BadRequest(c, "输入错误")
|
|
return
|
|
}
|
|
|
|
if _, err := verifyUser(userInfo.Username, req.OldPassword); err != nil {
|
|
response.BadRequest(c, "原密码错误")
|
|
return
|
|
}
|
|
|
|
candidate := model.User{Username: userInfo.Username, Password: req.NewPassword}
|
|
if err := candidate.IsValid(); err != nil {
|
|
response.BadRequest(c, err.Error())
|
|
return
|
|
}
|
|
|
|
hashed, err := encryptPassword(req.NewPassword)
|
|
if err != nil {
|
|
response.ServerError(c, "修改密码失败")
|
|
return
|
|
}
|
|
|
|
gormDB, err := db.GetGormDB()
|
|
if err != nil {
|
|
response.ServerError(c, "服务器内部错误")
|
|
return
|
|
}
|
|
if err := gormDB.Model(&model.User{}).Where("id = ?", userInfo.ID).
|
|
Update("password", hashed).Error; err != nil {
|
|
response.ServerError(c, "修改密码失败")
|
|
return
|
|
}
|
|
|
|
response.OK(c, "密码修改成功")
|
|
}
|
|
|
|
// HandleUpdateNotifyConfig 更新提醒渠道(钉钉机器人)配置
|
|
func HandleUpdateNotifyConfig(c *gin.Context) {
|
|
userInfo, err := GetUserInfoByGinCtx(c)
|
|
if err != nil {
|
|
response.Unauthorized(c, "获取用户信息失败")
|
|
return
|
|
}
|
|
|
|
var req model.NotifyConfigRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
response.BadRequest(c, "输入错误")
|
|
return
|
|
}
|
|
|
|
if err := validateNotifyConfig(req.DingTalkConfig); err != nil {
|
|
response.BadRequest(c, err.Error())
|
|
return
|
|
}
|
|
|
|
gormDB, err := db.GetGormDB()
|
|
if err != nil {
|
|
response.ServerError(c, "服务器内部错误")
|
|
return
|
|
}
|
|
|
|
if err := gormDB.Model(&model.User{}).Where("id = ?", userInfo.ID).
|
|
Update("ding_talk_config", req.DingTalkConfig).Error; err != nil {
|
|
response.ServerError(c, "保存提醒配置失败")
|
|
return
|
|
}
|
|
|
|
response.OK(c, "提醒配置已保存")
|
|
}
|
|
|
|
// AuthMiddleware 认证中间件,同时把当前用户放进 context 供后续 handler 复用
|
|
func AuthMiddleware() gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
session := sessions.Default(c)
|
|
uid, ok := session.Get("uid").(uint)
|
|
if !ok {
|
|
response.AbortWith(c, http.StatusUnauthorized, "未登录")
|
|
return
|
|
}
|
|
|
|
user, err := GetUserByID(uid)
|
|
if err != nil {
|
|
// 用户已被删除,清理会话
|
|
session.Clear()
|
|
_ = session.Save()
|
|
response.AbortWith(c, http.StatusUnauthorized, "未登录")
|
|
return
|
|
}
|
|
|
|
c.Set(contextKey, user)
|
|
c.Next()
|
|
}
|
|
}
|
|
|
|
// AdminMiddleware 要求当前用户是管理员,需挂在 AuthMiddleware 之后
|
|
func AdminMiddleware() gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
userInfo, err := GetUserInfoByGinCtx(c)
|
|
if err != nil {
|
|
response.AbortWith(c, http.StatusUnauthorized, "未登录")
|
|
return
|
|
}
|
|
if !userInfo.IsAdmin {
|
|
response.AbortWith(c, http.StatusForbidden, "需要管理员权限")
|
|
return
|
|
}
|
|
c.Next()
|
|
}
|
|
}
|