Some checks failed
Build and Deploy GooSeek / build-and-deploy (push) Failing after 8m22s
- Create pkg/email package (sender, templates, types) - SMTP client with TLS, rate limiting, async sending - HTML email templates with GooSeek branding - Integrate welcome + password reset emails in auth-svc - Add limit warning emails (80%/100%) in llm-svc middleware - Add space invite endpoint with email notification in thread-svc - Add GetUserEmail helper in JWT middleware - Add SMTP config to .env, config.go, K8s configmap Made-with: Cursor
711 lines
19 KiB
Go
711 lines
19 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"crypto/rand"
|
|
"encoding/hex"
|
|
"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/internal/llm"
|
|
"github.com/gooseek/backend/internal/pages"
|
|
"github.com/gooseek/backend/pkg/config"
|
|
"github.com/gooseek/backend/pkg/email"
|
|
"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 threadRepo *db.ThreadRepository
|
|
var spaceRepo *db.SpaceRepository
|
|
var memoryRepo *db.MemoryRepository
|
|
var pageRepo *db.PageRepository
|
|
|
|
if cfg.DatabaseURL != "" {
|
|
maxRetries := 30
|
|
for i := 0; i < maxRetries; i++ {
|
|
database, err = db.NewPostgresDB(cfg.DatabaseURL)
|
|
if err == nil {
|
|
break
|
|
}
|
|
log.Printf("Waiting for database (attempt %d/%d): %v", i+1, maxRetries, err)
|
|
time.Sleep(2 * time.Second)
|
|
}
|
|
if err != nil {
|
|
log.Fatal("Database required for thread-svc:", err)
|
|
}
|
|
log.Println("PostgreSQL connected successfully")
|
|
defer database.Close()
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
|
|
defer cancel()
|
|
|
|
if err := database.RunMigrations(ctx); err != nil {
|
|
log.Printf("Base migrations warning: %v", err)
|
|
}
|
|
|
|
spaceRepo = db.NewSpaceRepository(database)
|
|
if err := spaceRepo.RunMigrations(ctx); err != nil {
|
|
log.Printf("Space migrations warning: %v", err)
|
|
}
|
|
|
|
threadRepo = db.NewThreadRepository(database)
|
|
if err := threadRepo.RunMigrations(ctx); err != nil {
|
|
log.Printf("Thread migrations warning: %v", err)
|
|
}
|
|
|
|
memoryRepo = db.NewMemoryRepository(database)
|
|
if err := memoryRepo.RunMigrations(ctx); err != nil {
|
|
log.Printf("Memory migrations warning: %v", err)
|
|
}
|
|
|
|
pageRepo = db.NewPageRepository(database)
|
|
if err := pageRepo.RunMigrations(ctx); err != nil {
|
|
log.Printf("Page migrations warning: %v", err)
|
|
}
|
|
|
|
log.Println("PostgreSQL connected, all migrations complete")
|
|
} else {
|
|
log.Fatal("DATABASE_URL required for thread-svc")
|
|
}
|
|
|
|
emailSender := email.NewSender(email.SMTPConfig{
|
|
Host: cfg.SMTPHost,
|
|
Port: cfg.SMTPPort,
|
|
User: cfg.SMTPUser,
|
|
Password: cfg.SMTPPassword,
|
|
From: cfg.SMTPFrom,
|
|
TLS: cfg.SMTPTLS,
|
|
SiteURL: cfg.SiteURL,
|
|
SiteName: cfg.SiteName,
|
|
})
|
|
|
|
var llmClient llm.Client
|
|
if cfg.OpenAIAPIKey != "" {
|
|
llmClient, err = llm.NewClient(llm.ProviderConfig{
|
|
ProviderID: "openai",
|
|
ModelKey: "gpt-4o-mini",
|
|
APIKey: cfg.OpenAIAPIKey,
|
|
})
|
|
if err != nil {
|
|
log.Printf("Failed to create LLM client: %v", err)
|
|
}
|
|
}
|
|
|
|
app := fiber.New(fiber.Config{
|
|
BodyLimit: 10 * 1024 * 1024,
|
|
ReadTimeout: 30 * time.Second,
|
|
WriteTimeout: 30 * 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"})
|
|
})
|
|
|
|
threads := app.Group("/api/v1/threads", middleware.JWT(middleware.JWTConfig{
|
|
Secret: cfg.JWTSecret,
|
|
AuthSvcURL: cfg.AuthSvcURL,
|
|
AllowGuest: true,
|
|
}))
|
|
|
|
threads.Get("/", func(c *fiber.Ctx) error {
|
|
userID := middleware.GetUserID(c)
|
|
if userID == "" {
|
|
return c.JSON(fiber.Map{"threads": []interface{}{}})
|
|
}
|
|
|
|
limit := c.QueryInt("limit", 50)
|
|
offset := c.QueryInt("offset", 0)
|
|
|
|
threadList, err := threadRepo.GetByUserID(c.Context(), userID, limit, offset)
|
|
if err != nil {
|
|
return c.Status(500).JSON(fiber.Map{"error": "Failed to get threads"})
|
|
}
|
|
|
|
return c.JSON(fiber.Map{"threads": threadList})
|
|
})
|
|
|
|
threads.Post("/", func(c *fiber.Ctx) error {
|
|
userID := middleware.GetUserID(c)
|
|
if userID == "" {
|
|
return c.Status(401).JSON(fiber.Map{"error": "Authentication required"})
|
|
}
|
|
|
|
var req struct {
|
|
Title string `json:"title"`
|
|
FocusMode string `json:"focusMode"`
|
|
SpaceID *string `json:"spaceId"`
|
|
}
|
|
|
|
if err := c.BodyParser(&req); err != nil {
|
|
return c.Status(400).JSON(fiber.Map{"error": "Invalid request"})
|
|
}
|
|
|
|
thread := &db.Thread{
|
|
UserID: userID,
|
|
SpaceID: req.SpaceID,
|
|
Title: req.Title,
|
|
FocusMode: req.FocusMode,
|
|
}
|
|
|
|
if thread.Title == "" {
|
|
thread.Title = "New Thread"
|
|
}
|
|
if thread.FocusMode == "" {
|
|
thread.FocusMode = "all"
|
|
}
|
|
|
|
if err := threadRepo.Create(c.Context(), thread); err != nil {
|
|
return c.Status(500).JSON(fiber.Map{"error": "Failed to create thread"})
|
|
}
|
|
|
|
return c.Status(201).JSON(thread)
|
|
})
|
|
|
|
threads.Get("/:id", func(c *fiber.Ctx) error {
|
|
threadID := c.Params("id")
|
|
userID := middleware.GetUserID(c)
|
|
|
|
thread, err := threadRepo.GetByID(c.Context(), threadID)
|
|
if err != nil || thread == nil {
|
|
return c.Status(404).JSON(fiber.Map{"error": "Thread not found"})
|
|
}
|
|
|
|
if thread.UserID != userID && !thread.IsPublic {
|
|
return c.Status(403).JSON(fiber.Map{"error": "Access denied"})
|
|
}
|
|
|
|
messages, err := threadRepo.GetMessages(c.Context(), threadID, userID, 100, 0)
|
|
if err != nil && err != db.ErrForbidden {
|
|
return c.Status(500).JSON(fiber.Map{"error": "Failed to get messages"})
|
|
}
|
|
thread.Messages = messages
|
|
|
|
return c.JSON(thread)
|
|
})
|
|
|
|
threads.Post("/:id/messages", func(c *fiber.Ctx) error {
|
|
threadID := c.Params("id")
|
|
userID := middleware.GetUserID(c)
|
|
|
|
thread, err := threadRepo.GetByID(c.Context(), threadID)
|
|
if err != nil || thread == nil {
|
|
return c.Status(404).JSON(fiber.Map{"error": "Thread not found"})
|
|
}
|
|
|
|
if thread.UserID != userID {
|
|
return c.Status(403).JSON(fiber.Map{"error": "Access denied"})
|
|
}
|
|
|
|
var req struct {
|
|
Role string `json:"role"`
|
|
Content string `json:"content"`
|
|
Sources []db.ThreadSource `json:"sources"`
|
|
Widgets []map[string]interface{} `json:"widgets"`
|
|
RelatedQuestions []string `json:"relatedQuestions"`
|
|
Model string `json:"model"`
|
|
TokensUsed int `json:"tokensUsed"`
|
|
}
|
|
|
|
if err := c.BodyParser(&req); err != nil {
|
|
return c.Status(400).JSON(fiber.Map{"error": "Invalid request"})
|
|
}
|
|
|
|
msg := &db.ThreadMessage{
|
|
ThreadID: threadID,
|
|
Role: req.Role,
|
|
Content: req.Content,
|
|
Sources: req.Sources,
|
|
Widgets: req.Widgets,
|
|
RelatedQuestions: req.RelatedQuestions,
|
|
Model: req.Model,
|
|
TokensUsed: req.TokensUsed,
|
|
}
|
|
|
|
if err := threadRepo.AddMessage(c.Context(), msg, userID); 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 add message"})
|
|
}
|
|
|
|
if thread.Title == "New Thread" && req.Role == "user" {
|
|
threadRepo.GenerateTitle(c.Context(), threadID, req.Content, userID)
|
|
}
|
|
|
|
return c.Status(201).JSON(msg)
|
|
})
|
|
|
|
threads.Post("/:id/share", func(c *fiber.Ctx) error {
|
|
threadID := c.Params("id")
|
|
userID := middleware.GetUserID(c)
|
|
|
|
thread, err := threadRepo.GetByID(c.Context(), threadID)
|
|
if err != nil || thread == nil {
|
|
return c.Status(404).JSON(fiber.Map{"error": "Thread not found"})
|
|
}
|
|
|
|
if thread.UserID != userID {
|
|
return c.Status(403).JSON(fiber.Map{"error": "Access denied"})
|
|
}
|
|
|
|
shareID := generateShareID()
|
|
if err := threadRepo.SetShareID(c.Context(), threadID, shareID, userID); err != nil {
|
|
if err == db.ErrNotFound {
|
|
return c.Status(404).JSON(fiber.Map{"error": "Thread not found"})
|
|
}
|
|
return c.Status(500).JSON(fiber.Map{"error": "Failed to share thread"})
|
|
}
|
|
|
|
return c.JSON(fiber.Map{
|
|
"shareId": shareID,
|
|
"shareUrl": fmt.Sprintf("/share/%s", shareID),
|
|
})
|
|
})
|
|
|
|
threads.Delete("/:id", func(c *fiber.Ctx) error {
|
|
threadID := c.Params("id")
|
|
userID := middleware.GetUserID(c)
|
|
|
|
if err := threadRepo.Delete(c.Context(), threadID, userID); err != nil {
|
|
if err == db.ErrNotFound {
|
|
return c.Status(404).JSON(fiber.Map{"error": "Thread not found"})
|
|
}
|
|
return c.Status(500).JSON(fiber.Map{"error": "Failed to delete thread"})
|
|
}
|
|
|
|
return c.Status(204).Send(nil)
|
|
})
|
|
|
|
share := app.Group("/api/v1/share")
|
|
|
|
share.Get("/:shareId", func(c *fiber.Ctx) error {
|
|
shareID := c.Params("shareId")
|
|
|
|
thread, err := threadRepo.GetByShareID(c.Context(), shareID)
|
|
if err != nil || thread == nil {
|
|
return c.Status(404).JSON(fiber.Map{"error": "Shared thread not found"})
|
|
}
|
|
|
|
messages, _ := threadRepo.GetMessages(c.Context(), thread.ID, thread.UserID, 100, 0)
|
|
thread.Messages = messages
|
|
|
|
return c.JSON(thread)
|
|
})
|
|
|
|
spaces := app.Group("/api/v1/spaces", middleware.JWT(middleware.JWTConfig{
|
|
Secret: cfg.JWTSecret,
|
|
AuthSvcURL: cfg.AuthSvcURL,
|
|
AllowGuest: false,
|
|
}))
|
|
|
|
spaces.Get("/", func(c *fiber.Ctx) error {
|
|
userID := middleware.GetUserID(c)
|
|
|
|
spaceList, err := spaceRepo.GetByUserID(c.Context(), userID)
|
|
if err != nil {
|
|
return c.Status(500).JSON(fiber.Map{"error": "Failed to get spaces"})
|
|
}
|
|
|
|
return c.JSON(fiber.Map{"spaces": spaceList})
|
|
})
|
|
|
|
spaces.Post("/", func(c *fiber.Ctx) error {
|
|
userID := middleware.GetUserID(c)
|
|
|
|
var req db.Space
|
|
if err := c.BodyParser(&req); err != nil {
|
|
return c.Status(400).JSON(fiber.Map{"error": "Invalid request"})
|
|
}
|
|
|
|
req.UserID = userID
|
|
if req.Name == "" {
|
|
return c.Status(400).JSON(fiber.Map{"error": "Name required"})
|
|
}
|
|
|
|
if err := spaceRepo.Create(c.Context(), &req); err != nil {
|
|
return c.Status(500).JSON(fiber.Map{"error": "Failed to create space"})
|
|
}
|
|
|
|
return c.Status(201).JSON(req)
|
|
})
|
|
|
|
spaces.Get("/:id", func(c *fiber.Ctx) error {
|
|
spaceID := c.Params("id")
|
|
userID := middleware.GetUserID(c)
|
|
|
|
space, err := spaceRepo.GetByID(c.Context(), spaceID)
|
|
if err != nil || space == nil {
|
|
return c.Status(404).JSON(fiber.Map{"error": "Space not found"})
|
|
}
|
|
|
|
if space.UserID != userID && !space.IsPublic {
|
|
return c.Status(403).JSON(fiber.Map{"error": "Access denied"})
|
|
}
|
|
|
|
return c.JSON(space)
|
|
})
|
|
|
|
spaces.Put("/:id", func(c *fiber.Ctx) error {
|
|
spaceID := c.Params("id")
|
|
userID := middleware.GetUserID(c)
|
|
|
|
var req db.Space
|
|
if err := c.BodyParser(&req); err != nil {
|
|
return c.Status(400).JSON(fiber.Map{"error": "Invalid request"})
|
|
}
|
|
|
|
req.ID = spaceID
|
|
req.UserID = userID
|
|
|
|
if err := spaceRepo.Update(c.Context(), &req, userID); err != nil {
|
|
if err == db.ErrNotFound {
|
|
return c.Status(404).JSON(fiber.Map{"error": "Space not found"})
|
|
}
|
|
return c.Status(500).JSON(fiber.Map{"error": "Failed to update space"})
|
|
}
|
|
|
|
return c.JSON(req)
|
|
})
|
|
|
|
spaces.Delete("/:id", func(c *fiber.Ctx) error {
|
|
spaceID := c.Params("id")
|
|
userID := middleware.GetUserID(c)
|
|
|
|
if err := spaceRepo.Delete(c.Context(), spaceID, userID); err != nil {
|
|
if err == db.ErrNotFound {
|
|
return c.Status(404).JSON(fiber.Map{"error": "Space not found"})
|
|
}
|
|
return c.Status(500).JSON(fiber.Map{"error": "Failed to delete space"})
|
|
}
|
|
|
|
return c.Status(204).Send(nil)
|
|
})
|
|
|
|
spaces.Post("/:id/invite", func(c *fiber.Ctx) error {
|
|
spaceID := c.Params("id")
|
|
userID := middleware.GetUserID(c)
|
|
|
|
space, err := spaceRepo.GetByID(c.Context(), spaceID)
|
|
if err != nil || space == nil {
|
|
return c.Status(404).JSON(fiber.Map{"error": "Space not found"})
|
|
}
|
|
|
|
if space.UserID != userID {
|
|
return c.Status(403).JSON(fiber.Map{"error": "Only space owner can invite"})
|
|
}
|
|
|
|
var req struct {
|
|
Email string `json:"email"`
|
|
Role string `json:"role"`
|
|
}
|
|
if err := c.BodyParser(&req); err != nil {
|
|
return c.Status(400).JSON(fiber.Map{"error": "Invalid request body"})
|
|
}
|
|
|
|
if req.Email == "" {
|
|
return c.Status(400).JSON(fiber.Map{"error": "Email is required"})
|
|
}
|
|
|
|
if req.Role == "" {
|
|
req.Role = "member"
|
|
}
|
|
|
|
tokenBytes := make([]byte, 32)
|
|
if _, err := rand.Read(tokenBytes); err != nil {
|
|
return c.Status(500).JSON(fiber.Map{"error": "Failed to generate invite token"})
|
|
}
|
|
inviteToken := hex.EncodeToString(tokenBytes)
|
|
|
|
invite := &db.SpaceInvite{
|
|
SpaceID: spaceID,
|
|
Email: req.Email,
|
|
Role: req.Role,
|
|
InvitedBy: userID,
|
|
Token: inviteToken,
|
|
ExpiresAt: time.Now().Add(7 * 24 * time.Hour),
|
|
}
|
|
|
|
if err := spaceRepo.CreateInvite(c.Context(), invite); err != nil {
|
|
return c.Status(500).JSON(fiber.Map{"error": "Failed to create invite"})
|
|
}
|
|
|
|
inviteURL := fmt.Sprintf("%s/spaces/join?token=%s", cfg.SiteURL, inviteToken)
|
|
inviterName := middleware.GetUserEmail(c)
|
|
|
|
emailSender.SendAsync(func() error {
|
|
return emailSender.SendSpaceInvite(req.Email, inviterName, space.Name, inviteURL)
|
|
})
|
|
|
|
return c.Status(201).JSON(fiber.Map{
|
|
"invite": invite,
|
|
"inviteUrl": inviteURL,
|
|
})
|
|
})
|
|
|
|
spaces.Get("/:id/invites", func(c *fiber.Ctx) error {
|
|
spaceID := c.Params("id")
|
|
userID := middleware.GetUserID(c)
|
|
|
|
space, err := spaceRepo.GetByID(c.Context(), spaceID)
|
|
if err != nil || space == nil {
|
|
return c.Status(404).JSON(fiber.Map{"error": "Space not found"})
|
|
}
|
|
|
|
if space.UserID != userID {
|
|
return c.Status(403).JSON(fiber.Map{"error": "Access denied"})
|
|
}
|
|
|
|
invites, err := spaceRepo.GetInvitesBySpace(c.Context(), spaceID)
|
|
if err != nil {
|
|
return c.Status(500).JSON(fiber.Map{"error": "Failed to get invites"})
|
|
}
|
|
|
|
return c.JSON(fiber.Map{"invites": invites})
|
|
})
|
|
|
|
memory := app.Group("/api/v1/memory", middleware.JWT(middleware.JWTConfig{
|
|
Secret: cfg.JWTSecret,
|
|
AuthSvcURL: cfg.AuthSvcURL,
|
|
AllowGuest: false,
|
|
}))
|
|
|
|
memory.Get("/", func(c *fiber.Ctx) error {
|
|
userID := middleware.GetUserID(c)
|
|
memType := c.Query("type", "")
|
|
|
|
memories, err := memoryRepo.GetByUserID(c.Context(), userID, memType, 50)
|
|
if err != nil {
|
|
return c.Status(500).JSON(fiber.Map{"error": "Failed to get memories"})
|
|
}
|
|
|
|
return c.JSON(fiber.Map{"memories": memories})
|
|
})
|
|
|
|
memory.Post("/", func(c *fiber.Ctx) error {
|
|
userID := middleware.GetUserID(c)
|
|
|
|
var req db.UserMemory
|
|
if err := c.BodyParser(&req); err != nil {
|
|
return c.Status(400).JSON(fiber.Map{"error": "Invalid request"})
|
|
}
|
|
|
|
req.UserID = userID
|
|
|
|
if err := memoryRepo.Save(c.Context(), &req); err != nil {
|
|
return c.Status(500).JSON(fiber.Map{"error": "Failed to save memory"})
|
|
}
|
|
|
|
return c.Status(201).JSON(req)
|
|
})
|
|
|
|
memory.Get("/context", func(c *fiber.Ctx) error {
|
|
userID := middleware.GetUserID(c)
|
|
|
|
ctx, err := memoryRepo.GetContextForUser(c.Context(), userID)
|
|
if err != nil {
|
|
return c.Status(500).JSON(fiber.Map{"error": "Failed to get context"})
|
|
}
|
|
|
|
return c.JSON(fiber.Map{"context": ctx})
|
|
})
|
|
|
|
memory.Delete("/:id", func(c *fiber.Ctx) error {
|
|
memID := c.Params("id")
|
|
userID := middleware.GetUserID(c)
|
|
|
|
if err := memoryRepo.Delete(c.Context(), memID, userID); err != nil {
|
|
if err == db.ErrNotFound {
|
|
return c.Status(404).JSON(fiber.Map{"error": "Memory not found"})
|
|
}
|
|
return c.Status(500).JSON(fiber.Map{"error": "Failed to delete memory"})
|
|
}
|
|
|
|
return c.Status(204).Send(nil)
|
|
})
|
|
|
|
pagesAPI := app.Group("/api/v1/pages", middleware.JWT(middleware.JWTConfig{
|
|
Secret: cfg.JWTSecret,
|
|
AuthSvcURL: cfg.AuthSvcURL,
|
|
AllowGuest: true,
|
|
}))
|
|
|
|
pagesAPI.Get("/", func(c *fiber.Ctx) error {
|
|
userID := middleware.GetUserID(c)
|
|
if userID == "" {
|
|
return c.JSON(fiber.Map{"pages": []interface{}{}})
|
|
}
|
|
|
|
limit := c.QueryInt("limit", 50)
|
|
offset := c.QueryInt("offset", 0)
|
|
|
|
pageList, err := pageRepo.GetByUserID(c.Context(), userID, limit, offset)
|
|
if err != nil {
|
|
return c.Status(500).JSON(fiber.Map{"error": "Failed to get pages"})
|
|
}
|
|
|
|
return c.JSON(fiber.Map{"pages": pageList})
|
|
})
|
|
|
|
pagesAPI.Post("/from-thread/:threadId", func(c *fiber.Ctx) error {
|
|
threadID := c.Params("threadId")
|
|
userID := middleware.GetUserID(c)
|
|
|
|
if userID == "" {
|
|
return c.Status(401).JSON(fiber.Map{"error": "Authentication required"})
|
|
}
|
|
|
|
thread, err := threadRepo.GetByID(c.Context(), threadID)
|
|
if err != nil || thread == nil {
|
|
return c.Status(404).JSON(fiber.Map{"error": "Thread not found"})
|
|
}
|
|
|
|
if thread.UserID != userID {
|
|
return c.Status(403).JSON(fiber.Map{"error": "Access denied"})
|
|
}
|
|
|
|
messages, _ := threadRepo.GetMessages(c.Context(), threadID, userID, 100, 0)
|
|
|
|
var query, answer string
|
|
for _, msg := range messages {
|
|
if msg.Role == "user" && query == "" {
|
|
query = msg.Content
|
|
}
|
|
if msg.Role == "assistant" {
|
|
answer += msg.Content + "\n\n"
|
|
}
|
|
}
|
|
|
|
if llmClient == nil {
|
|
return c.Status(503).JSON(fiber.Map{"error": "LLM not configured"})
|
|
}
|
|
|
|
generator := pages.NewPageGenerator(pages.PageGeneratorConfig{
|
|
LLMClient: llmClient,
|
|
Locale: c.Query("locale", "en"),
|
|
})
|
|
|
|
page, err := generator.GenerateFromThread(c.Context(), query, answer, nil)
|
|
if err != nil {
|
|
return c.Status(500).JSON(fiber.Map{"error": "Failed to generate page"})
|
|
}
|
|
|
|
page.UserID = userID
|
|
page.ThreadID = threadID
|
|
|
|
if err := pageRepo.Create(c.Context(), page); err != nil {
|
|
return c.Status(500).JSON(fiber.Map{"error": "Failed to save page"})
|
|
}
|
|
|
|
return c.Status(201).JSON(page)
|
|
})
|
|
|
|
pagesAPI.Get("/:id", func(c *fiber.Ctx) error {
|
|
pageID := c.Params("id")
|
|
userID := middleware.GetUserID(c)
|
|
|
|
page, err := pageRepo.GetByID(c.Context(), pageID)
|
|
if err != nil || page == nil {
|
|
return c.Status(404).JSON(fiber.Map{"error": "Page not found"})
|
|
}
|
|
|
|
if page.UserID != userID && !page.IsPublic {
|
|
return c.Status(403).JSON(fiber.Map{"error": "Access denied"})
|
|
}
|
|
|
|
return c.JSON(page)
|
|
})
|
|
|
|
pagesAPI.Post("/:id/share", func(c *fiber.Ctx) error {
|
|
pageID := c.Params("id")
|
|
userID := middleware.GetUserID(c)
|
|
|
|
page, err := pageRepo.GetByID(c.Context(), pageID)
|
|
if err != nil || page == nil {
|
|
return c.Status(404).JSON(fiber.Map{"error": "Page not found"})
|
|
}
|
|
|
|
if page.UserID != userID {
|
|
return c.Status(403).JSON(fiber.Map{"error": "Access denied"})
|
|
}
|
|
|
|
shareID := generateShareID()
|
|
if err := pageRepo.SetShareID(c.Context(), pageID, shareID, userID); err != nil {
|
|
if err == db.ErrNotFound {
|
|
return c.Status(404).JSON(fiber.Map{"error": "Page not found"})
|
|
}
|
|
return c.Status(500).JSON(fiber.Map{"error": "Failed to share page"})
|
|
}
|
|
|
|
return c.JSON(fiber.Map{
|
|
"shareId": shareID,
|
|
"shareUrl": fmt.Sprintf("/page/%s", shareID),
|
|
})
|
|
})
|
|
|
|
pagesAPI.Get("/share/:shareId", func(c *fiber.Ctx) error {
|
|
shareID := c.Params("shareId")
|
|
|
|
page, err := pageRepo.GetByShareID(c.Context(), shareID)
|
|
if err != nil || page == nil {
|
|
return c.Status(404).JSON(fiber.Map{"error": "Page not found"})
|
|
}
|
|
|
|
pageRepo.IncrementViewCount(c.Context(), page.ID)
|
|
|
|
return c.JSON(page)
|
|
})
|
|
|
|
pagesAPI.Delete("/:id", func(c *fiber.Ctx) error {
|
|
pageID := c.Params("id")
|
|
userID := middleware.GetUserID(c)
|
|
|
|
if err := pageRepo.Delete(c.Context(), pageID, userID); err != nil {
|
|
if err == db.ErrNotFound {
|
|
return c.Status(404).JSON(fiber.Map{"error": "Page not found"})
|
|
}
|
|
return c.Status(500).JSON(fiber.Map{"error": "Failed to delete page"})
|
|
}
|
|
|
|
return c.Status(204).Send(nil)
|
|
})
|
|
|
|
port := getEnvInt("THREAD_SVC_PORT", 3027)
|
|
log.Printf("thread-svc listening on :%d", port)
|
|
log.Fatal(app.Listen(fmt.Sprintf(":%d", port)))
|
|
}
|
|
|
|
func generateShareID() string {
|
|
b := make([]byte, 8)
|
|
rand.Read(b)
|
|
return hex.EncodeToString(b)
|
|
}
|
|
|
|
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
|
|
}
|