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