mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-11 17:56:37 +08:00
migrate
This commit is contained in:
@@ -0,0 +1,251 @@
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package legacy
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
ofauth "github.com/Rain-kl/Wavelet/internal/apps/openflare/auth"
|
||||
"github.com/Rain-kl/Wavelet/internal/apps/openflare/compat"
|
||||
"github.com/Rain-kl/Wavelet/internal/model"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type legacyUserPayload struct {
|
||||
ID int `json:"id"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
DisplayName string `json:"display_name"`
|
||||
Role int `json:"role"`
|
||||
Email string `json:"email"`
|
||||
}
|
||||
|
||||
type manageUserRequest struct {
|
||||
Username string `json:"username"`
|
||||
Action string `json:"action"`
|
||||
}
|
||||
|
||||
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"`
|
||||
}
|
||||
|
||||
type authSourceTogglePayload struct {
|
||||
IsActive bool `json:"is_active"`
|
||||
}
|
||||
|
||||
func GetAllUsers(c *gin.Context) {
|
||||
page, _ := strconv.Atoi(c.Query("p"))
|
||||
users, err := ofauth.ListUsers(c.Request.Context(), page)
|
||||
if err != nil {
|
||||
compat.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
compat.OK(c, users)
|
||||
}
|
||||
|
||||
func SearchUsers(c *gin.Context) {
|
||||
users, err := ofauth.SearchUsers(c.Request.Context(), c.Query("keyword"))
|
||||
if err != nil {
|
||||
compat.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
compat.OK(c, users)
|
||||
}
|
||||
|
||||
func GetUser(c *gin.Context) {
|
||||
id, ok := compat.IDParam(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
user, err := ofauth.GetUserByID(c.Request.Context(), callerRole(c), uint64(id))
|
||||
if err != nil {
|
||||
compat.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
compat.OK(c, user)
|
||||
}
|
||||
|
||||
func CreateUser(c *gin.Context) {
|
||||
var req legacyUserPayload
|
||||
if !compat.BindJSON(c, &req) {
|
||||
return
|
||||
}
|
||||
if err := ofauth.CreateUser(c.Request.Context(), callerRole(c), ofauth.CreateUserInput{
|
||||
Username: req.Username,
|
||||
Password: req.Password,
|
||||
DisplayName: req.DisplayName,
|
||||
Role: req.Role,
|
||||
Email: req.Email,
|
||||
}); err != nil {
|
||||
compat.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
compat.OKMessage(c, "")
|
||||
}
|
||||
|
||||
func UpdateUser(c *gin.Context) {
|
||||
var req legacyUserPayload
|
||||
if !compat.BindJSON(c, &req) {
|
||||
return
|
||||
}
|
||||
if err := ofauth.UpdateUser(c.Request.Context(), callerRole(c), ofauth.UpdateUserInput{
|
||||
ID: req.ID,
|
||||
Username: req.Username,
|
||||
Password: req.Password,
|
||||
DisplayName: req.DisplayName,
|
||||
Role: req.Role,
|
||||
Email: req.Email,
|
||||
}); err != nil {
|
||||
compat.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
compat.OKMessage(c, "")
|
||||
}
|
||||
|
||||
func DeleteUser(c *gin.Context) {
|
||||
id, ok := compat.IDParam(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if err := ofauth.DeleteUserByID(c.Request.Context(), callerRole(c), uint64(id)); err != nil {
|
||||
compat.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
compat.OKMessage(c, "")
|
||||
}
|
||||
|
||||
func ManageUser(c *gin.Context) {
|
||||
var req manageUserRequest
|
||||
if !compat.BindJSON(c, &req) {
|
||||
return
|
||||
}
|
||||
user, err := ofauth.ManageUser(c.Request.Context(), callerRole(c), ofauth.ManageUserInput{
|
||||
Username: req.Username,
|
||||
Action: req.Action,
|
||||
})
|
||||
if err != nil {
|
||||
compat.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
compat.OK(c, user)
|
||||
}
|
||||
|
||||
func ListAuthSources(c *gin.Context) {
|
||||
sources, err := model.GetAuthSources(c.Request.Context())
|
||||
if err != nil {
|
||||
compat.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
compat.OK(c, sources)
|
||||
}
|
||||
|
||||
func CreateAuthSource(c *gin.Context) {
|
||||
var payload authSourcePayload
|
||||
if !compat.BindJSON(c, &payload) {
|
||||
return
|
||||
}
|
||||
source := payload.toModel()
|
||||
if err := model.CreateAuthSource(c.Request.Context(), &source); err != nil {
|
||||
compat.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
source.Sanitize()
|
||||
compat.OK(c, source)
|
||||
}
|
||||
|
||||
func UpdateAuthSource(c *gin.Context) {
|
||||
id, err := parseAuthSourceID(c)
|
||||
if err != nil {
|
||||
compat.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
var payload authSourcePayload
|
||||
if !compat.BindJSON(c, &payload) {
|
||||
return
|
||||
}
|
||||
source := payload.toModel()
|
||||
source.ID = id
|
||||
keepSecret := strings.TrimSpace(source.ClientSecret) == ""
|
||||
if err := model.UpdateAuthSource(c.Request.Context(), &source, keepSecret); err != nil {
|
||||
compat.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
updated, err := model.GetAuthSourceByID(c.Request.Context(), id)
|
||||
if err != nil {
|
||||
compat.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
updated.Sanitize()
|
||||
compat.OK(c, updated)
|
||||
}
|
||||
|
||||
func DeleteAuthSource(c *gin.Context) {
|
||||
id, err := parseAuthSourceID(c)
|
||||
if err != nil {
|
||||
compat.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
if err := model.DeleteAuthSource(c.Request.Context(), id); err != nil {
|
||||
compat.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
compat.OKMessage(c, "")
|
||||
}
|
||||
|
||||
func ToggleAuthSource(c *gin.Context) {
|
||||
id, err := parseAuthSourceID(c)
|
||||
if err != nil {
|
||||
compat.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
var payload authSourceTogglePayload
|
||||
if !compat.BindJSON(c, &payload) {
|
||||
return
|
||||
}
|
||||
if err := model.ToggleAuthSource(c.Request.Context(), id, payload.IsActive); err != nil {
|
||||
compat.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
compat.OKMessage(c, "")
|
||||
}
|
||||
|
||||
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 parseAuthSourceID(c *gin.Context) (uint64, error) {
|
||||
raw := strings.TrimSpace(c.Param("id"))
|
||||
if raw == "" {
|
||||
return 0, fmt.Errorf("认证源 ID 无效")
|
||||
}
|
||||
source, err := model.GetAuthSourceByName(c.Request.Context(), raw)
|
||||
if err == nil {
|
||||
return source.ID, nil
|
||||
}
|
||||
parsed, err := strconv.ParseUint(raw, 10, 64)
|
||||
if err != nil || parsed == 0 {
|
||||
return 0, fmt.Errorf("认证源 ID 无效")
|
||||
}
|
||||
return parsed, nil
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package legacy
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
ofauth "github.com/Rain-kl/Wavelet/internal/apps/openflare/auth"
|
||||
"github.com/Rain-kl/Wavelet/internal/apps/openflare/compat"
|
||||
"github.com/Rain-kl/Wavelet/internal/model"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type linkExistingRequest struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
// OAuthAuthorize starts OAuth authorization for a legacy auth source.
|
||||
func OAuthAuthorize(c *gin.Context) {
|
||||
url, err := ofauth.OAuthAuthorize(c.Request.Context(), c, c.Param("source"))
|
||||
if err != nil {
|
||||
compat.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
compat.OK(c, gin.H{"authorize_url": url})
|
||||
}
|
||||
|
||||
// OAuthCallback handles the legacy GET OAuth callback.
|
||||
func OAuthCallback(c *gin.Context) {
|
||||
result, err := ofauth.OAuthCallback(c.Request.Context(), c, c.Param("source"))
|
||||
if err != nil {
|
||||
compat.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
compat.OK(c, result)
|
||||
}
|
||||
|
||||
// LinkExistingOAuthAccount binds a pending OAuth account to an existing user.
|
||||
func LinkExistingOAuthAccount(c *gin.Context) {
|
||||
var req linkExistingRequest
|
||||
if !compat.BindJSON(c, &req) {
|
||||
return
|
||||
}
|
||||
result, err := ofauth.LinkExistingOAuthAccount(c.Request.Context(), c, ofauth.LinkExistingInput{
|
||||
Username: req.Username,
|
||||
Password: req.Password,
|
||||
})
|
||||
if err != nil {
|
||||
compat.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
compat.OK(c, result)
|
||||
}
|
||||
|
||||
// ListExternalAccounts returns external account bindings for the current user.
|
||||
func ListExternalAccounts(c *gin.Context) {
|
||||
userID := callerUserID(c)
|
||||
accounts, err := model.ListExternalAccountsByUserID(c.Request.Context(), userID)
|
||||
if err != nil {
|
||||
compat.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
compat.OK(c, accounts)
|
||||
}
|
||||
|
||||
// DeleteExternalAccount removes an external account binding.
|
||||
func DeleteExternalAccount(c *gin.Context) {
|
||||
userID := callerUserID(c)
|
||||
if userID == 0 {
|
||||
compat.Unauthorized(c, "无权进行此操作,未登录或 token 无效")
|
||||
return
|
||||
}
|
||||
rawID := strings.TrimSpace(c.Param("id"))
|
||||
id, err := strconv.ParseUint(rawID, 10, 64)
|
||||
if err != nil || id == 0 {
|
||||
compat.Fail(c, "绑定记录 ID 无效")
|
||||
return
|
||||
}
|
||||
if err := model.DeleteExternalAccountForUser(c.Request.Context(), id, userID); err != nil {
|
||||
compat.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
compat.OKMessage(c, "")
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package legacy
|
||||
|
||||
import (
|
||||
ofauth "github.com/Rain-kl/Wavelet/internal/apps/openflare/auth"
|
||||
"github.com/Rain-kl/Wavelet/internal/apps/openflare/compat"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type loginRequest struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
Code string `json:"code"`
|
||||
}
|
||||
|
||||
type registerRequest struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
Nickname string `json:"nickname"`
|
||||
DisplayName string `json:"display_name"`
|
||||
Email string `json:"email"`
|
||||
Code string `json:"code"`
|
||||
}
|
||||
|
||||
type updateSelfRequest struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
DisplayName string `json:"display_name"`
|
||||
Email string `json:"email"`
|
||||
}
|
||||
|
||||
type passwordResetRequest struct {
|
||||
Email string `json:"email"`
|
||||
Token string `json:"token"`
|
||||
}
|
||||
|
||||
func Login(c *gin.Context) {
|
||||
var req loginRequest
|
||||
if !compat.BindJSON(c, &req) {
|
||||
return
|
||||
}
|
||||
user, err := ofauth.Login(c.Request.Context(), c, ofauth.LoginInput{
|
||||
Username: req.Username,
|
||||
Password: req.Password,
|
||||
Code: req.Code,
|
||||
})
|
||||
if err != nil {
|
||||
compat.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
compat.OK(c, user)
|
||||
}
|
||||
|
||||
func Logout(c *gin.Context) {
|
||||
if err := ofauth.Logout(c.Request.Context(), c); err != nil {
|
||||
compat.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
compat.OKMessage(c, "")
|
||||
}
|
||||
|
||||
func GetSelf(c *gin.Context) {
|
||||
user, err := ofauth.GetSelf(c.Request.Context(), callerUserID(c))
|
||||
if err != nil {
|
||||
compat.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
compat.OK(c, user)
|
||||
}
|
||||
|
||||
func UpdateSelf(c *gin.Context) {
|
||||
var req updateSelfRequest
|
||||
if !compat.BindJSON(c, &req) {
|
||||
return
|
||||
}
|
||||
if err := ofauth.UpdateSelf(c.Request.Context(), callerUserID(c), ofauth.UpdateSelfInput{
|
||||
Username: req.Username,
|
||||
Password: req.Password,
|
||||
DisplayName: req.DisplayName,
|
||||
Email: req.Email,
|
||||
}); err != nil {
|
||||
compat.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
compat.OKMessage(c, "")
|
||||
}
|
||||
|
||||
func DeleteSelf(c *gin.Context) {
|
||||
if err := ofauth.DeleteSelf(c.Request.Context(), callerUserID(c)); err != nil {
|
||||
compat.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
compat.OKMessage(c, "")
|
||||
}
|
||||
|
||||
func GenerateToken(c *gin.Context) {
|
||||
token, err := ofauth.GenerateUserToken(c.Request.Context(), callerUserID(c))
|
||||
if err != nil {
|
||||
compat.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
compat.OK(c, token)
|
||||
}
|
||||
|
||||
func Register(c *gin.Context) {
|
||||
var req registerRequest
|
||||
if !compat.BindJSON(c, &req) {
|
||||
return
|
||||
}
|
||||
user, err := ofauth.Register(c.Request.Context(), c, ofauth.RegisterInput{
|
||||
Username: req.Username,
|
||||
Password: req.Password,
|
||||
Nickname: req.Nickname,
|
||||
DisplayName: req.DisplayName,
|
||||
Email: req.Email,
|
||||
Code: req.Code,
|
||||
})
|
||||
if err != nil {
|
||||
compat.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
compat.OK(c, user)
|
||||
}
|
||||
|
||||
func SendEmailVerification(c *gin.Context) {
|
||||
email := c.Query("email")
|
||||
if err := ofauth.SendRegisterVerificationEmail(c.Request.Context(), email); err != nil {
|
||||
compat.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
compat.OKMessage(c, "")
|
||||
}
|
||||
|
||||
func ResetPassword(c *gin.Context) {
|
||||
var req passwordResetRequest
|
||||
if !compat.BindJSON(c, &req) {
|
||||
return
|
||||
}
|
||||
password, err := ofauth.ResetPassword(c.Request.Context(), ofauth.PasswordResetInput{
|
||||
Email: req.Email,
|
||||
Token: req.Token,
|
||||
})
|
||||
if err != nil {
|
||||
compat.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
compat.OK(c, password)
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package legacy
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/Rain-kl/Wavelet/internal/apps/cap"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type capRedeemRequest struct {
|
||||
Token string `json:"token" binding:"required"`
|
||||
Solutions []int `json:"solutions" binding:"required"`
|
||||
}
|
||||
|
||||
// GetCapChallenge generates a CAP challenge for the legacy frontend.
|
||||
func GetCapChallenge(c *gin.Context) {
|
||||
scope := c.Param("scope")
|
||||
if scope == "" {
|
||||
scope = c.Query("scope")
|
||||
}
|
||||
if scope == "" {
|
||||
scope = "login"
|
||||
}
|
||||
|
||||
mgr := cap.GetDefaultManager()
|
||||
resp, err := mgr.Generate(c.Request.Context(), scope)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, cap.RedeemResponse{
|
||||
Success: false,
|
||||
Error: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, resp)
|
||||
}
|
||||
|
||||
// RedeemCapChallenge redeems a CAP challenge for the legacy frontend.
|
||||
func RedeemCapChallenge(c *gin.Context) {
|
||||
scope := c.Param("scope")
|
||||
if scope == "" {
|
||||
scope = c.Query("scope")
|
||||
}
|
||||
if scope == "" {
|
||||
scope = "login"
|
||||
}
|
||||
|
||||
var req capRedeemRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, cap.RedeemResponse{
|
||||
Success: false,
|
||||
Error: "无效的参数",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
mgr := cap.GetDefaultManager()
|
||||
resp, err := mgr.Redeem(c.Request.Context(), req.Token, req.Solutions, scope)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, cap.RedeemResponse{
|
||||
Success: false,
|
||||
Error: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
if !resp.Success {
|
||||
c.JSON(http.StatusBadRequest, resp)
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, resp)
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package legacy
|
||||
|
||||
import (
|
||||
"github.com/Rain-kl/Wavelet/internal/apps/cap"
|
||||
"github.com/Rain-kl/Wavelet/internal/apps/openflare/compat"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// bridgeOpenFlareToken maps OpenFlare-Token to X-Access-Token for compat auth middleware.
|
||||
func bridgeOpenFlareToken() gin.HandlerFunc {
|
||||
return compat.BridgeOpenFlareToken()
|
||||
}
|
||||
|
||||
// legacyCapAuth verifies PoW CAPTCHA for legacy login using OpenFlare response format.
|
||||
func legacyCapAuth(scope string) gin.HandlerFunc {
|
||||
mgr := cap.GetDefaultManager()
|
||||
return func(c *gin.Context) {
|
||||
if !cap.ProtectionEnabled(c.Request.Context()) {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
token := c.GetHeader("X-Cap-Token")
|
||||
if token == "" {
|
||||
compat.Fail(c, "缺少人机验证凭证")
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
valid, err := mgr.VerifyToken(c.Request.Context(), token, scope)
|
||||
if err != nil || !valid {
|
||||
compat.Fail(c, "人机验证凭证无效或已过期")
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
func callerRole(c *gin.Context) int {
|
||||
if role, ok := c.Get("of_role"); ok {
|
||||
if v, ok := role.(int); ok {
|
||||
return v
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func callerUserID(c *gin.Context) uint64 {
|
||||
if id, ok := c.Get("of_user_id"); ok {
|
||||
if v, ok := id.(uint64); ok {
|
||||
return v
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package legacy
|
||||
|
||||
import (
|
||||
ofauth "github.com/Rain-kl/Wavelet/internal/apps/openflare/auth"
|
||||
"github.com/Rain-kl/Wavelet/internal/apps/openflare/compat"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// SendPasswordResetEmail sends a password reset email for the legacy frontend.
|
||||
func SendPasswordResetEmail(c *gin.Context) {
|
||||
email := c.Query("email")
|
||||
if err := ofauth.SendPasswordResetEmail(c.Request.Context(), email); err != nil {
|
||||
compat.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
compat.OKMessage(c, "")
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package legacy registers OpenFlare /api/* compatibility routes for the old frontend.
|
||||
package legacy
|
||||
|
||||
import "github.com/gin-gonic/gin"
|
||||
|
||||
// RegisterRoutes mounts all OpenFlare legacy API routes under the /api group.
|
||||
func RegisterRoutes(apiGroup *gin.RouterGroup) {
|
||||
registerAuthRoutes(apiGroup)
|
||||
registerOptionRoutes(apiGroup)
|
||||
registerOriginRoutes(apiGroup)
|
||||
registerApplyLogRoutes(apiGroup)
|
||||
registerProxyRouteRoutes(apiGroup)
|
||||
registerNodeRoutes(apiGroup)
|
||||
registerWAFRoutes(apiGroup)
|
||||
registerTLSRoutes(apiGroup)
|
||||
registerConfigVersionRoutes(apiGroup)
|
||||
registerAgentRoutes(apiGroup)
|
||||
registerPagesRoutes(apiGroup)
|
||||
registerRelayFlaredRoutes(apiGroup)
|
||||
registerDashboardObsRoutes(apiGroup)
|
||||
registerMiscRoutes(apiGroup)
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package legacy
|
||||
|
||||
import (
|
||||
"github.com/Rain-kl/Wavelet/internal/apps/openflare/agent"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func registerAgentRoutes(apiGroup *gin.RouterGroup) {
|
||||
agent.RegisterRoutes(apiGroup)
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package legacy
|
||||
|
||||
import (
|
||||
"github.com/Rain-kl/Wavelet/internal/apps/openflare/apply_log"
|
||||
"github.com/Rain-kl/Wavelet/internal/apps/openflare/compat"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func registerApplyLogRoutes(apiGroup *gin.RouterGroup) {
|
||||
applyLogRoute := apiGroup.Group("/apply-logs")
|
||||
applyLogRoute.Use(compat.AdminAuth())
|
||||
{
|
||||
applyLogRoute.GET("/", apply_log.GetApplyLogs)
|
||||
applyLogRoute.POST("/cleanup", apply_log.CleanupApplyLogs)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package legacy
|
||||
|
||||
import (
|
||||
"github.com/Rain-kl/Wavelet/internal/apps/openflare/compat"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func registerAuthRoutes(apiGroup *gin.RouterGroup) {
|
||||
// /status, /notice, /about are registered by T-OPTION (option.RegisterRoutes).
|
||||
apiGroup.GET("/verification", SendEmailVerification)
|
||||
apiGroup.GET("/reset_password", SendPasswordResetEmail)
|
||||
apiGroup.POST("/user/reset", ResetPassword)
|
||||
|
||||
oauthGroup := apiGroup.Group("/oauth")
|
||||
{
|
||||
oauthGroup.GET("/:source/authorize", OAuthAuthorize)
|
||||
oauthGroup.GET("/:source/callback", OAuthCallback)
|
||||
oauthGroup.POST("/link-existing", LinkExistingOAuthAccount)
|
||||
|
||||
externalAccounts := oauthGroup.Group("/external-accounts")
|
||||
externalAccounts.Use(bridgeOpenFlareToken(), compat.UserAuth())
|
||||
{
|
||||
externalAccounts.GET("/", ListExternalAccounts)
|
||||
externalAccounts.POST("/:id/delete", DeleteExternalAccount)
|
||||
}
|
||||
}
|
||||
|
||||
capGroup := apiGroup.Group("/cap")
|
||||
{
|
||||
capGroup.POST("/:scope/challenge", GetCapChallenge)
|
||||
capGroup.POST("/:scope/redeem", RedeemCapChallenge)
|
||||
}
|
||||
|
||||
userGroup := apiGroup.Group("/user")
|
||||
{
|
||||
userGroup.POST("/register", Register)
|
||||
userGroup.POST("/login", legacyCapAuth("login"), Login)
|
||||
userGroup.GET("/logout", Logout)
|
||||
|
||||
selfGroup := userGroup.Group("/")
|
||||
selfGroup.Use(bridgeOpenFlareToken(), compat.UserAuth())
|
||||
{
|
||||
selfGroup.GET("/self", GetSelf)
|
||||
selfGroup.POST("/self/update", UpdateSelf)
|
||||
selfGroup.POST("/self/delete", DeleteSelf)
|
||||
selfGroup.GET("/token", GenerateToken)
|
||||
}
|
||||
|
||||
adminGroup := userGroup.Group("/")
|
||||
adminGroup.Use(bridgeOpenFlareToken(), compat.AdminAuth())
|
||||
{
|
||||
adminGroup.GET("/", GetAllUsers)
|
||||
adminGroup.GET("/search", SearchUsers)
|
||||
adminGroup.GET("/:id", GetUser)
|
||||
adminGroup.POST("/", CreateUser)
|
||||
adminGroup.POST("/manage", ManageUser)
|
||||
adminGroup.POST("/update", UpdateUser)
|
||||
adminGroup.POST("/:id/delete", DeleteUser)
|
||||
}
|
||||
}
|
||||
|
||||
authSourceGroup := apiGroup.Group("/auth-sources")
|
||||
authSourceGroup.Use(bridgeOpenFlareToken(), compat.RootAuth())
|
||||
{
|
||||
authSourceGroup.GET("/", ListAuthSources)
|
||||
authSourceGroup.POST("/", CreateAuthSource)
|
||||
authSourceGroup.POST("/:id/update", UpdateAuthSource)
|
||||
authSourceGroup.POST("/:id/delete", DeleteAuthSource)
|
||||
authSourceGroup.POST("/:id/toggle", ToggleAuthSource)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package legacy
|
||||
|
||||
import (
|
||||
"github.com/Rain-kl/Wavelet/internal/apps/openflare/compat"
|
||||
"github.com/Rain-kl/Wavelet/internal/apps/openflare/config_version"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func registerConfigVersionRoutes(apiGroup *gin.RouterGroup) {
|
||||
configVersionGroup := apiGroup.Group("/config-versions")
|
||||
configVersionGroup.Use(compat.AdminAuth())
|
||||
{
|
||||
configVersionGroup.GET("/", config_version.ListConfigVersionsHandler)
|
||||
configVersionGroup.GET("/active", config_version.GetActiveConfigVersionHandler)
|
||||
configVersionGroup.GET("/preview", config_version.PreviewConfigVersionHandler)
|
||||
configVersionGroup.GET("/diff", config_version.DiffConfigVersionHandler)
|
||||
configVersionGroup.GET("/:id", config_version.GetConfigVersionHandler)
|
||||
configVersionGroup.POST("/publish", config_version.PublishConfigVersionHandler)
|
||||
configVersionGroup.POST("/:id/activate", config_version.ActivateConfigVersionHandler)
|
||||
configVersionGroup.POST("/cleanup", config_version.CleanupConfigVersionsHandler)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package legacy
|
||||
|
||||
import (
|
||||
"github.com/Rain-kl/Wavelet/internal/apps/openflare/dashboard"
|
||||
"github.com/Rain-kl/Wavelet/internal/apps/openflare/observability"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func registerDashboardObsRoutes(apiGroup *gin.RouterGroup) {
|
||||
dashboard.RegisterRoutes(apiGroup)
|
||||
observability.RegisterRoutes(apiGroup)
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package legacy
|
||||
|
||||
import (
|
||||
"github.com/Rain-kl/Wavelet/internal/apps/openflare/compat"
|
||||
"github.com/Rain-kl/Wavelet/internal/apps/openflare/update"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func registerMiscRoutes(apiGroup *gin.RouterGroup) {
|
||||
updateRoute := apiGroup.Group("/update")
|
||||
updateRoute.Use(compat.RootAuth())
|
||||
{
|
||||
updateRoute.GET("/latest-release", update.GetLatestReleaseHandler)
|
||||
updateRoute.GET("/logs/ws", update.StreamServerUpgradeLogsHandler)
|
||||
updateRoute.POST("/manual-upload", update.UploadManualServerBinaryHandler)
|
||||
updateRoute.POST("/manual-upgrade", update.ConfirmManualServerUpgradeHandler)
|
||||
updateRoute.POST("/upgrade", update.UpgradeServerHandler)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package legacy
|
||||
|
||||
import (
|
||||
"github.com/Rain-kl/Wavelet/internal/apps/openflare/compat"
|
||||
"github.com/Rain-kl/Wavelet/internal/apps/openflare/node"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func registerNodeRoutes(apiGroup *gin.RouterGroup) {
|
||||
nodeRoute := apiGroup.Group("/nodes")
|
||||
nodeRoute.Use(compat.AdminAuth())
|
||||
{
|
||||
nodeRoute.GET("/bootstrap-token", node.GetBootstrapTokenHandler)
|
||||
nodeRoute.POST("/bootstrap-token/rotate", node.RotateBootstrapTokenHandler)
|
||||
nodeRoute.GET("/", node.ListNodesHandler)
|
||||
nodeRoute.POST("/", node.CreateNodeHandler)
|
||||
nodeRoute.GET("/:id/agent-release", node.GetAgentReleaseHandler)
|
||||
nodeRoute.POST("/:id/update", node.UpdateNodeHandler)
|
||||
nodeRoute.POST("/:id/delete", node.DeleteNodeHandler)
|
||||
nodeRoute.POST("/:id/agent-update", node.RequestAgentUpdateHandler)
|
||||
nodeRoute.POST("/:id/openresty-restart", node.RequestOpenrestyRestartHandler)
|
||||
nodeRoute.POST("/:id/force-sync", node.RequestForceSyncHandler)
|
||||
nodeRoute.GET("/:id/observability", node.GetObservabilityHandler)
|
||||
nodeRoute.POST("/:id/observability/cleanup", node.CleanupHealthEventsHandler)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package legacy
|
||||
|
||||
import (
|
||||
"github.com/Rain-kl/Wavelet/internal/apps/openflare/option"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func registerOptionRoutes(apiGroup *gin.RouterGroup) {
|
||||
option.RegisterRoutes(apiGroup)
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package legacy
|
||||
|
||||
import (
|
||||
"github.com/Rain-kl/Wavelet/internal/apps/openflare/compat"
|
||||
"github.com/Rain-kl/Wavelet/internal/apps/openflare/origin"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func registerOriginRoutes(apiGroup *gin.RouterGroup) {
|
||||
originRoute := apiGroup.Group("/origins")
|
||||
originRoute.Use(compat.AdminAuth())
|
||||
{
|
||||
originRoute.GET("/", origin.GetOrigins)
|
||||
originRoute.GET("/:id", origin.GetOrigin)
|
||||
originRoute.POST("/", origin.CreateOriginHandler)
|
||||
originRoute.POST("/:id/update", origin.UpdateOriginHandler)
|
||||
originRoute.POST("/:id/delete", origin.DeleteOriginHandler)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package legacy
|
||||
|
||||
import (
|
||||
"github.com/Rain-kl/Wavelet/internal/apps/openflare/compat"
|
||||
"github.com/Rain-kl/Wavelet/internal/apps/openflare/pages"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func registerPagesRoutes(apiGroup *gin.RouterGroup) {
|
||||
pagesRoute := apiGroup.Group("/pages")
|
||||
pagesRoute.Use(compat.AdminAuth())
|
||||
{
|
||||
pagesRoute.GET("/", pages.ListProjectsHandler)
|
||||
pagesRoute.GET("/:id", pages.GetProjectHandler)
|
||||
pagesRoute.POST("/", pages.CreateProjectHandler)
|
||||
pagesRoute.POST("/:id/update", pages.UpdateProjectHandler)
|
||||
pagesRoute.POST("/:id/delete", pages.DeleteProjectHandler)
|
||||
pagesRoute.GET("/:id/deployments", pages.ListDeploymentsHandler)
|
||||
pagesRoute.POST("/:id/deployments/upload", pages.UploadDeploymentHandler)
|
||||
pagesRoute.POST("/:id/deployments/:deployment_id/activate", pages.ActivateDeploymentHandler)
|
||||
pagesRoute.POST("/:id/deployments/:deployment_id/delete", pages.DeleteDeploymentHandler)
|
||||
pagesRoute.GET("/deployments/:deployment_id/files", pages.ListDeploymentFilesHandler)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package legacy
|
||||
|
||||
import (
|
||||
"github.com/Rain-kl/Wavelet/internal/apps/openflare/compat"
|
||||
"github.com/Rain-kl/Wavelet/internal/apps/openflare/proxy_route"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func registerProxyRouteRoutes(apiGroup *gin.RouterGroup) {
|
||||
proxyRouteGroup := apiGroup.Group("/proxy-routes")
|
||||
proxyRouteGroup.Use(compat.AdminAuth())
|
||||
{
|
||||
proxyRouteGroup.GET("/", proxy_route.GetProxyRoutes)
|
||||
proxyRouteGroup.GET("/:id", proxy_route.GetProxyRouteHandler)
|
||||
proxyRouteGroup.POST("/", proxy_route.CreateProxyRouteHandler)
|
||||
proxyRouteGroup.POST("/:id/update", proxy_route.UpdateProxyRouteHandler)
|
||||
proxyRouteGroup.POST("/:id/delete", proxy_route.DeleteProxyRouteHandler)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package legacy
|
||||
|
||||
import (
|
||||
"github.com/Rain-kl/Wavelet/internal/apps/openflare/flared"
|
||||
"github.com/Rain-kl/Wavelet/internal/apps/openflare/relay"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func registerRelayFlaredRoutes(apiGroup *gin.RouterGroup) {
|
||||
relayRoute := apiGroup.Group("/relay")
|
||||
relayRoute.Use(relay.RelayAuth())
|
||||
{
|
||||
relayRoute.POST("/heartbeat", relay.PostHeartbeat)
|
||||
relayRoute.GET("/ws", relay.GetWebSocket)
|
||||
}
|
||||
|
||||
flaredRoute := apiGroup.Group("/flared")
|
||||
flaredRoute.Use(flared.TunnelAuth())
|
||||
{
|
||||
flaredRoute.POST("/heartbeat", flared.PostHeartbeat)
|
||||
flaredRoute.GET("/config/active", flared.GetActiveConfig)
|
||||
flaredRoute.POST("/apply-log", flared.PostApplyLog)
|
||||
flaredRoute.GET("/ws", flared.GetWebSocket)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package legacy
|
||||
|
||||
import (
|
||||
"github.com/Rain-kl/Wavelet/internal/apps/openflare/compat"
|
||||
"github.com/Rain-kl/Wavelet/internal/apps/openflare/tls"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func registerTLSRoutes(apiGroup *gin.RouterGroup) {
|
||||
managedDomainRoute := apiGroup.Group("/managed-domains")
|
||||
managedDomainRoute.Use(compat.AdminAuth())
|
||||
{
|
||||
managedDomainRoute.GET("/", tls.GetManagedDomains)
|
||||
managedDomainRoute.GET("/match", tls.MatchManagedDomainCertificateHandler)
|
||||
managedDomainRoute.POST("/", tls.CreateManagedDomainHandler)
|
||||
managedDomainRoute.POST("/:id/update", tls.UpdateManagedDomainHandler)
|
||||
managedDomainRoute.POST("/:id/delete", tls.DeleteManagedDomainHandler)
|
||||
}
|
||||
|
||||
tlsCertificateRoute := apiGroup.Group("/tls-certificates")
|
||||
tlsCertificateRoute.Use(compat.AdminAuth())
|
||||
{
|
||||
tlsCertificateRoute.GET("/", tls.GetCertificates)
|
||||
tlsCertificateRoute.GET("/:id", tls.GetCertificateDetail)
|
||||
tlsCertificateRoute.GET("/:id/content", tls.GetCertificateContentHandler)
|
||||
tlsCertificateRoute.POST("/", tls.CreateCertificateHandler)
|
||||
tlsCertificateRoute.POST("/:id/update", tls.UpdateCertificateHandler)
|
||||
tlsCertificateRoute.POST("/:id/update-acme", tls.UpdateACMECertificateHandler)
|
||||
tlsCertificateRoute.POST("/:id/convert-acme", tls.ConvertCertificateToACMEHandler)
|
||||
tlsCertificateRoute.POST("/import-file", tls.ImportCertificateFile)
|
||||
tlsCertificateRoute.POST("/:id/delete", tls.DeleteCertificateHandler)
|
||||
tlsCertificateRoute.POST("/apply", tls.ApplyCertificateHandler)
|
||||
tlsCertificateRoute.POST("/:id/renew", tls.RenewCertificateHandler)
|
||||
}
|
||||
|
||||
acmeAccountRoute := apiGroup.Group("/acme-accounts")
|
||||
acmeAccountRoute.Use(compat.AdminAuth())
|
||||
{
|
||||
acmeAccountRoute.GET("/default", tls.GetDefaultAcmeAccountHandler)
|
||||
}
|
||||
|
||||
dnsAccountRoute := apiGroup.Group("/dns-accounts")
|
||||
dnsAccountRoute.Use(compat.AdminAuth())
|
||||
{
|
||||
dnsAccountRoute.GET("/", tls.GetDNSAccounts)
|
||||
dnsAccountRoute.POST("/", tls.CreateDNSAccountHandler)
|
||||
dnsAccountRoute.POST("/:id/update", tls.UpdateDNSAccountHandler)
|
||||
dnsAccountRoute.POST("/:id/delete", tls.DeleteDNSAccountHandler)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package legacy
|
||||
|
||||
import (
|
||||
"github.com/Rain-kl/Wavelet/internal/apps/openflare/compat"
|
||||
"github.com/Rain-kl/Wavelet/internal/apps/openflare/waf"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func registerWAFRoutes(apiGroup *gin.RouterGroup) {
|
||||
wafRoute := apiGroup.Group("/waf")
|
||||
wafRoute.Use(compat.AdminAuth())
|
||||
{
|
||||
wafRoute.GET("/ip-groups", waf.ListIPGroupsHandler)
|
||||
wafRoute.GET("/ip-groups/:id", waf.GetIPGroupHandler)
|
||||
wafRoute.POST("/ip-groups", waf.CreateIPGroupHandler)
|
||||
wafRoute.POST("/ip-groups/test", waf.TestIPGroupAutoConfigHandler)
|
||||
wafRoute.POST("/ip-groups/:id/update", waf.UpdateIPGroupHandler)
|
||||
wafRoute.POST("/ip-groups/:id/delete", waf.DeleteIPGroupHandler)
|
||||
wafRoute.POST("/ip-groups/:id/sync", waf.SyncIPGroupHandler)
|
||||
|
||||
wafRoute.GET("/rule-groups", waf.ListRuleGroupsHandler)
|
||||
wafRoute.GET("/rule-groups/:id", waf.GetRuleGroupHandler)
|
||||
wafRoute.POST("/rule-groups", waf.CreateRuleGroupHandler)
|
||||
wafRoute.POST("/rule-groups/:id/update", waf.UpdateRuleGroupHandler)
|
||||
wafRoute.POST("/rule-groups/:id/delete", waf.DeleteRuleGroupHandler)
|
||||
wafRoute.POST("/rule-groups/:id/sites", waf.ReplaceRuleGroupSitesHandler)
|
||||
|
||||
wafRoute.GET("/sites/:route_id/rule-groups", waf.GetSiteRuleGroupsHandler)
|
||||
wafRoute.POST("/sites/:route_id/rule-groups", waf.ReplaceSiteRuleGroupsHandler)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package legacy
|
||||
|
||||
import (
|
||||
ofauth "github.com/Rain-kl/Wavelet/internal/apps/openflare/auth"
|
||||
"github.com/Rain-kl/Wavelet/internal/apps/openflare/compat"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// GetStatus returns public server status for the legacy frontend.
|
||||
func GetStatus(c *gin.Context) {
|
||||
data, err := ofauth.BuildPublicStatus(c.Request.Context())
|
||||
if err != nil {
|
||||
compat.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
compat.OK(c, data)
|
||||
}
|
||||
|
||||
// GetNotice returns the legacy notice content.
|
||||
func GetNotice(c *gin.Context) {
|
||||
compat.OK(c, ofauth.GetNotice(c.Request.Context()))
|
||||
}
|
||||
|
||||
// GetAbout returns the legacy about content.
|
||||
func GetAbout(c *gin.Context) {
|
||||
compat.OK(c, ofauth.GetAbout(c.Request.Context()))
|
||||
}
|
||||
Reference in New Issue
Block a user