this repo has no description
1use crate::api::ApiError;
2use crate::auth::BearerAuth;
3use crate::state::AppState;
4use crate::util::get_user_id_by_did;
5use axum::{
6 Json,
7 extract::State,
8 response::{IntoResponse, Response},
9};
10use serde::{Deserialize, Serialize};
11use serde_json::json;
12use tracing::error;
13
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
22#[derive(Serialize)]
23pub struct ListAppPasswordsOutput {
24 pub passwords: Vec<AppPassword>,
25}
26
27pub async fn list_app_passwords(
28 State(state): State<AppState>,
29 BearerAuth(auth_user): BearerAuth,
30) -> Response {
31 let user_id = match get_user_id_by_did(&state.db, &auth_user.did).await {
32 Ok(id) => id,
33 Err(e) => return ApiError::from(e).into_response(),
34 };
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
53 Json(ListAppPasswordsOutput { passwords }).into_response()
54 }
55 Err(e) => {
56 error!("DB error listing app passwords: {:?}", e);
57 ApiError::InternalError.into_response()
58 }
59 }
60}
61
62#[derive(Deserialize)]
63pub struct CreateAppPasswordInput {
64 pub name: String,
65 pub privileged: Option<bool>,
66}
67
68#[derive(Serialize)]
69#[serde(rename_all = "camelCase")]
70pub struct CreateAppPasswordOutput {
71 pub name: String,
72 pub password: String,
73 pub created_at: String,
74 pub privileged: bool,
75}
76
77pub async fn create_app_password(
78 State(state): State<AppState>,
79 BearerAuth(auth_user): BearerAuth,
80 Json(input): Json<CreateAppPasswordInput>,
81) -> Response {
82 let user_id = match get_user_id_by_did(&state.db, &auth_user.did).await {
83 Ok(id) => id,
84 Err(e) => return ApiError::from(e).into_response(),
85 };
86
87 let name = input.name.trim();
88 if name.is_empty() {
89 return ApiError::InvalidRequest("name is required".into()).into_response();
90 }
91
92 let existing = sqlx::query!(
93 "SELECT id FROM app_passwords WHERE user_id = $1 AND name = $2",
94 user_id,
95 name
96 )
97 .fetch_optional(&state.db)
98 .await;
99
100 if let Ok(Some(_)) = existing {
101 return ApiError::DuplicateAppPassword.into_response();
102 }
103
104 let password: String = (0..4)
105 .map(|_| {
106 use rand::Rng;
107 let mut rng = rand::thread_rng();
108 let chars: Vec<char> = "abcdefghijklmnopqrstuvwxyz234567".chars().collect();
109 (0..4)
110 .map(|_| chars[rng.gen_range(0..chars.len())])
111 .collect::<String>()
112 })
113 .collect::<Vec<String>>()
114 .join("-");
115
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
124 let privileged = input.privileged.unwrap_or(false);
125 let created_at = chrono::Utc::now();
126
127 match sqlx::query!(
128 "INSERT INTO app_passwords (user_id, name, password_hash, created_at, privileged) VALUES ($1, $2, $3, $4, $5)",
129 user_id,
130 name,
131 password_hash,
132 created_at,
133 privileged
134 )
135 .execute(&state.db)
136 .await
137 {
138 Ok(_) => Json(CreateAppPasswordOutput {
139 name: name.to_string(),
140 password,
141 created_at: created_at.to_rfc3339(),
142 privileged,
143 })
144 .into_response(),
145 Err(e) => {
146 error!("DB error creating app password: {:?}", e);
147 ApiError::InternalError.into_response()
148 }
149 }
150}
151
152#[derive(Deserialize)]
153pub struct RevokeAppPasswordInput {
154 pub name: String,
155}
156
157pub async fn revoke_app_password(
158 State(state): State<AppState>,
159 BearerAuth(auth_user): BearerAuth,
160 Json(input): Json<RevokeAppPasswordInput>,
161) -> Response {
162 let user_id = match get_user_id_by_did(&state.db, &auth_user.did).await {
163 Ok(id) => id,
164 Err(e) => return ApiError::from(e).into_response(),
165 };
166
167 let name = input.name.trim();
168 if name.is_empty() {
169 return ApiError::InvalidRequest("name is required".into()).into_response();
170 }
171
172 match sqlx::query!(
173 "DELETE FROM app_passwords WHERE user_id = $1 AND name = $2",
174 user_id,
175 name
176 )
177 .execute(&state.db)
178 .await
179 {
180 Ok(r) => {
181 if r.rows_affected() == 0 {
182 return ApiError::AppPasswordNotFound.into_response();
183 }
184 Json(json!({})).into_response()
185 }
186 Err(e) => {
187 error!("DB error revoking app password: {:?}", e);
188 ApiError::InternalError.into_response()
189 }
190 }
191}