Files
soroush.asadi 90ac0b81d1 feat: V2 microservices stack — backend services, gateway, JWT auth
Add full V2 architecture: identity, content, studio (.NET 10) and file,
render, notification, gateway (Go) services with vendored deps, plus DB
migrations, event/API contracts, and an init-db script.

Wire the Next.js frontend to the gateway: server-side JWT auth routes
(login/register/refresh/logout/me), gateway fetch helper, and session/
cookie/jwt helpers under src/lib.

Containerize the stack via docker-compose.v2.yml and per-service
Dockerfiles. Base images resolve through a Nexus mirror (Docker Hub) and
MCR directly; npm/NuGet pull from Nexus groups. Self-host fonts via
next/font/local to avoid Google Fonts (geo-blocked).

Add CI workflow and ignore .env.v2, *.stackdump, and .NET bin/obj.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 23:29:31 +03:30

111 lines
3.6 KiB
Go

package main
import (
"context"
"log"
"net/http"
"os"
"github.com/flatrender/file-svc/internal/db"
"github.com/flatrender/file-svc/internal/handlers"
"github.com/flatrender/file-svc/internal/middleware"
"github.com/flatrender/file-svc/internal/storage"
"github.com/gin-gonic/gin"
)
func main() {
// ── Config from env ───────────────────────────────────────────────────────
dsn := mustEnv("DATABASE_URL")
jwtSecret := mustEnv("JWT_SECRET")
minioEndpoint := envOr("MINIO_ENDPOINT", "localhost:9000")
minioAccessKey := envOr("MINIO_ACCESS_KEY", "minioadmin")
minioSecretKey := envOr("MINIO_SECRET_KEY", "minioadmin")
minioUseSSL := os.Getenv("MINIO_USE_SSL") == "true"
uploadBucket := envOr("MINIO_UPLOAD_BUCKET", "user-uploads")
port := envOr("PORT", "8080")
// ── Database ──────────────────────────────────────────────────────────────
store, err := db.New(dsn)
if err != nil {
log.Fatalf("db: %v", err)
}
defer store.Close()
if err := store.Ping(context.Background()); err != nil {
log.Fatalf("db ping: %v", err)
}
log.Println("db: connected")
// ── MinIO ─────────────────────────────────────────────────────────────────
minioClient, err := storage.NewMinioClient(storage.Config{
Endpoint: minioEndpoint,
AccessKeyID: minioAccessKey,
SecretAccessKey: minioSecretKey,
UseSSL: minioUseSSL,
})
if err != nil {
log.Fatalf("minio: %v", err)
}
log.Println("minio: connected")
// ── Handlers ──────────────────────────────────────────────────────────────
fileHandler := handlers.NewFileHandler(store, minioClient, uploadBucket)
// ── Router ────────────────────────────────────────────────────────────────
if os.Getenv("GIN_MODE") == "" {
gin.SetMode(gin.ReleaseMode)
}
r := gin.New()
r.Use(gin.Logger(), gin.Recovery())
r.GET("/health", func(c *gin.Context) {
if err := store.Ping(c.Request.Context()); err != nil {
c.JSON(http.StatusServiceUnavailable, gin.H{"status": "unhealthy", "error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"status": "healthy"})
})
v1 := r.Group("/v1", middleware.Auth(jwtSecret))
{
// Files
v1.GET("/files", fileHandler.ListFiles)
v1.GET("/files/:id", fileHandler.GetFile)
v1.POST("/files/presigned-upload", fileHandler.PresignedUpload)
v1.POST("/files/:id/confirm", fileHandler.ConfirmUpload)
v1.DELETE("/files/:id", fileHandler.DeleteFile)
v1.GET("/files/:id/download", fileHandler.GetDownloadURL)
// Folders
v1.GET("/folders", fileHandler.ListFolders)
v1.POST("/folders", fileHandler.CreateFolder)
v1.DELETE("/folders/:id", fileHandler.DeleteFolder)
// Quota
v1.GET("/quota", fileHandler.GetQuota)
}
log.Printf("file-svc listening on :%s", port)
if err := r.Run(":" + port); err != nil {
log.Fatalf("server: %v", err)
}
}
func mustEnv(key string) string {
v := os.Getenv(key)
if v == "" {
log.Fatalf("required env var %s not set", key)
}
return v
}
func envOr(key, def string) string {
if v := os.Getenv(key); v != "" {
return v
}
return def
}