migrate frontend

This commit is contained in:
ryan
2026-06-18 18:12:10 +08:00
parent e7b8993181
commit 15fa943d41
15 changed files with 57 additions and 24 deletions
@@ -16,7 +16,12 @@ export class LegacyOpenFlareBaseService {
protected static readonly basePath: string = ''; protected static readonly basePath: string = '';
protected static getFullPath(path: string): string { protected static getFullPath(path: string): string {
return `${this.basePath}${path}`; const full = `${this.basePath}${path}`;
// Avoid Next.js 308 (strip slash) <-> Gin 301 (add slash) redirect loops in dev proxy.
if (full.length > 1 && full.endsWith('/')) {
return full.slice(0, -1);
}
return full;
} }
protected static parseLegacyResponse<T>(body: LegacyApiResponse<T>): T { protected static parseLegacyResponse<T>(body: LegacyApiResponse<T>): T {
+2
View File
@@ -4,6 +4,8 @@ const isExport = process.env.NEXT_STANDALONE_EXPORT === 'true';
const nextConfig: NextConfig = { const nextConfig: NextConfig = {
reactCompiler: true, reactCompiler: true,
// Prevent 308 redirects on /api/* trailing slashes; dev rewrites proxy legacy APIs as-is.
skipTrailingSlashRedirect: true,
experimental: { experimental: {
}, },
...(isExport ? { output: 'export' } : { ...(isExport ? { output: 'export' } : {
@@ -0,0 +1,23 @@
// Copyright 2026 Arctel.net
// SPDX-License-Identifier: Apache-2.0
package compat
import (
"strings"
"github.com/gin-gonic/gin"
)
// RegisterCollection registers a collection endpoint on both "" and "/" so requests
// work with or without a trailing slash. This avoids 301/308 redirect loops between
// Next.js dev proxy and Gin when paths disagree on trailing slashes.
//
// When the group base already ends with "/" (e.g. userGroup.Group("/")), only "/" is
// registered to avoid duplicate route panic.
func RegisterCollection(route *gin.RouterGroup, method string, handlers ...gin.HandlerFunc) {
route.Handle(method, "/", handlers...)
if !strings.HasSuffix(route.BasePath(), "/") {
route.Handle(method, "", handlers...)
}
}
@@ -13,7 +13,7 @@ func registerApplyLogRoutes(apiGroup *gin.RouterGroup) {
applyLogRoute := apiGroup.Group("/apply-logs") applyLogRoute := apiGroup.Group("/apply-logs")
applyLogRoute.Use(compat.AdminAuth()) applyLogRoute.Use(compat.AdminAuth())
{ {
applyLogRoute.GET("/", apply_log.GetApplyLogs) compat.RegisterCollection(applyLogRoute, "GET", apply_log.GetApplyLogs)
applyLogRoute.POST("/cleanup", apply_log.CleanupApplyLogs) applyLogRoute.POST("/cleanup", apply_log.CleanupApplyLogs)
} }
} }
@@ -27,7 +27,7 @@ func registerAuthRoutes(apiGroup *gin.RouterGroup) {
externalAccounts := oauthGroup.Group("/external-accounts") externalAccounts := oauthGroup.Group("/external-accounts")
externalAccounts.Use(compat.UserAuth()) externalAccounts.Use(compat.UserAuth())
{ {
externalAccounts.GET("/", ListExternalAccounts) compat.RegisterCollection(externalAccounts, "GET", ListExternalAccounts)
externalAccounts.POST("/:id/delete", DeleteExternalAccount) externalAccounts.POST("/:id/delete", DeleteExternalAccount)
} }
} }
@@ -56,10 +56,10 @@ func registerAuthRoutes(apiGroup *gin.RouterGroup) {
adminGroup := userGroup.Group("/") adminGroup := userGroup.Group("/")
adminGroup.Use(compat.AdminAuth()) adminGroup.Use(compat.AdminAuth())
{ {
adminGroup.GET("/", GetAllUsers) compat.RegisterCollection(adminGroup, "GET", GetAllUsers)
adminGroup.GET("/search", SearchUsers) adminGroup.GET("/search", SearchUsers)
adminGroup.GET("/:id", GetUser) adminGroup.GET("/:id", GetUser)
adminGroup.POST("/", CreateUser) compat.RegisterCollection(adminGroup, "POST", CreateUser)
adminGroup.POST("/manage", ManageUser) adminGroup.POST("/manage", ManageUser)
adminGroup.POST("/update", UpdateUser) adminGroup.POST("/update", UpdateUser)
adminGroup.POST("/:id/delete", DeleteUser) adminGroup.POST("/:id/delete", DeleteUser)
@@ -69,8 +69,8 @@ func registerAuthRoutes(apiGroup *gin.RouterGroup) {
authSourceGroup := apiGroup.Group("/auth-sources") authSourceGroup := apiGroup.Group("/auth-sources")
authSourceGroup.Use(compat.RootAuth()) authSourceGroup.Use(compat.RootAuth())
{ {
authSourceGroup.GET("/", ListAuthSources) compat.RegisterCollection(authSourceGroup, "GET", ListAuthSources)
authSourceGroup.POST("/", CreateAuthSource) compat.RegisterCollection(authSourceGroup, "POST", CreateAuthSource)
authSourceGroup.POST("/:id/update", UpdateAuthSource) authSourceGroup.POST("/:id/update", UpdateAuthSource)
authSourceGroup.POST("/:id/delete", DeleteAuthSource) authSourceGroup.POST("/:id/delete", DeleteAuthSource)
authSourceGroup.POST("/:id/toggle", ToggleAuthSource) authSourceGroup.POST("/:id/toggle", ToggleAuthSource)
@@ -13,7 +13,7 @@ func registerConfigVersionRoutes(apiGroup *gin.RouterGroup) {
configVersionGroup := apiGroup.Group("/config-versions") configVersionGroup := apiGroup.Group("/config-versions")
configVersionGroup.Use(compat.AdminAuth()) configVersionGroup.Use(compat.AdminAuth())
{ {
configVersionGroup.GET("/", config_version.ListConfigVersionsHandler) compat.RegisterCollection(configVersionGroup, "GET", config_version.ListConfigVersionsHandler)
configVersionGroup.GET("/active", config_version.GetActiveConfigVersionHandler) configVersionGroup.GET("/active", config_version.GetActiveConfigVersionHandler)
configVersionGroup.GET("/preview", config_version.PreviewConfigVersionHandler) configVersionGroup.GET("/preview", config_version.PreviewConfigVersionHandler)
configVersionGroup.GET("/diff", config_version.DiffConfigVersionHandler) configVersionGroup.GET("/diff", config_version.DiffConfigVersionHandler)
@@ -15,8 +15,8 @@ func registerNodeRoutes(apiGroup *gin.RouterGroup) {
{ {
nodeRoute.GET("/bootstrap-token", node.GetBootstrapTokenHandler) nodeRoute.GET("/bootstrap-token", node.GetBootstrapTokenHandler)
nodeRoute.POST("/bootstrap-token/rotate", node.RotateBootstrapTokenHandler) nodeRoute.POST("/bootstrap-token/rotate", node.RotateBootstrapTokenHandler)
nodeRoute.GET("/", node.ListNodesHandler) compat.RegisterCollection(nodeRoute, "GET", node.ListNodesHandler)
nodeRoute.POST("/", node.CreateNodeHandler) compat.RegisterCollection(nodeRoute, "POST", node.CreateNodeHandler)
nodeRoute.GET("/:id/agent-release", node.GetAgentReleaseHandler) nodeRoute.GET("/:id/agent-release", node.GetAgentReleaseHandler)
nodeRoute.POST("/:id/update", node.UpdateNodeHandler) nodeRoute.POST("/:id/update", node.UpdateNodeHandler)
nodeRoute.POST("/:id/delete", node.DeleteNodeHandler) nodeRoute.POST("/:id/delete", node.DeleteNodeHandler)
@@ -13,9 +13,9 @@ func registerOriginRoutes(apiGroup *gin.RouterGroup) {
originRoute := apiGroup.Group("/origins") originRoute := apiGroup.Group("/origins")
originRoute.Use(compat.AdminAuth()) originRoute.Use(compat.AdminAuth())
{ {
originRoute.GET("/", origin.GetOrigins) compat.RegisterCollection(originRoute, "GET", origin.GetOrigins)
originRoute.GET("/:id", origin.GetOrigin) originRoute.GET("/:id", origin.GetOrigin)
originRoute.POST("/", origin.CreateOriginHandler) compat.RegisterCollection(originRoute, "POST", origin.CreateOriginHandler)
originRoute.POST("/:id/update", origin.UpdateOriginHandler) originRoute.POST("/:id/update", origin.UpdateOriginHandler)
originRoute.POST("/:id/delete", origin.DeleteOriginHandler) originRoute.POST("/:id/delete", origin.DeleteOriginHandler)
} }
@@ -13,9 +13,9 @@ func registerPagesRoutes(apiGroup *gin.RouterGroup) {
pagesRoute := apiGroup.Group("/pages") pagesRoute := apiGroup.Group("/pages")
pagesRoute.Use(compat.AdminAuth()) pagesRoute.Use(compat.AdminAuth())
{ {
pagesRoute.GET("/", pages.ListProjectsHandler) compat.RegisterCollection(pagesRoute, "GET", pages.ListProjectsHandler)
pagesRoute.GET("/:id", pages.GetProjectHandler) pagesRoute.GET("/:id", pages.GetProjectHandler)
pagesRoute.POST("/", pages.CreateProjectHandler) compat.RegisterCollection(pagesRoute, "POST", pages.CreateProjectHandler)
pagesRoute.POST("/:id/update", pages.UpdateProjectHandler) pagesRoute.POST("/:id/update", pages.UpdateProjectHandler)
pagesRoute.POST("/:id/delete", pages.DeleteProjectHandler) pagesRoute.POST("/:id/delete", pages.DeleteProjectHandler)
pagesRoute.GET("/:id/deployments", pages.ListDeploymentsHandler) pagesRoute.GET("/:id/deployments", pages.ListDeploymentsHandler)
@@ -13,9 +13,9 @@ func registerProxyRouteRoutes(apiGroup *gin.RouterGroup) {
proxyRouteGroup := apiGroup.Group("/proxy-routes") proxyRouteGroup := apiGroup.Group("/proxy-routes")
proxyRouteGroup.Use(compat.AdminAuth()) proxyRouteGroup.Use(compat.AdminAuth())
{ {
proxyRouteGroup.GET("/", proxy_route.GetProxyRoutes) compat.RegisterCollection(proxyRouteGroup, "GET", proxy_route.GetProxyRoutes)
proxyRouteGroup.GET("/:id", proxy_route.GetProxyRouteHandler) proxyRouteGroup.GET("/:id", proxy_route.GetProxyRouteHandler)
proxyRouteGroup.POST("/", proxy_route.CreateProxyRouteHandler) compat.RegisterCollection(proxyRouteGroup, "POST", proxy_route.CreateProxyRouteHandler)
proxyRouteGroup.POST("/:id/update", proxy_route.UpdateProxyRouteHandler) proxyRouteGroup.POST("/:id/update", proxy_route.UpdateProxyRouteHandler)
proxyRouteGroup.POST("/:id/delete", proxy_route.DeleteProxyRouteHandler) proxyRouteGroup.POST("/:id/delete", proxy_route.DeleteProxyRouteHandler)
} }
@@ -13,9 +13,9 @@ func registerTLSRoutes(apiGroup *gin.RouterGroup) {
managedDomainRoute := apiGroup.Group("/managed-domains") managedDomainRoute := apiGroup.Group("/managed-domains")
managedDomainRoute.Use(compat.AdminAuth()) managedDomainRoute.Use(compat.AdminAuth())
{ {
managedDomainRoute.GET("/", tls.GetManagedDomains) compat.RegisterCollection(managedDomainRoute, "GET", tls.GetManagedDomains)
managedDomainRoute.GET("/match", tls.MatchManagedDomainCertificateHandler) managedDomainRoute.GET("/match", tls.MatchManagedDomainCertificateHandler)
managedDomainRoute.POST("/", tls.CreateManagedDomainHandler) compat.RegisterCollection(managedDomainRoute, "POST", tls.CreateManagedDomainHandler)
managedDomainRoute.POST("/:id/update", tls.UpdateManagedDomainHandler) managedDomainRoute.POST("/:id/update", tls.UpdateManagedDomainHandler)
managedDomainRoute.POST("/:id/delete", tls.DeleteManagedDomainHandler) managedDomainRoute.POST("/:id/delete", tls.DeleteManagedDomainHandler)
} }
@@ -23,10 +23,10 @@ func registerTLSRoutes(apiGroup *gin.RouterGroup) {
tlsCertificateRoute := apiGroup.Group("/tls-certificates") tlsCertificateRoute := apiGroup.Group("/tls-certificates")
tlsCertificateRoute.Use(compat.AdminAuth()) tlsCertificateRoute.Use(compat.AdminAuth())
{ {
tlsCertificateRoute.GET("/", tls.GetCertificates) compat.RegisterCollection(tlsCertificateRoute, "GET", tls.GetCertificates)
tlsCertificateRoute.GET("/:id", tls.GetCertificateDetail) tlsCertificateRoute.GET("/:id", tls.GetCertificateDetail)
tlsCertificateRoute.GET("/:id/content", tls.GetCertificateContentHandler) tlsCertificateRoute.GET("/:id/content", tls.GetCertificateContentHandler)
tlsCertificateRoute.POST("/", tls.CreateCertificateHandler) compat.RegisterCollection(tlsCertificateRoute, "POST", tls.CreateCertificateHandler)
tlsCertificateRoute.POST("/:id/update", tls.UpdateCertificateHandler) tlsCertificateRoute.POST("/:id/update", tls.UpdateCertificateHandler)
tlsCertificateRoute.POST("/:id/update-acme", tls.UpdateACMECertificateHandler) tlsCertificateRoute.POST("/:id/update-acme", tls.UpdateACMECertificateHandler)
tlsCertificateRoute.POST("/:id/convert-acme", tls.ConvertCertificateToACMEHandler) tlsCertificateRoute.POST("/:id/convert-acme", tls.ConvertCertificateToACMEHandler)
@@ -45,8 +45,8 @@ func registerTLSRoutes(apiGroup *gin.RouterGroup) {
dnsAccountRoute := apiGroup.Group("/dns-accounts") dnsAccountRoute := apiGroup.Group("/dns-accounts")
dnsAccountRoute.Use(compat.AdminAuth()) dnsAccountRoute.Use(compat.AdminAuth())
{ {
dnsAccountRoute.GET("/", tls.GetDNSAccounts) compat.RegisterCollection(dnsAccountRoute, "GET", tls.GetDNSAccounts)
dnsAccountRoute.POST("/", tls.CreateDNSAccountHandler) compat.RegisterCollection(dnsAccountRoute, "POST", tls.CreateDNSAccountHandler)
dnsAccountRoute.POST("/:id/update", tls.UpdateDNSAccountHandler) dnsAccountRoute.POST("/:id/update", tls.UpdateDNSAccountHandler)
dnsAccountRoute.POST("/:id/delete", tls.DeleteDNSAccountHandler) dnsAccountRoute.POST("/:id/delete", tls.DeleteDNSAccountHandler)
} }
@@ -15,7 +15,7 @@ func RegisterRoutes(apiGroup *gin.RouterGroup) {
accessLogRoute := apiGroup.Group("/access-logs") accessLogRoute := apiGroup.Group("/access-logs")
accessLogRoute.Use(compat.AdminAuth()) accessLogRoute.Use(compat.AdminAuth())
{ {
accessLogRoute.GET("/", getAccessLogsHandler) compat.RegisterCollection(accessLogRoute, "GET", getAccessLogsHandler)
accessLogRoute.GET("/folds", getFoldedAccessLogsHandler) accessLogRoute.GET("/folds", getFoldedAccessLogsHandler)
accessLogRoute.GET("/folds/ip-summary", getFoldedAccessLogIPsHandler) accessLogRoute.GET("/folds/ip-summary", getFoldedAccessLogIPsHandler)
accessLogRoute.GET("/ip-summary", getAccessLogIPSummariesHandler) accessLogRoute.GET("/ip-summary", getAccessLogIPSummariesHandler)
@@ -22,7 +22,7 @@ func RegisterRoutes(apiGroup *gin.RouterGroup) {
optionRoute := apiGroup.Group("/option") optionRoute := apiGroup.Group("/option")
optionRoute.Use(compat.RootAuth()) optionRoute.Use(compat.RootAuth())
{ {
optionRoute.GET("/", listOptionsHandler) compat.RegisterCollection(optionRoute, "GET", listOptionsHandler)
optionRoute.POST("/update", updateOptionHandler) optionRoute.POST("/update", updateOptionHandler)
optionRoute.POST("/update-batch", updateOptionsBatchHandler) optionRoute.POST("/update-batch", updateOptionsBatchHandler)
optionRoute.POST("/geoip/lookup", lookupGeoIPHandler) optionRoute.POST("/geoip/lookup", lookupGeoIPHandler)
+2
View File
@@ -38,6 +38,8 @@ func Serve() {
// 初始化路由 // 初始化路由
r := gin.New() r := gin.New()
// Legacy OpenFlare list endpoints register both /resource and /resource/; disable auto slash redirects.
r.RedirectTrailingSlash = false
r.Use(gin.Recovery()) r.Use(gin.Recovery())
r.Use(corsMiddleware()) r.Use(corsMiddleware())
+1
View File
@@ -43,6 +43,7 @@ sidebar: false
- 修复静态导出构建中 `useSearchParams` 未包裹 Suspense 的问题;`pnpm build:embed` 全量 46 页通过。 - 修复静态导出构建中 `useSearchParams` 未包裹 Suspense 的问题;`pnpm build:embed` 全量 46 页通过。
- 补全前端迁移收尾:新增 `/openflare/about` 关于页与 `AboutService`;补全 `UpdateService` 升级流程(在线升级、手动上传、WebSocket 日志流)及顶栏版本升级入口。 - 补全前端迁移收尾:新增 `/openflare/about` 关于页与 `AboutService`;补全 `UpdateService` 升级流程(在线升级、手动上传、WebSocket 日志流)及顶栏版本升级入口。
- 前端 UI 打磨:节点/WAF/DNS/配置清理等 Dialog 补 RHF+Zod;代理规则详情与访问日志 Tab 补 error 态;`pnpm build:embed` 增至 47 静态页。 - 前端 UI 打磨:节点/WAF/DNS/配置清理等 Dialog 补 RHF+Zod;代理规则详情与访问日志 Tab 补 error 态;`pnpm build:embed` 增至 47 静态页。
- 修复开发模式下 `/api/*` 尾斜杠引发 Next.js 308 与 Gin 301 循环重定向:前端 Service 规范化路径、Next `skipTrailingSlashRedirect`、后端双路径注册。
## [v2.3.4] - 2026-06-17 ## [v2.3.4] - 2026-06-17