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