Skip to main content

max / makenotwork

8.9 KB · 251 lines History Blame Raw
1 //! Internal API endpoints for service-to-service communication.
2 //!
3 //! Most endpoints are protected by `ServiceAuth`, a shared-secret bearer token,
4 //! and are called by the CLI SSH server running on the same host. Two are not:
5 //! `/api/internal/alerts` and `/api/internal/synckit/client-versions` take
6 //! `AlertsAuth`, a separate bearer token carried by PoM and MT.
7
8 pub(crate) mod alerts;
9 mod cli_features;
10 mod content;
11 mod creators;
12 mod git;
13 mod items;
14 mod repos;
15 mod synckit;
16 mod uploads;
17
18 pub(super) use git::restart_status;
19
20 use axum::routing::get;
21
22 use crate::{
23 AppState,
24 csrf::{CsrfRouter, delete_csrf_skip, post_csrf_skip, put_csrf_skip, with_csrf_skip},
25 };
26
27 /// Routes carrying this reason are authed by `ServiceAuth`, a shared-secret
28 /// bearer token (no session), so CSRF is not applicable. The two `AlertsAuth`
29 /// routes carry `ALERTS_SKIP` instead. Each registration names its reason so
30 /// the posture is visible at the call site.
31 const INTERNAL_SKIP: &str = "internal API: shared-secret bearer (ServiceAuth), no session";
32
33 /// Internal service-to-service routes (ServiceAuth, no rate limit).
34 pub(super) fn internal_routes() -> CsrfRouter<AppState> {
35 CsrfRouter::new()
36 .route_get("/api/internal/ssh-key-lookup", get(git::ssh_key_lookup))
37 .route_get(
38 "/api/internal/synckit/client-versions",
39 get(synckit::client_versions),
40 )
41 .route(
42 "/api/internal/creator/projects",
43 with_csrf_skip(
44 INTERNAL_SKIP,
45 get(creators::creator_projects).post(cli_features::create_project),
46 ),
47 )
48 .route_get(
49 "/api/internal/creator/projects/{id}/items",
50 get(creators::creator_project_items),
51 )
52 .route_get("/api/internal/creator/stats", get(creators::creator_stats))
53 // Git repo + SSH key management for the CLI. Keyed by repo NAME, not
54 // id: the caller is a person at a terminal who has the name and would
55 // otherwise have to look a UUID up first. See internal/repos.rs.
56 .route_get("/api/internal/creator/repos", get(repos::repo_list))
57 .route(
58 "/api/internal/creator/repos/{name}",
59 with_csrf_skip(
60 INTERNAL_SKIP,
61 get(repos::repo_info).delete(repos::repo_delete),
62 ),
63 )
64 .route(
65 "/api/internal/creator/repos/{name}/visibility",
66 put_csrf_skip(INTERNAL_SKIP, repos::repo_set_visibility),
67 )
68 .route(
69 "/api/internal/creator/repos/{name}/description",
70 put_csrf_skip(INTERNAL_SKIP, repos::repo_set_description),
71 )
72 .route(
73 "/api/internal/creator/ssh-keys/{fingerprint}",
74 delete_csrf_skip(INTERNAL_SKIP, repos::key_remove),
75 )
76 .route(
77 "/api/internal/creator/items",
78 post_csrf_skip(INTERNAL_SKIP, items::create_item),
79 )
80 .route(
81 "/api/internal/upload/presign",
82 post_csrf_skip(INTERNAL_SKIP, uploads::presign_upload),
83 )
84 .route(
85 "/api/internal/upload/confirm",
86 post_csrf_skip(INTERNAL_SKIP, uploads::confirm_upload),
87 )
88 .route(
89 "/api/internal/upload/multipart/start",
90 post_csrf_skip(INTERNAL_SKIP, uploads::multipart_start),
91 )
92 .route(
93 "/api/internal/upload/multipart/parts",
94 post_csrf_skip(INTERNAL_SKIP, uploads::multipart_parts),
95 )
96 .route(
97 "/api/internal/upload/multipart/complete",
98 post_csrf_skip(INTERNAL_SKIP, uploads::multipart_complete),
99 )
100 .route(
101 "/api/internal/upload/multipart/abort",
102 post_csrf_skip(INTERNAL_SKIP, uploads::multipart_abort),
103 )
104 .route_get(
105 "/api/internal/creator/storage",
106 get(uploads::creator_storage),
107 )
108 .route_get("/api/internal/creator/items/{id}", get(items::get_item))
109 .route(
110 "/api/internal/creator/items/{id}",
111 put_csrf_skip(INTERNAL_SKIP, items::update_item),
112 )
113 .route(
114 "/api/internal/creator/items/{id}",
115 delete_csrf_skip(INTERNAL_SKIP, items::delete_item),
116 )
117 .route(
118 "/api/internal/creator/items/{id}/publish",
119 post_csrf_skip(INTERNAL_SKIP, items::publish_item),
120 )
121 .route(
122 "/api/internal/creator/items/{id}/unpublish",
123 post_csrf_skip(INTERNAL_SKIP, items::unpublish_item),
124 )
125 .route_get(
126 "/api/internal/creator/items/{id}/versions",
127 get(items::item_versions),
128 )
129 // Blog posts
130 .route_get(
131 "/api/internal/creator/projects/{id}/blog",
132 get(content::list_blog_posts),
133 )
134 .route(
135 "/api/internal/creator/blog",
136 post_csrf_skip(INTERNAL_SKIP, content::create_blog_post),
137 )
138 .route(
139 "/api/internal/creator/blog/{id}",
140 delete_csrf_skip(INTERNAL_SKIP, content::delete_blog_post),
141 )
142 // Promo codes
143 .route(
144 "/api/internal/creator/promo-codes",
145 with_csrf_skip(
146 INTERNAL_SKIP,
147 get(content::list_promo_codes).post(content::create_promo_code),
148 ),
149 )
150 .route(
151 "/api/internal/creator/promo-codes/{id}",
152 delete_csrf_skip(INTERNAL_SKIP, content::delete_promo_code),
153 )
154 // License keys
155 .route(
156 "/api/internal/creator/items/{id}/keys",
157 with_csrf_skip(
158 INTERNAL_SKIP,
159 get(content::list_license_keys).post(content::generate_license_key),
160 ),
161 )
162 .route(
163 "/api/internal/creator/keys/{id}/revoke",
164 post_csrf_skip(INTERNAL_SKIP, content::revoke_license_key),
165 )
166 // Analytics + export
167 .route_get(
168 "/api/internal/creator/analytics",
169 get(creators::creator_analytics),
170 )
171 .route_get(
172 "/api/internal/creator/transactions",
173 get(creators::creator_transactions),
174 )
175 .route_get(
176 "/api/internal/creator/export/sales",
177 get(creators::export_sales),
178 )
179 // Settings
180 .route_get("/api/internal/creator/ssh-keys", get(git::list_ssh_keys))
181 // Git authorization
182 .route(
183 "/api/internal/git/authorize",
184 post_csrf_skip(INTERNAL_SKIP, git::git_authorize),
185 )
186 .route(
187 "/api/internal/restart-warning",
188 post_csrf_skip(INTERNAL_SKIP, git::set_restart_warning),
189 )
190 // CLI features: tags
191 .route_get(
192 "/api/internal/creator/items/{id}/tags",
193 get(cli_features::list_item_tags),
194 )
195 .route(
196 "/api/internal/creator/items/tags",
197 post_csrf_skip(INTERNAL_SKIP, cli_features::add_item_tag),
198 )
199 .route(
200 "/api/internal/creator/items/tags/remove",
201 post_csrf_skip(INTERNAL_SKIP, cli_features::remove_item_tag),
202 )
203 .route_get("/api/internal/tags/search", get(cli_features::search_tags))
204 // CLI features: broadcast
205 .route(
206 "/api/internal/creator/broadcast",
207 post_csrf_skip(INTERNAL_SKIP, cli_features::send_broadcast),
208 )
209 // CLI features: tiers
210 .route_get(
211 "/api/internal/creator/projects/{id}/tiers",
212 get(cli_features::list_tiers),
213 )
214 // CLI features: collections
215 .route(
216 "/api/internal/creator/collections",
217 with_csrf_skip(
218 INTERNAL_SKIP,
219 get(cli_features::list_collections).post(cli_features::create_collection),
220 ),
221 )
222 .route(
223 "/api/internal/creator/collections/{id}",
224 delete_csrf_skip(INTERNAL_SKIP, cli_features::delete_collection),
225 )
226 // CLI features: custom domains
227 .route(
228 "/api/internal/creator/domain",
229 with_csrf_skip(
230 INTERNAL_SKIP,
231 get(cli_features::get_domain)
232 .post(cli_features::add_domain)
233 .delete(cli_features::remove_domain),
234 ),
235 )
236 .route(
237 "/api/internal/creator/domain/verify",
238 post_csrf_skip(INTERNAL_SKIP, cli_features::verify_domain),
239 )
240 // Inbound infra alerts (PoM/MT). Bearer-authed via `AlertsAuth` on a
241 // dedicated token, not `ServiceAuth`; no session, so CSRF is skipped.
242 .route(
243 "/api/internal/alerts",
244 post_csrf_skip(ALERTS_SKIP, alerts::ingest_alert),
245 )
246 }
247
248 /// The alerts ingestion route is bearer-authed via `AlertsAuth` (dedicated
249 /// token, no session); CSRF is not applicable.
250 const ALERTS_SKIP: &str = "internal alerts: AlertsAuth bearer, no session";
251