diff --git a/platform-api/README.md b/platform-api/README.md index fb19e17ac3..e5922f0d2b 100644 --- a/platform-api/README.md +++ b/platform-api/README.md @@ -40,13 +40,19 @@ go run ./cmd/main.go ### Step-by-Step Workflow +Across the API, resources with a handle expose it as `id` (an immutable, URL-safe +slug), with a separate human-readable `displayName`. Path parameters are +handle-based, not UUIDs — e.g. `{projectId}`, `{gatewayId}`, `{restApiId}` are +all handles. See [`src/resources/openapi.yaml`](src/resources/openapi.yaml) +for the full contract. + **1. Register an Organization** ```bash curl -k -X POST https://localhost:9243/api/v0.9/organizations \ -H 'Content-Type: application/json' \ -H 'Authorization: Bearer ' \ - -d '{"id":"","handle":"acme","name":"ACME Corporation","region":"us-east-1"}' + -d '{"id":"acme","displayName":"ACME Corporation","region":"us-east-1"}' ``` **2. Create a Project** @@ -56,10 +62,21 @@ curl -k -X POST https://localhost:9243/api/v0.9/projects \ -H 'Content-Type: application/json' \ -H 'Authorization: Bearer ' \ -d '{ - "name": "Production APIs" + "displayName": "Production APIs" }' ``` +Response includes the project handle, auto-generated from `displayName` if `id` is omitted: +```json +{ + "id": "production-apis", + "displayName": "Production APIs", + "organizationId": "acme", + "createdAt": "2026-06-21T15:12:44+05:30", + "updatedAt": "2026-06-21T15:12:44+05:30" +} +``` + **3. Create a Gateway** ```bash @@ -68,28 +85,33 @@ curl -k -X POST https://localhost:9243/api/v0.9/gateways \ -H 'Accept: application/json' \ -H 'Authorization: Bearer ' \ -d '{ - "name": "prod-gateway-01", + "id": "prod-gateway-01", "displayName": "Production Gateway 01", - "vhost": "localhost", + "endpoints": ["https://prod-gateway-01.example.com:8443/api/v1"], "functionalityType": "regular" }' ``` -Response includes the gateway UUID: +Response includes the gateway handle (used as `{gatewayId}` in all subsequent calls): ```json { - "id": "4dac93bd-07ba-417e-aef8-353cebe3ba73", - "name": "prod-gateway-01", + "id": "prod-gateway-01", "displayName": "Production Gateway 01", - "createdAt": "2025-10-21T15:12:44.168980842+05:30", - "updatedAt": "2025-10-21T15:12:44.16898088+05:30" + "organizationId": "acme", + "endpoints": ["https://prod-gateway-01.example.com:8443/api/v1"], + "functionalityType": "regular", + "isCritical": false, + "version": "1.0", + "isActive": false, + "createdAt": "2026-06-21T15:12:44+05:30", + "updatedAt": "2026-06-21T15:12:44+05:30" } ``` **4. Generate Gateway Token** ```bash -curl -k -X POST https://localhost:9243/api/v0.9/gateways//tokens \ +curl -k -X POST https://localhost:9243/api/v0.9/gateways/prod-gateway-01/tokens \ -H 'Accept: application/json' \ -H 'Authorization: Bearer ' ``` @@ -99,17 +121,19 @@ Response includes the gateway authentication token: { "id": "7ed55286-66a4-43ae-9271-bd1ead475a55", "token": "QY8Rnm9bJ-incsGU0xtFz2vx16I1IVhEf0Ma_4O5F9s", - "createdAt": "2025-10-21T15:12:57.60936197+05:30", + "createdAt": "2026-06-21T15:12:57+05:30", "message": "New token generated successfully. Old token remains active until revoked." } ``` **List Gateway Tokens:** ```bash -curl -k https://localhost:9243/api/v0.9/gateways//tokens \ +curl -k https://localhost:9243/api/v0.9/gateways/prod-gateway-01/tokens \ -H 'Authorization: Bearer ' ``` +Returns a bare array of token summaries (`[{"id": "...", "status": "active", "createdAt": "...", "revokedAt": null}]`) — token hashes are never exposed. + **5. Connect Gateway to Platform (WebSocket)** Install wscat if not already installed: @@ -126,9 +150,13 @@ wscat -n -c wss://localhost:9243/api/internal/v1/ws/gateways/connect \ Expected output: ``` Connected (press CTRL+C to quit) -< {"type":"connection.ack","gatewayId":"4dac93bd-07ba-417e-aef8-353cebe3ba73","connectionId":"3150a8b6-649d-4d12-8512-7d72e8ec7f13","timestamp":"2025-10-21T14:42:13+05:30"} +< {"type":"connection.ack","gatewayId":"4dac93bd-07ba-417e-aef8-353cebe3ba73","connectionId":"3150a8b6-649d-4d12-8512-7d72e8ec7f13","timestamp":"2026-06-21T14:42:13+05:30"} ``` +Note: `gatewayId` on WebSocket events is the gateway's internal UUID, not the +handle returned by the REST API — the gateway itself doesn't need to know its +handle. + Keep this connection open to receive real-time deployment events. **6. Create an API** @@ -139,11 +167,11 @@ curl -k -X POST 'https://localhost:9243/api/v0.9/rest-apis' \ -H 'Authorization: Bearer ' \ -d '{ "id": "weather-api", - "name": "Weather API", + "displayName": "Weather API", "description": "Weather API with main and sandbox upstreams", - "context": "/weather", - "version": "v1.0", - "projectId": "", + "context": "weather", + "version": "1.0.0", + "projectId": "production-apis", "lifeCycleStatus": "CREATED", "transport": ["http","https"], "upstream": { @@ -153,6 +181,8 @@ curl -k -X POST 'https://localhost:9243/api/v0.9/rest-apis' \ }' ``` +`projectId` is the project's handle (from step 2), not its UUID. + **7. Deploy API to Gateway** ```bash @@ -163,36 +193,40 @@ curl -k -X POST 'https://localhost:9243/api/v0.9/rest-apis/weather-api/deploymen -d '{ "name": "weather-v1-prod", "base": "current", - "gatewayId": "", - "vhost": { - "main": "example.wso2.com", - "sandbox": "sand-example.wso2.com" + "gatewayId": "prod-gateway-01", + "metadata": { + "vhostMain": "example.wso2.com", + "vhostSandbox": "sand-example.wso2.com" } }' ``` +`gatewayId` is the gateway's handle (from step 3), not its UUID. + Expected response: ```json { "deploymentId": "90d10e1c-8560-5c36-9d5a-124ecaa17485", "name": "weather-v1-prod", - "gatewayId": "4dac93bd-07ba-417e-aef8-353cebe3ba73", + "gatewayId": "prod-gateway-01", "status": "DEPLOYED", - "vhost": { - "main": "example.wso2.com", - "sandbox": "sand-example.wso2.com" + "metadata": { + "vhostMain": "example.wso2.com", + "vhostSandbox": "sand-example.wso2.com" }, - "createdAt": "2025-10-21T16:15:18+05:30", - "updatedAt": "2025-10-21T16:15:18+05:30", + "createdAt": "2026-06-21T16:15:18+05:30", + "updatedAt": "2026-06-21T16:15:18+05:30", "baseDeploymentId": null } ``` The connected gateway will receive a deployment event via WebSocket: ``` -< {"type":"api.deployed","payload":{"apiId":"54588845-c860-4a56-8802-c06b03028543","revisionId":"90d10e1c-8560-5c36-9d5a-124ecaa17485","vhost":"mg.wso2.com","environment":"production"},"timestamp":"2025-10-21T16:15:18+05:30","correlationId":"ae7488ec-9559-4a81-bddd-b85e1391d2c0"} +< {"type":"api.deployed","payload":{"apiId":"54588845-c860-4a56-8802-c06b03028543","deploymentId":"90d10e1c-8560-5c36-9d5a-124ecaa17485","performedAt":"2026-06-21T16:15:18+05:30"},"gatewayId":"4dac93bd-07ba-417e-aef8-353cebe3ba73","timestamp":"2026-06-21T16:15:18+05:30","correlationId":"ae7488ec-9559-4a81-bddd-b85e1391d2c0"} ``` +`apiId` and `gatewayId` in the event payload are internal UUIDs, distinct from the handle-based `id` used in the REST API. + ## Configuration All configuration is supplied via environment variables. diff --git a/platform-api/spec/impls/api-lifecycle-management.md b/platform-api/spec/impls/api-lifecycle-management.md index 645018e166..982f6bed0c 100644 --- a/platform-api/spec/impls/api-lifecycle-management.md +++ b/platform-api/spec/impls/api-lifecycle-management.md @@ -11,20 +11,20 @@ ## Behaviour -1. Create requests require name, context, version, and project ID, then enforce uniqueness within the project and seed defaults (provider, transport, lifecycle, operations). +1. Create requests require `displayName`, `context`, `version`, `projectId` (project handle), and `upstream`, then enforce uniqueness within the project and seed defaults (`id` handle auto-generated if omitted, transport, lifecycle status, operations). 2. Repository layer writes the main API record and related configuration tables within a single transaction. -3. GET routes return fully hydrated API structures, including nested security and backend definitions. -4. Update requests replace mutable fields and rebuild related configuration sets; deletes cascade via foreign keys. -5. Gateway deployment tracking uses the `api_deployments` table to maintain relationships between APIs and gateways. -6. The gateways endpoint queries deployed APIs by joining API deployments with gateway records, filtered by organization for security. -7. Generates comprehensive deployment API YAML including: +3. GET routes (`{restApiId}` = API handle) return fully hydrated API structures, including nested security and backend definitions. +4. Update requests replace mutable fields and rebuild related configuration sets; the handle (`id`) is immutable — a PUT body `id` that differs from the path handle returns `400`. Deletes cascade via foreign keys. +5. Gateway deployment tracking uses the `deployments` table to maintain immutable deployment artifacts per API/gateway pair, with lifecycle status (`DEPLOYED`, `UNDEPLOYED`, `DEPLOYING`, `UNDEPLOYING`, `FAILED`, `ARCHIVED`). +6. The `/gateways` sub-resource queries deployed APIs by joining deployments with gateway records, filtered by organization for security. +7. Deployment generates the gateway-facing deployment artifact including: - API metadata and configuration - Security policies (mTLS, OAuth2, API Key) - API-level and operation-level policies ## Verification -- Create: `curl -k -X POST https://localhost:9243/api/v0.9/rest-apis -H 'Content-Type: application/json' -d '{"name":"inventory","context":"/inventory","version":"v1","projectId":""}'`. -- Fetch: `curl -k https://localhost:9243/api/v0.9/rest-apis/`; confirm nested structures. -- List: `curl -k https://localhost:9243/api/v0.9/projects//apis` to verify pagination metadata and entries. -- Deploy API: `curl -k -X POST https://localhost:9243/api/v0.9/rest-apis//deployments -H 'Content-Type: application/json' -d '[{"name": "production-deployment","gatewayId": "987e6543-e21b-45d3-a789-426614174999", "displayOnDevportal": true}]'` to trigger API deployment. -- Get API Gateways: `curl -k https://localhost:9243/api/v0.9/rest-apis//gateways` to retrieve all gateways where the API is deployed; expect JSON array with gateway details (id, name, displayName, vhost, isActive, etc.). +- Create: `curl -k -X POST https://localhost:9243/api/v0.9/rest-apis -H 'Content-Type: application/json' -H 'Authorization: Bearer ' -d '{"displayName":"inventory","context":"inventory","version":"1.0","projectId":"","upstream":{"main":{"url":"http://sample-backend:5000"}}}'`. +- Fetch: `curl -k https://localhost:9243/api/v0.9/rest-apis/ -H 'Authorization: Bearer '`; confirm nested structures. +- List: `curl -k 'https://localhost:9243/api/v0.9/rest-apis?projectId=' -H 'Authorization: Bearer '` to verify the `{list, pagination}` envelope. +- Deploy API: `curl -k -X POST https://localhost:9243/api/v0.9/rest-apis//deployments -H 'Content-Type: application/json' -H 'Authorization: Bearer ' -d '{"name":"production-deployment","base":"current","gatewayId":""}'` to trigger API deployment. +- Get API Gateways: `curl -k https://localhost:9243/api/v0.9/rest-apis//gateways -H 'Authorization: Bearer '` to retrieve all gateways where the API is deployed; expect a `RESTAPIGatewayListResponse` with gateway details (id, displayName, endpoints, isActive, etc.) plus deployment status. diff --git a/platform-api/spec/impls/gateway-management/gateway-management.md b/platform-api/spec/impls/gateway-management/gateway-management.md index e6ca4e9814..3e0b72cbda 100644 --- a/platform-api/spec/impls/gateway-management/gateway-management.md +++ b/platform-api/spec/impls/gateway-management/gateway-management.md @@ -1,13 +1,15 @@ # Gateway Management Implementation -**Last Updated**: October 26, 2025 +**Last Updated**: July 3, 2026 **Authentication**: Thunder STS JWT (organization claim) -**Status**: Gateway Registration, Listing, Retrieval, Token Rotation, and Deletion implemented +**Status**: Gateway Registration, Listing, Retrieval, Update, Token Rotation, Token Revocation, and Deletion implemented ## Overview Gateway Management provides APIs for registering, managing, and deleting API gateways within organizations. All operations are scoped to the organization specified in the JWT token, ensuring complete multi-tenant isolation. +Gateways follow the platform-wide handle convention: `id` is the immutable, URL-safe handle (used as `{gatewayId}` in every path), `displayName` is the human-readable name, and the internal UUID is never exposed on the wire (it only appears on WebSocket events sent to the gateway itself). + ## Implementation Files - **Handler**: `src/internal/handler/gateway.go` - HTTP request handling and routing @@ -25,11 +27,14 @@ Gateway Management provides APIs for registering, managing, and deleting API gat |--------|----------|-------------|--------| | POST | `/api/v0.9/gateways` | Register new gateway | ✅ Implemented | | GET | `/api/v0.9/gateways` | List all gateways | ✅ Implemented | -| GET | `/api/v0.9/gateways/{id}` | Get gateway details | ✅ Implemented | -| PUT | `/api/v0.9/gateways/{id}` | Update gateway | ✅ Implemented | -| DELETE | `/api/v0.9/gateways/{id}` | Delete gateway | ✅ Implemented | -| POST | `/api/v0.9/gateways/{id}/tokens` | Rotate gateway token | ✅ Implemented | -| DELETE | `/api/v0.9/gateways/{id}/tokens/{tokenId}` | Revoke token | ⏳ Planned | +| GET | `/api/v0.9/gateways/{gatewayId}` | Get gateway details | ✅ Implemented | +| PUT | `/api/v0.9/gateways/{gatewayId}` | Update gateway | ✅ Implemented | +| DELETE | `/api/v0.9/gateways/{gatewayId}` | Delete gateway | ✅ Implemented | +| GET | `/api/v0.9/gateways/{gatewayId}/tokens` | List active tokens | ✅ Implemented | +| POST | `/api/v0.9/gateways/{gatewayId}/tokens` | Rotate gateway token | ✅ Implemented | +| DELETE | `/api/v0.9/gateways/{gatewayId}/tokens/{tokenId}` | Revoke token | ✅ Implemented | + +`{gatewayId}` is the gateway's handle (e.g. `prod-gateway-01`), not its UUID. ## Authentication & Authorization @@ -60,19 +65,21 @@ The organization ID is automatically extracted from the JWT token and used for a **Behavior**: 1. Validates JWT token and extracts organization claim -2. Validates request payload (name, displayName, description, vhost) +2. Validates request payload (id/handle, displayName, description, endpoints, functionalityType) 3. Verifies organization exists -4. Prevents duplicate names within organization -5. Generates secure registration token -6. Returns gateway details with initial token (201 Created) +4. Prevents duplicate handles within organization +5. Returns gateway details (registration itself does not generate or return a token — call `POST /gateways/{gatewayId}/tokens` afterward to obtain one) **Request Fields**: -- **name**: Lowercase alphanumeric with hyphens, 3-64 characters, pattern: `^[a-z0-9-]+$` -- **displayName**: 1-128 characters +- **id**: Optional. Lowercase alphanumeric with hyphens, 3-40 characters, pattern: `^[a-z0-9-]+$`. Auto-generated from `displayName` if omitted. Immutable after creation. +- **displayName**: Required, 1-128 characters - **description**: Optional text -- **vhost**: Required, valid hostname format +- **endpoints**: Required, array of full URL strings (network endpoints exposed by the gateway) — replaces the old single `vhost` field +- **functionalityType**: Required enum — `regular`, `ai`, or `event` +- **isCritical**: Optional boolean, defaults to `false` +- **version**: Optional, defaults to `1.0` -**Uniqueness**: Gateway names must be unique within an organization. Different organizations can use the same gateway name. +**Uniqueness**: Gateway handles must be unique within an organization. Different organizations can use the same gateway handle. ### 2. List Gateways @@ -110,7 +117,7 @@ The organization ID is automatically extracted from the JWT token and used for a **Behavior**: 1. Validates JWT token and extracts organization claim -2. Validates UUID format for gateway ID +2. Resolves the gateway by handle (`{gatewayId}`) 3. Verifies gateway exists and belongs to user's organization 4. Executes transaction-wrapped DELETE with organization isolation 5. Automatic CASCADE deletion of: @@ -136,13 +143,14 @@ The organization ID is automatically extracted from the JWT token and used for a **Schema**: `src/internal/database/schema.sql` **Tables**: -- `gateways` - Gateway entities with organization scoping +- `gateways` - Gateway entities with organization scoping (handle + display name; no `vhost` column) +- `gateway_endpoints` - Network endpoints exposed by a gateway (one row per URL, replaces the old single `vhost` column) - `gateway_tokens` - Authentication tokens with CASCADE delete **Key Constraints**: -- Composite unique constraint on `(organization_uuid, name)` prevents duplicate gateway names within organization +- Composite unique constraint on `(organization_uuid, handle)` prevents duplicate gateway handles within organization - CASCADE delete: Deleting organization removes all gateways and related data -- CASCADE delete: Deleting gateway removes all tokens, deployments, and deployment status +- CASCADE delete: Deleting gateway removes all tokens, endpoints, deployments, and deployment status - Token status validation: 'active' or 'revoked' ## Token Security @@ -167,16 +175,20 @@ The organization ID is automatically extracted from the JWT token and used for a ### Handler Implementation ```go -func (h *GatewayHandler) CreateGateway(c *gin.Context) { +func (h *GatewayHandler) CreateGateway(w http.ResponseWriter, r *http.Request) { // Extract organization from JWT token (not request body) - organizationID, exists := middleware.GetOrganizationFromContext(c) + orgId, exists := middleware.GetOrganizationFromRequest(r) if !exists { - c.JSON(http.StatusUnauthorized, ...) + httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponse(401, "Unauthorized", + "Organization claim not found in token")) + return + } + + var req api.CreateGatewayRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", err.Error())) return } - - // Use organizationID from token - response, err := h.gatewayService.RegisterGateway(organizationID, req.Name, req.DisplayName) // ... } ``` @@ -187,34 +199,33 @@ func (h *GatewayHandler) CreateGateway(c *gin.Context) { 1. **Authentication**: Middleware validates JWT token and extracts organization claim 2. **Request Validation**: Validates presence of required fields: - - `name`: lowercase alphanumeric with hyphens, 3-64 chars + - `id`: optional handle, lowercase alphanumeric with hyphens, 3-40 chars (auto-generated from `displayName` if omitted) - `displayName`: 1-128 chars - - `vhost`: virtual host for the gateway + - `endpoints`: required array of full endpoint URLs - `isCritical`: boolean indicating gateway criticality - `functionalityType`: enum value (required) - one of "regular", "ai", "event" - `description`: optional gateway description 3. **Gateway Type Validation**: Uses global constants from `constants.go` to validate enum values 4. **Organization Scoping**: Uses organization ID from JWT token (not request body) -5. Service confirms organization existence and prevents duplicate names within the same organization using composite unique constraint `(organization_id, name)` +5. Service confirms organization existence and prevents duplicate handles within the same organization using composite unique constraint `(organization_uuid, handle)` 6. **Default Values**: New gateways default to `isActive: false` until WebSocket connection is established -7. System generates cryptographically secure 32-byte token using `crypto/rand`, hashes it with SHA-256 and unique 32-byte salt, stores hash and salt (never plain-text) -8. Response returns gateway details with initial registration token -9. Different organizations can register gateways with identical names +7. Registration does not generate a token — call `POST /gateways/{gatewayId}/tokens` separately, which generates a cryptographically secure 32-byte token via `crypto/rand`, hashes it with SHA-256 and a unique 32-byte salt, and stores only the hash and salt (never plain-text) +8. Different organizations can register gateways with identical handles ### Token Lifecycle 1. **Creation**: Generated during gateway registration or rotation 2. **Active**: Token can authenticate gateway requests 3. **Rotation**: New token created, old tokens remain active (max 2 active) -4. **Revocation**: Token marked as revoked, can no longer authenticate (planned) +4. **Revocation**: Token marked as revoked, can no longer authenticate; implemented and idempotent ## Error Responses | Code | Message | Common Scenarios | |------|---------|------------------| -| 400 | Bad Request | Validation failures, invalid UUID format, max tokens reached | +| 400 | Bad Request | Validation failures, max tokens reached | | 401 | Unauthorized | Missing/invalid JWT token, missing organization claim | | 404 | Not Found | Gateway not found, organization not found, wrong organization | -| 409 | Conflict | Duplicate gateway name, active deployments/connections | +| 409 | Conflict | Duplicate gateway handle, active deployments/connections | | 500 | Internal Server Error | Database errors, token generation failures | ## Key Design Decisions @@ -222,15 +233,7 @@ func (h *GatewayHandler) CreateGateway(c *gin.Context) { 1. Token rotation generates new token while keeping existing tokens active, enforces maximum 2 active tokens per gateway 2. Each token has UUID for tracking, creation timestamp, status (active/revoked), and optional revocation timestamp 3. Token verification compares submitted token against stored hashes using constant-time comparison (`crypto/subtle`) to prevent timing attacks -4. Future implementation: Token revocation updates status to 'revoked' and sets revocation timestamp (idempotent operation). - -### Gateway Status Monitoring - -1. **Lightweight Status API**: New endpoint `/api/v0.9/status/gateways` provides minimal gateway information for frequent polling by management portals -2. **Optional Filtering**: Query parameter `gatewayId` allows filtering to a specific gateway -3. **Response Structure**: Returns only essential fields (id, name, isActive, isCritical) for efficient polling -4. **Organization Scoping**: Automatically filtered by organization from JWT token -5. **Constitution Compliance**: Returns data in standard `{count, list, pagination}` envelope format +4. Token revocation (`DELETE /gateways/{gatewayId}/tokens/{tokenId}`) updates status to 'revoked' and sets revocation timestamp; implemented and idempotent. ### WebSocket Connection Status Management @@ -246,20 +249,32 @@ func (h *GatewayHandler) CreateGateway(c *gin.Context) { **Gateways Table:** ```sql CREATE TABLE gateways ( - uuid TEXT PRIMARY KEY, - organization_uuid TEXT NOT NULL, - name TEXT NOT NULL, - display_name TEXT NOT NULL, - description TEXT, - vhost TEXT NOT NULL, - is_critical BOOLEAN DEFAULT FALSE, - gateway_functionality_type TEXT DEFAULT 'regular' NOT NULL, - is_active BOOLEAN DEFAULT FALSE, + uuid VARCHAR(40) PRIMARY KEY, + organization_uuid VARCHAR(40) NOT NULL, + handle VARCHAR(40) NOT NULL, + display_name VARCHAR(255) NOT NULL, + description VARCHAR(1023), + version VARCHAR(30) NOT NULL DEFAULT '1.0', + gateway_functionality_type VARCHAR(20) NOT NULL DEFAULT 'regular', + properties BLOB NOT NULL, + manifest BLOB, + is_active INTEGER DEFAULT 0, + is_critical INTEGER DEFAULT 0, + data_version VARCHAR(20) NOT NULL DEFAULT '1.0', + created_by VARCHAR(200), created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_by VARCHAR(200), updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (organization_uuid) REFERENCES organizations(uuid) ON DELETE CASCADE, - UNIQUE(organization_uuid, name), - CHECK (gateway_functionality_type IN ('regular', 'ai', 'event')) + UNIQUE(organization_uuid, handle) +); + +-- Network endpoints exposed by the gateway (replaces the old single `vhost` column) +CREATE TABLE gateway_endpoints ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + gateway_uuid VARCHAR(40) NOT NULL, + url VARCHAR(255) NOT NULL, + FOREIGN KEY (gateway_uuid) REFERENCES gateways(uuid) ON DELETE CASCADE ); ``` @@ -310,8 +325,8 @@ CREATE TABLE gateway_tokens ( ## Testing Scenarios ### Duplicate Prevention -1. Register gateway with name "prod-gateway-01" -2. Attempt to register another gateway with same name in same organization +1. Register gateway with handle "prod-gateway-01" +2. Attempt to register another gateway with the same handle in the same organization 3. Expected: 409 Conflict error ### Max Tokens Enforcement @@ -340,10 +355,10 @@ curl -k -X POST https://localhost:9243/api/v0.9/gateways \ -H 'Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...' \ -H 'Content-Type: application/json' \ -d '{ - "name": "prod-gateway-01", + "id": "prod-gateway-01", "displayName": "Production Gateway 01", "description": "Primary production gateway for API traffic", - "vhost": "api.example.com", + "endpoints": ["https://api.example.com:8443/api/v1"], "isCritical": true, "functionalityType": "regular" }' @@ -352,17 +367,17 @@ curl -k -X POST https://localhost:9243/api/v0.9/gateways \ **Expected Response (201 Created):** ```json { - "id": "987e6543-e21b-45d3-a789-426614174999", - "organizationId": "123e4567-e89b-12d3-a456-426614174000", - "name": "prod-gateway-01", + "id": "prod-gateway-01", + "organizationId": "acme", "displayName": "Production Gateway 01", "description": "Primary production gateway for API traffic", - "vhost": "api.example.com", + "endpoints": ["https://api.example.com:8443/api/v1"], "isCritical": true, "functionalityType": "regular", + "version": "1.0", "isActive": false, - "createdAt": "2025-10-26T10:30:00Z", - "updatedAt": "2025-10-26T10:30:00Z" + "createdAt": "2026-06-21T10:30:00Z", + "updatedAt": "2026-06-21T10:30:00Z" } ``` @@ -380,30 +395,30 @@ curl -k https://localhost:9243/api/v0.9/gateways \ "count": 2, "list": [ { - "id": "987e6543-e21b-45d3-a789-426614174999", - "organizationId": "123e4567-e89b-12d3-a456-426614174000", - "name": "prod-gateway-01", + "id": "prod-gateway-01", + "organizationId": "acme", "displayName": "Production Gateway 01", "description": "Primary production gateway for API traffic", - "vhost": "api.example.com", + "endpoints": ["https://api.example.com:8443/api/v1"], "isCritical": true, "functionalityType": "regular", + "version": "1.0", "isActive": true, - "createdAt": "2025-10-26T10:30:00Z", - "updatedAt": "2025-10-26T10:30:00Z" + "createdAt": "2026-06-21T10:30:00Z", + "updatedAt": "2026-06-21T10:30:00Z" }, { - "id": "abc12345-f678-90de-f123-456789abcdef", - "organizationId": "123e4567-e89b-12d3-a456-426614174000", - "name": "ai-gateway-01", + "id": "ai-gateway-01", + "organizationId": "acme", "displayName": "AI Gateway 01", "description": "AI workloads gateway", - "vhost": "ai-api.example.com", + "endpoints": ["https://ai-api.example.com:8443/api/v1"], "isCritical": false, "functionalityType": "ai", + "version": "1.0", "isActive": false, - "createdAt": "2025-10-26T11:00:00Z", - "updatedAt": "2025-10-26T11:00:00Z" + "createdAt": "2026-06-21T11:00:00Z", + "updatedAt": "2026-06-21T11:00:00Z" } ], "pagination": { @@ -417,104 +432,52 @@ curl -k https://localhost:9243/api/v0.9/gateways \ ### Get Gateway by ID ```bash -curl -k https://localhost:9243/api/v0.9/gateways/987e6543-e21b-45d3-a789-426614174999 \ +curl -k https://localhost:9243/api/v0.9/gateways/prod-gateway-01 \ -H 'Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...' ``` **Expected Response (200 OK):** ```json { - "id": "987e6543-e21b-45d3-a789-426614174999", - "organizationId": "123e4567-e89b-12d3-a456-426614174000", - "name": "prod-gateway-01", + "id": "prod-gateway-01", + "organizationId": "acme", "displayName": "Production Gateway 01", "description": "Primary production gateway for API traffic", - "vhost": "api.example.com", + "endpoints": ["https://api.example.com:8443/api/v1"], "isCritical": true, "functionalityType": "regular", + "version": "1.0", "isActive": true, - "createdAt": "2025-10-26T10:30:00Z", - "updatedAt": "2025-10-26T10:30:00Z" + "createdAt": "2026-06-21T10:30:00Z", + "updatedAt": "2026-06-21T10:30:00Z" } ``` -### Get Gateway Status (for polling) - -**Get all gateway statuses:** -```bash -curl -k https://localhost:9243/api/v0.9/status/gateways \ - -H 'Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...' -``` - -**Expected Response (200 OK):** -```json -{ - "count": 2, - "list": [ - { - "id": "987e6543-e21b-45d3-a789-426614174999", - "name": "prod-gateway-01", - "isActive": true, - "isCritical": true - }, - { - "id": "abc12345-f678-90de-f123-456789abcdef", - "name": "ai-gateway-01", - "isActive": false, - "isCritical": false - } - ], - "pagination": { - "total": 2, - "offset": 0, - "limit": 2 - } -} -``` +### Rotate Gateway Token -**Get specific gateway status:** ```bash -curl -k https://localhost:9243/api/v0.9/status/gateways?gatewayId=987e6543-e21b-45d3-a789-426614174999 \ +curl -k -X POST https://localhost:9243/api/v0.9/gateways/prod-gateway-01/tokens \ -H 'Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...' ``` -**Expected Response (200 OK):** +**Expected Response (201 Created):** ```json { - "count": 1, - "list": [ - { - "id": "987e6543-e21b-45d3-a789-426614174999", - "name": "prod-gateway-01", - "isActive": true, - "isCritical": true, - "functionalityType": "regular" - } - ], - "pagination": { - "total": 1, - "offset": 0, - "limit": 1 - } + "id": "def45678-1234-4567-89ab-789012cdefgh", + "token": "kR3mF9pL2vX8qN5wY7jK4sT1hU6gB0cD9aE8fI2mN5oP7qR3sT6uV9xY2zA5bC8e", + "createdAt": "2026-06-21T14:20:00Z", + "message": "New token generated successfully. Old token remains active until revoked." } ``` -### Rotate Gateway Token +### Revoke Gateway Token ```bash -curl -k -X POST https://localhost:9243/api/v0.9/gateways/987e6543-e21b-45d3-a789-426614174999/tokens \ +curl -k -X DELETE https://localhost:9243/api/v0.9/gateways/prod-gateway-01/tokens/def45678-1234-4567-89ab-789012cdefgh \ -H 'Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...' ``` -**Expected Response (201 Created):** -```json -{ - "tokenId": "def45678-g901-23hi-j456-789012klmnop", - "token": "kR3mF9pL2vX8qN5wY7jK4sT1hU6gB0cD9aE8fI2mN5oP7qR3sT6uV9xY2zA5bC8e", - "createdAt": "2025-10-15T14:20:00Z", - "message": "New token generated successfully. Old token remains active until revoked." -} -``` +**Expected Response:** `204 No Content`. Revoking an already-revoked token is idempotent. ### Authentication Error Responses @@ -544,17 +507,21 @@ curl -k -X POST https://localhost:9243/api/v0.9/gateways \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ - "name": "prod-gateway-01", - "displayName": "Production Gateway 01" + "id": "prod-gateway-01", + "displayName": "Production Gateway 01", + "endpoints": ["https://api.example.com:8443/api/v1"], + "functionalityType": "regular" }' -# Attempt duplicate (should return 409 Conflict) +# Attempt duplicate handle (should return 409 Conflict) curl -k -X POST https://localhost:9243/api/v0.9/gateways \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ - "name": "prod-gateway-01", - "displayName": "Duplicate Gateway" + "id": "prod-gateway-01", + "displayName": "Duplicate Gateway", + "endpoints": ["https://api.example.com:8443/api/v1"], + "functionalityType": "regular" }' ``` @@ -563,7 +530,7 @@ curl -k -X POST https://localhost:9243/api/v0.9/gateways \ { "code": 409, "message": "Conflict", - "description": "gateway with name 'prod-gateway-01' already exists in this organization" + "description": "gateway with handle 'prod-gateway-01' already exists in this organization" } ``` @@ -571,16 +538,16 @@ curl -k -X POST https://localhost:9243/api/v0.9/gateways \ ```bash # Rotate once (2 active tokens: initial + rotation 1) -curl -k -X POST https://localhost:9243/api/v0.9/gateways/987e6543-e21b-45d3-a789-426614174999/tokens \ - -H 'Authorization: Bearer ' \ +curl -k -X POST https://localhost:9243/api/v0.9/gateways/prod-gateway-01/tokens \ + -H 'Authorization: Bearer ' # Rotate again (3 active tokens: initial + rotation 1 + rotation 2) -curl -k -X POST https://localhost:9243/api/v0.9/gateways/987e6543-e21b-45d3-a789-426614174999/tokens \ - -H 'Authorization: Bearer ' \ +curl -k -X POST https://localhost:9243/api/v0.9/gateways/prod-gateway-01/tokens \ + -H 'Authorization: Bearer ' # Attempt third rotation (should return 400 Bad Request) -curl -k -X POST https://localhost:9243/api/v0.9/gateways/987e6543-e21b-45d3-a789-426614174999/tokens \ - -H 'Authorization: Bearer ' \ +curl -k -X POST https://localhost:9243/api/v0.9/gateways/prod-gateway-01/tokens \ + -H 'Authorization: Bearer ' ``` **Expected Response (400 Bad Request):** @@ -617,14 +584,15 @@ Detailed design and planning documents are available in the `artifacts/` directo ## Future Enhancements -### Token Revocation (Planned) - -**Endpoint**: `DELETE /api/v0.9/gateways/{gatewayId}/tokens/{tokenId}` - -Immediate token revocation with idempotent behavior. - -### Gateway Deletion Safety Checks (User Story 2) - -- Pre-deletion validation for active API deployments -- Pre-deletion validation for active WebSocket connections -- 409 Conflict response with details when validation fails +### Gateway Deletion Safety Checks (Not Yet Implemented) + +`src/resources/openapi.yaml` documents `DELETE /gateways/{gatewayId}` as returning `409 Conflict` +when the gateway has active API deployments or active WebSocket connections +(`constants.ErrGatewayHasDeployments` and the `activeConnections`/`activeDeployments` examples +in the spec). As of this writing, `GatewayService.DeleteGateway` and `GatewayRepo.Delete` +(`src/internal/service/gateway.go`, `src/internal/repository/gateway.go`) do not perform either +check — deletion unconditionally removes artifact-gateway mappings and cascades tokens/deployments +via foreign keys. The handler (`src/internal/handler/gateway.go`) also has a dead branch for +`constants.ErrGatewayHasAssociatedAPIs`, which is defined in `constants/error.go` but never +returned by the service. Treat the openapi.yaml description of pre-deletion validation as +aspirational until this guard is implemented. diff --git a/platform-api/spec/impls/gateway-websocket-events/gateway-websocket-events.md b/platform-api/spec/impls/gateway-websocket-events/gateway-websocket-events.md index b53892a802..75d0f3cb42 100644 --- a/platform-api/spec/impls/gateway-websocket-events/gateway-websocket-events.md +++ b/platform-api/spec/impls/gateway-websocket-events/gateway-websocket-events.md @@ -28,13 +28,13 @@ Gateways need real-time notification of API deployments and configuration change ### Connection Establishment -1. Gateway initiates WebSocket upgrade request to `wss://platform-api:9243/api/internal/v1/ws/gateways/connect` -2. Platform validates API key via existing gateway service -3. Platform enforces rate limiting (10 attempts/minute/IP) -4. Platform checks maximum connection limit (default 1000) +1. Gateway initiates WebSocket upgrade request to `wss://platform-api:9243/api/internal/v1/ws/gateways/connect` with an `api-key` header carrying the gateway token +2. Platform validates the token via `GatewayService.VerifyToken` +3. Platform enforces rate limiting (default 1000 attempts/minute/IP) +4. Platform checks the per-organization connection limit (default 3) before upgrading 5. HTTP connection upgrades to WebSocket protocol -6. Platform sends connection acknowledgment with gateway ID and connection ID -7. Connection registered in sync.Map registry keyed by gateway ID +6. Platform sends a `connection.ack` message with the gateway's internal UUID (`gatewayId`) and a new connection ID +7. Connection registered in sync.Map registry keyed by gateway UUID; gateway `isActive` is set to `true` ### Heartbeat Mechanism @@ -151,16 +151,18 @@ for { { "type": "api.deployed", "payload": { - "apiUuid": "uuid", - "revisionId": "revision-id", - "vhost": "mg.wso2.com", - "environment": "production" + "apiId": "api-uuid", + "deploymentId": "deployment-uuid", + "performedAt": "2026-06-21T..." }, - "timestamp": "2025-10-19T...", + "gatewayId": "gateway-uuid", + "timestamp": "2026-06-21T...", "correlationId": "uuid" } ``` +`apiId` and `gatewayId` are the platform's internal UUIDs — not the handle-based `id` used in the REST API. + **Event Ordering**: Events sent sequentially per connection using mutex-protected Send method. ### Delivery Statistics @@ -184,20 +186,26 @@ Per-connection atomic counters: ## Configuration -**File**: `config/config.go` +**File**: `config/config.go` (loaded via koanf, not the old envconfig setup) ```go type WebSocket struct { - MaxConnections int `envconfig:"WS_MAX_CONNECTIONS" default:"1000"` - ConnectionTimeout int `envconfig:"WS_CONNECTION_TIMEOUT" default:"30"` - RateLimitPerMin int `envconfig:"WS_RATE_LIMIT_PER_MINUTE" default:"10"` + MaxConnections int `koanf:"max_connections"` + ConnectionTimeout int `koanf:"connection_timeout"` + RateLimitPerMin int `koanf:"rate_limit_per_min"` + MaxConnectionsPerOrg int `koanf:"max_connections_per_org"` + MetricsLogEnabled bool `koanf:"metrics_log_enabled"` + MetricsLogInterval int `koanf:"metrics_log_interval"` } ``` -**Environment Variables**: -- `WS_MAX_CONNECTIONS` - Maximum concurrent connections (default: 1000) -- `WS_CONNECTION_TIMEOUT` - Heartbeat timeout in seconds (default: 30) -- `WS_RATE_LIMIT_PER_MINUTE` - Connection attempts per IP per minute (default: 10) +**Environment Variables** (see `config.go:envToKoanfKey` for the full mapping, including legacy `WEBSOCKET_WS_*` aliases): +- `WEBSOCKET_MAX_CONNECTIONS` - Maximum concurrent connections (default: 1000) +- `WEBSOCKET_CONNECTION_TIMEOUT` - Heartbeat timeout in seconds (default: 30) +- `WEBSOCKET_RATE_LIMIT_PER_MIN` - Connection attempts per IP per minute (default: 1000) +- `WEBSOCKET_MAX_CONNECTIONS_PER_ORG` - Concurrent connections allowed per organization (default: 3) +- `WEBSOCKET_METRICS_LOG_ENABLED` - Periodically log connection metrics (default: true) +- `WEBSOCKET_METRICS_LOG_INTERVAL` - Metrics log interval in seconds (default: 10) ## Build & Run @@ -211,9 +219,9 @@ cd ../bin ./platform-api # Run with custom WebSocket configuration -export WS_MAX_CONNECTIONS=5000 -export WS_CONNECTION_TIMEOUT=60 -export WS_RATE_LIMIT_PER_MINUTE=20 +export WEBSOCKET_MAX_CONNECTIONS=5000 +export WEBSOCKET_CONNECTION_TIMEOUT=60 +export WEBSOCKET_RATE_LIMIT_PER_MIN=20 ./platform-api ``` @@ -221,32 +229,37 @@ export WS_RATE_LIMIT_PER_MINUTE=20 ### 1. Gateway Connection -**Register Gateway**: +**Register Gateway** (organization ID comes from the JWT token, not the request body): ```bash curl -k -X POST https://localhost:9243/api/v0.9/gateways \ -H 'Content-Type: application/json' \ + -H 'Authorization: Bearer ' \ -d '{ - "organizationId": "", - "name": "test-gateway", - "displayName": "Test Gateway" + "id": "test-gateway", + "displayName": "Test Gateway", + "endpoints": ["https://test-gateway.example.com:8443/api/v1"], + "functionalityType": "regular" }' ``` **Expected Response**: ```json { - "id": "d1aa71bc-8cb5-4294-8a26-fe1273c28632", - "name": "test-gateway", + "id": "test-gateway", "displayName": "Test Gateway", - "organizationId": "", - "createdAt": "2025-10-19T..." + "organizationId": "acme", + "endpoints": ["https://test-gateway.example.com:8443/api/v1"], + "functionalityType": "regular", + "isActive": false, + "createdAt": "2026-06-21T..." } ``` -**Generate Gateway Token**: +**Generate Gateway Token** (`{gatewayId}` is the handle from the response above, not a UUID): ```bash -curl -k -X POST https://localhost:9243/api/v0.9/gateways/d1aa71bc-8cb5-4294-8a26-fe1273c28632/tokens \ - -H 'Accept: application/json' +curl -k -X POST https://localhost:9243/api/v0.9/gateways/test-gateway/tokens \ + -H 'Accept: application/json' \ + -H 'Authorization: Bearer ' ``` **Expected Response**: @@ -254,7 +267,7 @@ curl -k -X POST https://localhost:9243/api/v0.9/gateways/d1aa71bc-8cb5-4294-8a26 { "id": "1db8b0e4-f237-4aa3-a6f2-e466c878de0f", "token": "guDgqzePBJTMD8iElVH-q4_hc3IWZE87PgBqfzS_qPA", - "createdAt": "2025-10-19T14:00:43.848145213+05:30", + "createdAt": "2026-06-21T14:00:43+05:30", "message": "New token generated successfully. Old token remains active until revoked." } ``` @@ -268,28 +281,29 @@ wscat -n -c wss://localhost:9243/api/internal/v1/ws/gateways/connect \ **Expected Output**: ``` Connected (press CTRL+C to quit) -< {"type":"connection.ack","gatewayId":"d1aa71bc-8cb5-4294-8a26-fe1273c28632","connectionId":"85d759e5-f152-4a39-8cd1-e0923657268a","timestamp":"2025-10-19T07:11:04+05:30"} +< {"type":"connection.ack","gatewayId":"d1aa71bc-8cb5-4294-8a26-fe1273c28632","connectionId":"85d759e5-f152-4a39-8cd1-e0923657268a","timestamp":"2026-06-21T07:11:04+05:30"} ``` +`gatewayId` here is the gateway's internal UUID (`gateway.ID`), not the handle (`test-gateway`) used in the REST API. + ### 2. Event Delivery -**Deploy API Revision** (with gateway connected): +**Deploy an API** (with gateway connected; requires a REST API already created via `POST /rest-apis` and a `projectId`/`upstream` — see [API Lifecycle Management](../api-lifecycle-management.md)): ```bash -curl -k -X POST 'https://localhost:9243/api/v0.9/rest-apis//deployments?revisionId=' \ +curl -k -X POST 'https://localhost:9243/api/v0.9/rest-apis//deployments' \ -H 'Content-Type: application/json' \ -H 'Accept: application/json' \ - -d '[{ - "revisionId": "", - "gatewayId": "d1aa71bc-8cb5-4294-8a26-fe1273c28632", - "status": "CREATED", - "vhost": "mg.wso2.com", - "displayOnDevportal": true - }]' + -H 'Authorization: Bearer ' \ + -d '{ + "name": "test-deployment", + "base": "current", + "gatewayId": "test-gateway" + }' ``` **Expected Gateway Output (received via WebSocket)**: ``` -< {"type":"api.deployed","payload":{"apiUuid":"23826f9e-8daf-4638-b295-14898312759f","revisionId":"90d10e1c-8560-5c36-9d5a-124ecaa17485","vhost":"mg.wso2.com","environment":"production"},"timestamp":"2025-10-19T07:11:07+05:30","correlationId":"16408ddb-0cc9-48bc-a35d-6debb8d90c28"} +< {"type":"api.deployed","payload":{"apiId":"23826f9e-8daf-4638-b295-14898312759f","deploymentId":"90d10e1c-8560-5c36-9d5a-124ecaa17485","performedAt":"2026-06-21T07:11:07+05:30"},"gatewayId":"d1aa71bc-8cb5-4294-8a26-fe1273c28632","timestamp":"2026-06-21T07:11:07+05:30","correlationId":"16408ddb-0cc9-48bc-a35d-6debb8d90c28"} ``` ### 3. Authentication Rejection @@ -335,17 +349,12 @@ curl -k -s -o /dev/null -w "%{http_code}" \ ## Testing Approach -### Automated Tests (Phase 3 Validation) - -**File**: `specs/002-gateway-websockets-i/test-phase3.sh` +### Automated Tests -Tests: -- ✅ Build verification (all packages compile) -- ✅ Binary build (platform-api executable created) -- ✅ Server health check (HTTP 200 from /health) -- ✅ Invalid API key rejection (HTTP 401) -- ✅ Missing API key rejection (HTTP 401) -- ✅ WebSocket test client availability +The standalone `test-phase3.sh` script referenced in earlier revisions of this doc no longer +exists, and there is currently no dedicated Go test coverage for `internal/websocket/` or the +WebSocket upgrade handler (`internal/handler/websocket.go`) — verification today is manual (see +below). This is a coverage gap worth closing, not an intentional design choice. ### Manual Tests @@ -394,22 +403,26 @@ type DeliveryStats struct { ### Event Payload +Defined in `internal/model/gateway_event.go`: + ```go -type GatewayEventDTO struct { - Type string `json:"type"` // Event type identifier - Payload interface{} `json:"payload"` // Event-specific data - Timestamp string `json:"timestamp"` // RFC3339 format - CorrelationID string `json:"correlationId"` // Distributed tracing ID +type GatewayEvent struct { + Type string `json:"type"` // Event type identifier + Payload json.RawMessage `json:"payload"` // Event-specific data + GatewayID string `json:"gatewayId"` // Target gateway's internal UUID + Timestamp time.Time `json:"timestamp"` + CorrelationID string `json:"correlationId"` // Distributed tracing ID } -type APIDeploymentEvent struct { - ApiUuid string `json:"apiUuid"` // API UUID - RevisionID string `json:"revisionId"` // Revision identifier - Vhost string `json:"vhost"` // Virtual host - Environment string `json:"environment"` // Target environment +type DeploymentEvent struct { + ApiId string `json:"apiId"` // API's internal UUID + DeploymentID string `json:"deploymentId"` // Deployment's internal UUID + PerformedAt time.Time `json:"performedAt"` // Concurrency token } ``` +Sibling event types (`APIUndeploymentEvent`, `APIDeletionEvent`, and the LLM/MCP-proxy deployment events) follow the same `apiId`/`deploymentId`/`performedAt` shape. + ## Security Considerations ### Authentication diff --git a/platform-api/spec/impls/organization-management.md b/platform-api/spec/impls/organization-management.md index 297375d8c6..7f0a66efa3 100644 --- a/platform-api/spec/impls/organization-management.md +++ b/platform-api/spec/impls/organization-management.md @@ -10,12 +10,12 @@ ## Behaviour -1. POST requests bind to `dto.Organization`, ensuring UUID, handle, name and region presence before calling the service. -2. Service enforces lowercase URL-friendly handles and uniqueness checks via repository lookups for both ID and handle. +1. POST requests bind to the `Organization` schema, requiring `id` (handle), `displayName`, and `region` before calling the service. +2. Service enforces lowercase URL-friendly handles and uniqueness checks via repository lookups for both handle and UUID. 3. Upon registration, service inserts the organization with region information and immediately creates a default project. -4. GET requests fetch by UUID, returning `404` when the organization is absent. +4. GET/HEAD requests fetch by handle (`{organizationId}` path param), returning `404` when the organization is absent. ## Verification -- Register: `curl -k -X POST https://localhost:9243/api/v0.9/organizations -d '{"id":"123e4567-e89b-12d3-a456-426614174000","handle":"alpha","name":"Alpha","region":"us"}' -H 'Content-Type: application/json'`. -- Fetch: `curl -k https://localhost:9243/api/v0.9/organizations/`; expect JSON payload with organization metadata (handle, name, region, timestamps). +- Register: `curl -k -X POST https://localhost:9243/api/v0.9/organizations -d '{"id":"alpha","displayName":"Alpha","region":"us"}' -H 'Content-Type: application/json'`. +- Fetch: `curl -k https://localhost:9243/api/v0.9/organizations/alpha`; expect JSON payload with organization metadata (`id`, `displayName`, `region`, timestamps). diff --git a/platform-api/spec/impls/project-management.md b/platform-api/spec/impls/project-management.md index a51fdf7843..8f63908718 100644 --- a/platform-api/spec/impls/project-management.md +++ b/platform-api/spec/impls/project-management.md @@ -2,23 +2,23 @@ ## Entry Points -- `platform-api/internal/handler/project.go` – registers `/api/v0.9/projects` and `/api/v0.9/organizations/:orgId/projects` routes. -- `platform-api/internal/service/project.go` – handles validation, duplicate checks, and deletion constraints (last project, projects with APIs). -- `platform-api/internal/repository/project.go` – executes SQL CRUD operations scoped to organizations. -- `platform-api/internal/database/schema.sql` – defines the `projects` table with foreign key and index support. -- `platform-api/resources/openapi.yaml` – captures the project management operations surfaced to clients. +- `platform-api/src/internal/handler/project.go` – registers `/api/v0.9/projects` and `/api/v0.9/projects/{projectId}` routes. +- `platform-api/src/internal/service/project.go` – handles validation, duplicate checks, and deletion constraints (last project, projects with APIs). +- `platform-api/src/internal/repository/project.go` – executes SQL CRUD operations scoped to organizations. +- `platform-api/src/internal/database/schema.sql` – defines the `projects` table with foreign key and index support. +- `platform-api/src/resources/openapi.yaml` – captures the project management operations surfaced to clients. ## Behaviour -1. Create requests validate presence of `name` and `organizationId`, then confirm organization existence and uniqueness within that org. -2. Service blocks duplicate project names per organization and prevents deleting the last remaining project or one that still owns APIs. -3. Listing routes return all projects for an organization; update routes enforce uniqueness before persisting. +1. Create requests validate presence of `displayName`; `organizationId` is not part of the request body — it is extracted from the JWT. The `id` (handle) is optional and auto-generated from `displayName` if omitted. +2. Service blocks duplicate `displayName` values and duplicate handles per organization, and prevents deleting the last remaining project or one that still owns APIs. +3. `GET /projects` returns all projects for the organization in the caller's JWT; `{projectId}` routes (GET/PUT/DELETE) address a project by its handle and enforce uniqueness before persisting on update. 4. Delete operations guard the constraints above and return informative errors when a project cannot be removed. ## Verification -- Create: `curl -k -X POST https://localhost:9243/api/v0.9/projects -H 'Content-Type: application/json' -d '{"name":"Beta","organizationId":""}'`. -- List: `curl -k https://localhost:9243/api/v0.9/organizations//projects`. +- Create: `curl -k -X POST https://localhost:9243/api/v0.9/projects -H 'Content-Type: application/json' -H 'Authorization: Bearer ' -d '{"displayName":"Beta"}'`. +- List: `curl -k https://localhost:9243/api/v0.9/projects -H 'Authorization: Bearer '`. - Delete guards: - Attempt to delete the only project in an organization and expect a `400` response. - Attempt to delete a project that still has APIs attached and expect a `400` response.