90ac0b81d1
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>
127 lines
3.4 KiB
Go
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
|