Files
gooseek/backend/cmd/collection-svc/main.go
home a0e3748dde feat: auth service + security audit fixes + cleanup legacy services
Major changes:
- Add auth-svc: JWT auth, register/login/refresh, password reset
- Add auth UI: modals, pages (/login, /register, /forgot-password)
- Add usage tracking (usage_metrics table, daily limits)
- Add tiered rate limiting (free/pro/business)
- Add LLM usage limits per tier

Security fixes:
- All repos now require userID for Update/Delete operations
- JWT middleware in chat-svc, llm-svc, agent-svc, discover-svc
- ErrNotFound/ErrForbidden errors for proper access control

Cleanup:
- Remove legacy TypeScript services/ directory
- Remove computer-svc (to be reimplemented)
- Remove old deploy/docker configs

New files:
- backend/cmd/auth-svc/main.go
- backend/internal/auth/{types,repository}.go
- backend/internal/usage/{types,repository}.go
- backend/pkg/middleware/{llm_limits,ratelimit_tiered}.go
- backend/webui/src/components/auth/*
- backend/webui/src/app/(auth)/*

Made-with: Cursor
2026-02-28 01:33:49 +03:00

349 lines
9.6 KiB
Go

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, userID)
if err != nil {
if err == db.ErrForbidden {
return c.Status(403).JSON(fiber.Map{"error": "Access denied"})
}
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, userID); err != nil {
if err == db.ErrNotFound {
return c.Status(404).JSON(fiber.Map{"error": "Collection not found"})
}
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)
if err := collectionRepo.Delete(c.Context(), collectionID, userID); err != nil {
if err == db.ErrNotFound {
return c.Status(404).JSON(fiber.Map{"error": "Collection not found"})
}
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, userID); err != nil {
if err == db.ErrNotFound {
return c.Status(404).JSON(fiber.Map{"error": "Item not found"})
}
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, userID)
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
}