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>
This commit is contained in:
@@ -0,0 +1,151 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/flatrender/render-svc/internal/db"
|
||||
"github.com/flatrender/render-svc/internal/middleware"
|
||||
"github.com/flatrender/render-svc/internal/models"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
"github.com/minio/minio-go/v7"
|
||||
)
|
||||
|
||||
type ExportHandler struct {
|
||||
store *db.Store
|
||||
minio *minio.Client
|
||||
bucket string
|
||||
}
|
||||
|
||||
func NewExportHandler(store *db.Store, mc *minio.Client, bucket string) *ExportHandler {
|
||||
return &ExportHandler{store: store, minio: mc, bucket: bucket}
|
||||
}
|
||||
|
||||
// GET /v1/exports
|
||||
func (h *ExportHandler) List(c *gin.Context) {
|
||||
userID := middleware.GetUserID(c)
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
exports, total, err := h.store.ListExports(c.Request.Context(), userID, page, 20)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.APIError{Code: "internal_error", Message: err.Error()})
|
||||
return
|
||||
}
|
||||
if exports == nil {
|
||||
exports = []*models.Export{}
|
||||
}
|
||||
c.JSON(http.StatusOK, models.PagedResponse[*models.Export]{
|
||||
Data: exports,
|
||||
Meta: models.PaginationMeta{
|
||||
Page: page,
|
||||
PageSize: 20,
|
||||
Total: total,
|
||||
HasMore: int64(page*20) < total,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// GET /v1/exports/:export_id
|
||||
func (h *ExportHandler) Get(c *gin.Context) {
|
||||
userID := middleware.GetUserID(c)
|
||||
exportID, err := uuid.Parse(c.Param("export_id"))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.APIError{Code: "bad_request", Message: "invalid export_id"})
|
||||
return
|
||||
}
|
||||
exp, err := h.store.GetExportByID(c.Request.Context(), exportID, userID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, models.APIError{Code: "not_found", Message: err.Error()})
|
||||
return
|
||||
}
|
||||
files, _ := h.store.ListExportFiles(c.Request.Context(), exportID)
|
||||
if files == nil {
|
||||
files = []*models.ExportFile{}
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"id": exp.ID,
|
||||
"saved_project_id": exp.SavedProjectID,
|
||||
"path": exp.Path,
|
||||
"image": exp.Image,
|
||||
"size_bytes": exp.SizeBytes,
|
||||
"duration_sec": exp.DurationSec,
|
||||
"width": exp.Width,
|
||||
"height": exp.Height,
|
||||
"file_extension": exp.FileExtension,
|
||||
"file_type": exp.FileType,
|
||||
"render_quality": exp.RenderQuality,
|
||||
"create_type": exp.CreateType,
|
||||
"produce_date": exp.ProduceDate,
|
||||
"auto_delete_date": exp.AutoDeleteDate,
|
||||
"files": files,
|
||||
})
|
||||
}
|
||||
|
||||
// DELETE /v1/exports/:export_id
|
||||
func (h *ExportHandler) Delete(c *gin.Context) {
|
||||
userID := middleware.GetUserID(c)
|
||||
exportID, err := uuid.Parse(c.Param("export_id"))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.APIError{Code: "bad_request", Message: "invalid export_id"})
|
||||
return
|
||||
}
|
||||
if err := h.store.SoftDeleteExport(c.Request.Context(), exportID, userID); err != nil {
|
||||
c.JSON(http.StatusNotFound, models.APIError{Code: "not_found", Message: err.Error()})
|
||||
return
|
||||
}
|
||||
c.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// POST /v1/exports/:export_id/extend
|
||||
func (h *ExportHandler) Extend(c *gin.Context) {
|
||||
userID := middleware.GetUserID(c)
|
||||
exportID, err := uuid.Parse(c.Param("export_id"))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.APIError{Code: "bad_request", Message: "invalid export_id"})
|
||||
return
|
||||
}
|
||||
var body struct {
|
||||
Days int `json:"days" binding:"required,min=1,max=365"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&body); err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.APIError{Code: "bad_request", Message: err.Error()})
|
||||
return
|
||||
}
|
||||
newDate, err := h.store.ExtendExportDeleteDate(c.Request.Context(), exportID, userID, body.Days)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, models.APIError{Code: "not_found", Message: err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"new_auto_delete_date": newDate})
|
||||
}
|
||||
|
||||
// GET /v1/exports/:export_id/download-url
|
||||
func (h *ExportHandler) DownloadURL(c *gin.Context) {
|
||||
userID := middleware.GetUserID(c)
|
||||
exportID, err := uuid.Parse(c.Param("export_id"))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.APIError{Code: "bad_request", Message: "invalid export_id"})
|
||||
return
|
||||
}
|
||||
exp, err := h.store.GetExportByID(c.Request.Context(), exportID, userID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, models.APIError{Code: "not_found", Message: err.Error()})
|
||||
return
|
||||
}
|
||||
// Generate presigned URL (15 min TTL)
|
||||
expiry := 15 * time.Minute
|
||||
url, err := h.minio.PresignedGetObject(context.Background(), h.bucket, exp.Path, expiry, nil)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.APIError{Code: "internal_error", Message: "could not generate download URL"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"url": url.String(),
|
||||
"expires_at": time.Now().Add(expiry),
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user