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:
home
2026-02-28 01:33:49 +03:00
parent 120fbbaafb
commit a0e3748dde
523 changed files with 10776 additions and 59630 deletions

View File

@@ -18,6 +18,7 @@ import (
"github.com/gooseek/backend/internal/search"
"github.com/gooseek/backend/pkg/cache"
"github.com/gooseek/backend/pkg/config"
"github.com/gooseek/backend/pkg/middleware"
)
type DigestCitation struct {
@@ -237,7 +238,9 @@ func main() {
)
})
app.Get("/api/v1/discover/digest", func(c *fiber.Ctx) error {
discover := app.Group("/api/v1/discover")
discover.Get("/digest", func(c *fiber.Ctx) error {
url := c.Query("url")
if url != "" {
digest := store.GetDigestByURL(url)
@@ -263,7 +266,13 @@ func main() {
return c.JSON(digest)
})
app.Post("/api/v1/discover/digest", func(c *fiber.Ctx) error {
discoverAuth := app.Group("/api/v1/discover", middleware.JWT(middleware.JWTConfig{
Secret: cfg.JWTSecret,
AuthSvcURL: cfg.AuthSvcURL,
AllowGuest: false,
}))
discoverAuth.Post("/digest", func(c *fiber.Ctx) error {
var d Digest
if err := c.BodyParser(&d); err != nil {
return c.Status(400).JSON(fiber.Map{"error": "Invalid request body"})
@@ -277,7 +286,7 @@ func main() {
return c.Status(204).Send(nil)
})
app.Delete("/api/v1/discover/digest", func(c *fiber.Ctx) error {
discoverAuth.Delete("/digest", func(c *fiber.Ctx) error {
topic := c.Query("topic")
region := c.Query("region")
@@ -289,7 +298,7 @@ func main() {
return c.JSON(fiber.Map{"deleted": deleted})
})
app.Get("/api/v1/discover/article-summary", func(c *fiber.Ctx) error {
discover.Get("/article-summary", func(c *fiber.Ctx) error {
url := c.Query("url")
if url == "" {
return c.Status(400).JSON(fiber.Map{"message": "url required"})
@@ -320,7 +329,7 @@ func main() {
return c.JSON(fiber.Map{"events": summary.Events})
})
app.Post("/api/v1/discover/article-summary", func(c *fiber.Ctx) error {
discoverAuth.Post("/article-summary", func(c *fiber.Ctx) error {
var body struct {
URL string `json:"url"`
Events []string `json:"events"`
@@ -354,7 +363,7 @@ func main() {
return c.Status(204).Send(nil)
})
app.Delete("/api/v1/discover/article-summary", func(c *fiber.Ctx) error {
discoverAuth.Delete("/article-summary", func(c *fiber.Ctx) error {
url := c.Query("url")
if url == "" {
return c.Status(400).JSON(fiber.Map{"message": "url required"})
@@ -365,7 +374,7 @@ func main() {
return c.Status(204).Send(nil)
})
app.Get("/api/v1/discover/search", func(c *fiber.Ctx) error {
discover.Get("/search", func(c *fiber.Ctx) error {
q := c.Query("q")
if q == "" {
return c.Status(400).JSON(fiber.Map{"message": "Query q is required"})
@@ -386,14 +395,38 @@ func main() {
return c.JSON(fiber.Map{"results": result.Results})
})
app.Get("/api/v1/discover", func(c *fiber.Ctx) error {
discover.Get("/", func(c *fiber.Ctx) error {
topic := c.Query("topic", "tech")
region := c.Query("region", "world")
page := c.QueryInt("page", 1)
limit := c.QueryInt("limit", 10)
if page < 1 {
page = 1
}
if limit < 1 || limit > 30 {
limit = 10
}
digests := store.GetDigests(topic, region)
if len(digests) > 0 {
blogs := make([]fiber.Map, len(digests))
for i, d := range digests {
start := (page - 1) * limit
end := start + limit
if start >= len(digests) {
return c.JSON(fiber.Map{
"blogs": []fiber.Map{},
"hasMore": false,
"page": page,
"total": len(digests),
})
}
if end > len(digests) {
end = len(digests)
}
pagedDigests := digests[start:end]
blogs := make([]fiber.Map, len(pagedDigests))
for i, d := range pagedDigests {
content := d.ShortDescription
if content == "" && len(d.SummaryRu) > 200 {
content = d.SummaryRu[:200] + "…"
@@ -410,7 +443,12 @@ func main() {
"digestId": fmt.Sprintf("%s:%s:%s", d.Topic, d.Region, d.ClusterTitle),
}
}
return c.JSON(fiber.Map{"blogs": blogs})
return c.JSON(fiber.Map{
"blogs": blogs,
"hasMore": end < len(digests),
"page": page,
"total": len(digests),
})
}
ctx, cancel := context.WithTimeout(context.Background(), cfg.SearchTimeout*2)
@@ -419,15 +457,15 @@ func main() {
queries := getQueriesForTopic(topic, region)
results, err := searchClient.Search(ctx, queries[0], &search.SearchOptions{
Categories: []string{"news"},
PageNo: 1,
PageNo: page,
})
if err != nil {
return c.Status(503).JSON(fiber.Map{"message": "Search failed"})
}
blogs := make([]fiber.Map, 0, 7)
blogs := make([]fiber.Map, 0, limit)
for i, r := range results.Results {
if i >= 7 {
if i >= limit {
break
}
thumbnail := r.Thumbnail
@@ -454,7 +492,12 @@ func main() {
})
}
return c.JSON(fiber.Map{"blogs": blogs})
hasMore := len(results.Results) > limit
return c.JSON(fiber.Map{
"blogs": blogs,
"hasMore": hasMore,
"page": page,
})
})
port := getEnvInt("DISCOVER_SVC_PORT", 3002)
@@ -466,19 +509,54 @@ func getQueriesForTopic(topic, region string) []string {
queries := map[string]map[string][]string{
"tech": {
"world": {"technology news AI innovation"},
"russia": {"технологии новости IT инновации"},
"russia": {"технологии новости IT инновации искусственный интеллект"},
"eu": {"technology news Europe AI"},
},
"finance": {
"world": {"finance news economy markets"},
"russia": {"финансы новости экономика рынки"},
"eu": {"finance news Europe economy"},
"world": {"finance news economy markets stocks"},
"russia": {"финансы новости экономика рынки акции"},
"eu": {"finance news Europe economy markets"},
},
"sports": {
"world": {"sports news football Olympics"},
"russia": {"спорт новости футбол хоккей"},
"world": {"sports news football basketball Olympics"},
"russia": {"спорт новости футбол хоккей КХЛ РПЛ"},
"eu": {"sports news football Champions League"},
},
"politics": {
"world": {"politics news government elections policy"},
"russia": {"политика новости Россия правительство законы"},
"eu": {"politics news Europe EU parliament"},
},
"science": {
"world": {"science news research discovery space"},
"russia": {"наука новости исследования открытия космос"},
"eu": {"science news Europe research discovery"},
},
"health": {
"world": {"health news medicine medical research"},
"russia": {"здоровье новости медицина лечение"},
"eu": {"health news Europe medicine healthcare"},
},
"entertainment": {
"world": {"entertainment news movies music celebrities"},
"russia": {"развлечения новости кино музыка шоу-бизнес"},
"eu": {"entertainment news Europe movies music"},
},
"world": {
"world": {"world news international global events"},
"russia": {"мировые новости международные события"},
"eu": {"world news Europe international"},
},
"business": {
"world": {"business news companies startups industry"},
"russia": {"бизнес новости компании стартапы предпринимательство"},
"eu": {"business news Europe companies industry"},
},
"culture": {
"world": {"culture news art exhibitions theatre"},
"russia": {"культура новости искусство выставки театр"},
"eu": {"culture news Europe art exhibitions"},
},
}
if topicQueries, ok := queries[topic]; ok {