this repo has no description
1use crate::state::AppState;
2use axum::{
3 Json,
4 extract::State,
5 http::StatusCode,
6 response::{IntoResponse, Response},
7};
8use serde::{Deserialize, Serialize};
9use serde_json::json;
10use tracing::error;
11
12#[derive(Serialize)]
13#[serde(rename_all = "camelCase")]
14pub struct AppPassword {
15 pub name: String,
16 pub created_at: String,
17 pub privileged: bool,
18}
19
20#[derive(Serialize)]
21pub struct ListAppPasswordsOutput {
22 pub passwords: Vec<AppPassword>,
23}
24
25pub async fn list_app_passwords(
26 State(state): State<AppState>,
27 headers: axum::http::HeaderMap,
28) -> Response {
29 let auth_header = headers.get("Authorization");
30 if auth_header.is_none() {
31 return (
32 StatusCode::UNAUTHORIZED,
33 Json(json!({"error": "AuthenticationRequired"})),
34 )
35 .into_response();
36 }
37
38 let token = auth_header
39 .unwrap()
40 .to_str()
41 .unwrap_or("")
42 .replace("Bearer ", "");
43
44 let session = sqlx::query!(
45 r#"
46 SELECT s.did, k.key_bytes, u.id as user_id
47 FROM sessions s
48 JOIN users u ON s.did = u.did
49 JOIN user_keys k ON u.id = k.user_id
50 WHERE s.access_jwt = $1
51 "#,
52 token
53 )
54 .fetch_optional(&state.db)
55 .await;
56
57 let (_did, key_bytes, user_id) = match session {
58 Ok(Some(row)) => (row.did, row.key_bytes, row.user_id),
59 Ok(None) => {
60 return (
61 StatusCode::UNAUTHORIZED,
62 Json(json!({"error": "AuthenticationFailed"})),
63 )
64 .into_response();
65 }
66 Err(e) => {
67 error!("DB error in list_app_passwords: {:?}", e);
68 return (
69 StatusCode::INTERNAL_SERVER_ERROR,
70 Json(json!({"error": "InternalError"})),
71 )
72 .into_response();
73 }
74 };
75
76 if let Err(_) = crate::auth::verify_token(&token, &key_bytes) {
77 return (
78 StatusCode::UNAUTHORIZED,
79 Json(json!({"error": "AuthenticationFailed", "message": "Invalid token signature"})),
80 )
81 .into_response();
82 }
83
84 let result = sqlx::query!("SELECT name, created_at, privileged FROM app_passwords WHERE user_id = $1 ORDER BY created_at DESC", user_id)
85 .fetch_all(&state.db)
86 .await;
87
88 match result {
89 Ok(rows) => {
90 let passwords: Vec<AppPassword> = rows
91 .iter()
92 .map(|row| {
93 AppPassword {
94 name: row.name.clone(),
95 created_at: row.created_at.to_rfc3339(),
96 privileged: row.privileged,
97 }
98 })
99 .collect();
100
101 (StatusCode::OK, Json(ListAppPasswordsOutput { passwords })).into_response()
102 }
103 Err(e) => {
104 error!("DB error listing app passwords: {:?}", e);
105 (
106 StatusCode::INTERNAL_SERVER_ERROR,
107 Json(json!({"error": "InternalError"})),
108 )
109 .into_response()
110 }
111 }
112}
113
114#[derive(Deserialize)]
115pub struct CreateAppPasswordInput {
116 pub name: String,
117 pub privileged: Option<bool>,
118}
119
120#[derive(Serialize)]
121#[serde(rename_all = "camelCase")]
122pub struct CreateAppPasswordOutput {
123 pub name: String,
124 pub password: String,
125 pub created_at: String,
126 pub privileged: bool,
127}
128
129pub async fn create_app_password(
130 State(state): State<AppState>,
131 headers: axum::http::HeaderMap,
132 Json(input): Json<CreateAppPasswordInput>,
133) -> Response {
134 let auth_header = headers.get("Authorization");
135 if auth_header.is_none() {
136 return (
137 StatusCode::UNAUTHORIZED,
138 Json(json!({"error": "AuthenticationRequired"})),
139 )
140 .into_response();
141 }
142
143 let token = auth_header
144 .unwrap()
145 .to_str()
146 .unwrap_or("")
147 .replace("Bearer ", "");
148
149 let session = sqlx::query!(
150 r#"
151 SELECT s.did, k.key_bytes, u.id as user_id
152 FROM sessions s
153 JOIN users u ON s.did = u.did
154 JOIN user_keys k ON u.id = k.user_id
155 WHERE s.access_jwt = $1
156 "#,
157 token
158 )
159 .fetch_optional(&state.db)
160 .await;
161
162 let (_did, key_bytes, user_id) = match session {
163 Ok(Some(row)) => (row.did, row.key_bytes, row.user_id),
164 Ok(None) => {
165 return (
166 StatusCode::UNAUTHORIZED,
167 Json(json!({"error": "AuthenticationFailed"})),
168 )
169 .into_response();
170 }
171 Err(e) => {
172 error!("DB error in create_app_password: {:?}", e);
173 return (
174 StatusCode::INTERNAL_SERVER_ERROR,
175 Json(json!({"error": "InternalError"})),
176 )
177 .into_response();
178 }
179 };
180
181 if let Err(_) = crate::auth::verify_token(&token, &key_bytes) {
182 return (
183 StatusCode::UNAUTHORIZED,
184 Json(json!({"error": "AuthenticationFailed", "message": "Invalid token signature"})),
185 )
186 .into_response();
187 }
188
189 let name = input.name.trim();
190 if name.is_empty() {
191 return (
192 StatusCode::BAD_REQUEST,
193 Json(json!({"error": "InvalidRequest", "message": "name is required"})),
194 )
195 .into_response();
196 }
197
198 let existing = sqlx::query!("SELECT id FROM app_passwords WHERE user_id = $1 AND name = $2", user_id, name)
199 .fetch_optional(&state.db)
200 .await;
201
202 if let Ok(Some(_)) = existing {
203 return (
204 StatusCode::BAD_REQUEST,
205 Json(json!({"error": "DuplicateAppPassword", "message": "App password with this name already exists"})),
206 )
207 .into_response();
208 }
209
210 let password: String = (0..4)
211 .map(|_| {
212 use rand::Rng;
213 let mut rng = rand::thread_rng();
214 let chars: Vec<char> = "abcdefghijklmnopqrstuvwxyz234567".chars().collect();
215 (0..4).map(|_| chars[rng.gen_range(0..chars.len())]).collect::<String>()
216 })
217 .collect::<Vec<String>>()
218 .join("-");
219
220 let password_hash = match bcrypt::hash(&password, bcrypt::DEFAULT_COST) {
221 Ok(h) => h,
222 Err(e) => {
223 error!("Failed to hash password: {:?}", e);
224 return (
225 StatusCode::INTERNAL_SERVER_ERROR,
226 Json(json!({"error": "InternalError"})),
227 )
228 .into_response();
229 }
230 };
231
232 let privileged = input.privileged.unwrap_or(false);
233 let created_at = chrono::Utc::now();
234
235 let result = sqlx::query!(
236 "INSERT INTO app_passwords (user_id, name, password_hash, created_at, privileged) VALUES ($1, $2, $3, $4, $5)",
237 user_id,
238 name,
239 password_hash,
240 created_at,
241 privileged
242 )
243 .execute(&state.db)
244 .await;
245
246 match result {
247 Ok(_) => (
248 StatusCode::OK,
249 Json(CreateAppPasswordOutput {
250 name: name.to_string(),
251 password,
252 created_at: created_at.to_rfc3339(),
253 privileged,
254 }),
255 )
256 .into_response(),
257 Err(e) => {
258 error!("DB error creating app password: {:?}", e);
259 (
260 StatusCode::INTERNAL_SERVER_ERROR,
261 Json(json!({"error": "InternalError"})),
262 )
263 .into_response()
264 }
265 }
266}
267
268#[derive(Deserialize)]
269pub struct RevokeAppPasswordInput {
270 pub name: String,
271}
272
273pub async fn revoke_app_password(
274 State(state): State<AppState>,
275 headers: axum::http::HeaderMap,
276 Json(input): Json<RevokeAppPasswordInput>,
277) -> Response {
278 let auth_header = headers.get("Authorization");
279 if auth_header.is_none() {
280 return (
281 StatusCode::UNAUTHORIZED,
282 Json(json!({"error": "AuthenticationRequired"})),
283 )
284 .into_response();
285 }
286
287 let token = auth_header
288 .unwrap()
289 .to_str()
290 .unwrap_or("")
291 .replace("Bearer ", "");
292
293 let session = sqlx::query!(
294 r#"
295 SELECT s.did, k.key_bytes, u.id as user_id
296 FROM sessions s
297 JOIN users u ON s.did = u.did
298 JOIN user_keys k ON u.id = k.user_id
299 WHERE s.access_jwt = $1
300 "#,
301 token
302 )
303 .fetch_optional(&state.db)
304 .await;
305
306 let (_did, key_bytes, user_id) = match session {
307 Ok(Some(row)) => (row.did, row.key_bytes, row.user_id),
308 Ok(None) => {
309 return (
310 StatusCode::UNAUTHORIZED,
311 Json(json!({"error": "AuthenticationFailed"})),
312 )
313 .into_response();
314 }
315 Err(e) => {
316 error!("DB error in revoke_app_password: {:?}", e);
317 return (
318 StatusCode::INTERNAL_SERVER_ERROR,
319 Json(json!({"error": "InternalError"})),
320 )
321 .into_response();
322 }
323 };
324
325 if let Err(_) = crate::auth::verify_token(&token, &key_bytes) {
326 return (
327 StatusCode::UNAUTHORIZED,
328 Json(json!({"error": "AuthenticationFailed", "message": "Invalid token signature"})),
329 )
330 .into_response();
331 }
332
333 let name = input.name.trim();
334 if name.is_empty() {
335 return (
336 StatusCode::BAD_REQUEST,
337 Json(json!({"error": "InvalidRequest", "message": "name is required"})),
338 )
339 .into_response();
340 }
341
342 let result = sqlx::query!("DELETE FROM app_passwords WHERE user_id = $1 AND name = $2", user_id, name)
343 .execute(&state.db)
344 .await;
345
346 match result {
347 Ok(r) => {
348 if r.rows_affected() == 0 {
349 return (
350 StatusCode::NOT_FOUND,
351 Json(json!({"error": "AppPasswordNotFound", "message": "App password not found"})),
352 )
353 .into_response();
354 }
355 (StatusCode::OK, Json(json!({}))).into_response()
356 }
357 Err(e) => {
358 error!("DB error revoking app password: {:?}", e);
359 (
360 StatusCode::INTERNAL_SERVER_ERROR,
361 Json(json!({"error": "InternalError"})),
362 )
363 .into_response()
364 }
365 }
366}