+51
-27
@@ -16,9 +16,11 @@ import (
|
||||
|
||||
// authxHandler 处理两步验证与外部身份(OAuth)接口。
|
||||
type authxHandler struct {
|
||||
auth *service.AuthService
|
||||
oauth *service.OAuthService
|
||||
logs *service.SystemLogService
|
||||
auth *service.AuthService
|
||||
oauth *service.OAuthService
|
||||
passkeys *service.PasskeyService
|
||||
wallets *service.WalletService
|
||||
logs *service.SystemLogService
|
||||
}
|
||||
|
||||
// ---- TOTP(JWT 组内) ----
|
||||
@@ -66,7 +68,7 @@ func (h *authxHandler) totpSetup(c *gin.Context) {
|
||||
// @Tags 认证
|
||||
// @Param body body object true "{code: 6 位验证码}"
|
||||
// @Success 200 {object} tokenResponse "已启用,返回换发的新 token"
|
||||
// @Success 204 "已启用但新 token 签发失败,需重新登录"
|
||||
// @Failure 401 {object} errorResponse "请求处理期间会话已撤销"
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/auth/totp/activate [post]
|
||||
func (h *authxHandler) totpActivate(c *gin.Context) {
|
||||
@@ -77,7 +79,9 @@ func (h *authxHandler) totpActivate(c *gin.Context) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
err := h.auth.ActivateTotp(c.Request.Context(), c.GetString(usernameKey), req.Code)
|
||||
token, expires, err := h.auth.ActivateTotp(
|
||||
c.Request.Context(), c.GetString(usernameKey), req.Code,
|
||||
bearerToken(c), sessionMetaOf(c), tokenProofOf(c))
|
||||
if err != nil {
|
||||
if errors.Is(err, service.ErrTotpInvalid) || errors.Is(err, service.ErrTotpNotSetup) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
@@ -86,7 +90,7 @@ func (h *authxHandler) totpActivate(c *gin.Context) {
|
||||
respondError(c, err)
|
||||
return
|
||||
}
|
||||
h.respondFreshToken(c)
|
||||
c.JSON(http.StatusOK, gin.H{"token": token, "expiresAt": expires})
|
||||
}
|
||||
|
||||
// totpDisable 停用两步验证;需当前验证码或登录密码任一确认。
|
||||
@@ -95,7 +99,7 @@ func (h *authxHandler) totpActivate(c *gin.Context) {
|
||||
// @Tags 认证
|
||||
// @Param body body object true "{password 或 code 任一确认}"
|
||||
// @Success 200 {object} tokenResponse "已停用,返回换发的新 token"
|
||||
// @Success 204 "已停用但新 token 签发失败,需重新登录"
|
||||
// @Failure 401 {object} errorResponse "请求处理期间会话已撤销"
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/auth/totp/disable [post]
|
||||
func (h *authxHandler) totpDisable(c *gin.Context) {
|
||||
@@ -107,7 +111,9 @@ func (h *authxHandler) totpDisable(c *gin.Context) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
err := h.auth.DisableTotp(c.Request.Context(), c.GetString(usernameKey), req.Password, req.Code)
|
||||
token, expires, err := h.auth.DisableTotp(
|
||||
c.Request.Context(), c.GetString(usernameKey), req.Password, req.Code,
|
||||
bearerToken(c), sessionMetaOf(c), tokenProofOf(c))
|
||||
if err != nil {
|
||||
if errors.Is(err, service.ErrTotpConfirm) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
@@ -116,13 +122,14 @@ func (h *authxHandler) totpDisable(c *gin.Context) {
|
||||
respondError(c, err)
|
||||
return
|
||||
}
|
||||
h.respondFreshToken(c)
|
||||
c.JSON(http.StatusOK, gin.H{"token": token, "expiresAt": expires})
|
||||
}
|
||||
|
||||
// respondFreshToken 敏感变更后为操作者签发新令牌返回(版本已递增,
|
||||
// 旧令牌全部失效);签发失败降级 204,前端按会话失效走重新登录。
|
||||
func (h *authxHandler) respondFreshToken(c *gin.Context) {
|
||||
token, expires, err := h.auth.IssueToken(c.Request.Context(), c.GetString(usernameKey))
|
||||
// 旧令牌全部失效);旧令牌的会话行接续到新令牌,列表中当前设备保持连续。
|
||||
// 签发失败降级 204,前端按会话失效走重新登录。
|
||||
func respondFreshToken(c *gin.Context, auth *service.AuthService) {
|
||||
token, expires, err := auth.RenewToken(c.Request.Context(), c.GetString(usernameKey), bearerToken(c), sessionMetaOf(c))
|
||||
if err != nil {
|
||||
c.Status(http.StatusNoContent)
|
||||
return
|
||||
@@ -138,7 +145,7 @@ func (h *authxHandler) respondFreshToken(c *gin.Context) {
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/auth/revoke-sessions [post]
|
||||
func (h *authxHandler) revokeSessions(c *gin.Context) {
|
||||
token, expires, err := h.auth.RevokeSessions(c.Request.Context(), c.GetString(usernameKey))
|
||||
token, expires, err := h.auth.RevokeSessions(c.Request.Context(), c.GetString(usernameKey), bearerToken(c), sessionMetaOf(c), tokenProofOf(c))
|
||||
if err != nil {
|
||||
respondError(c, err)
|
||||
return
|
||||
@@ -183,7 +190,7 @@ func (h *authxHandler) updateCredentials(c *gin.Context) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
finalName, err := h.auth.UpdateCredentials(c.Request.Context(), c.GetString(usernameKey), req)
|
||||
finalName, err := h.auth.UpdateCredentials(c.Request.Context(), c.GetString(usernameKey), req, tokenProofOf(c))
|
||||
if errors.Is(err, service.ErrCredentialConfirm) {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
|
||||
return
|
||||
@@ -197,7 +204,7 @@ func (h *authxHandler) updateCredentials(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
c.Set(usernameKey, finalName)
|
||||
h.respondFreshToken(c)
|
||||
respondFreshToken(c, h.auth)
|
||||
}
|
||||
|
||||
// updatePasswordLogin 保存密码登录禁用开关;开启需至少绑定一个外部身份。
|
||||
@@ -218,7 +225,7 @@ func (h *authxHandler) updatePasswordLogin(c *gin.Context) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
err := h.auth.SetPasswordLoginDisabled(c.Request.Context(), c.GetString(usernameKey), *req.Disabled)
|
||||
err := h.auth.SetPasswordLoginDisabled(c.Request.Context(), c.GetString(usernameKey), *req.Disabled, tokenProofOf(c))
|
||||
if errors.Is(err, service.ErrNeedIdentity) {
|
||||
c.JSON(http.StatusConflict, gin.H{"error": err.Error()})
|
||||
return
|
||||
@@ -227,26 +234,31 @@ func (h *authxHandler) updatePasswordLogin(c *gin.Context) {
|
||||
respondError(c, err)
|
||||
return
|
||||
}
|
||||
h.respondFreshToken(c)
|
||||
respondFreshToken(c, h.auth)
|
||||
}
|
||||
|
||||
// ---- OAuth ----
|
||||
|
||||
// oauthProviders 返回已配置的 provider 列表(公开,登录页展示按钮),
|
||||
// 并附带密码登录禁用开关:开启且存在可用外部身份时,登录页隐藏密码表单;
|
||||
// provider 清空时不下发禁用,保证界面始终留有登录入口(后端 Login 仍会拒绝)。
|
||||
// 并附带密码登录禁用开关与通行密钥登录可用性:开关开启且存在可用外部身份时,
|
||||
// 登录页隐藏密码表单;provider 清空时不下发禁用,保证界面始终留有登录入口
|
||||
// (后端 Login 仍会拒绝)。
|
||||
//
|
||||
// @Summary 外部登录 provider 列表
|
||||
// @Tags 认证
|
||||
// @Success 200 {object} oauthProvidersResponse "providers 与 passwordLoginDisabled"
|
||||
// @Success 200 {object} oauthProvidersResponse "providers、passwordLoginDisabled 与 passkeyLogin"
|
||||
// @Router /api/v1/auth/oauth/providers [get]
|
||||
func (h *authxHandler) oauthProviders(c *gin.Context) {
|
||||
providers := h.oauth.Providers(c.Request.Context())
|
||||
passkey := h.passkeys != nil && h.passkeys.HasAny(c.Request.Context())
|
||||
wallet := h.wallets != nil && h.wallets.HasAny(c.Request.Context())
|
||||
disabled := false
|
||||
if off, err := h.auth.PasswordLoginDisabled(c.Request.Context()); err == nil && off && len(providers) > 0 {
|
||||
// 有任一免密入口(OAuth/通行密钥/钱包)才如实下发禁用状态;全部缺席时隐藏,
|
||||
// 保证登录页始终留有入口(后端 Login 仍会拒绝)
|
||||
if off, err := h.auth.PasswordLoginDisabled(c.Request.Context()); err == nil && off && (len(providers) > 0 || passkey || wallet) {
|
||||
disabled = true
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"providers": providers, "passwordLoginDisabled": disabled})
|
||||
c.JSON(http.StatusOK, gin.H{"providers": providers, "passwordLoginDisabled": disabled, "passkeyLogin": passkey, "walletLogin": wallet})
|
||||
}
|
||||
|
||||
// oauthAuthorize 返回授权跳转 URL;mode=bind 需有效 JWT(绑定到当前账号),login 公开。
|
||||
@@ -256,6 +268,9 @@ func (h *authxHandler) oauthProviders(c *gin.Context) {
|
||||
// @Param provider path string true "oidc / github"
|
||||
// @Param mode query string false "bind=绑定当前账号(需 Bearer),缺省登录"
|
||||
// @Success 200 {object} urlResponse "url"
|
||||
// @Failure 401 {object} errorResponse "bind 模式未携带有效 Bearer"
|
||||
// @Failure 404 {object} errorResponse "未知 provider"
|
||||
// @Failure 409 {object} errorResponse "provider 未配置 / 面板地址缺失 / 已禁用"
|
||||
// @Router /api/v1/auth/oauth/{provider}/authorize [get]
|
||||
func (h *authxHandler) oauthAuthorize(c *gin.Context) {
|
||||
provider := c.Param("provider")
|
||||
@@ -272,7 +287,7 @@ func (h *authxHandler) oauthAuthorize(c *gin.Context) {
|
||||
}
|
||||
mode, username = "bind", name
|
||||
}
|
||||
authURL, err := h.oauth.AuthorizeURL(c.Request.Context(), provider, mode, username)
|
||||
authURL, err := h.oauth.AuthorizeURL(c.Request.Context(), provider, mode, username, bearerToken(c))
|
||||
if err != nil {
|
||||
if errors.Is(err, service.ErrOAuthNotConfigured) || errors.Is(err, service.ErrOAuthNoAppURL) || errors.Is(err, service.ErrOAuthDisabled) {
|
||||
c.JSON(http.StatusConflict, gin.H{"error": err.Error()})
|
||||
@@ -310,7 +325,7 @@ func (h *authxHandler) oauthCallback(c *gin.Context) {
|
||||
start := time.Now()
|
||||
provider := c.Param("provider")
|
||||
token, username, mode, err := h.oauth.HandleCallback(
|
||||
c.Request.Context(), provider, c.Query("state"), c.Query("code"))
|
||||
c.Request.Context(), provider, c.Query("state"), c.Query("code"), sessionMetaOf(c))
|
||||
if err != nil {
|
||||
h.recordOauth(c, username, http.StatusUnauthorized, oauthErrText(err), start)
|
||||
target := "/login"
|
||||
@@ -390,7 +405,7 @@ func (h *authxHandler) unbindIdentity(c *gin.Context) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
|
||||
return
|
||||
}
|
||||
if err := h.oauth.Unbind(c.Request.Context(), c.GetString(usernameKey), uint(id)); err != nil {
|
||||
if err := h.oauth.Unbind(c.Request.Context(), c.GetString(usernameKey), uint(id), tokenProofOf(c)); err != nil {
|
||||
if errors.Is(err, service.ErrLastIdentity) {
|
||||
c.JSON(http.StatusConflict, gin.H{"error": err.Error()})
|
||||
return
|
||||
@@ -398,7 +413,7 @@ func (h *authxHandler) unbindIdentity(c *gin.Context) {
|
||||
respondError(c, err)
|
||||
return
|
||||
}
|
||||
h.respondFreshToken(c)
|
||||
respondFreshToken(c, h.auth)
|
||||
}
|
||||
|
||||
// ---- OAuth provider 设置(JWT 组内) ----
|
||||
@@ -426,6 +441,9 @@ func (h *authxHandler) getOAuthSettings(c *gin.Context, settings *service.Settin
|
||||
// @Tags 设置
|
||||
// @Param body body service.UpdateOAuthInput true "出现的字段才会被更新"
|
||||
// @Success 200 {object} service.OAuthProvidersView
|
||||
// @Failure 400 {object} errorResponse "请求体非法"
|
||||
// @Failure 401 {object} errorResponse "请求处理期间会话已撤销"
|
||||
// @Failure 409 {object} errorResponse "密码登录禁用期间,该变更将移除最后可用登录方式"
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/settings/oauth [patch]
|
||||
func (h *authxHandler) updateOAuthSettings(c *gin.Context, settings *service.SettingService) {
|
||||
@@ -434,7 +452,13 @@ func (h *authxHandler) updateOAuthSettings(c *gin.Context, settings *service.Set
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if err := settings.UpdateOAuth(c.Request.Context(), req); err != nil {
|
||||
err := settings.UpdateOAuthAuthenticated(
|
||||
c.Request.Context(), req, h.auth, c.GetString(usernameKey), tokenProofOf(c))
|
||||
if err != nil {
|
||||
if errors.Is(err, service.ErrProviderLastLogin) {
|
||||
c.JSON(http.StatusConflict, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
respondError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user