502 lines
13 KiB
Go
502 lines
13 KiB
Go
package handlers
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net/http"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/tradarr/backend/internal/ai"
|
|
"github.com/tradarr/backend/internal/httputil"
|
|
"github.com/tradarr/backend/internal/models"
|
|
)
|
|
|
|
// ── Credentials ────────────────────────────────────────────────────────────
|
|
|
|
type credentialsRequest struct {
|
|
SourceID string `json:"source_id" binding:"required"`
|
|
Username string `json:"username" binding:"required"`
|
|
Password string `json:"password"`
|
|
}
|
|
|
|
func (h *Handler) GetCredentials(c *gin.Context) {
|
|
sources, err := h.repo.ListSources()
|
|
if err != nil {
|
|
httputil.InternalError(c, err)
|
|
return
|
|
}
|
|
type credResponse struct {
|
|
SourceID string `json:"source_id"`
|
|
SourceName string `json:"source_name"`
|
|
Username string `json:"username"`
|
|
HasPassword bool `json:"has_password"`
|
|
}
|
|
var result []credResponse
|
|
for _, src := range sources {
|
|
if src.Type != "bloomberg" {
|
|
continue
|
|
}
|
|
cred, _ := h.repo.GetCredentials(src.ID)
|
|
r := credResponse{SourceID: src.ID, SourceName: src.Name}
|
|
if cred != nil {
|
|
r.Username = cred.Username
|
|
r.HasPassword = cred.PasswordEncrypted != ""
|
|
}
|
|
result = append(result, r)
|
|
}
|
|
httputil.OK(c, result)
|
|
}
|
|
|
|
func (h *Handler) UpsertCredentials(c *gin.Context) {
|
|
var req credentialsRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
httputil.BadRequest(c, err)
|
|
return
|
|
}
|
|
encPwd := ""
|
|
if req.Password != "" {
|
|
var err error
|
|
encPwd, err = h.enc.Encrypt(req.Password)
|
|
if err != nil {
|
|
httputil.InternalError(c, err)
|
|
return
|
|
}
|
|
}
|
|
if err := h.repo.UpsertCredentials(req.SourceID, req.Username, encPwd); err != nil {
|
|
httputil.InternalError(c, err)
|
|
return
|
|
}
|
|
httputil.OK(c, gin.H{"ok": true})
|
|
}
|
|
|
|
// ── AI Providers ───────────────────────────────────────────────────────────
|
|
|
|
type aiProviderRequest struct {
|
|
Name string `json:"name" binding:"required"`
|
|
APIKey string `json:"api_key"`
|
|
Model string `json:"model"`
|
|
Endpoint string `json:"endpoint"`
|
|
}
|
|
|
|
func (h *Handler) ListAIProviders(c *gin.Context) {
|
|
providers, err := h.repo.ListAIProviders()
|
|
if err != nil {
|
|
httputil.InternalError(c, err)
|
|
return
|
|
}
|
|
// Ne pas exposer les clés chiffrées — juste indiquer si elle existe
|
|
type safeProvider struct {
|
|
ID string `json:"id"`
|
|
Name string `json:"name"`
|
|
Model string `json:"model"`
|
|
Endpoint string `json:"endpoint"`
|
|
IsActive bool `json:"is_active"`
|
|
HasKey bool `json:"has_key"`
|
|
}
|
|
var result []safeProvider
|
|
for _, p := range providers {
|
|
result = append(result, safeProvider{
|
|
ID: p.ID,
|
|
Name: p.Name,
|
|
Model: p.Model,
|
|
Endpoint: p.Endpoint,
|
|
IsActive: p.IsActive,
|
|
HasKey: p.APIKeyEncrypted != "",
|
|
})
|
|
}
|
|
httputil.OK(c, result)
|
|
}
|
|
|
|
func (h *Handler) CreateAIProvider(c *gin.Context) {
|
|
var req aiProviderRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
httputil.BadRequest(c, err)
|
|
return
|
|
}
|
|
encKey := ""
|
|
if req.APIKey != "" {
|
|
var err error
|
|
encKey, err = h.enc.Encrypt(req.APIKey)
|
|
if err != nil {
|
|
httputil.InternalError(c, err)
|
|
return
|
|
}
|
|
}
|
|
p, err := h.repo.CreateAIProvider(req.Name, encKey, req.Model, req.Endpoint)
|
|
if err != nil {
|
|
httputil.InternalError(c, err)
|
|
return
|
|
}
|
|
httputil.Created(c, p)
|
|
}
|
|
|
|
func (h *Handler) UpdateAIProvider(c *gin.Context) {
|
|
id := c.Param("id")
|
|
var req aiProviderRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
httputil.BadRequest(c, err)
|
|
return
|
|
}
|
|
existing, err := h.repo.GetAIProviderByID(id)
|
|
if err != nil || existing == nil {
|
|
httputil.NotFound(c)
|
|
return
|
|
}
|
|
encKey := existing.APIKeyEncrypted
|
|
if req.APIKey != "" {
|
|
encKey, err = h.enc.Encrypt(req.APIKey)
|
|
if err != nil {
|
|
httputil.InternalError(c, err)
|
|
return
|
|
}
|
|
}
|
|
if err := h.repo.UpdateAIProvider(id, encKey, req.Model, req.Endpoint); err != nil {
|
|
httputil.InternalError(c, err)
|
|
return
|
|
}
|
|
httputil.OK(c, gin.H{"ok": true})
|
|
}
|
|
|
|
func (h *Handler) SetActiveAIProvider(c *gin.Context) {
|
|
id := c.Param("id")
|
|
if err := h.repo.SetActiveAIProvider(id); err != nil {
|
|
httputil.InternalError(c, err)
|
|
return
|
|
}
|
|
httputil.OK(c, gin.H{"ok": true})
|
|
}
|
|
|
|
func (h *Handler) DeleteAIProvider(c *gin.Context) {
|
|
if err := h.repo.DeleteAIProvider(c.Param("id")); err != nil {
|
|
httputil.InternalError(c, err)
|
|
return
|
|
}
|
|
httputil.NoContent(c)
|
|
}
|
|
|
|
func (h *Handler) ListAIModels(c *gin.Context) {
|
|
id := c.Param("id")
|
|
p, err := h.repo.GetAIProviderByID(id)
|
|
if err != nil || p == nil {
|
|
httputil.NotFound(c)
|
|
return
|
|
}
|
|
apiKey := ""
|
|
if p.APIKeyEncrypted != "" {
|
|
apiKey, err = h.enc.Decrypt(p.APIKeyEncrypted)
|
|
if err != nil {
|
|
httputil.InternalError(c, err)
|
|
return
|
|
}
|
|
}
|
|
provider, err := h.pipeline.BuildProvider(p.Name, apiKey, p.Endpoint)
|
|
if err != nil {
|
|
httputil.InternalError(c, err)
|
|
return
|
|
}
|
|
models, err := provider.ListModels(c.Request.Context())
|
|
if err != nil {
|
|
httputil.InternalError(c, err)
|
|
return
|
|
}
|
|
httputil.OK(c, models)
|
|
}
|
|
|
|
// ── Sources ────────────────────────────────────────────────────────────────
|
|
|
|
func (h *Handler) ListSources(c *gin.Context) {
|
|
sources, err := h.repo.ListSources()
|
|
if err != nil {
|
|
httputil.InternalError(c, err)
|
|
return
|
|
}
|
|
httputil.OK(c, sources)
|
|
}
|
|
|
|
type updateSourceRequest struct {
|
|
Enabled bool `json:"enabled"`
|
|
}
|
|
|
|
func (h *Handler) UpdateSource(c *gin.Context) {
|
|
var req updateSourceRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
httputil.BadRequest(c, err)
|
|
return
|
|
}
|
|
if err := h.repo.UpdateSource(c.Param("id"), req.Enabled); err != nil {
|
|
httputil.InternalError(c, err)
|
|
return
|
|
}
|
|
httputil.OK(c, gin.H{"ok": true})
|
|
}
|
|
|
|
// ── Scrape Jobs ────────────────────────────────────────────────────────────
|
|
|
|
func (h *Handler) ListScrapeJobs(c *gin.Context) {
|
|
jobs, err := h.repo.ListScrapeJobs(100)
|
|
if err != nil {
|
|
httputil.InternalError(c, err)
|
|
return
|
|
}
|
|
httputil.OK(c, jobs)
|
|
}
|
|
|
|
func (h *Handler) TriggerScrapeJob(c *gin.Context) {
|
|
type triggerRequest struct {
|
|
SourceID string `json:"source_id" binding:"required"`
|
|
}
|
|
var req triggerRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
httputil.BadRequest(c, err)
|
|
return
|
|
}
|
|
go func() {
|
|
if err := h.registry.Run(req.SourceID); err != nil {
|
|
fmt.Printf("scrape job error: %v\n", err)
|
|
}
|
|
}()
|
|
c.JSON(http.StatusAccepted, gin.H{"ok": true, "message": "job started"})
|
|
}
|
|
|
|
// ── Settings ───────────────────────────────────────────────────────────────
|
|
|
|
func (h *Handler) ListSettings(c *gin.Context) {
|
|
settings, err := h.repo.ListSettings()
|
|
if err != nil {
|
|
httputil.InternalError(c, err)
|
|
return
|
|
}
|
|
httputil.OK(c, settings)
|
|
}
|
|
|
|
type updateSettingsRequest struct {
|
|
Settings []models.Setting `json:"settings"`
|
|
}
|
|
|
|
func (h *Handler) UpdateSettings(c *gin.Context) {
|
|
var req updateSettingsRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
httputil.BadRequest(c, err)
|
|
return
|
|
}
|
|
for _, s := range req.Settings {
|
|
if err := h.repo.SetSetting(s.Key, s.Value); err != nil {
|
|
httputil.InternalError(c, err)
|
|
return
|
|
}
|
|
}
|
|
httputil.OK(c, gin.H{"ok": true})
|
|
}
|
|
|
|
// ── Schedule ───────────────────────────────────────────────────────────────
|
|
|
|
func (h *Handler) GetSchedule(c *gin.Context) {
|
|
slots, err := h.repo.ListScheduleSlots()
|
|
if err != nil {
|
|
httputil.InternalError(c, err)
|
|
return
|
|
}
|
|
httputil.OK(c, slots)
|
|
}
|
|
|
|
type scheduleRequest struct {
|
|
Slots []struct {
|
|
DayOfWeek int `json:"day_of_week"`
|
|
Hour int `json:"hour"`
|
|
Minute int `json:"minute"`
|
|
} `json:"slots"`
|
|
}
|
|
|
|
func (h *Handler) UpdateSchedule(c *gin.Context) {
|
|
var req scheduleRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
httputil.BadRequest(c, err)
|
|
return
|
|
}
|
|
slots := make([]models.ScheduleSlot, len(req.Slots))
|
|
for i, s := range req.Slots {
|
|
slots[i] = models.ScheduleSlot{DayOfWeek: s.DayOfWeek, Hour: s.Hour, Minute: s.Minute}
|
|
}
|
|
if err := h.repo.ReplaceSchedule(slots); err != nil {
|
|
httputil.InternalError(c, err)
|
|
return
|
|
}
|
|
if err := h.scheduler.Reload(); err != nil {
|
|
fmt.Printf("schedule reload: %v\n", err)
|
|
}
|
|
httputil.OK(c, gin.H{"ok": true})
|
|
}
|
|
|
|
func (h *Handler) GetDefaultSystemPrompt(c *gin.Context) {
|
|
httputil.OK(c, gin.H{"prompt": ai.DefaultSystemPrompt})
|
|
}
|
|
|
|
// ── AI Roles ───────────────────────────────────────────────────────────────
|
|
|
|
func (h *Handler) GetAIRoles(c *gin.Context) {
|
|
roles := []string{"summary", "report", "filter"}
|
|
resp := gin.H{}
|
|
for _, role := range roles {
|
|
providerID, _ := h.repo.GetSetting("ai_role_" + role + "_provider")
|
|
model, _ := h.repo.GetSetting("ai_role_" + role + "_model")
|
|
resp[role] = gin.H{"provider_id": providerID, "model": model}
|
|
}
|
|
httputil.OK(c, resp)
|
|
}
|
|
|
|
type updateAIRoleRequest struct {
|
|
ProviderID string `json:"provider_id"`
|
|
Model string `json:"model"`
|
|
}
|
|
|
|
func (h *Handler) UpdateAIRole(c *gin.Context) {
|
|
role := c.Param("role")
|
|
if role != "summary" && role != "report" && role != "filter" {
|
|
httputil.BadRequest(c, fmt.Errorf("invalid role: must be summary, report, or filter"))
|
|
return
|
|
}
|
|
var req updateAIRoleRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
httputil.BadRequest(c, err)
|
|
return
|
|
}
|
|
if err := h.repo.SetRoleProvider(role, req.ProviderID, req.Model); err != nil {
|
|
httputil.InternalError(c, err)
|
|
return
|
|
}
|
|
httputil.OK(c, gin.H{"ok": true})
|
|
}
|
|
|
|
// ── Ollama Model Management ────────────────────────────────────────────────
|
|
|
|
func (h *Handler) getOllamaManager() (*ai.OllamaProvider, error) {
|
|
providers, err := h.repo.ListAIProviders()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
endpoint := ""
|
|
for _, p := range providers {
|
|
if p.Name == "ollama" {
|
|
endpoint = p.Endpoint
|
|
break
|
|
}
|
|
}
|
|
if endpoint == "" {
|
|
endpoint = "http://ollama:11434"
|
|
}
|
|
return ai.NewOllamaManager(endpoint), nil
|
|
}
|
|
|
|
func (h *Handler) ListOllamaModels(c *gin.Context) {
|
|
mgr, err := h.getOllamaManager()
|
|
if err != nil {
|
|
httputil.InternalError(c, err)
|
|
return
|
|
}
|
|
models, err := mgr.ListModelsDetailed(c.Request.Context())
|
|
if err != nil {
|
|
httputil.InternalError(c, err)
|
|
return
|
|
}
|
|
if models == nil {
|
|
models = []ai.OllamaModelInfo{}
|
|
}
|
|
httputil.OK(c, models)
|
|
}
|
|
|
|
type pullModelRequest struct {
|
|
Name string `json:"name" binding:"required"`
|
|
}
|
|
|
|
func (h *Handler) PullOllamaModel(c *gin.Context) {
|
|
var req pullModelRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
httputil.BadRequest(c, err)
|
|
return
|
|
}
|
|
mgr, err := h.getOllamaManager()
|
|
if err != nil {
|
|
httputil.InternalError(c, err)
|
|
return
|
|
}
|
|
go func() {
|
|
if err := mgr.PullModel(context.Background(), req.Name); err != nil {
|
|
fmt.Printf("[ollama] pull %s failed: %v\n", req.Name, err)
|
|
} else {
|
|
fmt.Printf("[ollama] pull %s completed\n", req.Name)
|
|
}
|
|
}()
|
|
c.JSON(202, gin.H{"ok": true, "message": "pull started"})
|
|
}
|
|
|
|
func (h *Handler) DeleteOllamaModel(c *gin.Context) {
|
|
name := c.Param("name")
|
|
mgr, err := h.getOllamaManager()
|
|
if err != nil {
|
|
httputil.InternalError(c, err)
|
|
return
|
|
}
|
|
if err := mgr.DeleteModel(c.Request.Context(), name); err != nil {
|
|
httputil.InternalError(c, err)
|
|
return
|
|
}
|
|
httputil.NoContent(c)
|
|
}
|
|
|
|
// ── Admin Users ────────────────────────────────────────────────────────────
|
|
|
|
func (h *Handler) ListUsers(c *gin.Context) {
|
|
users, err := h.repo.ListUsers()
|
|
if err != nil {
|
|
httputil.InternalError(c, err)
|
|
return
|
|
}
|
|
httputil.OK(c, users)
|
|
}
|
|
|
|
type updateUserRequest struct {
|
|
Email string `json:"email" binding:"required,email"`
|
|
Role string `json:"role" binding:"required"`
|
|
}
|
|
|
|
func (h *Handler) UpdateAdminUser(c *gin.Context) {
|
|
id := c.Param("id")
|
|
var req updateUserRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
httputil.BadRequest(c, err)
|
|
return
|
|
}
|
|
if req.Role != "admin" && req.Role != "user" {
|
|
httputil.BadRequest(c, fmt.Errorf("role must be admin or user"))
|
|
return
|
|
}
|
|
user, err := h.repo.UpdateUser(id, req.Email, models.Role(req.Role))
|
|
if err != nil {
|
|
httputil.InternalError(c, err)
|
|
return
|
|
}
|
|
httputil.OK(c, user)
|
|
}
|
|
|
|
func (h *Handler) DeleteAdminUser(c *gin.Context) {
|
|
id := c.Param("id")
|
|
// Empêcher la suppression du dernier admin
|
|
user, err := h.repo.GetUserByID(id)
|
|
if err != nil || user == nil {
|
|
httputil.NotFound(c)
|
|
return
|
|
}
|
|
if user.Role == "admin" {
|
|
count, _ := h.repo.CountAdmins()
|
|
if count <= 1 {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "cannot delete the last admin"})
|
|
return
|
|
}
|
|
}
|
|
if err := h.repo.DeleteUser(id); err != nil {
|
|
httputil.InternalError(c, err)
|
|
return
|
|
}
|
|
httputil.NoContent(c)
|
|
}
|