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