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
This commit is contained in:
440
backend/cmd/admin-svc/main.go
Normal file
440
backend/cmd/admin-svc/main.go
Normal file
@@ -0,0 +1,440 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user