this repo has no description
1use crate::api::ApiError;
2use crate::auth::BearerAuth;
3use crate::state::AppState;
4use crate::util::get_user_id_by_did;
5use axum::{
6 Json,
7 extract::State,
8 http::HeaderMap,
9 response::{IntoResponse, Response},
10};
11use serde::{Deserialize, Serialize};
12use serde_json::json;
13use tracing::{error, warn};
14
15#[derive(Serialize)]
16#[serde(rename_all = "camelCase")]
17pub struct AppPassword {
18 pub name: String,
19 pub created_at: String,
20 pub privileged: bool,
21}
22
23#[derive(Serialize)]
24pub struct ListAppPasswordsOutput {
25 pub passwords: Vec<AppPassword>,
26}
27
28pub async fn list_app_passwords(
29 State(state): State<AppState>,
30 BearerAuth(auth_user): BearerAuth,
31) -> Response {
32 let user_id = match get_user_id_by_did(&state.db, &auth_user.did).await {
33 Ok(id) => id,
34 Err(e) => return ApiError::from(e).into_response(),
35 };
36
37 match sqlx::query!(
38 "SELECT name, created_at, privileged FROM app_passwords WHERE user_id = $1 ORDER BY created_at DESC",
39 user_id
40 )
41 .fetch_all(&state.db)
42 .await
43 {
44 Ok(rows) => {
45 let passwords: Vec<AppPassword> = rows
46 .iter()
47 .map(|row| AppPassword {
48 name: row.name.clone(),
49 created_at: row.created_at.to_rfc3339(),
50 privileged: row.privileged,
51 })
52 .collect();
53
54 Json(ListAppPasswordsOutput { passwords }).into_response()
55 }
56 Err(e) => {
57 error!("DB error listing app passwords: {:?}", e);
58 ApiError::InternalError.into_response()
59 }
60 }
61}
62
63#[derive(Deserialize)]
64pub struct CreateAppPasswordInput {
65 pub name: String,
66 pub privileged: Option<bool>,
67}
68
69#[derive(Serialize)]
70#[serde(rename_all = "camelCase")]
71pub struct CreateAppPasswordOutput {
72 pub name: String,
73 pub password: String,
74 pub created_at: String,
75 pub privileged: bool,
76}
77
78pub async fn create_app_password(
79 State(state): State<AppState>,
80 headers: HeaderMap,
81 BearerAuth(auth_user): BearerAuth,
82 Json(input): Json<CreateAppPasswordInput>,
83) -> Response {
84 let client_ip = crate::rate_limit::extract_client_ip(&headers, None);
85 if !state.distributed_rate_limiter.check_rate_limit(
86 &format!("app_password:{}", client_ip),
87 10,
88 60_000,
89 ).await {
90 if state.rate_limiters.app_password.check_key(&client_ip).is_err() {
91 warn!(ip = %client_ip, "App password creation rate limit exceeded");
92 return (
93 axum::http::StatusCode::TOO_MANY_REQUESTS,
94 Json(json!({
95 "error": "RateLimitExceeded",
96 "message": "Too many requests. Please try again later."
97 })),
98 ).into_response();
99 }
100 }
101
102 let user_id = match get_user_id_by_did(&state.db, &auth_user.did).await {
103 Ok(id) => id,
104 Err(e) => return ApiError::from(e).into_response(),
105 };
106
107 let name = input.name.trim();
108 if name.is_empty() {
109 return ApiError::InvalidRequest("name is required".into()).into_response();
110 }
111
112 let existing = sqlx::query!(
113 "SELECT id FROM app_passwords WHERE user_id = $1 AND name = $2",
114 user_id,
115 name
116 )
117 .fetch_optional(&state.db)
118 .await;
119
120 if let Ok(Some(_)) = existing {
121 return ApiError::DuplicateAppPassword.into_response();
122 }
123
124 let password: String = (0..4)
125 .map(|_| {
126 use rand::Rng;
127 let mut rng = rand::thread_rng();
128 let chars: Vec<char> = "abcdefghijklmnopqrstuvwxyz234567".chars().collect();
129 (0..4)
130 .map(|_| chars[rng.gen_range(0..chars.len())])
131 .collect::<String>()
132 })
133 .collect::<Vec<String>>()
134 .join("-");
135
136 let password_hash = match bcrypt::hash(&password, bcrypt::DEFAULT_COST) {
137 Ok(h) => h,
138 Err(e) => {
139 error!("Failed to hash password: {:?}", e);
140 return ApiError::InternalError.into_response();
141 }
142 };
143
144 let privileged = input.privileged.unwrap_or(false);
145 let created_at = chrono::Utc::now();
146
147 match sqlx::query!(
148 "INSERT INTO app_passwords (user_id, name, password_hash, created_at, privileged) VALUES ($1, $2, $3, $4, $5)",
149 user_id,
150 name,
151 password_hash,
152 created_at,
153 privileged
154 )
155 .execute(&state.db)
156 .await
157 {
158 Ok(_) => Json(CreateAppPasswordOutput {
159 name: name.to_string(),
160 password,
161 created_at: created_at.to_rfc3339(),
162 privileged,
163 })
164 .into_response(),
165 Err(e) => {
166 error!("DB error creating app password: {:?}", e);
167 ApiError::InternalError.into_response()
168 }
169 }
170}
171
172#[derive(Deserialize)]
173pub struct RevokeAppPasswordInput {
174 pub name: String,
175}
176
177pub async fn revoke_app_password(
178 State(state): State<AppState>,
179 BearerAuth(auth_user): BearerAuth,
180 Json(input): Json<RevokeAppPasswordInput>,
181) -> Response {
182 let user_id = match get_user_id_by_did(&state.db, &auth_user.did).await {
183 Ok(id) => id,
184 Err(e) => return ApiError::from(e).into_response(),
185 };
186
187 let name = input.name.trim();
188 if name.is_empty() {
189 return ApiError::InvalidRequest("name is required".into()).into_response();
190 }
191
192 match sqlx::query!(
193 "DELETE FROM app_passwords WHERE user_id = $1 AND name = $2",
194 user_id,
195 name
196 )
197 .execute(&state.db)
198 .await
199 {
200 Ok(r) => {
201 if r.rows_affected() == 0 {
202 return ApiError::AppPasswordNotFound.into_response();
203 }
204 Json(json!({})).into_response()
205 }
206 Err(e) => {
207 error!("DB error revoking app password: {:?}", e);
208 ApiError::InternalError.into_response()
209 }
210 }
211}