初始提交:OCI 面板后端(含 GenAI 网关一期)
This commit is contained in:
@@ -0,0 +1,287 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"oci-portal/internal/service"
|
||||
)
|
||||
|
||||
// authxHandler 处理两步验证与外部身份(OAuth)接口。
|
||||
type authxHandler struct {
|
||||
auth *service.AuthService
|
||||
oauth *service.OAuthService
|
||||
}
|
||||
|
||||
// ---- TOTP(JWT 组内) ----
|
||||
|
||||
// totpStatus 返回当前账号两步验证启用状态。
|
||||
func (h *authxHandler) totpStatus(c *gin.Context) {
|
||||
enabled, err := h.auth.TotpStatus(c.Request.Context(), c.GetString(usernameKey))
|
||||
if err != nil {
|
||||
respondError(c, err)
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"enabled": enabled})
|
||||
}
|
||||
|
||||
// totpSetup 生成待激活密钥,返回手动输入码与 otpauth URI(前端渲染二维码)。
|
||||
func (h *authxHandler) totpSetup(c *gin.Context) {
|
||||
secret, uri, err := h.auth.SetupTotp(c.Request.Context(), c.GetString(usernameKey))
|
||||
if err != nil {
|
||||
if errors.Is(err, service.ErrTotpAlreadyOn) {
|
||||
c.JSON(http.StatusConflict, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
respondError(c, err)
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"secret": secret, "otpauthUri": uri})
|
||||
}
|
||||
|
||||
// totpActivate 校验验证码并正式启用两步验证。
|
||||
func (h *authxHandler) totpActivate(c *gin.Context) {
|
||||
var req struct {
|
||||
Code string `json:"code" binding:"required"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
err := h.auth.ActivateTotp(c.Request.Context(), c.GetString(usernameKey), req.Code)
|
||||
if err != nil {
|
||||
if errors.Is(err, service.ErrTotpInvalid) || errors.Is(err, service.ErrTotpNotSetup) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
respondError(c, err)
|
||||
return
|
||||
}
|
||||
c.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// totpDisable 停用两步验证;需当前验证码或登录密码任一确认。
|
||||
func (h *authxHandler) totpDisable(c *gin.Context) {
|
||||
var req struct {
|
||||
Password string `json:"password"`
|
||||
Code string `json:"code"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
err := h.auth.DisableTotp(c.Request.Context(), c.GetString(usernameKey), req.Password, req.Code)
|
||||
if err != nil {
|
||||
if errors.Is(err, service.ErrTotpConfirm) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
respondError(c, err)
|
||||
return
|
||||
}
|
||||
c.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// ---- 登录凭据(JWT 组内) ----
|
||||
|
||||
// getCredentials 返回登录凭据摘要:当前用户名与密码登录禁用态。
|
||||
func (h *authxHandler) getCredentials(c *gin.Context) {
|
||||
disabled, err := h.auth.PasswordLoginDisabled(c.Request.Context())
|
||||
if err != nil {
|
||||
respondError(c, err)
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"username": c.GetString(usernameKey),
|
||||
"passwordLoginDisabled": disabled,
|
||||
})
|
||||
}
|
||||
|
||||
// updateCredentials 修改用户名 / 密码;当前密码必验,改名后旧 JWT 随即失效(前端应重新登录)。
|
||||
func (h *authxHandler) updateCredentials(c *gin.Context) {
|
||||
var req service.UpdateCredentialsInput
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
err := h.auth.UpdateCredentials(c.Request.Context(), c.GetString(usernameKey), req)
|
||||
if errors.Is(err, service.ErrCredentialConfirm) {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if errors.Is(err, service.ErrCredentialInvalid) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
respondError(c, err)
|
||||
return
|
||||
}
|
||||
c.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// updatePasswordLogin 保存密码登录禁用开关;开启需至少绑定一个外部身份。
|
||||
func (h *authxHandler) updatePasswordLogin(c *gin.Context) {
|
||||
var req struct {
|
||||
Disabled *bool `json:"disabled" binding:"required"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
err := h.auth.SetPasswordLoginDisabled(c.Request.Context(), c.GetString(usernameKey), *req.Disabled)
|
||||
if errors.Is(err, service.ErrNeedIdentity) {
|
||||
c.JSON(http.StatusConflict, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
respondError(c, err)
|
||||
return
|
||||
}
|
||||
c.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// ---- OAuth ----
|
||||
|
||||
// oauthProviders 返回已配置的 provider 列表(公开,登录页展示按钮),
|
||||
// 并附带密码登录禁用开关:开启且存在可用外部身份时,登录页隐藏密码表单;
|
||||
// provider 清空时不下发禁用,保证界面始终留有登录入口(后端 Login 仍会拒绝)。
|
||||
func (h *authxHandler) oauthProviders(c *gin.Context) {
|
||||
providers := h.oauth.Providers(c.Request.Context())
|
||||
disabled := false
|
||||
if off, err := h.auth.PasswordLoginDisabled(c.Request.Context()); err == nil && off && len(providers) > 0 {
|
||||
disabled = true
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"providers": providers, "passwordLoginDisabled": disabled})
|
||||
}
|
||||
|
||||
// oauthAuthorize 返回授权跳转 URL;mode=bind 需有效 JWT(绑定到当前账号),login 公开。
|
||||
func (h *authxHandler) oauthAuthorize(c *gin.Context) {
|
||||
provider := c.Param("provider")
|
||||
if provider != "oidc" && provider != "github" {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "unknown provider"})
|
||||
return
|
||||
}
|
||||
mode, username := "login", ""
|
||||
if c.Query("mode") == "bind" {
|
||||
name, ok := h.bearerUser(c)
|
||||
if !ok {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "绑定需要先登录"})
|
||||
return
|
||||
}
|
||||
mode, username = "bind", name
|
||||
}
|
||||
authURL, err := h.oauth.AuthorizeURL(c.Request.Context(), provider, mode, username)
|
||||
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()})
|
||||
return
|
||||
}
|
||||
respondError(c, err)
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"url": authURL})
|
||||
}
|
||||
|
||||
// bearerUser 从 Authorization 头解析用户名(authorize 在公开组,绑定模式手动校验)。
|
||||
func (h *authxHandler) bearerUser(c *gin.Context) (string, bool) {
|
||||
header := c.GetHeader("Authorization")
|
||||
if len(header) < 8 || header[:7] != "Bearer " {
|
||||
return "", false
|
||||
}
|
||||
username, err := h.auth.ParseToken(header[7:])
|
||||
if err != nil {
|
||||
return "", false
|
||||
}
|
||||
return username, true
|
||||
}
|
||||
|
||||
// oauthCallback 是 provider 回调落点(浏览器直达):完成登录或绑定后 302 回前端。
|
||||
func (h *authxHandler) oauthCallback(c *gin.Context) {
|
||||
provider := c.Param("provider")
|
||||
token, _, mode, err := h.oauth.HandleCallback(
|
||||
c.Request.Context(), provider, c.Query("state"), c.Query("code"))
|
||||
if err != nil {
|
||||
target := "/login"
|
||||
if mode == "bind" {
|
||||
target = "/settings"
|
||||
}
|
||||
c.Redirect(http.StatusFound, target+"?oauthError="+url.QueryEscape(oauthErrText(err)))
|
||||
return
|
||||
}
|
||||
if token == "" {
|
||||
// 绑定模式:回设置页安全 tab
|
||||
c.Redirect(http.StatusFound, "/settings?oauth=bound")
|
||||
return
|
||||
}
|
||||
// 登录模式:token 放 fragment,不进服务端日志与 Referer
|
||||
c.Redirect(http.StatusFound, "/login#oauthToken="+url.QueryEscape(token))
|
||||
}
|
||||
|
||||
// oauthErrText 把流程错误转为用户可读文案;内部错误不透出细节。
|
||||
func oauthErrText(err error) string {
|
||||
for _, known := range []error{service.ErrOAuthNotConfigured, service.ErrOAuthNoAppURL, service.ErrOAuthDisabled, service.ErrOAuthState, service.ErrOAuthNotBound, service.ErrOAuthBound} {
|
||||
if errors.Is(err, known) {
|
||||
return known.Error()
|
||||
}
|
||||
}
|
||||
return "登录失败,请重试"
|
||||
}
|
||||
|
||||
// identities 列出当前账号绑定的外部身份。
|
||||
func (h *authxHandler) identities(c *gin.Context) {
|
||||
items, err := h.oauth.Identities(c.Request.Context(), c.GetString(usernameKey))
|
||||
if err != nil {
|
||||
respondError(c, err)
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"items": items})
|
||||
}
|
||||
|
||||
// unbindIdentity 解绑外部身份;密码登录禁用期间不允许解绑最后一个身份。
|
||||
func (h *authxHandler) unbindIdentity(c *gin.Context) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
if err != nil {
|
||||
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 errors.Is(err, service.ErrLastIdentity) {
|
||||
c.JSON(http.StatusConflict, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
respondError(c, err)
|
||||
return
|
||||
}
|
||||
c.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// ---- OAuth provider 设置(JWT 组内) ----
|
||||
|
||||
// getOAuthSettings 返回 provider 配置(secret 只回设置态)。
|
||||
func (h *authxHandler) getOAuthSettings(c *gin.Context, settings *service.SettingService) {
|
||||
view, err := settings.OAuthView(c.Request.Context())
|
||||
if err != nil {
|
||||
respondError(c, err)
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, view)
|
||||
}
|
||||
|
||||
// updateOAuthSettings 保存 provider 配置;secret 缺省沿用、空串清除。
|
||||
func (h *authxHandler) updateOAuthSettings(c *gin.Context, settings *service.SettingService) {
|
||||
var req service.UpdateOAuthInput
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if err := settings.UpdateOAuth(c.Request.Context(), req); err != nil {
|
||||
respondError(c, err)
|
||||
return
|
||||
}
|
||||
h.getOAuthSettings(c, settings)
|
||||
}
|
||||
Reference in New Issue
Block a user