Files
openteam/server/internal/api/router.go
T
SakurasanandClaude 9324a782d5 Passkey: 账户设置绑定 + 免密登录(WebAuthn)
- 引入 go-webauthn, Passkey 表存凭据, challenge 会话内存存储(带过期)
- API: /webauthn/register|login begin/complete, /webauthn/passkeys 列表/删除
- 配置 OT_WEBAUTHN_RP_ID/RP_ORIGIN/RP_NAME;登录成功发 JWT+refresh cookie
- 前端 lib/webauthn(编解码+凭据序列化+安全上下文检测), 账户设置绑定区, 登录页免密按钮
- 需 HTTPS 或 localhost(安全上下文)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-16 02:00:08 +08:00

133 lines
4.3 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package api
import (
"net/http"
"os"
"strings"
"github.com/gin-gonic/gin"
"github.com/openteam/server/internal/api/middleware"
"github.com/openteam/server/internal/app"
"github.com/openteam/server/internal/proxy"
)
// NewRouter 装配所有路由:
// - /v1/* 代理端点(Bearer API Key,OpenAI 格式错误体)
// - /api/v1/* 管理 API(会话 JWT)
func NewRouter(a *app.App, gw *proxy.Gateway) *gin.Engine {
if a.Cfg.Env == "production" {
gin.SetMode(gin.ReleaseMode)
}
r := gin.New()
r.Use(gin.Logger(), gin.Recovery(), middleware.CORS())
h := NewHandler(a, gw, a.Passkeys)
// --- 代理端点(对外)---
proxyGroup := r.Group("/v1")
{
proxyGroup.Any("/chat/completions", gw.Auth, gw.Handle)
proxyGroup.Any("/responses", gw.Auth, gw.Handle)
proxyGroup.Any("/messages", gw.Auth, gw.Handle)
proxyGroup.Any("/models", gw.Auth, gw.Handle)
}
// 静态资源(前端构建产物,存在时托管)
const dist = "web/dist"
if _, err := os.Stat(dist); err == nil {
r.Static("/assets", dist+"/assets")
r.StaticFile("/favicon.svg", dist+"/favicon.svg")
}
// 未匹配路由:/v1/* 走代理鉴权;其余回退 SPA 或 404
r.NoRoute(func(c *gin.Context) {
if strings.HasPrefix(c.Request.URL.Path, "/v1") {
gw.Auth(c)
if !c.IsAborted() {
gw.Handle(c)
}
return
}
if _, err := os.Stat(dist); err == nil {
c.File(dist + "/index.html")
return
}
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
})
// --- 管理 API ---
api := r.Group("/api/v1")
{
auth := api.Group("/auth")
{
auth.POST("/register", h.Register)
auth.POST("/login", h.Login)
auth.POST("/refresh", h.Refresh)
auth.POST("/logout", h.Logout)
auth.POST("/password", middleware.SessionAuth(a), h.ChangePassword)
auth.GET("/me", middleware.SessionAuth(a), h.Me)
}
webauthn := api.Group("/webauthn")
{
webauthn.POST("/register/begin", middleware.SessionAuth(a), h.PasskeyRegisterBegin)
webauthn.POST("/register/complete", middleware.SessionAuth(a), h.PasskeyRegisterComplete)
webauthn.POST("/login/begin", h.PasskeyLoginBegin)
webauthn.POST("/login/complete", h.PasskeyLoginComplete)
webauthn.GET("/passkeys", middleware.SessionAuth(a), h.PasskeyList)
webauthn.DELETE("/passkeys/:id", middleware.SessionAuth(a), h.PasskeyDelete)
}
user := api.Group("", middleware.SessionAuth(a))
{
user.GET("/user/profile", h.UserProfile)
user.GET("/user/balance", h.UserBalance)
user.GET("/user/models", h.UserModels)
user.GET("/usage/summary", h.UsageSummary)
user.GET("/usage/stats", h.UsageStats)
user.GET("/usage/logs", h.UsageLogs)
user.POST("/keys", h.CreateKey)
user.GET("/keys", h.ListKeys)
user.PATCH("/keys/:id", h.PatchKey)
user.DELETE("/keys/:id", h.DeleteKey)
}
admin := api.Group("/admin", middleware.SessionAuth(a), middleware.AdminOnly)
{
// 用户
admin.GET("/users", h.AdminUsers)
admin.PATCH("/users/:id", h.AdminPatchUser)
admin.POST("/users/:id/balance", h.AdminAdjustBalance)
// 渠道
admin.GET("/channels", h.AdminChannels)
admin.POST("/channels", h.AdminCreateChannel)
admin.PUT("/channels/:id", h.AdminUpdateChannel)
admin.DELETE("/channels/:id", h.AdminDeleteChannel)
admin.POST("/channels/:id/test", h.AdminTestChannel)
admin.POST("/channels/:id/models/import", h.AdminImportChannelModels)
admin.GET("/channels/:id/models", h.AdminChannelModels)
admin.POST("/channels/:id/models", h.AdminChannelAddModel)
admin.PATCH("/channels/:id/models/:bid", h.AdminChannelUpdateModel)
admin.DELETE("/channels/:id/models/:bid", h.AdminChannelDeleteModel)
// 模型与定价
admin.GET("/models", h.AdminModels)
admin.POST("/models", h.AdminCreateModel)
admin.PUT("/models/:id", h.AdminUpdateModel)
admin.DELETE("/models/:id", h.AdminDeleteModel)
admin.POST("/models/:id/bindings", h.AdminCreateModelBinding)
admin.DELETE("/models/:id/bindings/:bid", h.AdminDeleteModelBinding)
// 统计与用量
admin.GET("/stats/overview", h.AdminStatsOverview)
admin.GET("/usage", h.AdminUsage)
// 配置
admin.GET("/config", h.AdminConfig)
admin.PUT("/config", h.AdminPutConfig)
// 充值审核(M6 预留)
}
}
r.GET("/healthz", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"status": "ok"})
})
return r
}