Files
OpenFlare/internal/router/router.go
T
2026-06-08 20:38:17 +08:00

271 lines
8.8 KiB
Go

/*
Copyright 2025 linux.do
Modified by Arctel.net, 2026
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package router
import (
"context"
"errors"
"log"
"net/http"
"os"
"os/signal"
"strconv"
"syscall"
"time"
"github.com/Rain-kl/Wavelet/internal/apps/admin"
admin_auth_source "github.com/Rain-kl/Wavelet/internal/apps/admin/auth_source"
admin_logs "github.com/Rain-kl/Wavelet/internal/apps/admin/logs"
admin_status "github.com/Rain-kl/Wavelet/internal/apps/admin/status"
admin_task "github.com/Rain-kl/Wavelet/internal/apps/admin/task"
admin_template "github.com/Rain-kl/Wavelet/internal/apps/admin/template"
admin_user "github.com/Rain-kl/Wavelet/internal/apps/admin/user"
capApp "github.com/Rain-kl/Wavelet/internal/apps/cap"
publicconfig "github.com/Rain-kl/Wavelet/internal/apps/config"
"github.com/Rain-kl/Wavelet/internal/apps/health"
"github.com/Rain-kl/Wavelet/internal/apps/upload"
"github.com/Rain-kl/Wavelet/internal/apps/user"
"github.com/Rain-kl/Wavelet/internal/model"
"github.com/Rain-kl/Wavelet/internal/util"
capUtil "github.com/Rain-kl/Wavelet/internal/util/cap"
_ "github.com/Rain-kl/Wavelet/docs"
"github.com/Rain-kl/Wavelet/internal/apps/admin/system_config"
"github.com/Rain-kl/Wavelet/internal/apps/oauth"
"github.com/Rain-kl/Wavelet/internal/config"
"github.com/Rain-kl/Wavelet/internal/otel_trace"
"github.com/gin-contrib/sessions"
"github.com/gin-contrib/sessions/redis"
"github.com/gin-gonic/gin"
swaggerFiles "github.com/swaggo/files"
ginSwagger "github.com/swaggo/gin-swagger"
"go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin"
)
func Serve() {
// 运行模式
if config.Config.App.IsProduction() {
gin.SetMode(gin.ReleaseMode)
}
// 初始化路由
r := gin.New()
r.Use(gin.Recovery())
r.Use(corsMiddleware())
cfg := config.Config.Redis
addrs := cfg.Addrs
sessionAddr := "localhost:6379"
if len(addrs) > 0 {
sessionAddr = addrs[0]
}
sessionStore, err := redis.NewStoreWithDB(
cfg.MinIdleConn,
"tcp",
sessionAddr,
cfg.Username,
cfg.Password,
strconv.Itoa(cfg.DB),
[]byte(config.Config.App.SessionSecret),
)
if err != nil {
log.Fatalf("[API] init session store failed: %v\n", err)
}
// 设置 Session Redis Key 前缀
if cfg.KeyPrefix != "" {
if err := redis.SetKeyPrefix(sessionStore, cfg.KeyPrefix+"session:"); err != nil {
log.Printf("[API] set session key prefix failed: %v\n", err)
}
}
sessionStore.Options(util.GetSessionOptions(config.Config.App.SessionAge))
r.Use(sessions.Sessions(config.Config.App.SessionCookieName, sessionStore))
// 补充中间件
r.Use(otelgin.Middleware(config.Config.App.AppName), loggerMiddleware())
// Serve files by ID
r.GET("/f/:id", upload.ServeFileByID)
apiGroup := r.Group(config.Config.App.APIPrefix)
{
if !config.Config.App.IsProduction() {
// Swagger
apiGroup.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
}
// CAPTCHA
capGroup := apiGroup.Group("/cap")
{
capGroup.POST("/challenge", capApp.Challenge)
capGroup.POST("/redeem", capApp.Redeem)
}
// API V1
apiV1Router := apiGroup.Group("/v1")
{
// Health
apiV1Router.GET("/health", health.Health)
// OAuth
apiV1Router.GET("/oauth/sources", oauth.GetLoginSources)
apiV1Router.GET("/oauth/login", oauth.GetLoginURL)
apiV1Router.GET("/oauth/:source/authorize", oauth.Authorize)
apiV1Router.GET("/oauth/logout", oauth.Logout)
apiV1Router.POST("/oauth/callback", oauth.Callback)
apiV1Router.GET("/oauth/user-info", oauth.LoginRequired(), oauth.UserInfo)
apiV1Router.GET("/user-info", oauth.LoginRequired(), oauth.UserInfo)
apiV1Router.GET("/oauth/external-accounts", oauth.LoginRequired(), oauth.ListExternalAccounts)
apiV1Router.POST("/oauth/external-accounts/:id/delete", oauth.LoginRequired(), oauth.DeleteExternalAccount)
// User
userRouter := apiV1Router.Group("/user")
{
userRouter.POST("/login", capApp.VerifyMiddleware(capUtil.GetDefaultManager(), "login", func() bool {
enabled, err := model.GetBoolByKey(context.Background(), model.ConfigKeyCapLoginEnabled)
if err != nil {
return false
}
return enabled
}), user.Login)
userRouter.POST("/register", user.Register)
userRouter.POST("/send-email-code", user.SendEmailCode)
userRouter.GET("/logout", user.Logout)
userRouter.GET("/self", oauth.LoginRequired(), oauth.UserInfo)
userRouter.POST("/change-password", oauth.LoginRequired(), user.ChangePassword)
userRouter.PUT("/profile", oauth.LoginRequired(), user.UpdateProfile)
// Access Token
tokenRouter := userRouter.Group("/access-tokens")
tokenRouter.Use(oauth.LoginRequired())
{
tokenRouter.GET("", user.ListAccessTokens)
tokenRouter.POST("", user.CreateAccessToken)
tokenRouter.DELETE("/:id", user.DeleteAccessToken)
tokenRouter.POST("/:id/rotate", user.RotateAccessToken)
}
}
// Upload
uploadRouter := apiV1Router.Group("/upload")
uploadRouter.Use(oauth.LoginRequired())
{
uploadRouter.POST("", upload.UploadFile)
uploadRouter.GET("/my", upload.ListMyFiles)
uploadRouter.DELETE("/:id", upload.DeleteFile)
uploadRouter.GET("/download/:id", upload.DownloadFile)
uploadRouter.POST("/download/batch", upload.BatchDownloadFiles)
}
// Config (public)
configRouter := apiV1Router.Group("/config")
{
configRouter.GET("/public", publicconfig.GetPublicConfig)
}
// Admin
adminRouter := apiV1Router.Group("/admin")
adminRouter.Use(oauth.LoginRequired(), admin.LoginAdminRequired())
{
// System status
adminRouter.GET("/status", admin_status.GetSystemStatus)
// System logs
adminRouter.GET("/logs", admin_logs.GetLogs)
adminRouter.GET("/logs/ws", admin_logs.HandleLogWebSocket)
// Task dispatch
adminRouter.GET("/tasks/types", admin_task.ListTaskTypes)
adminRouter.POST("/tasks/dispatch", admin_task.DispatchTask)
// Task executions
adminRouter.GET("/tasks/executions", admin_task.ListTaskExecutions)
adminRouter.GET("/tasks/executions/:id", admin_task.GetTaskExecution)
adminRouter.POST("/tasks/executions/:id/retry", admin_task.RetryTask)
// Users
adminRouter.GET("/users", admin_user.ListUsers)
adminRouter.POST("/users", admin_user.CreateUser)
adminRouter.PUT("/users/:id/status", admin_user.UpdateUserStatus)
// System Config
adminRouter.POST("/system-configs", system_config.CreateSystemConfig)
adminRouter.GET("/system-configs", system_config.ListSystemConfigs)
adminRouter.POST("/system-configs/smtp/test", system_config.TestSMTP)
systemConfigRouter := adminRouter.Group("/system-configs/:key")
{
systemConfigRouter.GET("", system_config.GetSystemConfig)
systemConfigRouter.PUT("", system_config.UpdateSystemConfig)
}
// Templates
adminRouter.GET("/templates", admin_template.ListTemplates)
adminRouter.POST("/templates", admin_template.CreateTemplate)
templateRouter := adminRouter.Group("/templates/:key")
{
templateRouter.GET("", admin_template.GetTemplate)
templateRouter.PUT("", admin_template.UpdateTemplate)
templateRouter.DELETE("", admin_template.DeleteTemplate)
}
// Auth Sources
adminRouter.GET("/auth-sources", admin_auth_source.ListAuthSources)
adminRouter.POST("/auth-sources", admin_auth_source.CreateAuthSource)
adminRouter.PUT("/auth-sources/:id", admin_auth_source.UpdateAuthSource)
adminRouter.PUT("/auth-sources/:id/toggle", admin_auth_source.ToggleAuthSource)
adminRouter.DELETE("/auth-sources/:id", admin_auth_source.DeleteAuthSource)
}
}
}
// 注册前端静态路由(当启用 embed_frontend 编译标签时)
registerFrontend(r)
srv := &http.Server{
Addr: config.Config.App.Addr,
Handler: r,
}
go func() {
log.Printf("[API] server starting on %s\n", config.Config.App.Addr)
if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
log.Fatalf("[API] server failed: %v\n", err)
}
}()
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
shutdownCtx, cancel := context.WithTimeout(context.Background(), time.Duration(config.Config.App.GracefulShutdownTimeout)*time.Second)
defer cancel()
otel_trace.Shutdown(shutdownCtx)
if err := srv.Shutdown(shutdownCtx); err != nil {
log.Fatalf("[API] server forced to shutdown: %v\n", err)
}
log.Println("[API] server exited")
}