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:
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user