Skip to main content

max / makenotwork

8.4 KB · 185 lines History Blame Raw
1 //! OpenAPI spec generation and endpoint.
2 //!
3 //! Collects documented API operations into a single spec served at
4 //! `/api/openapi.json`. Only public/stable endpoints are included,
5 //! internal dashboard and HTMX endpoints are intentionally excluded.
6
7 use axum::{Json, response::IntoResponse};
8 use utoipa::OpenApi;
9
10 /// OpenAPI spec collecting all documented endpoints.
11 ///
12 /// Add new handler paths and schema types here as endpoints are annotated.
13 #[derive(OpenApi)]
14 #[openapi(
15 info(
16 title = "MakeNotWork API",
17 description = "Creator marketplace API. Only public and stable endpoints are documented.",
18 version = env!("CARGO_PKG_VERSION"),
19 license(name = "PolyForm Noncommercial 1.0.0"),
20 ),
21 paths(
22 // License Keys
23 crate::routes::api::license_keys::validate_key,
24 crate::routes::api::license_keys::deactivate_key,
25 crate::routes::api::license_keys::key_status_post,
26 crate::routes::api::license_keys::key_status,
27 crate::routes::api::license_keys::license_verify,
28 crate::routes::api::license_keys::license_deactivate,
29 crate::routes::api::license_keys::license_text,
30 // SyncKit, Auth
31 crate::routes::synckit::auth::sync_auth,
32 crate::routes::synckit::auth::validate_app,
33 // SyncKit, Sync
34 crate::routes::synckit::sync::sync_push,
35 crate::routes::synckit::sync::sync_pull,
36 crate::routes::synckit::sync::sync_status,
37 crate::routes::synckit::sync::register_device,
38 crate::routes::synckit::sync::list_devices,
39 crate::routes::synckit::sync::delete_device,
40 crate::routes::synckit::sync::put_sync_key,
41 crate::routes::synckit::sync::get_sync_key,
42 // SyncKit, Account & subscription
43 crate::routes::synckit::sync::sync_account,
44 crate::routes::synckit::sync::sync_subscription_status,
45 crate::routes::synckit::sync::get_app_pricing,
46 crate::routes::synckit::sync::quote_subscription_price,
47 crate::routes::synckit::sync::create_subscription_checkout,
48 crate::routes::synckit::sync::queue_storage_cap_change,
49 // SyncKit, Key rotation
50 crate::routes::synckit::sync::begin_rotation,
51 crate::routes::synckit::sync::rotation_entries,
52 crate::routes::synckit::sync::rotation_batch,
53 crate::routes::synckit::sync::complete_rotation,
54 crate::routes::synckit::sync::cancel_rotation,
55 // SyncKit, Blobs
56 crate::routes::synckit::blobs::blob_upload_url,
57 crate::routes::synckit::blobs::blob_multipart_start,
58 crate::routes::synckit::blobs::blob_multipart_parts,
59 crate::routes::synckit::blobs::blob_multipart_complete,
60 crate::routes::synckit::blobs::blob_multipart_abort,
61 crate::routes::synckit::blobs::blob_confirm_upload,
62 crate::routes::synckit::blobs::blob_download_url,
63 ),
64 components(schemas(
65 // License Keys
66 crate::routes::api::license_keys::ValidateKeyRequest,
67 crate::routes::api::license_keys::ValidateKeyResponse,
68 crate::routes::api::license_keys::ValidateKeyLicense,
69 crate::routes::api::license_keys::DeactivateKeyRequest,
70 crate::routes::api::license_keys::DeactivateKeyResponse,
71 crate::routes::api::license_keys::KeyStatusRequest,
72 crate::routes::api::license_keys::KeyStatusResponse,
73 crate::routes::api::license_keys::KeyStatusLicense,
74 crate::routes::api::license_keys::LicenseVerifyRequest,
75 crate::routes::api::license_keys::LicenseVerifyResponse,
76 crate::routes::api::license_keys::LicenseDeactivateRequest,
77 // SyncKit
78 crate::routes::synckit::SyncAuthRequest,
79 crate::routes::synckit::SyncAuthResponse,
80 crate::routes::synckit::ValidateAppQuery,
81 crate::routes::synckit::ValidateAppResponse,
82 crate::routes::synckit::PushRequest,
83 crate::routes::synckit::ChangeEntry,
84 crate::routes::synckit::PushResponse,
85 crate::routes::synckit::PullRequest,
86 crate::routes::synckit::PullResponse,
87 crate::routes::synckit::PullChangeEntry,
88 crate::routes::synckit::SyncDeviceResponse,
89 crate::routes::synckit::RegisterDeviceRequest,
90 crate::routes::synckit::SyncStatusResponse,
91 crate::routes::synckit::PutKeyRequest,
92 crate::routes::synckit::GetKeyResponse,
93 crate::routes::synckit::BlobUploadUrlRequest,
94 crate::routes::synckit::BlobUploadUrlResponse,
95 crate::routes::synckit::BlobConfirmRequest,
96 crate::routes::synckit::BlobMultipartStartRequest,
97 crate::routes::synckit::BlobMultipartStartResponse,
98 crate::routes::synckit::BlobMultipartPartsRequest,
99 crate::routes::synckit::BlobMultipartPartsResponse,
100 crate::routes::synckit::BlobMultipartPartUrl,
101 crate::routes::synckit::BlobMultipartCompleteRequest,
102 crate::routes::synckit::BlobMultipartCompletedPart,
103 crate::routes::synckit::BlobMultipartAbortRequest,
104 crate::routes::synckit::BlobDownloadUrlRequest,
105 crate::routes::synckit::BlobDownloadUrlResponse,
106 // SyncKit, Account & subscription
107 crate::routes::synckit::SyncAccountResponse,
108 crate::routes::synckit::SyncSubscriptionStatusResponse,
109 crate::routes::synckit::AppPricingRequest,
110 crate::routes::synckit::AppPricingResponse,
111 crate::routes::synckit::SyncQuoteRequest,
112 crate::routes::synckit::SyncQuoteResponse,
113 crate::routes::synckit::SyncSubscribeRequest,
114 crate::routes::synckit::SyncCheckoutResponse,
115 crate::routes::synckit::SyncCapChangeRequest,
116 // SyncKit, Key rotation
117 crate::routes::synckit::BeginRotationRequest,
118 crate::routes::synckit::BeginRotationResponse,
119 crate::routes::synckit::RotationEntriesRequest,
120 crate::routes::synckit::RotationEntriesResponse,
121 crate::routes::synckit::RotationBatchRequest,
122 crate::routes::synckit::RotationBatchEntry,
123 crate::routes::synckit::RotationBatchResponse,
124 crate::routes::synckit::CompleteRotationRequest,
125 )),
126 tags(
127 (name = "License Keys", description = "Public license key validation, activation, and deactivation. Stable API: response shapes are frozen."),
128 (name = "SyncKit", description = "E2E encrypted cloud sync for indie apps. JWT auth via /api/v1/sync/auth, then Bearer token on all other endpoints."),
129 ),
130 security(
131 ("bearer" = []),
132 ),
133 )]
134 pub struct ApiDoc;
135
136 /// Serve the OpenAPI spec as JSON.
137 pub async fn openapi_json() -> impl IntoResponse {
138 Json(ApiDoc::openapi())
139 }
140
141 #[cfg(test)]
142 mod tests {
143 use super::*;
144
145 /// The SyncKit rotation + subscription endpoints are fully annotated but were
146 /// historically missing from the published spec (Run 20). This asserts the
147 /// spec documents them, so dropping a handler from `paths(...)` regresses
148 /// here rather than silently shrinking the SDK's contract.
149 #[test]
150 fn spec_documents_synckit_rotation_and_subscription() {
151 let spec = ApiDoc::openapi();
152 let paths = &spec.paths.paths;
153 for p in [
154 "/api/v1/sync/account",
155 "/api/v1/sync/subscription",
156 "/api/v1/sync/subscription/checkout",
157 "/api/v1/sync/subscription/quote",
158 "/api/v1/sync/subscription/storage-cap",
159 "/api/v1/sync/app/pricing",
160 "/api/v1/sync/keys/rotate",
161 "/api/v1/sync/keys/rotate/batch",
162 "/api/v1/sync/keys/rotate/complete",
163 "/api/v1/sync/keys/rotate/entries",
164 ] {
165 assert!(paths.contains_key(p), "openapi spec is missing path {p}");
166 }
167 }
168
169 /// Regression (v0.10.14): the hand-rolled `/api/openapi.json` route and the
170 /// SwaggerUi mount must serve the spec at *distinct* paths. Reusing the same
171 /// path makes axum panic ("Overlapping method route") at `build_app` time,
172 /// which cascades every integration test that boots the app. This mirrors the
173 /// wiring in `lib.rs`; building the router here is DB-free and fails fast if
174 /// the two paths ever collide again.
175 #[test]
176 fn openapi_route_and_swagger_ui_do_not_collide() {
177 let _app: axum::Router = axum::Router::new()
178 .route("/api/openapi.json", axum::routing::get(openapi_json))
179 .merge(
180 utoipa_swagger_ui::SwaggerUi::new("/api/docs")
181 .url("/api-docs/openapi.json", ApiDoc::openapi()),
182 );
183 }
184 }
185