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
23 changes: 21 additions & 2 deletions platform-api/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ go run ./cmd/main.go
curl -k -X POST https://localhost:9243/api/v1/organizations \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer <your-oauth2-token>' \
-d '{"handle":"acme","name":"ACME Corporation"}'
-d '{"id":"<org-uuid>","handle":"acme","name":"ACME Corporation","region":"us-east-1"}'
```

**2. Create a Project**
Expand All @@ -58,7 +58,9 @@ curl -k -X POST https://localhost:9243/api/v1/gateways \
-H 'Authorization: Bearer <your-oauth2-token>' \
-d '{
"name": "prod-gateway-01",
"displayName": "Production Gateway 01"
"displayName": "Production Gateway 01",
"vhost": "localhost",
"functionalityType": "regular"
}'
```

Expand Down Expand Up @@ -91,6 +93,23 @@ Response includes the gateway authentication token:
}
```

**List Gateway Tokens:**
```bash
curl -k -s https://localhost:9243/api/v1/gateways/<gateway-uuid>/tokens \
-H 'Authorization: Bearer <your-oauth2-token>'
```

Response:
```json
[
{
"id": "7ed55286-66a4-43ae-9271-bd1ead475a55",
"status": "active",
"createdAt": "2025-10-21T15:12:57.60936197+05:30"
}
]
```

**5. Connect Gateway to Platform (WebSocket)**

Install wscat if not already installed:
Expand Down
79 changes: 79 additions & 0 deletions platform-api/src/internal/handler/gateway.go
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,41 @@ func (h *GatewayHandler) DeleteGateway(c *gin.Context) {
c.Status(http.StatusNoContent)
}

// ListTokens handles GET /api/v1/gateways/:gatewayId/tokens
func (h *GatewayHandler) ListTokens(c *gin.Context) {
orgId, exists := middleware.GetOrganizationFromContext(c)
if !exists {
c.JSON(http.StatusUnauthorized, utils.NewErrorResponse(401, "Unauthorized",
"Organization claim not found in token"))
return
}

gatewayId := c.Param("gatewayId")
if gatewayId == "" {
c.JSON(http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request",
"Gateway ID is required"))
return
}

tokens, err := h.gatewayService.ListTokens(gatewayId, orgId)
if err != nil {
errMsg := err.Error()

if strings.Contains(errMsg, "gateway not found") {
utils.LogError("Gateway not found during token listing", err)
c.JSON(http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", errMsg))
return
}

utils.LogError("Failed to list tokens", err)
c.JSON(http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error",
"Failed to list tokens"))
return
}

c.JSON(http.StatusOK, tokens)
}

// RotateToken handles POST /api/v1/gateways/:gatewayId/tokens
func (h *GatewayHandler) RotateToken(c *gin.Context) {
orgId, exists := middleware.GetOrganizationFromContext(c)
Expand Down Expand Up @@ -328,6 +363,48 @@ func (h *GatewayHandler) RotateToken(c *gin.Context) {
c.JSON(http.StatusCreated, response)
}

// RevokeToken handles DELETE /api/v1/gateways/:gatewayId/tokens/:tokenId
func (h *GatewayHandler) RevokeToken(c *gin.Context) {
orgId, exists := middleware.GetOrganizationFromContext(c)
if !exists {
c.JSON(http.StatusUnauthorized, utils.NewErrorResponse(401, "Unauthorized",
"Organization claim not found in token"))
return
}

gatewayId := c.Param("gatewayId")
if gatewayId == "" {
c.JSON(http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request",
"Gateway ID is required"))
return
}

tokenId := c.Param("tokenId")
if tokenId == "" {
c.JSON(http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request",
"Token ID is required"))
return
}

err := h.gatewayService.RevokeToken(gatewayId, tokenId, orgId)
if err != nil {
errMsg := err.Error()

if strings.Contains(errMsg, "not found") {
utils.LogError("Resource not found during token revocation", err)
c.JSON(http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", errMsg))
return
}

utils.LogError("Failed to revoke token", err)
c.JSON(http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error",
"Failed to revoke token"))
return
}

c.JSON(http.StatusOK, gin.H{"message": "Token revoked successfully"})
}

// GetGatewayArtifacts handles GET /api/v1/gateways/{gatewayId}/live-proxy-artifacts
func (h *GatewayHandler) GetGatewayArtifacts(c *gin.Context) {
orgId, exists := middleware.GetOrganizationFromContext(c)
Expand Down Expand Up @@ -382,7 +459,9 @@ func (h *GatewayHandler) RegisterRoutes(r *gin.Engine) {
gatewayGroup.GET("/:gatewayId", h.GetGateway)
gatewayGroup.PUT("/:gatewayId", h.UpdateGateway)
gatewayGroup.DELETE("/:gatewayId", h.DeleteGateway)
gatewayGroup.GET("/:gatewayId/tokens", h.ListTokens)
gatewayGroup.POST("/:gatewayId/tokens", h.RotateToken)
gatewayGroup.DELETE("/:gatewayId/tokens/:tokenId", h.RevokeToken)
gatewayGroup.GET("/:gatewayId/live-proxy-artifacts", h.GetGatewayArtifacts)
}

Expand Down
98 changes: 64 additions & 34 deletions platform-api/src/internal/service/gateway.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,44 +97,12 @@ func (s *GatewayService) RegisterGateway(orgID, name, displayName, description,
UpdatedAt: time.Now(),
}

// 6. Generate plain-text token and salt
plainToken, err := generateToken()
if err != nil {
return nil, fmt.Errorf("failed to generate token: %w", err)
}

saltBytes, err := generateSalt()
if err != nil {
return nil, fmt.Errorf("failed to generate salt: %w", err)
}

// 7. Hash token with salt
tokenHash := hashToken(plainToken, saltBytes)
saltHex := hex.EncodeToString(saltBytes)

// 8. Create GatewayToken model
tokenId := uuid.New().String()
gatewayToken := &model.GatewayToken{
ID: tokenId,
GatewayID: gatewayId,
TokenHash: tokenHash,
Salt: saltHex,
Status: "active",
CreatedAt: time.Now(),
RevokedAt: nil,
}

// 9. Insert gateway and token (in sequence - repository handles this)
// 6. Insert gateway
if err := s.gatewayRepo.Create(gateway); err != nil {
return nil, fmt.Errorf("failed to create gateway: %w", err)
}

if err := s.gatewayRepo.CreateToken(gatewayToken); err != nil {
// Note: In production, this should be wrapped in a transaction
return nil, fmt.Errorf("failed to create token: %w", err)
}

// 10. Return GatewayResponse with gateway details
// 7. Return GatewayResponse with gateway details
response := &dto.GatewayResponse{
ID: gateway.ID,
OrganizationID: gateway.OrganizationID,
Expand Down Expand Up @@ -350,6 +318,37 @@ func (s *GatewayService) VerifyToken(plainToken string) (*model.Gateway, error)
return nil, errors.New("invalid token")
}

// ListTokens retrieves all active tokens for a gateway
func (s *GatewayService) ListTokens(gatewayId, orgId string) ([]dto.TokenInfoResponse, error) {
gateway, err := s.gatewayRepo.GetByUUID(gatewayId)
if err != nil {
return nil, fmt.Errorf("failed to query gateway: %w", err)
}
if gateway == nil {
return nil, errors.New("gateway not found")
}
if gateway.OrganizationID != orgId {
return nil, errors.New("gateway not found")
}

activeTokens, err := s.gatewayRepo.GetActiveTokensByGatewayUUID(gatewayId)
if err != nil {
return nil, fmt.Errorf("failed to get tokens: %w", err)
}

tokens := make([]dto.TokenInfoResponse, 0, len(activeTokens))
for _, t := range activeTokens {
tokens = append(tokens, dto.TokenInfoResponse{
ID: t.ID,
Status: t.Status,
CreatedAt: t.CreatedAt,
RevokedAt: t.RevokedAt,
})
}

return tokens, nil
}

// RotateToken generates a new token for a gateway (max 2 active tokens)
func (s *GatewayService) RotateToken(gatewayId, orgId string) (*dto.TokenRotationResponse, error) {
// 1. Validate gateway exists
Expand Down Expand Up @@ -418,6 +417,37 @@ func (s *GatewayService) RotateToken(gatewayId, orgId string) (*dto.TokenRotatio
return response, nil
}

// RevokeToken revokes a specific token for a gateway
func (s *GatewayService) RevokeToken(gatewayId, tokenId, orgId string) error {
gateway, err := s.gatewayRepo.GetByUUID(gatewayId)
if err != nil {
return fmt.Errorf("failed to query gateway: %w", err)
}
if gateway == nil {
return errors.New("gateway not found")
}
if gateway.OrganizationID != orgId {
return errors.New("gateway not found")
}

token, err := s.gatewayRepo.GetTokenByUUID(tokenId)
if err != nil {
return fmt.Errorf("failed to query token: %w", err)
}
if token == nil {
return errors.New("token not found")
}
if token.GatewayID != gatewayId {
return errors.New("token not found")
}

if err := s.gatewayRepo.RevokeToken(tokenId); err != nil {
return fmt.Errorf("failed to revoke token: %w", err)
}

return nil
}

// GetGatewayStatus retrieves gateway status information for polling
func (s *GatewayService) GetGatewayStatus(orgID string, gatewayId *string) (*dto.GatewayStatusListResponse, error) {
// Validate organizationId is provided and valid
Expand Down
4 changes: 0 additions & 4 deletions platform-api/src/internal/service/gateway_properties_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -122,10 +122,6 @@ func TestRegisterGatewayProperties(t *testing.T) {
if !reflect.DeepEqual(mockGatewayRepo.createdGateway.Properties, properties) {
t.Errorf("Create() gateway properties = %v, want %v", mockGatewayRepo.createdGateway.Properties, properties)
}

if mockGatewayRepo.createdToken == nil {
t.Fatalf("CreateToken() was not called")
}
}

func TestUpdateGatewayProperties(t *testing.T) {
Expand Down
25 changes: 25 additions & 0 deletions platform-api/src/resources/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1177,6 +1177,31 @@ paths:
$ref: '#/components/responses/InternalServerError'

/gateways/{gatewayId}/tokens:
get:
summary: List active gateway tokens
description: |
Returns all active tokens for the specified gateway. Token hashes and salts are never exposed.
Access is validated against the organization in the JWT token.
operationId: listGatewayTokens
tags:
- Gateway Tokens
parameters:
- $ref: '#/components/parameters/GatewayID'
responses:
'200':
description: List of active tokens
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/TokenInfoResponse'
'401':
$ref: '#/components/responses/Unauthorized'
'404':
$ref: '#/components/responses/NotFound'
'500':
$ref: '#/components/responses/InternalServerError'
post:
summary: Rotate gateway token
description: |
Expand Down
Loading