130 lines
4.0 KiB
Go
130 lines
4.0 KiB
Go
// Package api implements the gateway HTTP surface: the OpenAI-compatible proxy
|
|
// (/v1/*) and the admin API (/api/admin/*).
|
|
package api
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
|
|
"github.com/go-chi/chi/v5"
|
|
|
|
"git.govekar.net/LovroG05/localmodelroutex/internal/catalog"
|
|
"git.govekar.net/LovroG05/localmodelroutex/internal/crypto"
|
|
"git.govekar.net/LovroG05/localmodelroutex/internal/oidcauth"
|
|
"git.govekar.net/LovroG05/localmodelroutex/internal/providermgr"
|
|
"git.govekar.net/LovroG05/localmodelroutex/internal/ratelimit"
|
|
"git.govekar.net/LovroG05/localmodelroutex/internal/store"
|
|
)
|
|
|
|
// oidcVerifier validates OIDC tokens.
|
|
// Satisfied by *oidcauth.Verifier.
|
|
type oidcVerifier interface {
|
|
Verify(ctx context.Context, rawToken string) (*oidcauth.Claims, error)
|
|
IsAdmin(c *oidcauth.Claims) bool
|
|
VerifyAdmin(ctx context.Context, rawToken string) error
|
|
AuthURL() string
|
|
TokenURL() string
|
|
}
|
|
|
|
// ctxKeyUser is the context key for the authenticated user.
|
|
type ctxKeyUser struct{}
|
|
|
|
// authedUser holds the resolved user and their admin status for the current request.
|
|
type authedUser struct {
|
|
User store.User
|
|
IsAdmin bool
|
|
}
|
|
|
|
// userFrom retrieves the authenticated user from the request context.
|
|
// Returns nil when called outside of requireAuth.
|
|
func userFrom(ctx context.Context) *authedUser {
|
|
u, _ := ctx.Value(ctxKeyUser{}).(*authedUser)
|
|
return u
|
|
}
|
|
|
|
// Deps are the dependencies the API handlers need.
|
|
type Deps struct {
|
|
Store *store.Store
|
|
Cache *catalog.Cache
|
|
Manager *providermgr.Manager
|
|
Cipher *crypto.Cipher
|
|
OIDCVerifier oidcVerifier
|
|
OIDCClientID string
|
|
OIDCClientSecret string
|
|
OIDCAuthURL string // authorization_endpoint from discovery
|
|
OIDCTokenURL string // token_endpoint from discovery
|
|
// PublicBaseURL is the canonical external URL of this server (e.g.
|
|
// "https://lmr.example.com"). Required when OIDC is enabled — used to
|
|
// build the redirect_uri and the post-login fragment redirect so neither
|
|
// can be overridden by a forged Host/X-Forwarded-Host header.
|
|
PublicBaseURL string
|
|
Limiter *ratelimit.Limiter
|
|
// SyncNow triggers an immediate provider reload + catalog sync (used after
|
|
// admin changes). May be nil.
|
|
SyncNow func(context.Context) error
|
|
}
|
|
|
|
// API holds handler dependencies.
|
|
type API struct {
|
|
Deps
|
|
}
|
|
|
|
// New returns an API.
|
|
func New(d Deps) *API { return &API{Deps: d} }
|
|
|
|
// Mount registers the proxy and admin routes on r.
|
|
func (a *API) Mount(r chi.Router) {
|
|
r.Route("/v1", func(r chi.Router) {
|
|
r.Use(a.virtualKeyAuth)
|
|
r.Post("/chat/completions", a.chatCompletions)
|
|
r.Post("/embeddings", a.embeddings)
|
|
r.Get("/models", a.listModels)
|
|
})
|
|
|
|
// Public auth endpoints — no auth middleware.
|
|
r.Get("/auth/login", a.oidcLogin)
|
|
r.Get("/auth/callback", a.oidcCallback)
|
|
|
|
// Authenticated user endpoints.
|
|
r.Group(func(r chi.Router) {
|
|
r.Use(a.requireAuth)
|
|
r.Get("/api/me", a.apiMe)
|
|
|
|
r.Route("/api/admin", func(r chi.Router) {
|
|
// Keys and logs are user-scoped (admins see all via nil filter).
|
|
r.Get("/keys", a.adminListKeys)
|
|
r.Post("/keys", a.adminCreateKey)
|
|
r.Delete("/keys/{id}", a.adminRevokeKey)
|
|
r.Get("/logs", a.adminListLogs)
|
|
r.Get("/stats/keys", a.adminKeyStats)
|
|
|
|
// The following routes require admin group membership.
|
|
r.Group(func(r chi.Router) {
|
|
r.Use(a.requireAdmin)
|
|
r.Get("/providers", a.adminListProviders)
|
|
r.Post("/providers", a.adminCreateProvider)
|
|
r.Delete("/providers/{id}", a.adminDeleteProvider)
|
|
r.Get("/models", a.adminListModels)
|
|
r.Patch("/models/{id}", a.adminUpdateModel)
|
|
r.Get("/settings", a.adminGetSettings)
|
|
r.Put("/settings/{key}", a.adminPutSetting)
|
|
r.Post("/catalog/refresh", a.adminRefreshCatalog)
|
|
})
|
|
})
|
|
})
|
|
|
|
// Legacy public endpoint — tells the dashboard OIDC is configured.
|
|
r.Get("/api/auth/config", a.authConfig)
|
|
}
|
|
|
|
// apiMe returns the current user's profile and admin status.
|
|
func (a *API) apiMe(w http.ResponseWriter, r *http.Request) {
|
|
u := userFrom(r.Context())
|
|
writeJSON(w, http.StatusOK, map[string]any{
|
|
"id": u.User.ID,
|
|
"email": u.User.Email,
|
|
"name": u.User.Name,
|
|
"is_admin": u.IsAdmin,
|
|
})
|
|
}
|