mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-09-29 22:06:38 +08:00
334 lines
8.6 KiB
Go
334 lines
8.6 KiB
Go
package controller
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/url"
|
|
"openflare/common"
|
|
"openflare/model"
|
|
"openflare/service"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/gin-contrib/sessions"
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
const pendingExternalAccountSessionKey = "pending_external_account"
|
|
|
|
type authSourceTogglePayload struct {
|
|
IsActive bool `json:"is_active"`
|
|
}
|
|
|
|
type authSourcePayload struct {
|
|
Name string `json:"name"`
|
|
Type string `json:"type"`
|
|
DisplayName string `json:"display_name"`
|
|
IsActive bool `json:"is_active"`
|
|
ClientID string `json:"client_id"`
|
|
ClientSecret string `json:"client_secret"`
|
|
OpenIDDiscoveryURL string `json:"openid_discovery_url"`
|
|
Scopes string `json:"scopes"`
|
|
IconURL string `json:"icon_url"`
|
|
}
|
|
|
|
func (payload authSourcePayload) toModel() model.AuthSource {
|
|
return model.AuthSource{
|
|
Name: payload.Name,
|
|
Type: payload.Type,
|
|
DisplayName: payload.DisplayName,
|
|
IsActive: payload.IsActive,
|
|
ClientID: payload.ClientID,
|
|
ClientSecret: payload.ClientSecret,
|
|
OpenIDDiscoveryURL: payload.OpenIDDiscoveryURL,
|
|
Scopes: payload.Scopes,
|
|
IconURL: payload.IconURL,
|
|
}
|
|
}
|
|
|
|
func ListAuthSources(c *gin.Context) {
|
|
sources, err := model.GetAuthSources()
|
|
if err != nil {
|
|
respondFailure(c, err.Error())
|
|
return
|
|
}
|
|
respondSuccess(c, sources)
|
|
}
|
|
|
|
func CreateAuthSource(c *gin.Context) {
|
|
var payload authSourcePayload
|
|
if err := decodeJSONBody(c.Request.Body, &payload); err != nil {
|
|
respondBadRequest(c, "无效的参数")
|
|
return
|
|
}
|
|
source := payload.toModel()
|
|
if err := model.CreateAuthSource(&source); err != nil {
|
|
respondFailure(c, err.Error())
|
|
return
|
|
}
|
|
source.Sanitize()
|
|
respondSuccess(c, source)
|
|
}
|
|
|
|
func UpdateAuthSource(c *gin.Context) {
|
|
id, err := parseAuthSourceID(c)
|
|
if err != nil {
|
|
respondBadRequest(c, err.Error())
|
|
return
|
|
}
|
|
var payload authSourcePayload
|
|
if err := decodeJSONBody(c.Request.Body, &payload); err != nil {
|
|
respondBadRequest(c, "无效的参数")
|
|
return
|
|
}
|
|
source := payload.toModel()
|
|
source.ID = id
|
|
keepSecret := strings.TrimSpace(source.ClientSecret) == ""
|
|
if err := model.UpdateAuthSource(&source, keepSecret); err != nil {
|
|
respondFailure(c, err.Error())
|
|
return
|
|
}
|
|
updated, err := model.GetAuthSourceByID(id)
|
|
if err != nil {
|
|
respondFailure(c, err.Error())
|
|
return
|
|
}
|
|
updated.Sanitize()
|
|
respondSuccess(c, updated)
|
|
}
|
|
|
|
func DeleteAuthSource(c *gin.Context) {
|
|
id, err := parseAuthSourceID(c)
|
|
if err != nil {
|
|
respondBadRequest(c, err.Error())
|
|
return
|
|
}
|
|
if err := model.DeleteAuthSource(id); err != nil {
|
|
respondFailure(c, err.Error())
|
|
return
|
|
}
|
|
respondSuccessMessage(c, "")
|
|
}
|
|
|
|
func ToggleAuthSource(c *gin.Context) {
|
|
id, err := parseAuthSourceID(c)
|
|
if err != nil {
|
|
respondBadRequest(c, err.Error())
|
|
return
|
|
}
|
|
var payload authSourceTogglePayload
|
|
if err := decodeJSONBody(c.Request.Body, &payload); err != nil {
|
|
respondBadRequest(c, "无效的参数")
|
|
return
|
|
}
|
|
if err := model.ToggleAuthSource(id, payload.IsActive); err != nil {
|
|
respondFailure(c, err.Error())
|
|
return
|
|
}
|
|
respondSuccessMessage(c, "")
|
|
}
|
|
|
|
func OAuthAuthorize(c *gin.Context) {
|
|
source, err := getAuthSourceFromRoute(c)
|
|
if err != nil {
|
|
respondBadRequest(c, err.Error())
|
|
return
|
|
}
|
|
if !source.IsActive {
|
|
respondFailure(c, "认证源未启用")
|
|
return
|
|
}
|
|
if err := source.Validate(); err != nil {
|
|
respondFailure(c, err.Error())
|
|
return
|
|
}
|
|
state, err := service.GenerateOAuthState()
|
|
if err != nil {
|
|
respondFailure(c, err.Error())
|
|
return
|
|
}
|
|
session := sessions.Default(c)
|
|
session.Set(oauthStateSessionKey(source.ID), state)
|
|
if err := session.Save(); err != nil {
|
|
respondFailure(c, "无法保存授权状态,请重试")
|
|
return
|
|
}
|
|
redirectURL := oauthFrontendCallbackURL(c, source.ID)
|
|
authorizeURL, err := service.BuildAuthorizeURL(c.Request.Context(), source, redirectURL, state)
|
|
if err != nil {
|
|
respondFailure(c, err.Error())
|
|
return
|
|
}
|
|
respondSuccess(c, gin.H{"authorize_url": authorizeURL})
|
|
}
|
|
|
|
func OAuthCallback(c *gin.Context) {
|
|
source, err := getAuthSourceFromRoute(c)
|
|
if err != nil {
|
|
respondBadRequest(c, err.Error())
|
|
return
|
|
}
|
|
if !source.IsActive {
|
|
respondFailure(c, "认证源未启用")
|
|
return
|
|
}
|
|
session := sessions.Default(c)
|
|
expectedState, _ := session.Get(oauthStateSessionKey(source.ID)).(string)
|
|
state := c.Query("state")
|
|
if expectedState == "" || state == "" || state != expectedState {
|
|
respondFailure(c, "授权状态无效,请重新登录")
|
|
return
|
|
}
|
|
session.Delete(oauthStateSessionKey(source.ID))
|
|
if err := session.Save(); err != nil {
|
|
respondFailure(c, "无法更新授权状态,请重试")
|
|
return
|
|
}
|
|
if oauthError := c.Query("error"); oauthError != "" {
|
|
description := c.Query("error_description")
|
|
if description == "" {
|
|
description = oauthError
|
|
}
|
|
respondFailure(c, description)
|
|
return
|
|
}
|
|
|
|
profile, err := service.ExchangeOAuthProfile(c.Request.Context(), source, c.Query("code"), oauthFrontendCallbackURL(c, source.ID))
|
|
if err != nil {
|
|
respondFailure(c, err.Error())
|
|
return
|
|
}
|
|
var currentUserID *int
|
|
if value := session.Get("id"); value != nil {
|
|
if idValue, ok := value.(int); ok {
|
|
currentUserID = &idValue
|
|
}
|
|
}
|
|
result, pending, err := service.CompleteOAuthLogin(source, profile, currentUserID)
|
|
if err != nil {
|
|
respondFailure(c, err.Error())
|
|
return
|
|
}
|
|
if pending != nil {
|
|
raw, err := json.Marshal(pending)
|
|
if err != nil {
|
|
respondFailure(c, err.Error())
|
|
return
|
|
}
|
|
session.Set(pendingExternalAccountSessionKey, string(raw))
|
|
if err := session.Save(); err != nil {
|
|
respondFailure(c, "无法保存待绑定账号,请重试")
|
|
return
|
|
}
|
|
respondSuccess(c, result)
|
|
return
|
|
}
|
|
if result.User != nil {
|
|
cleanUser, err := setLoginSession(result.User, c)
|
|
if err != nil {
|
|
respondFailure(c, "无法保存会话信息,请重试")
|
|
return
|
|
}
|
|
result.User = cleanUser
|
|
}
|
|
respondSuccess(c, result)
|
|
}
|
|
|
|
func LinkExistingOAuthAccount(c *gin.Context) {
|
|
session := sessions.Default(c)
|
|
raw, _ := session.Get(pendingExternalAccountSessionKey).(string)
|
|
if raw == "" {
|
|
respondFailure(c, "待绑定第三方账号已失效,请重新登录")
|
|
return
|
|
}
|
|
var pending service.PendingExternalAccount
|
|
if err := json.Unmarshal([]byte(raw), &pending); err != nil {
|
|
respondFailure(c, "待绑定第三方账号无效,请重新登录")
|
|
return
|
|
}
|
|
var input service.LinkExistingRequest
|
|
if err := decodeJSONBody(c.Request.Body, &input); err != nil {
|
|
respondBadRequest(c, "无效的参数")
|
|
return
|
|
}
|
|
user, err := service.LinkPendingExternalAccount(&pending, input)
|
|
if err != nil {
|
|
respondFailure(c, err.Error())
|
|
return
|
|
}
|
|
session.Delete(pendingExternalAccountSessionKey)
|
|
if err := session.Save(); err != nil {
|
|
respondFailure(c, "无法更新会话信息,请重试")
|
|
return
|
|
}
|
|
cleanUser, err := setLoginSession(user, c)
|
|
if err != nil {
|
|
respondFailure(c, "无法保存会话信息,请重试")
|
|
return
|
|
}
|
|
respondSuccess(c, service.OAuthCallbackResult{Status: "linked", User: cleanUser})
|
|
}
|
|
|
|
func parseAuthSourceID(c *gin.Context) (uint, error) {
|
|
raw := c.Param("source_id")
|
|
if raw == "" {
|
|
raw = c.Param("id")
|
|
}
|
|
parsed, err := strconv.ParseUint(raw, 10, 64)
|
|
if err != nil || parsed == 0 {
|
|
return 0, fmt.Errorf("认证源 ID 无效")
|
|
}
|
|
return uint(parsed), nil
|
|
}
|
|
|
|
func getAuthSourceFromRoute(c *gin.Context) (*model.AuthSource, error) {
|
|
raw := strings.TrimSpace(c.Param("source"))
|
|
if raw == "" {
|
|
raw = strings.TrimSpace(c.Param("source_id"))
|
|
}
|
|
if raw == "" {
|
|
raw = strings.TrimSpace(c.Param("id"))
|
|
}
|
|
if raw == "" {
|
|
return nil, fmt.Errorf("认证源不能为空")
|
|
}
|
|
if parsed, err := strconv.ParseUint(raw, 10, 64); err == nil && parsed > 0 {
|
|
source, err := model.GetAuthSourceByID(uint(parsed))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return source, nil
|
|
}
|
|
source, err := model.GetAuthSourceByName(raw)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return source, nil
|
|
}
|
|
|
|
func oauthStateSessionKey(sourceID uint) string {
|
|
return fmt.Sprintf("oauth_state_%d", sourceID)
|
|
}
|
|
|
|
func oauthFrontendCallbackURL(c *gin.Context, sourceID uint) string {
|
|
base := strings.TrimRight(common.ServerAddress, "/")
|
|
if base == "" {
|
|
scheme := "http"
|
|
if c.Request.TLS != nil || c.GetHeader("X-Forwarded-Proto") == "https" {
|
|
scheme = "https"
|
|
}
|
|
host := c.Request.Host
|
|
if forwardedHost := c.GetHeader("X-Forwarded-Host"); forwardedHost != "" {
|
|
host = forwardedHost
|
|
}
|
|
base = scheme + "://" + host
|
|
}
|
|
source, err := model.GetAuthSourceByID(sourceID)
|
|
sourceName := strconv.FormatUint(uint64(sourceID), 10)
|
|
if err == nil && strings.TrimSpace(source.Name) != "" {
|
|
sourceName = source.Name
|
|
}
|
|
callback, _ := url.JoinPath(base, "oauth", sourceName)
|
|
return callback
|
|
}
|