| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257 |
- package auth
- import (
- "context"
- "errors"
- "net/http"
- "github.com/gin-gonic/gin"
- )
- // 认证接口的 handler 实现,对照 docs/api.md 第二节。
- // bind 解析 JSON 请求体。
- func bind(c *gin.Context, dst any) bool {
- if err := c.ShouldBindJSON(dst); err != nil {
- BadRequest(c, err)
- return false
- }
- return true
- }
- // Login 实现 POST /api/auth/login(api.md 2.1)。
- func (h *Handler) Login(c *gin.Context) {
- var req loginRequest
- if !bind(c, &req) {
- return
- }
- deviceCookie, _ := c.Cookie(CookieTrustedDevice)
- res, err := h.svc.Login(c.Request.Context(), req.Username, req.Password,
- req.TrustDevice, deviceCookie, c.Request.UserAgent())
- if err != nil {
- switch {
- case errors.Is(err, ErrInvalidCredentials):
- // 失败审计(audit.md 第三节)
- h.failLogin(c, req.Username, ReasonBadPassword)
- Unauthorized(c, ErrInvalidCredentials.Error())
- default:
- InternalError(c, err)
- }
- return
- }
- if res.TotpRequired {
- c.JSON(http.StatusOK, loginResponse{
- User: userBriefResponse{ID: res.User.ID, Username: res.User.Username},
- TotpRequired: true,
- TempToken: res.TempToken,
- })
- return
- }
- h.succeedLogin(c, res)
- }
- // LoginTotp 实现 POST /api/auth/login/totp(api.md 2.2)。
- func (h *Handler) LoginTotp(c *gin.Context) {
- var req loginTotpRequest
- if !bind(c, &req) {
- return
- }
- res, err := h.svc.LoginTotp(c.Request.Context(), req.TempToken, req.Code, c.Request.UserAgent())
- if err != nil {
- switch {
- case errors.Is(err, ErrPendingExpired):
- Unauthorized(c, ErrPendingExpired.Error())
- case errors.Is(err, ErrBadTotp):
- h.failLogin(c, h.usernameFromPending(c.Request.Context(), req.TempToken), ReasonBadTotp)
- Unauthorized(c, ErrBadTotp.Error())
- default:
- InternalError(c, err)
- }
- return
- }
- h.succeedLogin(c, res)
- }
- // succeedLogin 下发 cookie 并写 login_success 审计。
- func (h *Handler) succeedLogin(c *gin.Context, res LoginResult) {
- setSessionCookie(c, h.CookieDomain, h.CookieSecure, res.SessionToken)
- if res.DeviceToken != "" {
- setTrustedDeviceCookie(c, h.CookieDomain, h.CookieSecure, res.DeviceToken)
- }
- if h.OnLoginSuccess != nil {
- h.OnLoginSuccess(c, LoginAudit{UserID: res.User.ID, Username: res.User.Username, Via: res.AuditVia})
- }
- if res.DeviceToken != "" && h.OnTrustedDeviceAdd != nil {
- h.OnTrustedDeviceAdd(c, AuditContext{UserID: res.User.ID, Username: res.User.Username,
- IP: c.ClientIP(), UA: c.Request.UserAgent()}, res.DeviceName)
- }
- c.JSON(http.StatusOK, loginResponse{
- User: userBriefResponse{ID: res.User.ID, Username: res.User.Username},
- TotpRequired: false,
- })
- }
- func (h *Handler) failLogin(c *gin.Context, username, reason string) {
- if h.OnLoginFailure != nil {
- h.OnLoginFailure(c, username, reason)
- }
- }
- // Logout 实现 POST /api/auth/logout(api.md 2.3)。
- func (h *Handler) Logout(c *gin.Context) {
- ac := h.auditContext(c)
- if err := h.svc.Logout(c.Request.Context(), SessionID(c)); err != nil {
- InternalError(c, err)
- return
- }
- clearSessionCookie(c, h.CookieDomain, h.CookieSecure)
- // 授信设备 cookie 保留(api.md 2.3)
- if SessionID(c) != "" && h.OnLogout != nil {
- h.OnLogout(c, ac)
- }
- c.Status(http.StatusNoContent)
- }
- // SetupTotp 实现 POST /api/auth/totp/setup(api.md 2.4)。
- func (h *Handler) SetupTotp(c *gin.Context) {
- userID := UserID(c)
- secret, uri, err := h.svc.SetupTotp(userID, h.issuer)
- if err != nil {
- switch {
- case errors.Is(err, ErrTotpAlreadyEnabled):
- Conflict(c, ErrTotpAlreadyEnabled.Error())
- default:
- InternalError(c, err)
- }
- return
- }
- c.JSON(http.StatusOK, gin.H{"secret": secret, "otpauth_uri": uri})
- }
- // VerifyTotp 实现 POST /api/auth/totp/verify(api.md 2.5)。
- func (h *Handler) VerifyTotp(c *gin.Context) {
- var req totpVerifyRequest
- if !bind(c, &req) {
- return
- }
- err := h.svc.VerifyTotp(c.Request.Context(), UserID(c), req.Secret, req.Code)
- if err != nil {
- switch {
- case errors.Is(err, ErrBadTotp):
- ValidationFailed(c, map[string]string{"code": ErrBadTotp.Error()})
- case errors.Is(err, ErrTotpAlreadyEnabled):
- Conflict(c, ErrTotpAlreadyEnabled.Error())
- default:
- InternalError(c, err)
- }
- return
- }
- if h.OnTotpBound != nil {
- h.OnTotpBound(c, h.auditContext(c))
- }
- c.JSON(http.StatusOK, gin.H{"totp_enabled": true})
- }
- // DisableTotp 实现 POST /api/auth/totp/disable(api.md 2.6)。
- func (h *Handler) DisableTotp(c *gin.Context) {
- var req totpDisableRequest
- if !bind(c, &req) {
- return
- }
- err := h.svc.DisableTotp(c.Request.Context(), UserID(c), req.Code)
- if err != nil {
- switch {
- case errors.Is(err, ErrBadTotp):
- Unauthorized(c, ErrBadTotp.Error())
- case errors.Is(err, ErrTotpNotEnabled):
- ValidationFailed(c, map[string]string{"code": ErrTotpNotEnabled.Error()})
- default:
- InternalError(c, err)
- }
- return
- }
- if h.OnTotpDisabled != nil {
- h.OnTotpDisabled(c, h.auditContext(c))
- }
- c.JSON(http.StatusOK, gin.H{"totp_enabled": false})
- }
- // ChangePassword 实现 POST /api/auth/password(api.md 2.7)。
- func (h *Handler) ChangePassword(c *gin.Context) {
- var req passwordChangeRequest
- if !bind(c, &req) {
- return
- }
- ac := h.auditContext(c)
- err := h.svc.ChangePassword(c.Request.Context(), UserID(c), req.OldPassword, req.NewPassword, req.TotpCode)
- if err != nil {
- switch {
- case errors.Is(err, ErrInvalidCredentials):
- Unauthorized(c, ErrInvalidCredentials.Error())
- case errors.Is(err, ErrBadTotp):
- Unauthorized(c, ErrBadTotp.Error())
- case IsPasswordPolicyError(err):
- ValidationFailed(c, map[string]string{"new_password": err.Error()})
- default:
- InternalError(c, err)
- }
- return
- }
- if h.OnPasswordChanged != nil {
- h.OnPasswordChanged(c, ac)
- }
- // 当前会话也被删除,客户端需要重新登录(api.md 2.7 备注)
- clearSessionCookie(c, h.CookieDomain, h.CookieSecure)
- c.Status(http.StatusNoContent)
- }
- // Me 实现 GET /api/me(api.md 11.1)。
- func (h *Handler) Me(c *gin.Context) {
- id := CurrentIdentity(c)
- if !id.IsLoggedIn() {
- c.JSON(http.StatusOK, meResponse{Anonymous: true})
- return
- }
- user, err := h.store.GetUser(c.Request.Context(), id.UserID)
- if err != nil {
- InternalError(c, err)
- return
- }
- c.JSON(http.StatusOK, meResponse{
- Anonymous: false,
- AuthType: id.String(),
- Scope: id.Scope,
- User: &userBriefResponse{ID: user.ID, Username: user.Username},
- })
- }
- // usernameFromPending 用临时 token 反查用户名,供 login_failure 审计使用。
- // 查不到时返回空串(audit_events.actor_username 允许空串)。
- func (h *Handler) usernameFromPending(ctx context.Context, tempToken string) string {
- base, _ := splitPendingToken(tempToken)
- username, err := h.svc.pendingUsername(ctx, HashToken(base))
- if err != nil {
- return ""
- }
- return username
- }
|