diff --git a/openapi/extensions-v2.json b/openapi/extensions-v2.json index 668803e..8a61019 100644 --- a/openapi/extensions-v2.json +++ b/openapi/extensions-v2.json @@ -3,7 +3,7 @@ "info": { "title": "FOSSBilling Extensions API (v2)", "version": "2.0.0", - "description": "Self-service extension submission, ownership, moderation, and public browsing. v1 (/extensions/v1) remains available for existing integrations." + "description": "Self-service extension publishing, ownership, moderation, and public browsing. v1 (/extensions/v1) remains available for existing integrations." }, "servers": [ { @@ -18,13 +18,13 @@ } }, "schemas": { - "ExtensionListResponse": { + "OwnedExtensionListResponse": { "type": "object", "properties": { "result": { "type": "array", "items": { - "$ref": "#/components/schemas/ExtensionListItem" + "$ref": "#/components/schemas/OwnedExtensionListItem" } }, "pagination": { @@ -33,7 +33,102 @@ }, "required": ["result", "pagination"] }, - "ExtensionListItem": { + "OwnedExtensionListItem": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "developer": { + "$ref": "#/components/schemas/PublicDeveloper" + }, + "published": { + "type": ["object", "null"], + "properties": { + "type": { + "type": "string", + "enum": [ + "mod", + "theme", + "payment-gateway", + "server-manager", + "domain-registrar", + "hook", + "translation" + ] + }, + "name": { + "type": "string", + "minLength": 1, + "maxLength": 120 + }, + "description": { + "type": "string", + "minLength": 1, + "maxLength": 4000 + }, + "website": { + "type": "string", + "maxLength": 2048, + "format": "uri" + }, + "license": { + "$ref": "#/components/schemas/License" + }, + "icon_url": { + "type": "string", + "maxLength": 2048, + "format": "uri" + }, + "source": { + "$ref": "#/components/schemas/Repository" + }, + "version": { + "type": "string", + "minLength": 1, + "maxLength": 100 + }, + "download_url": { + "type": "string", + "maxLength": 2048, + "format": "uri" + } + }, + "required": [ + "type", + "name", + "description", + "website", + "license", + "source", + "version", + "download_url" + ] + }, + "pending_revision": { + "$ref": "#/components/schemas/PendingRevisionRef" + }, + "last_review": { + "$ref": "#/components/schemas/RevisionReview" + }, + "created_at": { + "type": "string" + }, + "updated_at": { + "type": "string" + } + }, + "required": [ + "id", + "developer", + "published", + "pending_revision", + "last_review", + "created_at", + "updated_at" + ] + }, + "PublicDeveloper": { "type": "object", "properties": { "id": { @@ -42,69 +137,31 @@ }, "type": { "type": "string", - "enum": [ - "mod", - "theme", - "payment-gateway", - "server-manager", - "domain-registrar", - "hook", - "translation" - ] + "enum": ["user", "organization"] }, "name": { "type": "string", "minLength": 1, "maxLength": 120 }, - "description": { - "type": "string", - "minLength": 1, - "maxLength": 4000 - }, - "website": { + "URL": { "type": "string", "maxLength": 2048, "format": "uri" }, - "license": { - "$ref": "#/components/schemas/License" - }, - "icon_url": { + "avatar_url": { "type": "string", "maxLength": 2048, "format": "uri" }, - "source": { - "$ref": "#/components/schemas/Repository" - }, - "version": { - "type": "string", - "minLength": 1, - "maxLength": 100 - }, - "download_url": { - "type": "string", - "maxLength": 2048, - "format": "uri" + "approved": { + "type": "boolean" }, - "developer": { - "$ref": "#/components/schemas/PublicDeveloper" + "unclaimed": { + "type": "boolean" } }, - "required": [ - "id", - "type", - "name", - "description", - "website", - "license", - "source", - "version", - "download_url", - "developer" - ], - "additionalProperties": false + "required": ["id", "type", "name", "approved", "unclaimed"] }, "License": { "type": "object", @@ -139,40 +196,36 @@ "required": ["type", "repo"], "additionalProperties": false }, - "PublicDeveloper": { - "type": "object", + "PendingRevisionRef": { + "type": ["object", "null"], "properties": { "id": { - "type": "string", - "pattern": "^[a-z0-9]+(-[a-z0-9]+)*$" - }, - "type": { - "type": "string", - "enum": ["user", "organization"] - }, - "name": { - "type": "string", - "minLength": 1, - "maxLength": 120 + "type": "string" }, - "URL": { - "type": "string", - "maxLength": 2048, - "format": "uri" + "created_at": { + "type": "string" + } + }, + "required": ["id", "created_at"] + }, + "RevisionReview": { + "type": ["object", "null"], + "properties": { + "revision_id": { + "type": "string" }, - "avatar_url": { + "status": { "type": "string", - "maxLength": 2048, - "format": "uri" + "enum": ["approved", "rejected"] }, - "approved": { - "type": "boolean" + "review_note": { + "type": ["string", "null"] }, - "unclaimed": { - "type": "boolean" + "reviewed_at": { + "type": ["string", "null"] } }, - "required": ["id", "type", "name", "approved", "unclaimed"] + "required": ["revision_id", "status", "review_note", "reviewed_at"] }, "Pagination": { "type": "object", @@ -217,20 +270,50 @@ }, "required": ["error"] }, - "Extension": { + "OwnedExtension": { "allOf": [ { - "$ref": "#/components/schemas/ExtensionPayload" + "$ref": "#/components/schemas/OwnedExtensionListItem" }, { "type": "object", "properties": { - "developer": { - "$ref": "#/components/schemas/PublicDeveloper" + "published": { + "allOf": [ + { + "$ref": "#/components/schemas/ExtensionContent" + }, + { + "type": ["object", "null"], + "properties": { + "releases": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Release" + }, + "maxItems": 100 + } + } + } + ] + }, + "pending_revision": { + "allOf": [ + { + "$ref": "#/components/schemas/PendingRevisionRef" + }, + { + "type": ["object", "null"], + "properties": { + "content": { + "$ref": "#/components/schemas/StoredExtensionContent" + } + }, + "required": ["content"] + } + ] } - }, - "required": ["developer"], - "additionalProperties": false + } } ] }, @@ -266,13 +349,9 @@ "required": ["tag", "date", "download_url", "min_fossbilling_version"], "additionalProperties": false }, - "ExtensionPayload": { + "ExtensionContent": { "type": "object", "properties": { - "id": { - "type": "string", - "pattern": "^[a-z0-9]+(-[a-z0-9]+)*$" - }, "type": { "type": "string", "enum": [ @@ -336,7 +415,6 @@ } }, "required": [ - "id", "type", "name", "description", @@ -347,131 +425,244 @@ "source", "version", "download_url" - ], - "additionalProperties": false + ] }, - "User": { + "StoredExtensionContent": { "type": "object", "properties": { - "display_name": { - "type": ["string", "null"] + "type": { + "type": "string", + "enum": [ + "mod", + "theme", + "payment-gateway", + "server-manager", + "domain-registrar", + "hook", + "translation" + ] }, - "is_moderator": { - "type": "boolean" + "name": { + "type": "string", + "minLength": 1, + "maxLength": 120 }, - "github_linked": { - "type": "boolean" + "description": { + "type": "string", + "minLength": 1, + "maxLength": 4000 }, - "active": { - "type": "boolean" - } - }, - "required": ["display_name", "is_moderator", "github_linked", "active"] - }, - "UserIdentityInput": { - "type": "object", - "properties": { - "name": { - "type": ["string", "null"], - "maxLength": 200 + "releases": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Release" + }, + "maxItems": 100 }, - "email": { - "type": ["string", "null"], - "maxLength": 254, - "format": "email" + "website": { + "type": "string", + "maxLength": 2048, + "format": "uri" }, - "email_verified": { - "type": "boolean" + "license": { + "$ref": "#/components/schemas/License" }, - "picture": { - "type": ["string", "null"], - "maxLength": 2048 + "icon_url": { + "type": "string", + "maxLength": 2048, + "format": "uri" }, - "github_login": { - "type": ["string", "null"], - "maxLength": 200 + "readme": { + "type": "string", + "minLength": 1, + "maxLength": 100000 }, - "github_orgs": { - "type": ["array", "null"], - "items": { - "type": "string", - "maxLength": 200 - }, - "maxItems": 500 + "source": { + "$ref": "#/components/schemas/Repository" }, - "github_orgs_expires_at": { - "type": ["string", "null"], - "maxLength": 64 + "version": { + "type": "string", + "minLength": 1, + "maxLength": 100 + }, + "download_url": { + "type": "string", + "maxLength": 2048, + "format": "uri" } - }, - "required": [ - "name", - "email", - "email_verified", - "picture", - "github_login", - "github_orgs", - "github_orgs_expires_at" - ], - "additionalProperties": false + } }, - "UserProfileUpdate": { + "ExtensionCreate": { "type": "object", "properties": { - "display_name": { - "type": ["string", "null"], + "type": { + "type": "string", + "enum": [ + "mod", + "theme", + "payment-gateway", + "server-manager", + "domain-registrar", + "hook", + "translation" + ] + }, + "name": { + "type": "string", + "minLength": 1, "maxLength": 120 - } - }, - "required": ["display_name"], - "additionalProperties": false - }, - "SubmissionPayload": { - "type": "object", - "properties": { - "developer": { - "$ref": "#/components/schemas/SubmissionDeveloper" }, - "extension": { - "$ref": "#/components/schemas/ExtensionPayload" + "description": { + "type": "string", + "minLength": 1, + "maxLength": 4000 + }, + "releases": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Release" + }, + "minItems": 1, + "maxItems": 100 + }, + "website": { + "type": "string", + "maxLength": 2048, + "format": "uri" + }, + "license": { + "$ref": "#/components/schemas/License" + }, + "icon_url": { + "type": "string", + "maxLength": 2048, + "format": "uri" + }, + "readme": { + "type": "string", + "minLength": 1, + "maxLength": 100000 + }, + "source": { + "$ref": "#/components/schemas/Repository" + }, + "version": { + "type": "string", + "minLength": 1, + "maxLength": 100 + }, + "download_url": { + "type": "string", + "maxLength": 2048, + "format": "uri" + }, + "id": { + "type": "string", + "pattern": "^[a-z0-9]+(-[a-z0-9]+)*$" } }, - "required": ["developer", "extension"], + "required": [ + "type", + "name", + "description", + "releases", + "website", + "license", + "readme", + "source", + "version", + "download_url", + "id" + ], "additionalProperties": false }, - "SubmissionDeveloper": { + "ExtensionUpdate": { "type": "object", "properties": { - "id": { - "type": "string", - "pattern": "^[a-z0-9]+(-[a-z0-9]+)*$" - }, "type": { "type": "string", - "enum": ["user", "organization"] + "enum": [ + "mod", + "theme", + "payment-gateway", + "server-manager", + "domain-registrar", + "hook", + "translation" + ] }, "name": { "type": "string", "minLength": 1, "maxLength": 120 }, - "URL": { + "description": { + "type": "string", + "minLength": 1, + "maxLength": 4000 + }, + "releases": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Release" + }, + "minItems": 1, + "maxItems": 100 + }, + "website": { + "type": "string", + "maxLength": 2048, + "format": "uri" + }, + "license": { + "$ref": "#/components/schemas/License" + }, + "icon_url": { + "type": "string", + "maxLength": 2048, + "format": "uri" + }, + "readme": { + "type": "string", + "minLength": 1, + "maxLength": 100000 + }, + "source": { + "$ref": "#/components/schemas/Repository" + }, + "version": { + "type": "string", + "minLength": 1, + "maxLength": 100 + }, + "download_url": { "type": "string", "maxLength": 2048, "format": "uri" } }, - "required": ["id", "type", "name"], + "required": [ + "type", + "name", + "description", + "releases", + "website", + "license", + "readme", + "source", + "version", + "download_url" + ], "additionalProperties": false }, - "Submission": { + "ExtensionRevision": { "type": "object", "properties": { "id": { "type": "string" }, "extension_id": { - "type": ["string", "null"] + "type": "string" }, "developer_id": { "type": "string" @@ -483,8 +674,8 @@ "type": "string", "enum": ["pending", "approved", "rejected"] }, - "payload": { - "$ref": "#/components/schemas/SubmissionPayload" + "content": { + "$ref": "#/components/schemas/StoredExtensionContent" }, "reviewer_id": { "type": ["string", "null"] @@ -505,57 +696,238 @@ "developer_id", "submitted_by", "status", - "payload", + "content", "reviewer_id", "review_note", "created_at", "reviewed_at" ] }, - "DeveloperClaim": { + "ExtensionListResponse": { "type": "object", "properties": { - "id": { - "type": "string" + "result": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ExtensionListItem" + } }, - "developer_id": { - "type": "string" + "pagination": { + "$ref": "#/components/schemas/Pagination" + } + }, + "required": ["result", "pagination"] + }, + "ExtensionListItem": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "mod", + "theme", + "payment-gateway", + "server-manager", + "domain-registrar", + "hook", + "translation" + ] }, - "claimant_id": { - "type": "string" + "name": { + "type": "string", + "minLength": 1, + "maxLength": 120 }, - "status": { + "description": { "type": "string", - "enum": ["pending", "approved", "rejected"] + "minLength": 1, + "maxLength": 4000 }, - "note": { - "type": "string" + "website": { + "type": "string", + "maxLength": 2048, + "format": "uri" }, - "review_note": { - "type": "string" + "license": { + "$ref": "#/components/schemas/License" }, - "reviewer_id": { - "type": "string" + "icon_url": { + "type": "string", + "maxLength": 2048, + "format": "uri" }, - "created_at": { - "type": "string" + "source": { + "$ref": "#/components/schemas/Repository" }, - "reviewed_at": { - "type": "string" + "version": { + "type": "string", + "minLength": 1, + "maxLength": 100 }, - "github_org_verified": { - "type": "boolean" + "download_url": { + "type": "string", + "maxLength": 2048, + "format": "uri" }, - "github_verification_note": { + "id": { "type": "string" + }, + "developer": { + "$ref": "#/components/schemas/PublicDeveloper" } }, "required": [ + "type", + "name", + "description", + "website", + "license", + "source", + "version", + "download_url", "id", - "developer_id", - "claimant_id", - "status", - "created_at" + "developer" + ] + }, + "Extension": { + "allOf": [ + { + "$ref": "#/components/schemas/ExtensionContent" + }, + { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "developer": { + "$ref": "#/components/schemas/PublicDeveloper" + } + }, + "required": ["id", "developer"] + } + ] + }, + "User": { + "type": "object", + "properties": { + "display_name": { + "type": ["string", "null"] + }, + "is_moderator": { + "type": "boolean" + }, + "github_linked": { + "type": "boolean" + }, + "active": { + "type": "boolean" + } + }, + "required": ["display_name", "is_moderator", "github_linked", "active"] + }, + "UserIdentityInput": { + "type": "object", + "properties": { + "name": { + "type": ["string", "null"], + "maxLength": 200 + }, + "email": { + "type": ["string", "null"], + "maxLength": 254, + "format": "email" + }, + "email_verified": { + "type": "boolean" + }, + "picture": { + "type": ["string", "null"], + "maxLength": 2048 + }, + "github_login": { + "type": ["string", "null"], + "maxLength": 200 + }, + "github_orgs": { + "type": ["array", "null"], + "items": { + "type": "string", + "maxLength": 200 + }, + "maxItems": 500 + }, + "github_orgs_expires_at": { + "type": ["string", "null"], + "maxLength": 64 + } + }, + "required": [ + "name", + "email", + "email_verified", + "picture", + "github_login", + "github_orgs", + "github_orgs_expires_at" + ], + "additionalProperties": false + }, + "UserProfileUpdate": { + "type": "object", + "properties": { + "display_name": { + "type": ["string", "null"], + "maxLength": 120 + } + }, + "required": ["display_name"], + "additionalProperties": false + }, + "DeveloperClaim": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "developer_id": { + "type": "string" + }, + "claimant_id": { + "type": "string" + }, + "status": { + "type": "string", + "enum": ["pending", "approved", "rejected"] + }, + "note": { + "type": "string" + }, + "review_note": { + "type": "string" + }, + "reviewer_id": { + "type": "string" + }, + "created_at": { + "type": "string" + }, + "reviewed_at": { + "type": "string" + }, + "github_org_verified": { + "type": "boolean" + }, + "github_verification_note": { + "type": "string" + } + }, + "required": [ + "id", + "developer_id", + "claimant_id", + "status", + "created_at" ] }, "ClaimNote": { @@ -784,56 +1156,558 @@ }, "required": ["has_pending_transfer"] } - ] + ] + }, + { + "type": "null" + } + ] + }, + "DeveloperInput": { + "type": "object", + "properties": { + "id": { + "type": "string", + "pattern": "^[a-z0-9]+(-[a-z0-9]+)*$" + }, + "type": { + "type": "string", + "enum": ["user", "organization"] + }, + "name": { + "type": "string", + "minLength": 1, + "maxLength": 120 + }, + "URL": { + "type": "string", + "maxLength": 2048, + "format": "uri" + }, + "avatar_url": { + "type": "string", + "maxLength": 2048, + "format": "uri" + }, + "contact_email": { + "type": "string", + "maxLength": 254, + "format": "email" + } + }, + "required": ["id", "type", "name"], + "additionalProperties": false + } + }, + "parameters": {} + }, + "paths": { + "/extensions/mine": { + "get": { + "tags": ["Extensions"], + "summary": "List the caller's extensions, published or not", + "security": [ + { + "Bearer": [] + } + ], + "parameters": [ + { + "schema": { + "type": "string", + "enum": [ + "mod", + "theme", + "payment-gateway", + "server-manager", + "domain-registrar", + "hook", + "translation" + ] + }, + "required": false, + "name": "type", + "in": "query" + }, + { + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 50 + }, + "required": false, + "name": "limit", + "in": "query" + }, + { + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 1000, + "description": "Opaque cursor returned by the previous page" + }, + "required": false, + "description": "Opaque cursor returned by the previous page", + "name": "cursor", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Every extension under the caller's developer profile, each with its live content, any unreviewed edit, and the last moderator decision", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OwnedExtensionListResponse" + } + } + } + }, + "401": { + "description": "Missing or invalid bearer token", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "The bearer is valid but the account is inactive", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "422": { + "description": "Pagination query failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "500": { + "description": "Database error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, + "/extensions/mine/{id}": { + "get": { + "tags": ["Extensions"], + "summary": "Get one of the caller's extensions, published or not", + "security": [ + { + "Bearer": [] + } + ], + "parameters": [ + { + "schema": { + "type": "string", + "example": "b6e2c9c4-3f1a-4e9b-9c3a-2e4b1a2f9d10" + }, + "required": true, + "name": "id", + "in": "path" + } + ], + "responses": { + "200": { + "description": "The extension's live content, its unreviewed edit if any, and the last moderator decision", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "result": { + "$ref": "#/components/schemas/OwnedExtension" + } + }, + "required": ["result"] + } + } + } + }, + "401": { + "description": "Missing or invalid bearer token", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "The account is inactive, or the caller does not own this extension", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "No extension with that id", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "422": { + "description": "id param failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "500": { + "description": "Database error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, + "/extensions": { + "post": { + "tags": ["Extensions"], + "summary": "Create an extension and submit its first version for review", + "security": [ + { + "Bearer": [] + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ExtensionCreate" + } + } + } + }, + "responses": { + "201": { + "description": "Extension created. It holds the id immediately but stays out of the public catalogue until a moderator approves the revision.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "result": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "revision_id": { + "type": "string" + }, + "status": { + "type": "string", + "enum": ["pending"] + } + }, + "required": ["id", "revision_id", "status"] + } + }, + "required": ["result"] + } + } + } + }, + "401": { + "description": "Missing or invalid bearer token", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "The account is inactive, or the caller has no developer profile to publish under", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "409": { + "description": "The id is taken, ownership changed, or the pending-revision limit was reached", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "422": { + "description": "Body failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "500": { + "description": "Database error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + }, + "get": { + "tags": ["Extensions"], + "summary": "List published extensions", + "parameters": [ + { + "schema": { + "type": "string", + "enum": [ + "mod", + "theme", + "payment-gateway", + "server-manager", + "domain-registrar", + "hook", + "translation" + ] + }, + "required": false, + "name": "type", + "in": "query" + }, + { + "schema": { + "type": "string" + }, + "required": false, + "name": "developer_id", + "in": "query" + }, + { + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 50 + }, + "required": false, + "name": "limit", + "in": "query" + }, + { + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 1000, + "description": "Opaque cursor returned by the previous page" + }, + "required": false, + "description": "Opaque cursor returned by the previous page", + "name": "cursor", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Extensions matching the given filters", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ExtensionListResponse" + } + } + } + }, + "422": { + "description": "Filter or pagination query failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "500": { + "description": "Database error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, + "/extensions/{id}": { + "put": { + "tags": ["Extensions"], + "summary": "Submit an edit to an extension the caller owns", + "security": [ + { + "Bearer": [] + } + ], + "parameters": [ + { + "schema": { + "type": "string", + "example": "b6e2c9c4-3f1a-4e9b-9c3a-2e4b1a2f9d10" + }, + "required": true, + "name": "id", + "in": "path" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ExtensionUpdate" + } + } + } + }, + "responses": { + "202": { + "description": "Edit accepted as a pending revision. The published content is unchanged until a moderator approves it.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "result": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "revision_id": { + "type": "string" + }, + "status": { + "type": "string", + "enum": ["pending"] + } + }, + "required": ["id", "revision_id", "status"] + } + }, + "required": ["result"] + } + } + } }, - { - "type": "null" - } - ] - }, - "DeveloperInput": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^[a-z0-9]+(-[a-z0-9]+)*$" + "401": { + "description": "Missing or invalid bearer token", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } }, - "type": { - "type": "string", - "enum": ["user", "organization"] + "403": { + "description": "The account is inactive, or the caller does not own this extension", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } }, - "name": { - "type": "string", - "minLength": 1, - "maxLength": 120 + "404": { + "description": "No extension with that id", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } }, - "URL": { - "type": "string", - "maxLength": 2048, - "format": "uri" + "409": { + "description": "An edit is already awaiting review, or the pending-revision limit was reached", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } }, - "avatar_url": { - "type": "string", - "maxLength": 2048, - "format": "uri" + "422": { + "description": "Body failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } }, - "contact_email": { - "type": "string", - "maxLength": 254, - "format": "email" + "500": { + "description": "Database error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } } - }, - "required": ["id", "type", "name"], - "additionalProperties": false - } - }, - "parameters": {} - }, - "paths": { - "/extensions/mine": { - "get": { + } + }, + "delete": { "tags": ["Extensions"], - "summary": "List extensions published under the caller's developer profile", + "summary": "Withdraw an extension that has never been published", "security": [ { "Bearer": [] @@ -843,51 +1717,36 @@ { "schema": { "type": "string", - "enum": [ - "mod", - "theme", - "payment-gateway", - "server-manager", - "domain-registrar", - "hook", - "translation" - ] - }, - "required": false, - "name": "type", - "in": "query" - }, - { - "schema": { - "type": "integer", - "minimum": 1, - "maximum": 100, - "default": 50 - }, - "required": false, - "name": "limit", - "in": "query" - }, - { - "schema": { - "type": "string", - "minLength": 1, - "maxLength": 1000, - "description": "Opaque cursor returned by the previous page" + "example": "b6e2c9c4-3f1a-4e9b-9c3a-2e4b1a2f9d10" }, - "required": false, - "description": "Opaque cursor returned by the previous page", - "name": "cursor", - "in": "query" + "required": true, + "name": "id", + "in": "path" } ], "responses": { "200": { - "description": "The caller's published extensions", + "description": "Extension and its revisions deleted, and the id released", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ExtensionListResponse" + "type": "object", + "properties": { + "result": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "deleted": { + "type": "boolean", + "enum": [true] + } + }, + "required": ["id", "deleted"] + } + }, + "required": ["result"] } } } @@ -903,7 +1762,7 @@ } }, "403": { - "description": "The bearer is valid but the account is inactive", + "description": "The account is inactive, or the caller does not own this extension", "content": { "application/json": { "schema": { @@ -912,8 +1771,18 @@ } } }, - "422": { - "description": "Pagination query failed validation", + "404": { + "description": "No extension with that id", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "409": { + "description": "The extension is published and cannot be withdrawn", "content": { "application/json": { "schema": { @@ -933,75 +1802,50 @@ } } } - } - }, - "/extensions": { + }, "get": { "tags": ["Extensions"], - "summary": "List published extensions", + "summary": "Get a single published extension", "parameters": [ { "schema": { "type": "string", - "enum": [ - "mod", - "theme", - "payment-gateway", - "server-manager", - "domain-registrar", - "hook", - "translation" - ] - }, - "required": false, - "name": "type", - "in": "query" - }, - { - "schema": { - "type": "string" - }, - "required": false, - "name": "developer_id", - "in": "query" - }, - { - "schema": { - "type": "integer", - "minimum": 1, - "maximum": 100, - "default": 50 - }, - "required": false, - "name": "limit", - "in": "query" - }, - { - "schema": { - "type": "string", - "minLength": 1, - "maxLength": 1000, - "description": "Opaque cursor returned by the previous page" + "example": "b6e2c9c4-3f1a-4e9b-9c3a-2e4b1a2f9d10" }, - "required": false, - "description": "Opaque cursor returned by the previous page", - "name": "cursor", - "in": "query" + "required": true, + "name": "id", + "in": "path" } ], "responses": { "200": { - "description": "Extensions matching the given filters", + "description": "The extension", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ExtensionListResponse" + "type": "object", + "properties": { + "result": { + "$ref": "#/components/schemas/Extension" + } + }, + "required": ["result"] + } + } + } + }, + "404": { + "description": "No extension with that id", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" } } } }, "422": { - "description": "Filter or pagination query failed validation", + "description": "id param failed validation", "content": { "application/json": { "schema": { @@ -1023,10 +1867,15 @@ } } }, - "/extensions/{id}": { + "/extensions/{id}/revisions": { "get": { "tags": ["Extensions"], - "summary": "Get a single published extension", + "summary": "List an extension's revisions, newest first", + "security": [ + { + "Bearer": [] + } + ], "parameters": [ { "schema": { @@ -1036,21 +1885,68 @@ "required": true, "name": "id", "in": "path" + }, + { + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 50 + }, + "required": false, + "name": "limit", + "in": "query" + }, + { + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 1000 + }, + "required": false, + "name": "cursor", + "in": "query" } ], "responses": { "200": { - "description": "The extension", + "description": "Every version proposed for this extension, with its review outcome", "content": { "application/json": { "schema": { "type": "object", "properties": { "result": { - "$ref": "#/components/schemas/Extension" + "type": "array", + "items": { + "$ref": "#/components/schemas/ExtensionRevision" + } + }, + "pagination": { + "$ref": "#/components/schemas/Pagination" } }, - "required": ["result"] + "required": ["result", "pagination"] + } + } + } + }, + "401": { + "description": "Missing or invalid bearer token", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "The account is inactive, or the caller neither owns this extension nor moderates", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" } } } @@ -1066,7 +1962,7 @@ } }, "422": { - "description": "id param failed validation", + "description": "Pagination query failed validation", "content": { "application/json": { "schema": { @@ -1381,202 +2277,6 @@ } } }, - "/submissions": { - "post": { - "tags": ["Submissions"], - "summary": "Submit a new extension, or an edit to one you own", - "security": [ - { - "Bearer": [] - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SubmissionPayload" - } - } - } - }, - "responses": { - "201": { - "description": "Submission created and pending moderator review", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "result": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "status": { - "type": "string", - "enum": ["pending"] - } - }, - "required": ["id", "status"] - } - }, - "required": ["result"] - } - } - } - }, - "401": { - "description": "Missing or invalid bearer token", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - }, - "403": { - "description": "The account is inactive, or the caller does not own the target developer or extension", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - }, - "409": { - "description": "Ownership or target changed, a duplicate is pending, or the pending limit was reached", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - }, - "422": { - "description": "Payload failed validation", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - }, - "500": { - "description": "Database error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - } - } - } - }, - "/submissions/mine": { - "get": { - "tags": ["Submissions"], - "summary": "List the caller's own submissions, in any status", - "security": [ - { - "Bearer": [] - } - ], - "parameters": [ - { - "schema": { - "type": "integer", - "minimum": 1, - "maximum": 100, - "default": 50 - }, - "required": false, - "name": "limit", - "in": "query" - }, - { - "schema": { - "type": "string", - "minLength": 1, - "maxLength": 1000 - }, - "required": false, - "name": "cursor", - "in": "query" - } - ], - "responses": { - "200": { - "description": "The caller's submissions", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "result": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Submission" - } - }, - "pagination": { - "$ref": "#/components/schemas/Pagination" - } - }, - "required": ["result", "pagination"] - } - } - } - }, - "401": { - "description": "Missing or invalid bearer token", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - }, - "403": { - "description": "The bearer is valid but the account is inactive", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - }, - "422": { - "description": "Pagination query failed validation", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - }, - "500": { - "description": "Database error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - } - } - } - }, "/developers/{id}/claim": { "post": { "tags": ["Developers"], @@ -2417,10 +3117,10 @@ } } }, - "/submissions/queue": { + "/moderation/extensions": { "get": { "tags": ["Moderation"], - "summary": "List submissions in the moderation queue", + "summary": "List extension revisions awaiting review", "security": [ { "Bearer": [] @@ -2460,7 +3160,7 @@ ], "responses": { "200": { - "description": "Submissions matching the requested status (default: pending)", + "description": "Revisions matching the requested status (default: pending), oldest first", "content": { "application/json": { "schema": { @@ -2469,7 +3169,7 @@ "result": { "type": "array", "items": { - "$ref": "#/components/schemas/Submission" + "$ref": "#/components/schemas/ExtensionRevision" } }, "pagination": { @@ -2524,10 +3224,10 @@ } } }, - "/submissions/{id}/approve": { + "/extensions/{id}/revisions/{revisionId}/approve": { "post": { "tags": ["Moderation"], - "summary": "Approve a pending submission", + "summary": "Approve a pending revision and publish it", "security": [ { "Bearer": [] @@ -2537,11 +3237,20 @@ { "schema": { "type": "string", - "example": "b6e2c9c4-3f1a-4e9b-9c3a-2e4b1a2f9d10" + "example": "acme-gateway" }, "required": true, "name": "id", "in": "path" + }, + { + "schema": { + "type": "string", + "example": "b6e2c9c4-3f1a-4e9b-9c3a-2e4b1a2f9d10" + }, + "required": true, + "name": "revisionId", + "in": "path" } ], "requestBody": { @@ -2555,7 +3264,7 @@ }, "responses": { "200": { - "description": "Submission approved and written through to the live extension/developer", + "description": "Revision approved and published as the extension's live content", "content": { "application/json": { "schema": { @@ -2601,7 +3310,7 @@ } }, "404": { - "description": "No submission with that id", + "description": "No such revision on that extension", "content": { "application/json": { "schema": { @@ -2611,7 +3320,7 @@ } }, "409": { - "description": "Submission is not pending, or ownership has changed since it was submitted", + "description": "Revision is not pending, or ownership has changed since it was proposed", "content": { "application/json": { "schema": { @@ -2621,7 +3330,7 @@ } }, "422": { - "description": "id param or review_note body failed validation", + "description": "Path params or review_note body failed validation", "content": { "application/json": { "schema": { @@ -2643,10 +3352,10 @@ } } }, - "/submissions/{id}/reject": { + "/extensions/{id}/revisions/{revisionId}/reject": { "post": { "tags": ["Moderation"], - "summary": "Reject a pending submission", + "summary": "Reject a pending revision", "security": [ { "Bearer": [] @@ -2656,11 +3365,20 @@ { "schema": { "type": "string", - "example": "b6e2c9c4-3f1a-4e9b-9c3a-2e4b1a2f9d10" + "example": "acme-gateway" }, "required": true, "name": "id", "in": "path" + }, + { + "schema": { + "type": "string", + "example": "b6e2c9c4-3f1a-4e9b-9c3a-2e4b1a2f9d10" + }, + "required": true, + "name": "revisionId", + "in": "path" } ], "requestBody": { @@ -2674,7 +3392,7 @@ }, "responses": { "200": { - "description": "Submission rejected", + "description": "Revision rejected. The extension's published content is unchanged.", "content": { "application/json": { "schema": { @@ -2720,7 +3438,7 @@ } }, "404": { - "description": "No submission with that id", + "description": "No such revision on that extension", "content": { "application/json": { "schema": { @@ -2730,7 +3448,7 @@ } }, "409": { - "description": "Submission is not pending", + "description": "Revision is not pending", "content": { "application/json": { "schema": { @@ -3318,7 +4036,7 @@ } }, "409": { - "description": "Profile still has published extensions, or has a pending submission awaiting review", + "description": "Profile still has extensions attached, published or not", "content": { "application/json": { "schema": { diff --git a/src/components/ConfirmDeleteDialog.astro b/src/components/ConfirmDeleteDialog.astro index abbee9e..7107fde 100644 --- a/src/components/ConfirmDeleteDialog.astro +++ b/src/components/ConfirmDeleteDialog.astro @@ -7,6 +7,7 @@ interface Props { description: string; confirmText: string; intentField?: { name: string; value: string }; + formAction?: string; submitLabel?: string; } @@ -16,6 +17,7 @@ const { description, confirmText, intentField, + formAction, submitLabel = 'Delete', } = Astro.props; --- @@ -28,7 +30,7 @@ const { onclick="if (arguments[0].target === this) this.close()" onclose="this.querySelector('form').reset(); this.querySelector('[data-confirm-submit]').disabled = true;" > -