Skip to main content

max / makenotwork

6.3 KB · 203 lines History Blame Raw
1 //! SyncKit app management: create, list, delete, regenerate keys, update links.
2
3 use axum::{
4 extract::{Path, State},
5 response::IntoResponse,
6 Json,
7 };
8
9 use crate::{
10 auth::AuthUser,
11 db::{self, SyncAppId},
12 error::{AppError, Result},
13 validation,
14 AppState,
15 };
16
17 use super::UpdateAppSlugRequest;
18
19 use super::{CreateAppRequest, UpdateAppLinkRequest};
20
21 /// Create a new sync app and generate its API key.
22 ///
23 /// `POST /api/sync/apps`: Session auth required.
24 /// Returns the app data plus the plaintext API key (shown only once).
25 #[tracing::instrument(skip_all, name = "synckit::create_app")]
26 pub(super) async fn create_app(
27 State(state): State<AppState>,
28 AuthUser(user): AuthUser,
29 Json(req): Json<CreateAppRequest>,
30 ) -> Result<impl IntoResponse> {
31 user.check_not_sandbox()?;
32 validation::validate_sync_app_name(&req.name)?;
33
34 let project_id = parse_and_verify_project(&state, user.id, req.project_id.as_deref()).await?;
35 let item_id = parse_and_verify_item(&state, user.id, req.item_id.as_deref()).await?;
36
37 let api_key = super::generate_api_key();
38 let app = db::synckit::create_sync_app(
39 &state.db, user.id, &req.name, &api_key, project_id, item_id,
40 ).await?;
41
42 Ok((axum::http::StatusCode::CREATED, Json(super::AppWithKey { app, api_key })))
43 }
44
45 /// List all sync apps owned by the authenticated user.
46 ///
47 /// `GET /api/sync/apps`: Session auth required.
48 #[tracing::instrument(skip_all, name = "synckit::list_apps")]
49 pub(super) async fn list_apps(
50 State(state): State<AppState>,
51 AuthUser(user): AuthUser,
52 ) -> Result<impl IntoResponse> {
53 let apps = db::synckit::get_sync_apps_by_creator(&state.db, user.id).await?;
54
55 Ok(Json(apps))
56 }
57
58 /// Regenerate the API key for a sync app, invalidating the old one.
59 ///
60 /// `POST /api/sync/apps/{id}/regenerate-key`: Session auth required.
61 #[tracing::instrument(skip_all, name = "synckit::regenerate_app_key")]
62 pub(super) async fn regenerate_app_key(
63 State(state): State<AppState>,
64 AuthUser(user): AuthUser,
65 Path(app_id): Path<SyncAppId>,
66 ) -> Result<impl IntoResponse> {
67 let app = db::synckit::get_sync_app_by_id(&state.db, app_id)
68 .await?
69 .ok_or(AppError::NotFound)?;
70
71 if app.creator_id != user.id {
72 return Err(AppError::Forbidden);
73 }
74
75 let new_key = super::generate_api_key();
76 let updated = db::synckit::regenerate_sync_app_key(&state.db, app_id, &new_key).await?;
77
78 Ok(Json(super::AppWithKey { app: updated, api_key: new_key }))
79 }
80
81 /// Delete a sync app and all its associated data.
82 ///
83 /// `DELETE /api/sync/apps/{id}`: Session auth required.
84 #[tracing::instrument(skip_all, name = "synckit::delete_app")]
85 pub(super) async fn delete_app(
86 State(state): State<AppState>,
87 AuthUser(user): AuthUser,
88 Path(app_id): Path<SyncAppId>,
89 ) -> Result<impl IntoResponse> {
90 let app = db::synckit::get_sync_app_by_id(&state.db, app_id)
91 .await?
92 .ok_or(AppError::NotFound)?;
93
94 if app.creator_id != user.id {
95 return Err(AppError::Forbidden);
96 }
97
98 db::synckit::delete_sync_app(&state.db, app_id).await?;
99
100 Ok(axum::http::StatusCode::NO_CONTENT)
101 }
102
103 /// Update the project and/or item link for a sync app.
104 ///
105 /// `PUT /api/sync/apps/{id}/link`: Session auth required.
106 #[tracing::instrument(skip_all, name = "synckit::update_app_link")]
107 pub(super) async fn update_app_link(
108 State(state): State<AppState>,
109 AuthUser(user): AuthUser,
110 Path(app_id): Path<SyncAppId>,
111 Json(req): Json<UpdateAppLinkRequest>,
112 ) -> Result<impl IntoResponse> {
113 let app = db::synckit::get_sync_app_by_id(&state.db, app_id)
114 .await?
115 .ok_or(AppError::NotFound)?;
116
117 if app.creator_id != user.id {
118 return Err(AppError::Forbidden);
119 }
120
121 let project_id = parse_and_verify_project(&state, user.id, req.project_id.as_deref()).await?;
122 let item_id = parse_and_verify_item(&state, user.id, req.item_id.as_deref()).await?;
123
124 let updated =
125 db::synckit::update_sync_app_link(&state.db, app_id, project_id, item_id).await?;
126
127 Ok(Json(updated))
128 }
129
130 /// Set the OTA slug for a sync app.
131 ///
132 /// `PUT /api/sync/apps/{id}/slug`: Session auth required.
133 #[tracing::instrument(skip_all, name = "synckit::update_app_slug")]
134 pub(super) async fn update_app_slug(
135 State(state): State<AppState>,
136 AuthUser(user): AuthUser,
137 Path(app_id): Path<SyncAppId>,
138 Json(req): Json<UpdateAppSlugRequest>,
139 ) -> Result<impl IntoResponse> {
140 let app = db::synckit::get_sync_app_by_id(&state.db, app_id)
141 .await?
142 .ok_or(AppError::NotFound)?;
143
144 if app.creator_id != user.id {
145 return Err(AppError::Forbidden);
146 }
147
148 // Reuse the OTA slug validation
149 crate::routes::ota::validate_slug_public(&req.slug)?;
150
151 db::ota::set_app_slug(&state.db, app_id, &req.slug).await?;
152
153 Ok(axum::http::StatusCode::NO_CONTENT)
154 }
155
156 // ── Link helpers ──
157
158 /// Parse an optional UUID string and verify the project belongs to the user.
159 async fn parse_and_verify_project(
160 state: &AppState,
161 user_id: db::UserId,
162 raw: Option<&str>,
163 ) -> Result<Option<db::ProjectId>> {
164 let Some(s) = raw.filter(|s| !s.is_empty()) else {
165 return Ok(None);
166 };
167 let pid: db::ProjectId = s
168 .parse()
169 .map_err(|_| AppError::BadRequest("Invalid project_id".to_string()))?;
170 let project = db::projects::get_project_by_id(&state.db, pid)
171 .await?
172 .ok_or(AppError::BadRequest("Project not found".to_string()))?;
173 if project.user_id != user_id {
174 return Err(AppError::Forbidden);
175 }
176 Ok(Some(pid))
177 }
178
179 /// Parse an optional UUID string and verify the item belongs to the user
180 /// (via its parent project).
181 async fn parse_and_verify_item(
182 state: &AppState,
183 user_id: db::UserId,
184 raw: Option<&str>,
185 ) -> Result<Option<db::ItemId>> {
186 let Some(s) = raw.filter(|s| !s.is_empty()) else {
187 return Ok(None);
188 };
189 let iid: db::ItemId = s
190 .parse()
191 .map_err(|_| AppError::BadRequest("Invalid item_id".to_string()))?;
192 let item = db::items::get_item_by_id(&state.db, iid)
193 .await?
194 .ok_or(AppError::BadRequest("Item not found".to_string()))?;
195 let project = db::projects::get_project_by_id(&state.db, item.project_id)
196 .await?
197 .ok_or(AppError::BadRequest("Item's project not found".to_string()))?;
198 if project.user_id != user_id {
199 return Err(AppError::Forbidden);
200 }
201 Ok(Some(iid))
202 }
203