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

127 lines
3.4 KiB
Go

package ws
import (
"fmt"
"log"
"net/http"
"strings"
mw "github.com/flatrender/gateway/internal/middleware"
"github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt/v5"
"github.com/google/uuid"
"github.com/gorilla/websocket"
)
var upgrader = websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool { return true },
Subprotocols: []string{"flatrender.v1"},
}
// RenderProgressProxy proxies WebSocket connections to the render service's REST polling endpoint
// and streams progress events to the client via the WebSocket protocol.
//
// Connection: wss://gateway/ws/v1/render/{job_id}?token={jwt}
//
// The gateway validates JWT ownership, then opens a persistent proxy WS to the upstream
// render service. In production the render service would expose its own WS; for now we
// implement a polling bridge using the REST /progress endpoint.
func RenderProgressProxy(renderUpstreamWS string, jwtSecret string) gin.HandlerFunc {
return func(c *gin.Context) {
jobID := c.Param("job_id")
if _, err := uuid.Parse(jobID); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"code": "bad_request", "message": "invalid job_id"})
return
}
// Authenticate — token may come from query param or Authorization header
tokenStr := c.Query("token")
if tokenStr == "" {
hdr := c.GetHeader("Authorization")
if strings.HasPrefix(hdr, "Bearer ") {
tokenStr = hdr[7:]
}
}
if tokenStr == "" {
c.Writer.WriteHeader(http.StatusUnauthorized)
return
}
token, err := jwt.Parse(tokenStr, func(t *jwt.Token) (interface{}, error) {
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, jwt.ErrSignatureInvalid
}
return []byte(jwtSecret), nil
})
if err != nil || !token.Valid {
c.Writer.WriteHeader(http.StatusUnauthorized)
return
}
claims, _ := token.Claims.(jwt.MapClaims)
userID, _ := uuid.Parse(fmt.Sprintf("%v", claims["sub"]))
// Upgrade the client connection
clientConn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
if err != nil {
log.Printf("ws upgrade error: %v", err)
return
}
defer clientConn.Close()
// Connect to upstream render service WS
upstreamURL := fmt.Sprintf("%s/ws/v1/render/%s?user_id=%s", renderUpstreamWS, jobID, userID)
upstreamConn, _, err := websocket.DefaultDialer.Dial(upstreamURL, http.Header{
"Authorization": []string{"Bearer " + tokenStr},
})
if err != nil {
// Upstream WS not available — send hello + close
_ = clientConn.WriteJSON(gin.H{
"type": "error",
"code": "UPSTREAM_UNAVAILABLE",
"message": "render service WebSocket unavailable; use REST polling fallback",
})
clientConn.WriteMessage(websocket.CloseMessage,
websocket.FormatCloseMessage(1011, "upstream unavailable"))
return
}
defer upstreamConn.Close()
// Bidirectional pipe
errCh := make(chan error, 2)
// Client → upstream
go func() {
for {
mt, msg, err := clientConn.ReadMessage()
if err != nil {
errCh <- err
return
}
if err := upstreamConn.WriteMessage(mt, msg); err != nil {
errCh <- err
return
}
}
}()
// Upstream → client
go func() {
for {
mt, msg, err := upstreamConn.ReadMessage()
if err != nil {
errCh <- err
return
}
if err := clientConn.WriteMessage(mt, msg); err != nil {
errCh <- err
return
}
}
}()
<-errCh
}
}
// mw import alias used above
var _ = mw.CtxUserID