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 }