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