package main import ( "context" "fmt" "log" "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/admin" "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 if cfg.DatabaseURL != "" { database, err = db.NewPostgresDB(cfg.DatabaseURL) if err != nil { log.Fatal("Failed to connect to database:", err) } defer database.Close() ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) if err := database.RunMigrations(ctx); err != nil { log.Printf("Migration warning: %v", err) } if err := admin.RunAdminMigrations(ctx, database.DB()); err != nil { log.Printf("Admin migrations warning: %v", err) } cancel() log.Println("PostgreSQL connected") } else { log.Fatal("DATABASE_URL is required for admin-svc") } userRepo := admin.NewUserRepository(database.DB()) postRepo := admin.NewPostRepository(database.DB()) settingsRepo := admin.NewSettingsRepository(database.DB()) discoverRepo := admin.NewDiscoverConfigRepository(database.DB()) auditRepo := admin.NewAuditRepository(database.DB()) app := fiber.New(fiber.Config{ BodyLimit: 50 * 1024 * 1024, ReadTimeout: 30 * time.Second, WriteTimeout: 30 * time.Second, IdleTimeout: 60 * time.Second, }) app.Use(logger.New()) app.Use(cors.New()) app.Get("/health", func(c *fiber.Ctx) error { return c.JSON(fiber.Map{"status": "ok"}) }) app.Get("/ready", func(c *fiber.Ctx) error { return c.JSON(fiber.Map{"status": "ready"}) }) api := app.Group("/api/v1/admin") api.Use(middleware.JWT(middleware.JWTConfig{ Secret: cfg.JWTSecret, AuthSvcURL: cfg.AuthSvcURL, AllowGuest: false, })) api.Use(middleware.RequireRole("admin")) api.Get("/dashboard", func(c *fiber.Ctx) error { stats, err := getDashboardStats(c.Context(), userRepo, postRepo) if err != nil { return c.Status(500).JSON(fiber.Map{"error": err.Error()}) } return c.JSON(stats) }) usersGroup := api.Group("/users") { usersGroup.Get("/", func(c *fiber.Ctx) error { page := c.QueryInt("page", 1) perPage := c.QueryInt("perPage", 20) search := c.Query("search") users, total, err := userRepo.List(c.Context(), page, perPage, search) if err != nil { return c.Status(500).JSON(fiber.Map{"error": err.Error()}) } return c.JSON(admin.UserListResponse{ Users: users, Total: total, Page: page, PerPage: perPage, }) }) usersGroup.Get("/:id", func(c *fiber.Ctx) error { user, err := userRepo.GetByID(c.Context(), c.Params("id")) if err != nil { return c.Status(404).JSON(fiber.Map{"error": "User not found"}) } return c.JSON(user) }) usersGroup.Post("/", func(c *fiber.Ctx) error { var req admin.UserCreateRequest if err := c.BodyParser(&req); err != nil { return c.Status(400).JSON(fiber.Map{"error": "Invalid request body"}) } user, err := userRepo.Create(c.Context(), &req) if err != nil { return c.Status(500).JSON(fiber.Map{"error": err.Error()}) } logAudit(c, auditRepo, "create", "user", user.ID) return c.Status(201).JSON(user) }) usersGroup.Patch("/:id", func(c *fiber.Ctx) error { var req admin.UserUpdateRequest if err := c.BodyParser(&req); err != nil { return c.Status(400).JSON(fiber.Map{"error": "Invalid request body"}) } user, err := userRepo.Update(c.Context(), c.Params("id"), &req) if err != nil { return c.Status(500).JSON(fiber.Map{"error": err.Error()}) } logAudit(c, auditRepo, "update", "user", user.ID) return c.JSON(user) }) usersGroup.Delete("/:id", func(c *fiber.Ctx) error { if err := userRepo.Delete(c.Context(), c.Params("id")); err != nil { return c.Status(500).JSON(fiber.Map{"error": err.Error()}) } logAudit(c, auditRepo, "delete", "user", c.Params("id")) return c.SendStatus(204) }) } postsGroup := api.Group("/posts") { postsGroup.Get("/", func(c *fiber.Ctx) error { page := c.QueryInt("page", 1) perPage := c.QueryInt("perPage", 20) status := c.Query("status") category := c.Query("category") posts, total, err := postRepo.List(c.Context(), page, perPage, status, category) if err != nil { return c.Status(500).JSON(fiber.Map{"error": err.Error()}) } return c.JSON(admin.PostListResponse{ Posts: posts, Total: total, Page: page, PerPage: perPage, }) }) postsGroup.Get("/:id", func(c *fiber.Ctx) error { post, err := postRepo.GetByID(c.Context(), c.Params("id")) if err != nil { return c.Status(404).JSON(fiber.Map{"error": "Post not found"}) } return c.JSON(post) }) postsGroup.Post("/", func(c *fiber.Ctx) error { var req admin.PostCreateRequest if err := c.BodyParser(&req); err != nil { return c.Status(400).JSON(fiber.Map{"error": "Invalid request body"}) } authorID := middleware.GetUserID(c) post, err := postRepo.Create(c.Context(), authorID, &req) if err != nil { return c.Status(500).JSON(fiber.Map{"error": err.Error()}) } logAudit(c, auditRepo, "create", "post", post.ID) return c.Status(201).JSON(post) }) postsGroup.Patch("/:id", func(c *fiber.Ctx) error { var req admin.PostUpdateRequest if err := c.BodyParser(&req); err != nil { return c.Status(400).JSON(fiber.Map{"error": "Invalid request body"}) } post, err := postRepo.Update(c.Context(), c.Params("id"), &req) if err != nil { return c.Status(500).JSON(fiber.Map{"error": err.Error()}) } logAudit(c, auditRepo, "update", "post", post.ID) return c.JSON(post) }) postsGroup.Delete("/:id", func(c *fiber.Ctx) error { if err := postRepo.Delete(c.Context(), c.Params("id")); err != nil { return c.Status(500).JSON(fiber.Map{"error": err.Error()}) } logAudit(c, auditRepo, "delete", "post", c.Params("id")) return c.SendStatus(204) }) postsGroup.Post("/:id/publish", func(c *fiber.Ctx) error { post, err := postRepo.Publish(c.Context(), c.Params("id")) if err != nil { return c.Status(500).JSON(fiber.Map{"error": err.Error()}) } logAudit(c, auditRepo, "publish", "post", post.ID) return c.JSON(post) }) } settingsGroup := api.Group("/settings") { settingsGroup.Get("/", func(c *fiber.Ctx) error { settings, err := settingsRepo.Get(c.Context()) if err != nil { return c.Status(500).JSON(fiber.Map{"error": err.Error()}) } return c.JSON(settings) }) settingsGroup.Patch("/", func(c *fiber.Ctx) error { var settings admin.PlatformSettings if err := c.BodyParser(&settings); err != nil { return c.Status(400).JSON(fiber.Map{"error": "Invalid request body"}) } updated, err := settingsRepo.Update(c.Context(), &settings) if err != nil { return c.Status(500).JSON(fiber.Map{"error": err.Error()}) } logAudit(c, auditRepo, "update", "settings", "platform") return c.JSON(updated) }) settingsGroup.Get("/features", func(c *fiber.Ctx) error { features, err := settingsRepo.GetFeatures(c.Context()) if err != nil { return c.Status(500).JSON(fiber.Map{"error": err.Error()}) } return c.JSON(features) }) settingsGroup.Patch("/features", func(c *fiber.Ctx) error { var features admin.FeatureFlags if err := c.BodyParser(&features); err != nil { return c.Status(400).JSON(fiber.Map{"error": "Invalid request body"}) } if err := settingsRepo.UpdateFeatures(c.Context(), &features); err != nil { return c.Status(500).JSON(fiber.Map{"error": err.Error()}) } logAudit(c, auditRepo, "update", "settings", "features") return c.JSON(features) }) } discoverGroup := api.Group("/discover") { discoverGroup.Get("/categories", func(c *fiber.Ctx) error { categories, err := discoverRepo.ListCategories(c.Context()) if err != nil { return c.Status(500).JSON(fiber.Map{"error": err.Error()}) } return c.JSON(fiber.Map{"categories": categories}) }) discoverGroup.Post("/categories", func(c *fiber.Ctx) error { var req admin.DiscoverCategoryCreateRequest if err := c.BodyParser(&req); err != nil { return c.Status(400).JSON(fiber.Map{"error": "Invalid request body"}) } category, err := discoverRepo.CreateCategory(c.Context(), &req) if err != nil { return c.Status(500).JSON(fiber.Map{"error": err.Error()}) } logAudit(c, auditRepo, "create", "discover_category", category.ID) return c.Status(201).JSON(category) }) discoverGroup.Patch("/categories/:id", func(c *fiber.Ctx) error { var req admin.DiscoverCategoryUpdateRequest if err := c.BodyParser(&req); err != nil { return c.Status(400).JSON(fiber.Map{"error": "Invalid request body"}) } category, err := discoverRepo.UpdateCategory(c.Context(), c.Params("id"), &req) if err != nil { return c.Status(500).JSON(fiber.Map{"error": err.Error()}) } logAudit(c, auditRepo, "update", "discover_category", category.ID) return c.JSON(category) }) discoverGroup.Delete("/categories/:id", func(c *fiber.Ctx) error { if err := discoverRepo.DeleteCategory(c.Context(), c.Params("id")); err != nil { return c.Status(500).JSON(fiber.Map{"error": err.Error()}) } logAudit(c, auditRepo, "delete", "discover_category", c.Params("id")) return c.SendStatus(204) }) discoverGroup.Post("/categories/reorder", func(c *fiber.Ctx) error { var req struct { Order []string `json:"order"` } if err := c.BodyParser(&req); err != nil { return c.Status(400).JSON(fiber.Map{"error": "Invalid request body"}) } if err := discoverRepo.ReorderCategories(c.Context(), req.Order); err != nil { return c.Status(500).JSON(fiber.Map{"error": err.Error()}) } logAudit(c, auditRepo, "reorder", "discover_categories", "") return c.SendStatus(204) }) discoverGroup.Get("/sources", func(c *fiber.Ctx) error { sources, err := discoverRepo.ListSources(c.Context()) if err != nil { return c.Status(500).JSON(fiber.Map{"error": err.Error()}) } return c.JSON(fiber.Map{"sources": sources}) }) discoverGroup.Post("/sources", func(c *fiber.Ctx) error { var req admin.DiscoverSourceCreateRequest if err := c.BodyParser(&req); err != nil { return c.Status(400).JSON(fiber.Map{"error": "Invalid request body"}) } source, err := discoverRepo.CreateSource(c.Context(), &req) if err != nil { return c.Status(500).JSON(fiber.Map{"error": err.Error()}) } logAudit(c, auditRepo, "create", "discover_source", source.ID) return c.Status(201).JSON(source) }) discoverGroup.Delete("/sources/:id", func(c *fiber.Ctx) error { if err := discoverRepo.DeleteSource(c.Context(), c.Params("id")); err != nil { return c.Status(500).JSON(fiber.Map{"error": err.Error()}) } logAudit(c, auditRepo, "delete", "discover_source", c.Params("id")) return c.SendStatus(204) }) } auditGroup := api.Group("/audit") { auditGroup.Get("/", func(c *fiber.Ctx) error { page := c.QueryInt("page", 1) perPage := c.QueryInt("perPage", 50) action := c.Query("action") resource := c.Query("resource") logs, total, err := auditRepo.List(c.Context(), page, perPage, action, resource) if err != nil { return c.Status(500).JSON(fiber.Map{"error": err.Error()}) } return c.JSON(fiber.Map{ "logs": logs, "total": total, "page": page, "perPage": perPage, }) }) } port := config.GetEnvInt("ADMIN_SVC_PORT", 3040) log.Printf("admin-svc listening on :%d", port) log.Fatal(app.Listen(fmt.Sprintf(":%d", port))) } func getDashboardStats(ctx context.Context, userRepo *admin.UserRepository, postRepo *admin.PostRepository) (*admin.DashboardStats, error) { totalUsers, _ := userRepo.Count(ctx, "") activeUsers, _ := userRepo.CountActive(ctx) totalPosts, _ := postRepo.Count(ctx, "") publishedPosts, _ := postRepo.Count(ctx, "published") return &admin.DashboardStats{ TotalUsers: totalUsers, ActiveUsers: activeUsers, TotalPosts: totalPosts, PublishedPosts: publishedPosts, StorageUsedMB: 0, StorageLimitMB: 10240, }, nil } func logAudit(c *fiber.Ctx, repo *admin.AuditRepository, action, resource, resourceID string) { user := middleware.GetUser(c) if user == nil { return } log := &admin.AuditLog{ UserID: user.UserID, UserEmail: user.Email, Action: action, Resource: resource, ResourceID: resourceID, IPAddress: c.IP(), UserAgent: c.Get("User-Agent"), } go repo.Create(context.Background(), log) }