Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions backend/internal/hub/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (

"github.com/OrcaCD/orca-cd/internal/hub/middleware"
"github.com/OrcaCD/orca-cd/internal/hub/routes"
"github.com/OrcaCD/orca-cd/internal/hub/sse"
"github.com/OrcaCD/orca-cd/internal/hub/websocket"
"github.com/OrcaCD/orca-cd/internal/version"
"github.com/gin-gonic/gin"
Expand Down Expand Up @@ -67,6 +68,8 @@ func RegisterRoutes(router *gin.Engine, cfg Config) error {
protected.GET("/agents/:id", routes.GetAgentHandler)
protected.PUT("/agents/:id", routes.UpdateAgentHandler)
protected.DELETE("/agents/:id", routes.DeleteAgentHandler)

protected.GET("/events", routes.SSEHandler)
}

// Admin routes (authentication + admin role required)
Expand All @@ -87,6 +90,8 @@ func RegisterRoutes(router *gin.Engine, cfg Config) error {
admin.DELETE("/oidc-providers/:id", routes.AdminDeleteOIDCProviderHandler)
}

sse.DefaultBroker = sse.NewBroker(&Log)

h := websocket.NewHub(&Log)
w := websocket.NewWorker(h, &Log)
w.Start()
Expand Down
9 changes: 7 additions & 2 deletions backend/internal/hub/middleware/timeout.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,15 @@ import (
"github.com/gin-gonic/gin"
)

const sseRoutePath = "/api/v1/events"

func TimeoutMiddleware(timeout time.Duration) gin.HandlerFunc {
return func(c *gin.Context) {
// WebSocket connections are long-lived; skip the timeout.
if strings.EqualFold(c.Request.Header.Get("Upgrade"), "websocket") {
// WebSocket and SSE connections are long-lived; skip the timeout.
// The SSE exemption is tied to the registered route path, not the
// client-supplied Accept header, to prevent bypass via a crafted header.
if strings.EqualFold(c.Request.Header.Get("Upgrade"), "websocket") ||
c.FullPath() == sseRoutePath {
c.Next()
Comment thread
timokoessler marked this conversation as resolved.
return
}
Expand Down
45 changes: 45 additions & 0 deletions backend/internal/hub/middleware/timeout_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,51 @@ func TestTimeoutMiddleware_SkipsWebSocketRequests(t *testing.T) {
}
}

func TestTimeoutMiddleware_SkipsSSERoute(t *testing.T) {
t.Parallel()

router := gin.New()
router.Use(TimeoutMiddleware(100 * time.Millisecond))
router.GET("/api/v1/events", func(c *gin.Context) {
_, ok := c.Request.Context().Deadline()
if ok {
t.Fatal("expected no deadline for SSE route")
}
c.Status(http.StatusOK)
})

w := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/api/v1/events", nil)
router.ServeHTTP(w, req)

if w.Code != http.StatusOK {
t.Fatalf("expected 200, got %d", w.Code)
}
}

func TestTimeoutMiddleware_AcceptHeaderAloneDoesNotBypassTimeout(t *testing.T) {
t.Parallel()

router := gin.New()
router.Use(TimeoutMiddleware(100 * time.Millisecond))
router.GET("/other", func(c *gin.Context) {
_, ok := c.Request.Context().Deadline()
if !ok {
t.Fatal("expected deadline to be set despite Accept: text/event-stream header")
}
c.Status(http.StatusOK)
})

w := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/other", nil)
req.Header.Set("Accept", "text/event-stream")
router.ServeHTTP(w, req)

if w.Code != http.StatusOK {
t.Fatalf("expected 200, got %d", w.Code)
}
}

func TestTimeoutMiddleware_CancelsContextAfterHandlerReturns(t *testing.T) {
t.Parallel()

Expand Down
6 changes: 6 additions & 0 deletions backend/internal/hub/routes/admin_oidc.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,14 @@ import (
"github.com/OrcaCD/orca-cd/internal/hub/crypto"
"github.com/OrcaCD/orca-cd/internal/hub/db"
"github.com/OrcaCD/orca-cd/internal/hub/models"
"github.com/OrcaCD/orca-cd/internal/hub/sse"
gooidc "github.com/coreos/go-oidc/v3/oidc"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)

const AdminOIDCProvidersPath = "/api/v1/admin/oidc-providers"

type createOIDCProviderRequest struct {
Name string `json:"name" binding:"required,min=1,max=100"`
IssuerURL string `json:"issuerUrl" binding:"required,http_url"`
Expand Down Expand Up @@ -140,6 +143,7 @@ func AdminCreateOIDCProviderHandler(c *gin.Context) {
}

c.JSON(http.StatusCreated, toOIDCProviderResponse(&provider))
sse.PublishUpdate(AdminOIDCProvidersPath)
}

func AdminUpdateOIDCProviderHandler(c *gin.Context) {
Expand Down Expand Up @@ -190,6 +194,7 @@ func AdminUpdateOIDCProviderHandler(c *gin.Context) {
}

c.JSON(http.StatusOK, toOIDCProviderResponse(&existing))
sse.PublishUpdate(AdminOIDCProvidersPath)
}

func AdminDeleteOIDCProviderHandler(c *gin.Context) {
Expand All @@ -206,4 +211,5 @@ func AdminDeleteOIDCProviderHandler(c *gin.Context) {
}

c.JSON(http.StatusOK, gin.H{"message": "provider deleted"})
sse.PublishUpdate(AdminOIDCProvidersPath)
}
6 changes: 6 additions & 0 deletions backend/internal/hub/routes/admin_users.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,13 @@ import (
"github.com/OrcaCD/orca-cd/internal/hub/auth"
"github.com/OrcaCD/orca-cd/internal/hub/db"
"github.com/OrcaCD/orca-cd/internal/hub/models"
"github.com/OrcaCD/orca-cd/internal/hub/sse"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)

const AdminUsersPath = "/api/v1/admin/users"

type adminUserResponse struct {
Id string `json:"id"`
Name string `json:"name"`
Expand Down Expand Up @@ -155,6 +158,7 @@ func AdminCreateUserHandler(c *gin.Context) {
}

c.JSON(http.StatusCreated, toAdminUserWithGeneratedPasswordResponse(&user, nil, generatedPassword))
sse.PublishUpdate(AdminUsersPath)
}

func AdminUpdateUserHandler(c *gin.Context) {
Expand Down Expand Up @@ -233,6 +237,7 @@ func AdminUpdateUserHandler(c *gin.Context) {
}

c.JSON(http.StatusOK, toAdminUserWithGeneratedPasswordResponse(&user, oidcProviderNamesByUserId[user.Id], generatedPassword))
sse.PublishUpdate(AdminUsersPath)
}

func AdminDeleteUserHandler(c *gin.Context) {
Expand Down Expand Up @@ -266,6 +271,7 @@ func AdminDeleteUserHandler(c *gin.Context) {
}

c.JSON(http.StatusOK, gin.H{"message": "user deleted"})
sse.PublishUpdate(AdminUsersPath)
}

func countAdminUsers(c *gin.Context) (int64, error) {
Expand Down
6 changes: 6 additions & 0 deletions backend/internal/hub/routes/agents.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,13 @@ import (
"github.com/OrcaCD/orca-cd/internal/hub/crypto"
"github.com/OrcaCD/orca-cd/internal/hub/db"
"github.com/OrcaCD/orca-cd/internal/hub/models"
"github.com/OrcaCD/orca-cd/internal/hub/sse"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)

const AgentsPath = "/api/v1/agents"

type agentResponse struct {
Id string `json:"id"`
Name string `json:"name"`
Expand Down Expand Up @@ -148,6 +151,7 @@ func CreateAgentHandler(c *gin.Context) {
agentResponse: toAgentResponse(&agent),
AuthToken: authToken,
})
sse.PublishUpdate(AgentsPath)
}

func UpdateAgentHandler(c *gin.Context) {
Expand Down Expand Up @@ -182,6 +186,7 @@ func UpdateAgentHandler(c *gin.Context) {
}

c.JSON(http.StatusOK, toAgentResponse(&agent))
sse.PublishUpdate(AgentsPath)
}

func DeleteAgentHandler(c *gin.Context) {
Expand All @@ -198,4 +203,5 @@ func DeleteAgentHandler(c *gin.Context) {
}

c.JSON(http.StatusOK, gin.H{"message": "agent deleted"})
sse.PublishUpdate(AgentsPath)
}
6 changes: 6 additions & 0 deletions backend/internal/hub/routes/applications.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,13 @@ import (
"github.com/OrcaCD/orca-cd/internal/hub/crypto"
"github.com/OrcaCD/orca-cd/internal/hub/db"
"github.com/OrcaCD/orca-cd/internal/hub/models"
"github.com/OrcaCD/orca-cd/internal/hub/sse"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)

const ApplicationsPath = "/api/v1/applications"

type createApplicationRequest struct {
Name string `json:"name" binding:"required"`
RepositoryId string `json:"repositoryId" binding:"required"`
Expand Down Expand Up @@ -158,6 +161,7 @@ func CreateApplicationHandler(c *gin.Context) {
}

c.JSON(http.StatusCreated, toApplicationResponse(&createdApplication))
sse.PublishUpdate(ApplicationsPath)
}

func UpdateApplicationHandler(c *gin.Context) {
Expand Down Expand Up @@ -225,6 +229,7 @@ func UpdateApplicationHandler(c *gin.Context) {
}

c.JSON(http.StatusOK, toApplicationResponse(&updatedApplication))
sse.PublishUpdate(ApplicationsPath)
}

func DeleteApplicationHandler(c *gin.Context) {
Expand All @@ -241,6 +246,7 @@ func DeleteApplicationHandler(c *gin.Context) {
}

c.JSON(http.StatusOK, gin.H{"message": "application deleted"})
sse.PublishUpdate(ApplicationsPath)
}

func toApplicationListResponse(app *models.Application) applicationListResponse {
Expand Down
6 changes: 6 additions & 0 deletions backend/internal/hub/routes/repositories.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,13 @@ import (
"github.com/OrcaCD/orca-cd/internal/hub/db"
"github.com/OrcaCD/orca-cd/internal/hub/models"
"github.com/OrcaCD/orca-cd/internal/hub/repositories"
"github.com/OrcaCD/orca-cd/internal/hub/sse"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)

const RepositoriesPath = "/api/v1/repositories"

var appUrl string

func SetRepositoriesConfig(url string) {
Expand Down Expand Up @@ -188,6 +191,7 @@ func CreateRepositoryHandler(c *gin.Context) {
}

c.JSON(http.StatusCreated, toRepositoryResponse(&repo, true))
sse.PublishUpdate(RepositoriesPath)
}
Comment thread
timokoessler marked this conversation as resolved.

type testConnectionRequest struct {
Expand Down Expand Up @@ -255,6 +259,7 @@ func DeleteRepositoryHandler(c *gin.Context) {
}

c.JSON(http.StatusOK, gin.H{"message": "repository deleted"})
sse.PublishUpdate(RepositoriesPath)
}

type updateRepositoryRequest struct {
Expand Down Expand Up @@ -363,6 +368,7 @@ func UpdateRepositoryHandler(c *gin.Context) {

newWebhookSecret := req.SyncType == models.SyncTypeWebhook && prevSyncType != models.SyncTypeWebhook
c.JSON(http.StatusOK, toRepositoryResponse(&repo, newWebhookSecret))
sse.PublishUpdate(RepositoriesPath)
}

// resolveProvider validates the provider enum, URL, and authMethod, returning the
Expand Down
Loading
Loading