this repo has no description
1use crate::api::ApiError;
2use crate::auth::BearerAuth;
3use crate::state::{AppState, RateLimitKind};
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.check_rate_limit(RateLimitKind::AppPassword, &client_ip).await {
86 warn!(ip = %client_ip, "App password creation rate limit exceeded");
87 return (
88 axum::http::StatusCode::TOO_MANY_REQUESTS,
89 Json(json!({
90 "error": "RateLimitExceeded",
91 "message": "Too many requests. Please try again later."
92 })),
93 ).into_response();
94 }
95
96 let user_id = match get_user_id_by_did(&state.db, &auth_user.did).await {
97 Ok(id) => id,
98 Err(e) => return ApiError::from(e).into_response(),
99 };
100
101 let name = input.name.trim();
102 if name.is_empty() {
103 return ApiError::InvalidRequest("name is required".into()).into_response();
104 }
105
106 let existing = sqlx::query!(
107 "SELECT id FROM app_passwords WHERE user_id = $1 AND name = $2",
108 user_id,
109 name
110 )
111 .fetch_optional(&state.db)
112 .await;
113
114 if let Ok(Some(_)) = existing {
115 return ApiError::DuplicateAppPassword.into_response();
116 }
117
118 let password: String = (0..4)
119 .map(|_| {
120 use rand::Rng;
121 let mut rng = rand::thread_rng();
122 let chars: Vec<char> = "abcdefghijklmnopqrstuvwxyz234567".chars().collect();
123 (0..4)
124 .map(|_| chars[rng.gen_range(0..chars.len())])
125 .collect::<String>()
126 })
127 .collect::<Vec<String>>()
128 .join("-");
129
130 let password_hash = match bcrypt::hash(&password, bcrypt::DEFAULT_COST) {
131 Ok(h) => h,
132 Err(e) => {
133 error!("Failed to hash password: {:?}", e);
134 return ApiError::InternalError.into_response();
135 }
136 };
137
138 let privileged = input.privileged.unwrap_or(false);
139 let created_at = chrono::Utc::now();
140
141 match sqlx::query!(
142 "INSERT INTO app_passwords (user_id, name, password_hash, created_at, privileged) VALUES ($1, $2, $3, $4, $5)",
143 user_id,
144 name,
145 password_hash,
146 created_at,
147 privileged
148 )
149 .execute(&state.db)
150 .await
151 {
152 Ok(_) => Json(CreateAppPasswordOutput {
153 name: name.to_string(),
154 password,
155 created_at: created_at.to_rfc3339(),
156 privileged,
157 })
158 .into_response(),
159 Err(e) => {
160 error!("DB error creating app password: {:?}", e);
161 ApiError::InternalError.into_response()
162 }
163 }
164}
165
166#[derive(Deserialize)]
167pub struct RevokeAppPasswordInput {
168 pub name: String,
169}
170
171pub async fn revoke_app_password(
172 State(state): State<AppState>,
173 BearerAuth(auth_user): BearerAuth,
174 Json(input): Json<RevokeAppPasswordInput>,
175) -> Response {
176 let user_id = match get_user_id_by_did(&state.db, &auth_user.did).await {
177 Ok(id) => id,
178 Err(e) => return ApiError::from(e).into_response(),
179 };
180
181 let name = input.name.trim();
182 if name.is_empty() {
183 return ApiError::InvalidRequest("name is required".into()).into_response();
184 }
185
186 match sqlx::query!(
187 "DELETE FROM app_passwords WHERE user_id = $1 AND name = $2",
188 user_id,
189 name
190 )
191 .execute(&state.db)
192 .await
193 {
194 Ok(r) => {
195 if r.rows_affected() == 0 {
196 return ApiError::AppPasswordNotFound.into_response();
197 }
198 Json(json!({})).into_response()
199 }
200 Err(e) => {
201 error!("DB error revoking app password: {:?}", e);
202 ApiError::InternalError.into_response()
203 }
204 }
205}