feat: add frontend + backend + database to retrieve and compute news from Yahoo

This commit is contained in:
2026-04-18 23:53:57 +02:00
parent f9b6d35c49
commit 93668273ff
84 changed files with 15431 additions and 0 deletions

View File

@ -0,0 +1,349 @@
package handlers
import (
"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})
}
func (h *Handler) GetDefaultSystemPrompt(c *gin.Context) {
httputil.OK(c, gin.H{"prompt": ai.DefaultSystemPrompt})
}
// ── 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)
}