this repo has no description
1use crate::api::EmptyResponse;
2use crate::api::error::ApiError;
3use crate::auth::BearerAuthAdmin;
4use crate::state::AppState;
5use crate::types::{Did, Handle, PlainPassword};
6use axum::{
7 Json,
8 extract::State,
9 response::{IntoResponse, Response},
10};
11use serde::Deserialize;
12use tracing::{error, warn};
13
14#[derive(Deserialize)]
15pub struct UpdateAccountEmailInput {
16 pub account: String,
17 pub email: String,
18}
19
20pub async fn update_account_email(
21 State(state): State<AppState>,
22 _auth: BearerAuthAdmin,
23 Json(input): Json<UpdateAccountEmailInput>,
24) -> Response {
25 let account = input.account.trim();
26 let email = input.email.trim();
27 if account.is_empty() || email.is_empty() {
28 return ApiError::InvalidRequest("account and email are required".into()).into_response();
29 }
30 let result = sqlx::query!("UPDATE users SET email = $1 WHERE did = $2", email, account)
31 .execute(&state.db)
32 .await;
33 match result {
34 Ok(r) => {
35 if r.rows_affected() == 0 {
36 return ApiError::AccountNotFound.into_response();
37 }
38 EmptyResponse::ok().into_response()
39 }
40 Err(e) => {
41 error!("DB error updating email: {:?}", e);
42 ApiError::InternalError(None).into_response()
43 }
44 }
45}
46
47#[derive(Deserialize)]
48pub struct UpdateAccountHandleInput {
49 pub did: Did,
50 pub handle: String,
51}
52
53pub async fn update_account_handle(
54 State(state): State<AppState>,
55 _auth: BearerAuthAdmin,
56 Json(input): Json<UpdateAccountHandleInput>,
57) -> Response {
58 let did = &input.did;
59 let input_handle = input.handle.trim();
60 if input_handle.is_empty() {
61 return ApiError::InvalidRequest("handle is required".into()).into_response();
62 }
63 if !input_handle
64 .chars()
65 .all(|c| c.is_ascii_alphanumeric() || c == '.' || c == '-' || c == '_')
66 {
67 return ApiError::InvalidHandle(None).into_response();
68 }
69 let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
70 let handle = if !input_handle.contains('.') {
71 format!("{}.{}", input_handle, hostname)
72 } else {
73 input_handle.to_string()
74 };
75 let old_handle = sqlx::query_scalar!("SELECT handle FROM users WHERE did = $1", did.as_str())
76 .fetch_optional(&state.db)
77 .await
78 .ok()
79 .flatten();
80 let existing = sqlx::query!(
81 "SELECT id FROM users WHERE handle = $1 AND did != $2",
82 handle,
83 did.as_str()
84 )
85 .fetch_optional(&state.db)
86 .await;
87 if let Ok(Some(_)) = existing {
88 return ApiError::HandleTaken.into_response();
89 }
90 let result = sqlx::query!(
91 "UPDATE users SET handle = $1 WHERE did = $2",
92 handle,
93 did.as_str()
94 )
95 .execute(&state.db)
96 .await;
97 match result {
98 Ok(r) => {
99 if r.rows_affected() == 0 {
100 return ApiError::AccountNotFound.into_response();
101 }
102 if let Some(old) = old_handle {
103 let _ = state.cache.delete(&format!("handle:{}", old)).await;
104 }
105 let _ = state.cache.delete(&format!("handle:{}", handle)).await;
106 let handle_typed = Handle::new_unchecked(&handle);
107 if let Err(e) = crate::api::repo::record::sequence_identity_event(
108 &state,
109 did,
110 Some(&handle_typed),
111 )
112 .await
113 {
114 warn!(
115 "Failed to sequence identity event for admin handle update: {}",
116 e
117 );
118 }
119 if let Err(e) =
120 crate::api::identity::did::update_plc_handle(&state, did.as_str(), &handle).await
121 {
122 warn!("Failed to update PLC handle for admin handle update: {}", e);
123 }
124 EmptyResponse::ok().into_response()
125 }
126 Err(e) => {
127 error!("DB error updating handle: {:?}", e);
128 ApiError::InternalError(None).into_response()
129 }
130 }
131}
132
133#[derive(Deserialize)]
134pub struct UpdateAccountPasswordInput {
135 pub did: Did,
136 pub password: PlainPassword,
137}
138
139pub async fn update_account_password(
140 State(state): State<AppState>,
141 _auth: BearerAuthAdmin,
142 Json(input): Json<UpdateAccountPasswordInput>,
143) -> Response {
144 let did = &input.did;
145 let password = input.password.trim();
146 if password.is_empty() {
147 return ApiError::InvalidRequest("password is required".into()).into_response();
148 }
149 let password_hash = match bcrypt::hash(password, bcrypt::DEFAULT_COST) {
150 Ok(h) => h,
151 Err(e) => {
152 error!("Failed to hash password: {:?}", e);
153 return ApiError::InternalError(None).into_response();
154 }
155 };
156 let result = sqlx::query!(
157 "UPDATE users SET password_hash = $1 WHERE did = $2",
158 password_hash,
159 did.as_str()
160 )
161 .execute(&state.db)
162 .await;
163 match result {
164 Ok(r) => {
165 if r.rows_affected() == 0 {
166 return ApiError::AccountNotFound.into_response();
167 }
168 EmptyResponse::ok().into_response()
169 }
170 Err(e) => {
171 error!("DB error updating password: {:?}", e);
172 ApiError::InternalError(None).into_response()
173 }
174 }
175}