346 lines
10 KiB
Go
346 lines
10 KiB
Go
package httpapi
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/easyai/easyai-ai-gateway/apps/api/internal/auth"
|
|
"github.com/easyai/easyai-ai-gateway/apps/api/internal/store"
|
|
)
|
|
|
|
func (s *Server) health(w http.ResponseWriter, r *http.Request) {
|
|
writeJSON(w, http.StatusOK, map[string]any{
|
|
"ok": true,
|
|
"service": "easyai-ai-gateway",
|
|
"env": s.cfg.AppEnv,
|
|
"identityMode": s.cfg.IdentityMode,
|
|
})
|
|
}
|
|
|
|
func (s *Server) ready(w http.ResponseWriter, r *http.Request) {
|
|
if err := s.store.Ping(r.Context()); err != nil {
|
|
writeError(w, http.StatusServiceUnavailable, "postgres unavailable")
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, map[string]any{"ok": true})
|
|
}
|
|
|
|
func (s *Server) me(w http.ResponseWriter, r *http.Request) {
|
|
user, _ := auth.UserFromContext(r.Context())
|
|
writeJSON(w, http.StatusOK, user)
|
|
}
|
|
|
|
func (s *Server) register(w http.ResponseWriter, r *http.Request) {
|
|
if !s.localIdentityEnabled() {
|
|
writeError(w, http.StatusForbidden, "local registration is disabled")
|
|
return
|
|
}
|
|
var input store.LocalRegisterInput
|
|
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
|
|
writeError(w, http.StatusBadRequest, "invalid json body")
|
|
return
|
|
}
|
|
user, err := s.store.RegisterLocalUser(r.Context(), input)
|
|
if err != nil {
|
|
if errors.Is(err, store.ErrWeakPassword) {
|
|
writeError(w, http.StatusBadRequest, err.Error())
|
|
return
|
|
}
|
|
if errors.Is(err, store.ErrInvalidInvitation) {
|
|
writeError(w, http.StatusBadRequest, err.Error())
|
|
return
|
|
}
|
|
s.logger.Error("register local user failed", "error", err)
|
|
writeError(w, http.StatusConflict, "user already exists or tenant is unavailable")
|
|
return
|
|
}
|
|
s.writeAuthResponse(w, http.StatusCreated, user)
|
|
}
|
|
|
|
func (s *Server) login(w http.ResponseWriter, r *http.Request) {
|
|
if !s.localIdentityEnabled() {
|
|
writeError(w, http.StatusForbidden, "local login is disabled")
|
|
return
|
|
}
|
|
var input store.LocalLoginInput
|
|
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
|
|
writeError(w, http.StatusBadRequest, "invalid json body")
|
|
return
|
|
}
|
|
user, err := s.store.AuthenticateLocalUser(r.Context(), input)
|
|
if err != nil {
|
|
if errors.Is(err, store.ErrInvalidCredentials) {
|
|
writeError(w, http.StatusUnauthorized, "invalid account or password")
|
|
return
|
|
}
|
|
s.logger.Error("login local user failed", "error", err)
|
|
writeError(w, http.StatusInternalServerError, "login failed")
|
|
return
|
|
}
|
|
s.writeAuthResponse(w, http.StatusOK, user)
|
|
}
|
|
|
|
func (s *Server) localIdentityEnabled() bool {
|
|
mode := strings.ToLower(strings.TrimSpace(s.cfg.IdentityMode))
|
|
return mode == "" || mode == "standalone" || mode == "hybrid"
|
|
}
|
|
|
|
func (s *Server) writeAuthResponse(w http.ResponseWriter, status int, user store.GatewayUser) {
|
|
authUser := authUserFromGatewayUser(user)
|
|
const ttl = 24 * time.Hour
|
|
token, err := s.auth.SignJWT(authUser, ttl)
|
|
if err != nil {
|
|
s.logger.Error("sign local jwt failed", "error", err)
|
|
writeError(w, http.StatusInternalServerError, "token sign failed")
|
|
return
|
|
}
|
|
writeJSON(w, status, map[string]any{
|
|
"accessToken": token,
|
|
"tokenType": "Bearer",
|
|
"expiresIn": int(ttl.Seconds()),
|
|
"user": authUser,
|
|
})
|
|
}
|
|
|
|
func authUserFromGatewayUser(user store.GatewayUser) *auth.User {
|
|
roles := user.Roles
|
|
if len(roles) == 0 {
|
|
roles = []string{"user"}
|
|
}
|
|
tenantID := user.TenantID
|
|
if tenantID == "" {
|
|
tenantID = user.TenantKey
|
|
}
|
|
return &auth.User{
|
|
ID: user.ID,
|
|
Username: user.Username,
|
|
Roles: roles,
|
|
TenantID: tenantID,
|
|
GatewayTenantID: user.GatewayTenantID,
|
|
TenantKey: user.TenantKey,
|
|
Source: "gateway",
|
|
GatewayUserID: user.ID,
|
|
UserGroupID: user.DefaultUserGroupID,
|
|
}
|
|
}
|
|
|
|
func (s *Server) listPlatforms(w http.ResponseWriter, r *http.Request) {
|
|
platforms, err := s.store.ListPlatforms(r.Context())
|
|
if err != nil {
|
|
s.logger.Error("list platforms failed", "error", err)
|
|
writeError(w, http.StatusInternalServerError, "list platforms failed")
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, map[string]any{"items": platforms})
|
|
}
|
|
|
|
func (s *Server) createPlatform(w http.ResponseWriter, r *http.Request) {
|
|
var input store.CreatePlatformInput
|
|
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
|
|
writeError(w, http.StatusBadRequest, "invalid json body")
|
|
return
|
|
}
|
|
if input.Provider == "" || input.Name == "" {
|
|
writeError(w, http.StatusBadRequest, "provider and name are required")
|
|
return
|
|
}
|
|
if input.AuthType == "" {
|
|
input.AuthType = "bearer"
|
|
}
|
|
platform, err := s.store.CreatePlatform(r.Context(), input)
|
|
if err != nil {
|
|
s.logger.Error("create platform failed", "error", err)
|
|
writeError(w, http.StatusInternalServerError, "create platform failed")
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusCreated, platform)
|
|
}
|
|
|
|
func (s *Server) listModels(w http.ResponseWriter, r *http.Request) {
|
|
models, err := s.store.ListModels(r.Context())
|
|
if err != nil {
|
|
s.logger.Error("list models failed", "error", err)
|
|
writeError(w, http.StatusInternalServerError, "list models failed")
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, map[string]any{"items": models})
|
|
}
|
|
|
|
func (s *Server) listCatalogProviders(w http.ResponseWriter, r *http.Request) {
|
|
items, err := s.store.ListCatalogProviders(r.Context())
|
|
if err != nil {
|
|
s.logger.Error("list catalog providers failed", "error", err)
|
|
writeError(w, http.StatusInternalServerError, "list catalog providers failed")
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, map[string]any{"items": items})
|
|
}
|
|
|
|
func (s *Server) listBaseModels(w http.ResponseWriter, r *http.Request) {
|
|
items, err := s.store.ListBaseModels(r.Context())
|
|
if err != nil {
|
|
s.logger.Error("list base models failed", "error", err)
|
|
writeError(w, http.StatusInternalServerError, "list base models failed")
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, map[string]any{"items": items})
|
|
}
|
|
|
|
func (s *Server) listPricingRules(w http.ResponseWriter, r *http.Request) {
|
|
items, err := s.store.ListPricingRules(r.Context())
|
|
if err != nil {
|
|
s.logger.Error("list pricing rules failed", "error", err)
|
|
writeError(w, http.StatusInternalServerError, "list pricing rules failed")
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, map[string]any{"items": items})
|
|
}
|
|
|
|
func (s *Server) listTenants(w http.ResponseWriter, r *http.Request) {
|
|
items, err := s.store.ListTenants(r.Context())
|
|
if err != nil {
|
|
s.logger.Error("list tenants failed", "error", err)
|
|
writeError(w, http.StatusInternalServerError, "list tenants failed")
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, map[string]any{"items": items})
|
|
}
|
|
|
|
func (s *Server) listUsers(w http.ResponseWriter, r *http.Request) {
|
|
items, err := s.store.ListUsers(r.Context())
|
|
if err != nil {
|
|
s.logger.Error("list users failed", "error", err)
|
|
writeError(w, http.StatusInternalServerError, "list users failed")
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, map[string]any{"items": items})
|
|
}
|
|
|
|
func (s *Server) listUserGroups(w http.ResponseWriter, r *http.Request) {
|
|
items, err := s.store.ListUserGroups(r.Context())
|
|
if err != nil {
|
|
s.logger.Error("list user groups failed", "error", err)
|
|
writeError(w, http.StatusInternalServerError, "list user groups failed")
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, map[string]any{"items": items})
|
|
}
|
|
|
|
func (s *Server) estimatePricing(w http.ResponseWriter, r *http.Request) {
|
|
var body map[string]any
|
|
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
|
writeError(w, http.StatusBadRequest, "invalid json body")
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, map[string]any{
|
|
"items": []any{},
|
|
"resolver": "effective-pricing-placeholder",
|
|
"request": body,
|
|
})
|
|
}
|
|
|
|
func (s *Server) listRateLimitWindows(w http.ResponseWriter, r *http.Request) {
|
|
items, err := s.store.ListRateLimitWindows(r.Context())
|
|
if err != nil {
|
|
s.logger.Error("list rate limit windows failed", "error", err)
|
|
writeError(w, http.StatusInternalServerError, "list rate limit windows failed")
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, map[string]any{"items": items})
|
|
}
|
|
|
|
func (s *Server) createTask(kind string) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
user, ok := auth.UserFromContext(r.Context())
|
|
if !ok {
|
|
writeError(w, http.StatusUnauthorized, "unauthorized")
|
|
return
|
|
}
|
|
|
|
var body map[string]any
|
|
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
|
writeError(w, http.StatusBadRequest, "invalid json body")
|
|
return
|
|
}
|
|
model, _ := body["model"].(string)
|
|
if model == "" {
|
|
writeError(w, http.StatusBadRequest, "model is required")
|
|
return
|
|
}
|
|
|
|
task, err := s.store.CreateTask(r.Context(), store.CreateTaskInput{
|
|
Kind: kind,
|
|
Model: model,
|
|
Request: body,
|
|
}, user)
|
|
if err != nil {
|
|
s.logger.Error("create task failed", "kind", kind, "error", err)
|
|
writeError(w, http.StatusInternalServerError, "create task failed")
|
|
return
|
|
}
|
|
|
|
writeJSON(w, http.StatusAccepted, map[string]any{
|
|
"task": task,
|
|
"next": map[string]string{
|
|
"events": fmt.Sprintf("/api/v1/tasks/%s/events", task.ID),
|
|
"detail": fmt.Sprintf("/api/v1/tasks/%s", task.ID),
|
|
},
|
|
})
|
|
})
|
|
}
|
|
|
|
func (s *Server) getTask(w http.ResponseWriter, r *http.Request) {
|
|
task, err := s.store.GetTask(r.Context(), r.PathValue("taskID"))
|
|
if err == nil {
|
|
writeJSON(w, http.StatusOK, task)
|
|
return
|
|
}
|
|
if store.IsNotFound(err) {
|
|
writeError(w, http.StatusNotFound, "task not found")
|
|
return
|
|
}
|
|
s.logger.Error("get task failed", "error", err)
|
|
writeError(w, http.StatusInternalServerError, "get task failed")
|
|
}
|
|
|
|
func (s *Server) taskEvents(w http.ResponseWriter, r *http.Request) {
|
|
task, err := s.store.GetTask(r.Context(), r.PathValue("taskID"))
|
|
if err != nil {
|
|
if store.IsNotFound(err) {
|
|
writeError(w, http.StatusNotFound, "task not found")
|
|
return
|
|
}
|
|
writeError(w, http.StatusInternalServerError, "get task failed")
|
|
return
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "text/event-stream")
|
|
w.Header().Set("Cache-Control", "no-cache")
|
|
w.Header().Set("Connection", "keep-alive")
|
|
|
|
sendSSE(w, "task.accepted", map[string]any{
|
|
"taskId": task.ID,
|
|
"status": task.Status,
|
|
})
|
|
if flusher, ok := w.(http.Flusher); ok {
|
|
flusher.Flush()
|
|
}
|
|
|
|
timer := time.NewTimer(250 * time.Millisecond)
|
|
defer timer.Stop()
|
|
|
|
select {
|
|
case <-r.Context().Done():
|
|
return
|
|
case <-timer.C:
|
|
sendSSE(w, "task.placeholder", map[string]any{
|
|
"taskId": task.ID,
|
|
"message": "runtime worker is not wired yet",
|
|
})
|
|
}
|
|
}
|