Files
mce/internal/controller/auth_api_login.go
Victor_Jay d203447eb3 feat: 设置页新增登录管理 TAB
- Session 结构体新增 IP、UserAgent、Remark 字段,登录/注册时记录设备信息
- Store 接口新增 ListByUID、DeleteByUIDExclude 方法,MemoryStore 完整实现
- SessionManager 新增 ListByUID、DestroyOtherByUID、UpdateRemark 方法
- 新增 UA 轻量解析器(session/ua.go),提取浏览器/OS/设备类型
- 新增 settings_api_sessions.go,提供列表/踢出/一键踢出/备注 4 个 API
- 控制器层声明 sessionManager 最小接口(ISP),SettingsController 注入依赖
- Auth 中间件注入 sid 到 gin context,支持识别当前会话
- 设置页左侧导航增加登录管理入口,tab 白名单新增 sessions
- 前端实现设备列表展示(浏览器/OS/设备图标/IP/时间)、当前设备标识、备注输入、踢出按钮、一键踢出
- settings.css 新增会话管理全套样式及响应式适配
2026-05-30 23:58:37 +08:00

123 lines
3.3 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package controller
import (
"net/http"
"metazone.cc/metalab/internal/common"
"metazone.cc/metalab/internal/model"
"github.com/gin-gonic/gin"
)
// clientIP 获取客户端真实 IP考虑反向代理
func clientIP(c *gin.Context) string {
if fwd := c.GetHeader("X-Forwarded-For"); fwd != "" {
return fwd
}
if real := c.GetHeader("X-Real-IP"); real != "" {
return real
}
return c.ClientIP()
}
// Login 登录 API含双维度限流
func (ac *AuthController) Login(c *gin.Context) {
var req model.LoginRequest
if err := c.ShouldBindJSON(&req); err != nil {
common.Error(c, http.StatusBadRequest, "请检查输入")
return
}
email := req.Email
ip := clientIP(c)
// --- 限流:账户维度 ---
acctResult, recordAccount := ac.rateLimiter.AllowAccount(email)
if acctResult.Blocked {
common.Error(c, http.StatusTooManyRequests, acctResult.Message)
return
}
// --- 限流IP 维度 ---
ipResult, recordIP := ac.rateLimiter.AllowIP(ip)
if ipResult.Blocked {
common.Error(c, http.StatusTooManyRequests, ipResult.Message)
return
}
user, err := ac.authService.Login(req, ip)
if err != nil {
// 记录失败 → 两个维度各 +1
if recordAccount != nil {
recordAccount()
}
if recordIP != nil {
recordIP()
}
switch err {
case common.ErrInvalidCred:
common.Error(c, http.StatusUnauthorized, "邮箱或密码错误")
case common.ErrUserBanned:
common.Error(c, http.StatusForbidden, "账号已被封禁")
case common.ErrUserLocked:
common.Error(c, http.StatusUnauthorized, "邮箱或密码错误")
case common.ErrMaintenanceMode:
common.Error(c, http.StatusForbidden, "社区正在维护中,仅站长可登录")
case common.ErrNeedsConfirmRestore:
// 注销账号登录 → 需要二次确认恢复
c.JSON(http.StatusOK, gin.H{
"success": true,
"action": "confirm_restore",
"message": "你的账号正在注销中,登录将中止注销流程",
})
default:
common.Error(c, http.StatusInternalServerError, "登录失败,请稍后重试")
}
return
}
// 登录成功 → 清除失败计数
ac.rateLimiter.Clear(email, ip)
// 创建服务端 session
sid, err := ac.sessionManager.Create(user, req.RememberMe, ip, c.GetHeader("User-Agent"))
if err != nil {
common.Error(c, http.StatusInternalServerError, "登录失败,请稍后重试")
return
}
common.SetSessionCookie(c, sid, req.RememberMe, ac.cfg)
common.OkWithMessage(c, user, "登录成功")
}
// ConfirmRestore 二次确认恢复已注销账号
func (ac *AuthController) ConfirmRestore(c *gin.Context) {
var req model.LoginRequest
if err := c.ShouldBindJSON(&req); err != nil {
common.Error(c, http.StatusBadRequest, "请检查输入")
return
}
user, err := ac.authService.ConfirmRestore(req, clientIP(c))
if err != nil {
switch err {
case common.ErrInvalidCred:
common.Error(c, http.StatusUnauthorized, "邮箱或密码错误")
default:
common.Error(c, http.StatusInternalServerError, "操作失败,请稍后重试")
}
return
}
sid, err := ac.sessionManager.Create(user, req.RememberMe, clientIP(c), c.GetHeader("User-Agent"))
if err != nil {
common.Error(c, http.StatusInternalServerError, "操作失败,请稍后重试")
return
}
common.SetSessionCookie(c, sid, req.RememberMe, ac.cfg)
common.OkWithMessage(c, user, "账号已恢复,欢迎回来")
}