初始提交:OCI 面板后端(含 GenAI 网关一期)
This commit is contained in:
@@ -0,0 +1,93 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"oci-portal/internal/model"
|
||||
"oci-portal/internal/service"
|
||||
)
|
||||
|
||||
type authHandler struct {
|
||||
svc *service.AuthService
|
||||
logs *service.SystemLogService
|
||||
}
|
||||
|
||||
type loginRequest struct {
|
||||
Username string `json:"username" binding:"required"`
|
||||
Password string `json:"password" binding:"required"`
|
||||
// 两步验证码;账号已启用 TOTP 时必填,缺失返回 428
|
||||
TotpCode string `json:"totpCode"`
|
||||
}
|
||||
|
||||
func (h *authHandler) login(c *gin.Context) {
|
||||
var req loginRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
start := time.Now()
|
||||
token, expires, err := h.svc.Login(c.Request.Context(), req.Username, req.Password, requestIP(c), req.TotpCode)
|
||||
if errors.Is(err, service.ErrTotpRequired) {
|
||||
// 密码已通过,引导前端弹出二次验证输入;不算失败不留痕
|
||||
c.JSON(http.StatusPreconditionRequired, gin.H{"error": err.Error(), "totpRequired": true})
|
||||
return
|
||||
}
|
||||
if errors.Is(err, service.ErrPasswordLoginDisabled) {
|
||||
h.recordLogin(c, req.Username, http.StatusForbidden, start)
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if errors.Is(err, service.ErrLoginLocked) {
|
||||
h.recordLogin(c, req.Username, http.StatusTooManyRequests, start)
|
||||
c.JSON(http.StatusTooManyRequests, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if errors.Is(err, service.ErrInvalidCredentials) {
|
||||
h.recordLogin(c, req.Username, http.StatusUnauthorized, start)
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
h.recordLogin(c, req.Username, http.StatusOK, start)
|
||||
c.JSON(http.StatusOK, gin.H{"token": token, "expiresAt": expires})
|
||||
}
|
||||
|
||||
// logout 把当前令牌拉黑至自然过期(secured 组内,写请求由系统日志中间件留痕)。
|
||||
func (h *authHandler) logout(c *gin.Context) {
|
||||
token, ok := strings.CutPrefix(c.GetHeader("Authorization"), "Bearer ")
|
||||
if ok && token != "" {
|
||||
h.svc.Logout(token)
|
||||
}
|
||||
c.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// recordLogin 显式记录一次登录成败——登录不在 secured 组内,
|
||||
// 系统日志中间件覆盖不到;登录失败是安全关键事件,必须留痕。
|
||||
func (h *authHandler) recordLogin(c *gin.Context, username string, status int, start time.Time) {
|
||||
errMsg := ""
|
||||
if status == http.StatusUnauthorized {
|
||||
errMsg = "用户名、密码或验证码错误"
|
||||
} else if status == http.StatusTooManyRequests {
|
||||
errMsg = "连续失败已锁定"
|
||||
} else if status == http.StatusForbidden {
|
||||
errMsg = "密码登录已禁用"
|
||||
}
|
||||
h.logs.Record(model.SystemLog{
|
||||
Username: username,
|
||||
Method: http.MethodPost,
|
||||
Path: requestPath(c),
|
||||
Status: status,
|
||||
DurationMs: time.Since(start).Milliseconds(),
|
||||
ClientIP: requestIP(c),
|
||||
UserAgent: truncateLogField(c.Request.UserAgent(), 256),
|
||||
ErrMsg: errMsg,
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user