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