feat: Go backend, enhanced search, new widgets, Docker deploy
Major changes: - Add Go backend (backend/) with microservices architecture - Enhanced master-agents-svc: reranker, content-classifier, stealth-crawler, proxy-manager, media-search, fastClassifier, language detection - New web-svc widgets: KnowledgeCard, ProductCard, ProfileCard, VideoCard, UnifiedCard, CardGallery, InlineImageGallery, SourcesPanel, RelatedQuestions - Improved discover-svc with discover-db integration - Docker deployment improvements (Caddyfile, vendor.sh, BUILD.md) - Library-svc: project_id schema migration - Remove deprecated finance-svc and travel-svc - Localization improvements across services Made-with: Cursor
This commit is contained in:
345
backend/cmd/collection-svc/main.go
Normal file
345
backend/cmd/collection-svc/main.go
Normal file
@@ -0,0 +1,345 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/gofiber/fiber/v2/middleware/cors"
|
||||
"github.com/gofiber/fiber/v2/middleware/logger"
|
||||
"github.com/gooseek/backend/internal/db"
|
||||
"github.com/gooseek/backend/pkg/config"
|
||||
"github.com/gooseek/backend/pkg/middleware"
|
||||
)
|
||||
|
||||
func main() {
|
||||
cfg, err := config.Load()
|
||||
if err != nil {
|
||||
log.Fatal("Failed to load config:", err)
|
||||
}
|
||||
|
||||
var database *db.PostgresDB
|
||||
var collectionRepo *db.CollectionRepository
|
||||
|
||||
if cfg.DatabaseURL != "" {
|
||||
database, err = db.NewPostgresDB(cfg.DatabaseURL)
|
||||
if err != nil {
|
||||
log.Printf("Database unavailable: %v (some features disabled)", err)
|
||||
} else {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
if err := database.RunMigrations(ctx); err != nil {
|
||||
log.Printf("Migration warning: %v", err)
|
||||
}
|
||||
cancel()
|
||||
defer database.Close()
|
||||
|
||||
collectionRepo = db.NewCollectionRepository(database)
|
||||
log.Println("PostgreSQL connected")
|
||||
}
|
||||
}
|
||||
|
||||
app := fiber.New(fiber.Config{
|
||||
BodyLimit: 50 * 1024 * 1024,
|
||||
ReadTimeout: 30 * time.Second,
|
||||
WriteTimeout: 30 * time.Second,
|
||||
})
|
||||
|
||||
app.Use(logger.New())
|
||||
app.Use(cors.New())
|
||||
|
||||
if cfg.JWTSecret != "" || cfg.AuthSvcURL != "" {
|
||||
app.Use(middleware.JWT(middleware.JWTConfig{
|
||||
Secret: cfg.JWTSecret,
|
||||
AuthSvcURL: cfg.AuthSvcURL,
|
||||
AllowGuest: false,
|
||||
SkipPaths: []string{"/health", "/ready"},
|
||||
}))
|
||||
}
|
||||
|
||||
app.Get("/health", func(c *fiber.Ctx) error {
|
||||
return c.JSON(fiber.Map{"status": "ok"})
|
||||
})
|
||||
|
||||
app.Get("/ready", func(c *fiber.Ctx) error {
|
||||
if database == nil {
|
||||
return c.Status(503).JSON(fiber.Map{"status": "database unavailable"})
|
||||
}
|
||||
return c.JSON(fiber.Map{"status": "ready"})
|
||||
})
|
||||
|
||||
api := app.Group("/api/v1/collections")
|
||||
|
||||
api.Get("/", func(c *fiber.Ctx) error {
|
||||
if collectionRepo == nil {
|
||||
return c.Status(503).JSON(fiber.Map{"error": "Database unavailable"})
|
||||
}
|
||||
|
||||
userID := middleware.GetUserID(c)
|
||||
if userID == "" {
|
||||
return c.Status(401).JSON(fiber.Map{"error": "Unauthorized"})
|
||||
}
|
||||
|
||||
limit := c.QueryInt("limit", 50)
|
||||
offset := c.QueryInt("offset", 0)
|
||||
|
||||
collections, err := collectionRepo.GetByUserID(c.Context(), userID, limit, offset)
|
||||
if err != nil {
|
||||
return c.Status(500).JSON(fiber.Map{"error": "Failed to get collections"})
|
||||
}
|
||||
|
||||
return c.JSON(fiber.Map{"collections": collections})
|
||||
})
|
||||
|
||||
api.Post("/", func(c *fiber.Ctx) error {
|
||||
if collectionRepo == nil {
|
||||
return c.Status(503).JSON(fiber.Map{"error": "Database unavailable"})
|
||||
}
|
||||
|
||||
userID := middleware.GetUserID(c)
|
||||
if userID == "" {
|
||||
return c.Status(401).JSON(fiber.Map{"error": "Unauthorized"})
|
||||
}
|
||||
|
||||
var req struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
IsPublic bool `json:"isPublic"`
|
||||
ContextEnabled bool `json:"contextEnabled"`
|
||||
}
|
||||
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return c.Status(400).JSON(fiber.Map{"error": "Invalid request body"})
|
||||
}
|
||||
|
||||
if req.Name == "" {
|
||||
return c.Status(400).JSON(fiber.Map{"error": "Name is required"})
|
||||
}
|
||||
|
||||
collection := &db.Collection{
|
||||
UserID: userID,
|
||||
Name: req.Name,
|
||||
Description: req.Description,
|
||||
IsPublic: req.IsPublic,
|
||||
ContextEnabled: req.ContextEnabled,
|
||||
}
|
||||
|
||||
if err := collectionRepo.Create(c.Context(), collection); err != nil {
|
||||
return c.Status(500).JSON(fiber.Map{"error": "Failed to create collection"})
|
||||
}
|
||||
|
||||
return c.Status(201).JSON(collection)
|
||||
})
|
||||
|
||||
api.Get("/:id", func(c *fiber.Ctx) error {
|
||||
if collectionRepo == nil {
|
||||
return c.Status(503).JSON(fiber.Map{"error": "Database unavailable"})
|
||||
}
|
||||
|
||||
collectionID := c.Params("id")
|
||||
userID := middleware.GetUserID(c)
|
||||
|
||||
collection, err := collectionRepo.GetByID(c.Context(), collectionID)
|
||||
if err != nil {
|
||||
return c.Status(500).JSON(fiber.Map{"error": "Failed to get collection"})
|
||||
}
|
||||
if collection == nil {
|
||||
return c.Status(404).JSON(fiber.Map{"error": "Collection not found"})
|
||||
}
|
||||
|
||||
if collection.UserID != userID && !collection.IsPublic {
|
||||
return c.Status(403).JSON(fiber.Map{"error": "Access denied"})
|
||||
}
|
||||
|
||||
items, err := collectionRepo.GetItems(c.Context(), collectionID)
|
||||
if err != nil {
|
||||
return c.Status(500).JSON(fiber.Map{"error": "Failed to get items"})
|
||||
}
|
||||
collection.Items = items
|
||||
|
||||
return c.JSON(collection)
|
||||
})
|
||||
|
||||
api.Put("/:id", func(c *fiber.Ctx) error {
|
||||
if collectionRepo == nil {
|
||||
return c.Status(503).JSON(fiber.Map{"error": "Database unavailable"})
|
||||
}
|
||||
|
||||
collectionID := c.Params("id")
|
||||
userID := middleware.GetUserID(c)
|
||||
|
||||
collection, err := collectionRepo.GetByID(c.Context(), collectionID)
|
||||
if err != nil || collection == nil {
|
||||
return c.Status(404).JSON(fiber.Map{"error": "Collection not found"})
|
||||
}
|
||||
|
||||
if collection.UserID != userID {
|
||||
return c.Status(403).JSON(fiber.Map{"error": "Access denied"})
|
||||
}
|
||||
|
||||
var req struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
IsPublic bool `json:"isPublic"`
|
||||
ContextEnabled bool `json:"contextEnabled"`
|
||||
}
|
||||
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return c.Status(400).JSON(fiber.Map{"error": "Invalid request body"})
|
||||
}
|
||||
|
||||
collection.Name = req.Name
|
||||
collection.Description = req.Description
|
||||
collection.IsPublic = req.IsPublic
|
||||
collection.ContextEnabled = req.ContextEnabled
|
||||
|
||||
if err := collectionRepo.Update(c.Context(), collection); err != nil {
|
||||
return c.Status(500).JSON(fiber.Map{"error": "Failed to update collection"})
|
||||
}
|
||||
|
||||
return c.JSON(collection)
|
||||
})
|
||||
|
||||
api.Delete("/:id", func(c *fiber.Ctx) error {
|
||||
if collectionRepo == nil {
|
||||
return c.Status(503).JSON(fiber.Map{"error": "Database unavailable"})
|
||||
}
|
||||
|
||||
collectionID := c.Params("id")
|
||||
userID := middleware.GetUserID(c)
|
||||
|
||||
collection, err := collectionRepo.GetByID(c.Context(), collectionID)
|
||||
if err != nil || collection == nil {
|
||||
return c.Status(404).JSON(fiber.Map{"error": "Collection not found"})
|
||||
}
|
||||
|
||||
if collection.UserID != userID {
|
||||
return c.Status(403).JSON(fiber.Map{"error": "Access denied"})
|
||||
}
|
||||
|
||||
if err := collectionRepo.Delete(c.Context(), collectionID); err != nil {
|
||||
return c.Status(500).JSON(fiber.Map{"error": "Failed to delete collection"})
|
||||
}
|
||||
|
||||
return c.Status(204).Send(nil)
|
||||
})
|
||||
|
||||
api.Post("/:id/items", func(c *fiber.Ctx) error {
|
||||
if collectionRepo == nil {
|
||||
return c.Status(503).JSON(fiber.Map{"error": "Database unavailable"})
|
||||
}
|
||||
|
||||
collectionID := c.Params("id")
|
||||
userID := middleware.GetUserID(c)
|
||||
|
||||
collection, err := collectionRepo.GetByID(c.Context(), collectionID)
|
||||
if err != nil || collection == nil {
|
||||
return c.Status(404).JSON(fiber.Map{"error": "Collection not found"})
|
||||
}
|
||||
|
||||
if collection.UserID != userID {
|
||||
return c.Status(403).JSON(fiber.Map{"error": "Access denied"})
|
||||
}
|
||||
|
||||
var req struct {
|
||||
ItemType string `json:"itemType"`
|
||||
Title string `json:"title"`
|
||||
Content string `json:"content"`
|
||||
URL string `json:"url"`
|
||||
Metadata map[string]interface{} `json:"metadata"`
|
||||
}
|
||||
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return c.Status(400).JSON(fiber.Map{"error": "Invalid request body"})
|
||||
}
|
||||
|
||||
if req.ItemType == "" {
|
||||
return c.Status(400).JSON(fiber.Map{"error": "itemType is required"})
|
||||
}
|
||||
|
||||
item := &db.CollectionItem{
|
||||
CollectionID: collectionID,
|
||||
ItemType: req.ItemType,
|
||||
Title: req.Title,
|
||||
Content: req.Content,
|
||||
URL: req.URL,
|
||||
Metadata: req.Metadata,
|
||||
}
|
||||
|
||||
if err := collectionRepo.AddItem(c.Context(), item); err != nil {
|
||||
return c.Status(500).JSON(fiber.Map{"error": "Failed to add item"})
|
||||
}
|
||||
|
||||
return c.Status(201).JSON(item)
|
||||
})
|
||||
|
||||
api.Delete("/:id/items/:itemId", func(c *fiber.Ctx) error {
|
||||
if collectionRepo == nil {
|
||||
return c.Status(503).JSON(fiber.Map{"error": "Database unavailable"})
|
||||
}
|
||||
|
||||
collectionID := c.Params("id")
|
||||
itemID := c.Params("itemId")
|
||||
userID := middleware.GetUserID(c)
|
||||
|
||||
collection, err := collectionRepo.GetByID(c.Context(), collectionID)
|
||||
if err != nil || collection == nil {
|
||||
return c.Status(404).JSON(fiber.Map{"error": "Collection not found"})
|
||||
}
|
||||
|
||||
if collection.UserID != userID {
|
||||
return c.Status(403).JSON(fiber.Map{"error": "Access denied"})
|
||||
}
|
||||
|
||||
if err := collectionRepo.RemoveItem(c.Context(), itemID); err != nil {
|
||||
return c.Status(500).JSON(fiber.Map{"error": "Failed to remove item"})
|
||||
}
|
||||
|
||||
return c.Status(204).Send(nil)
|
||||
})
|
||||
|
||||
api.Get("/:id/context", func(c *fiber.Ctx) error {
|
||||
if collectionRepo == nil {
|
||||
return c.Status(503).JSON(fiber.Map{"error": "Database unavailable"})
|
||||
}
|
||||
|
||||
collectionID := c.Params("id")
|
||||
userID := middleware.GetUserID(c)
|
||||
|
||||
collection, err := collectionRepo.GetByID(c.Context(), collectionID)
|
||||
if err != nil || collection == nil {
|
||||
return c.Status(404).JSON(fiber.Map{"error": "Collection not found"})
|
||||
}
|
||||
|
||||
if collection.UserID != userID && !collection.IsPublic {
|
||||
return c.Status(403).JSON(fiber.Map{"error": "Access denied"})
|
||||
}
|
||||
|
||||
if !collection.ContextEnabled {
|
||||
return c.JSON(fiber.Map{"context": "", "enabled": false})
|
||||
}
|
||||
|
||||
context, err := collectionRepo.GetCollectionContext(c.Context(), collectionID)
|
||||
if err != nil {
|
||||
return c.Status(500).JSON(fiber.Map{"error": "Failed to get context"})
|
||||
}
|
||||
|
||||
return c.JSON(fiber.Map{"context": context, "enabled": true})
|
||||
})
|
||||
|
||||
port := getEnvInt("COLLECTION_SVC_PORT", 3025)
|
||||
log.Printf("collection-svc listening on :%d", port)
|
||||
log.Fatal(app.Listen(fmt.Sprintf(":%d", port)))
|
||||
}
|
||||
|
||||
func getEnvInt(key string, defaultValue int) int {
|
||||
if val := os.Getenv(key); val != "" {
|
||||
var result int
|
||||
if _, err := fmt.Sscanf(val, "%d", &result); err == nil {
|
||||
return result
|
||||
}
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
Reference in New Issue
Block a user