Add a read-only aggregate view per channel: its linked groups and a deterministic wildcard-free supported-model list with pricing details. Backend - service.Channel.SupportedModels(): combine ModelMapping keys with same-platform ModelPricing.Models; trailing "*" keys expand via pricing prefix match; platforms without a mapping produce no entries (intentional "no mapping = not shown" rule). - Extract splitWildcardSuffix() shared with toModelEntry. - Build a per-call pricing lookup map (platform+lowerName -> *pricing) to avoid O(N*M) scans in SupportedModels. - ChannelService.ListAvailable() aggregates channels + active groups; filters out group IDs no longer active. - Admin route GET /api/v1/admin/channels/available returns the full DTO (id, status, billing_model_source, restrict_models, groups, supported_models). - User route GET /api/v1/channels/available applies three filters: Status==active, visible-group intersection, and platform filter on supported_models (prevents cross-platform leak when a channel links to both a user-accessible group and an inaccessible one on another platform). Response is a plain array (matches the /groups/available sibling shape). Field whitelist omits billing_model_source, restrict_models, ids, status, sort_order. Frontend - New /admin/available-channels and /available-channels views backed by a shared AvailableChannelsTable component (admin adds status + billing-source columns via slots). - PricingRow extracted to its own SFC; SupportedModelChip references shared billing-mode constants in constants/channel.ts. - Sidebar: new entry above "渠道管理" for admin; matching entry in user nav. - i18n: zh + en coverage for both namespaces. Tests - SupportedModels: wildcard-only pricing skipped, prefix-matches- nothing, cross-platform bleed, case-insensitive dedup, empty platform mapping. - ListAvailable: nil groupRepo, inactive-group-ID dropped, stable case-insensitive name sort. - User handler: 401 on unauthenticated, visible-group intersection, platform filter on supported_models, JSON whitelist. - Admin handler: full DTO including default BillingModelSource fallback. Refs: issue #1729
121 lines
3.6 KiB
Go
121 lines
3.6 KiB
Go
package routes
|
|
|
|
import (
|
|
"github.com/Wei-Shaw/sub2api/internal/handler"
|
|
"github.com/Wei-Shaw/sub2api/internal/server/middleware"
|
|
"github.com/Wei-Shaw/sub2api/internal/service"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
// RegisterUserRoutes 注册用户相关路由(需要认证)
|
|
func RegisterUserRoutes(
|
|
v1 *gin.RouterGroup,
|
|
h *handler.Handlers,
|
|
jwtAuth middleware.JWTAuthMiddleware,
|
|
settingService *service.SettingService,
|
|
) {
|
|
authenticated := v1.Group("")
|
|
authenticated.Use(gin.HandlerFunc(jwtAuth))
|
|
authenticated.Use(middleware.BackendModeUserGuard(settingService))
|
|
{
|
|
// 用户接口
|
|
user := authenticated.Group("/user")
|
|
{
|
|
user.GET("/profile", h.User.GetProfile)
|
|
user.PUT("/password", h.User.ChangePassword)
|
|
user.PUT("", h.User.UpdateProfile)
|
|
user.POST("/account-bindings/email/send-code", h.User.SendEmailBindingCode)
|
|
user.POST("/account-bindings/email", h.User.BindEmailIdentity)
|
|
user.DELETE("/account-bindings/:provider", h.User.UnbindIdentity)
|
|
user.POST("/auth-identities/bind/start", h.User.StartIdentityBinding)
|
|
|
|
// 通知邮箱管理
|
|
notifyEmail := user.Group("/notify-email")
|
|
{
|
|
notifyEmail.POST("/send-code", h.User.SendNotifyEmailCode)
|
|
notifyEmail.POST("/verify", h.User.VerifyNotifyEmail)
|
|
notifyEmail.PUT("/toggle", h.User.ToggleNotifyEmail)
|
|
notifyEmail.DELETE("", h.User.RemoveNotifyEmail)
|
|
}
|
|
|
|
// TOTP 双因素认证
|
|
totp := user.Group("/totp")
|
|
{
|
|
totp.GET("/status", h.Totp.GetStatus)
|
|
totp.GET("/verification-method", h.Totp.GetVerificationMethod)
|
|
totp.POST("/send-code", h.Totp.SendVerifyCode)
|
|
totp.POST("/setup", h.Totp.InitiateSetup)
|
|
totp.POST("/enable", h.Totp.Enable)
|
|
totp.POST("/disable", h.Totp.Disable)
|
|
}
|
|
}
|
|
|
|
// API Key管理
|
|
keys := authenticated.Group("/keys")
|
|
{
|
|
keys.GET("", h.APIKey.List)
|
|
keys.GET("/:id", h.APIKey.GetByID)
|
|
keys.POST("", h.APIKey.Create)
|
|
keys.PUT("/:id", h.APIKey.Update)
|
|
keys.DELETE("/:id", h.APIKey.Delete)
|
|
}
|
|
|
|
// 用户可用分组(非管理员接口)
|
|
groups := authenticated.Group("/groups")
|
|
{
|
|
groups.GET("/available", h.APIKey.GetAvailableGroups)
|
|
groups.GET("/rates", h.APIKey.GetUserGroupRates)
|
|
}
|
|
|
|
// 用户可用渠道(非管理员接口)
|
|
channels := authenticated.Group("/channels")
|
|
{
|
|
channels.GET("/available", h.AvailableChannel.List)
|
|
}
|
|
|
|
// 使用记录
|
|
usage := authenticated.Group("/usage")
|
|
{
|
|
usage.GET("", h.Usage.List)
|
|
usage.GET("/:id", h.Usage.GetByID)
|
|
usage.GET("/stats", h.Usage.Stats)
|
|
// User dashboard endpoints
|
|
usage.GET("/dashboard/stats", h.Usage.DashboardStats)
|
|
usage.GET("/dashboard/trend", h.Usage.DashboardTrend)
|
|
usage.GET("/dashboard/models", h.Usage.DashboardModels)
|
|
usage.POST("/dashboard/api-keys-usage", h.Usage.DashboardAPIKeysUsage)
|
|
}
|
|
|
|
// 公告(用户可见)
|
|
announcements := authenticated.Group("/announcements")
|
|
{
|
|
announcements.GET("", h.Announcement.List)
|
|
announcements.POST("/:id/read", h.Announcement.MarkRead)
|
|
}
|
|
|
|
// 卡密兑换
|
|
redeem := authenticated.Group("/redeem")
|
|
{
|
|
redeem.POST("", h.Redeem.Redeem)
|
|
redeem.GET("/history", h.Redeem.GetHistory)
|
|
}
|
|
|
|
// 用户订阅
|
|
subscriptions := authenticated.Group("/subscriptions")
|
|
{
|
|
subscriptions.GET("", h.Subscription.List)
|
|
subscriptions.GET("/active", h.Subscription.GetActive)
|
|
subscriptions.GET("/progress", h.Subscription.GetProgress)
|
|
subscriptions.GET("/summary", h.Subscription.GetSummary)
|
|
}
|
|
|
|
// 渠道监控(用户只读)
|
|
monitors := authenticated.Group("/channel-monitors")
|
|
{
|
|
monitors.GET("", h.ChannelMonitor.List)
|
|
monitors.GET("/:id/status", h.ChannelMonitor.GetStatus)
|
|
}
|
|
}
|
|
}
|