Creating & posting records works. Also messed up newlines but will fix later.

This commit is contained in:
lewis
2025-12-14 23:55:04 +02:00
parent 86db6617af
commit c6f9062979
227 changed files with 2122 additions and 7842 deletions
-58
View File
@@ -12,7 +12,6 @@ use serde::{Deserialize, Serialize};
use serde_json::json;
use tracing::{error, info, warn};
use uuid::Uuid;
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CheckAccountStatusOutput {
@@ -26,7 +25,6 @@ pub struct CheckAccountStatusOutput {
pub expected_blobs: i64,
pub imported_blobs: i64,
}
pub async fn check_account_status(
State(state): State<AppState>,
headers: axum::http::HeaderMap,
@@ -37,12 +35,10 @@ pub async fn check_account_status(
Some(t) => t,
None => return ApiError::AuthenticationRequired.into_response(),
};
let did = match crate::auth::validate_bearer_token_allow_deactivated(&state.db, &token).await {
Ok(user) => user.did,
Err(e) => return ApiError::from(e).into_response(),
};
let user_id = match sqlx::query_scalar!("SELECT id FROM users WHERE did = $1", did)
.fetch_optional(&state.db)
.await
@@ -56,40 +52,32 @@ pub async fn check_account_status(
.into_response();
}
};
let user_status = sqlx::query!("SELECT deactivated_at FROM users WHERE did = $1", did)
.fetch_optional(&state.db)
.await;
let deactivated_at = match user_status {
Ok(Some(row)) => row.deactivated_at,
_ => None,
};
let repo_result = sqlx::query!("SELECT repo_root_cid FROM repos WHERE user_id = $1", user_id)
.fetch_optional(&state.db)
.await;
let repo_commit = match repo_result {
Ok(Some(row)) => row.repo_root_cid,
_ => String::new(),
};
let record_count: i64 = sqlx::query_scalar!("SELECT COUNT(*) FROM records WHERE repo_id = $1", user_id)
.fetch_one(&state.db)
.await
.unwrap_or(Some(0))
.unwrap_or(0);
let blob_count: i64 =
sqlx::query_scalar!("SELECT COUNT(*) FROM blobs WHERE created_by_user = $1", user_id)
.fetch_one(&state.db)
.await
.unwrap_or(Some(0))
.unwrap_or(0);
let valid_did = did.starts_with("did:");
(
StatusCode::OK,
Json(CheckAccountStatusOutput {
@@ -106,7 +94,6 @@ pub async fn check_account_status(
)
.into_response()
}
pub async fn activate_account(
State(state): State<AppState>,
headers: axum::http::HeaderMap,
@@ -117,22 +104,18 @@ pub async fn activate_account(
Some(t) => t,
None => return ApiError::AuthenticationRequired.into_response(),
};
let did = match crate::auth::validate_bearer_token_allow_deactivated(&state.db, &token).await {
Ok(user) => user.did,
Err(e) => return ApiError::from(e).into_response(),
};
let handle = sqlx::query_scalar!("SELECT handle FROM users WHERE did = $1", did)
.fetch_optional(&state.db)
.await
.ok()
.flatten();
let result = sqlx::query!("UPDATE users SET deactivated_at = NULL WHERE did = $1", did)
.execute(&state.db)
.await;
match result {
Ok(_) => {
if let Some(h) = handle {
@@ -150,13 +133,11 @@ pub async fn activate_account(
}
}
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DeactivateAccountInput {
pub delete_after: Option<String>,
}
pub async fn deactivate_account(
State(state): State<AppState>,
headers: axum::http::HeaderMap,
@@ -168,22 +149,18 @@ pub async fn deactivate_account(
Some(t) => t,
None => return ApiError::AuthenticationRequired.into_response(),
};
let did = match crate::auth::validate_bearer_token(&state.db, &token).await {
Ok(user) => user.did,
Err(e) => return ApiError::from(e).into_response(),
};
let handle = sqlx::query_scalar!("SELECT handle FROM users WHERE did = $1", did)
.fetch_optional(&state.db)
.await
.ok()
.flatten();
let result = sqlx::query!("UPDATE users SET deactivated_at = NOW() WHERE did = $1", did)
.execute(&state.db)
.await;
match result {
Ok(_) => {
if let Some(h) = handle {
@@ -201,7 +178,6 @@ pub async fn deactivate_account(
}
}
}
pub async fn request_account_delete(
State(state): State<AppState>,
headers: axum::http::HeaderMap,
@@ -212,12 +188,10 @@ pub async fn request_account_delete(
Some(t) => t,
None => return ApiError::AuthenticationRequired.into_response(),
};
let did = match crate::auth::validate_bearer_token_allow_deactivated(&state.db, &token).await {
Ok(user) => user.did,
Err(e) => return ApiError::from(e).into_response(),
};
let user_id = match sqlx::query_scalar!("SELECT id FROM users WHERE did = $1", did)
.fetch_optional(&state.db)
.await
@@ -231,10 +205,8 @@ pub async fn request_account_delete(
.into_response();
}
};
let confirmation_token = Uuid::new_v4().to_string();
let expires_at = Utc::now() + Duration::minutes(15);
let insert = sqlx::query!(
"INSERT INTO account_deletion_requests (token, did, expires_at) VALUES ($1, $2, $3)",
confirmation_token,
@@ -243,7 +215,6 @@ pub async fn request_account_delete(
)
.execute(&state.db)
.await;
if let Err(e) = insert {
error!("DB error creating deletion token: {:?}", e);
return (
@@ -252,26 +223,21 @@ pub async fn request_account_delete(
)
.into_response();
}
let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
if let Err(e) =
crate::notifications::enqueue_account_deletion(&state.db, user_id, &confirmation_token, &hostname).await
{
warn!("Failed to enqueue account deletion notification: {:?}", e);
}
info!("Account deletion requested for user {}", did);
(StatusCode::OK, Json(json!({}))).into_response()
}
#[derive(Deserialize)]
pub struct DeleteAccountInput {
pub did: String,
pub password: String,
pub token: String,
}
pub async fn delete_account(
State(state): State<AppState>,
Json(input): Json<DeleteAccountInput>,
@@ -279,7 +245,6 @@ pub async fn delete_account(
let did = input.did.trim();
let password = &input.password;
let token = input.token.trim();
if did.is_empty() {
return (
StatusCode::BAD_REQUEST,
@@ -287,7 +252,6 @@ pub async fn delete_account(
)
.into_response();
}
if password.is_empty() {
return (
StatusCode::BAD_REQUEST,
@@ -295,7 +259,6 @@ pub async fn delete_account(
)
.into_response();
}
if token.is_empty() {
return (
StatusCode::BAD_REQUEST,
@@ -303,14 +266,12 @@ pub async fn delete_account(
)
.into_response();
}
let user = sqlx::query!(
"SELECT id, password_hash, handle FROM users WHERE did = $1",
did
)
.fetch_optional(&state.db)
.await;
let (user_id, password_hash, handle) = match user {
Ok(Some(row)) => (row.id, row.password_hash, row.handle),
Ok(None) => {
@@ -329,7 +290,6 @@ pub async fn delete_account(
.into_response();
}
};
let password_valid = if verify(password, &password_hash).unwrap_or(false) {
true
} else {
@@ -340,12 +300,10 @@ pub async fn delete_account(
.fetch_all(&state.db)
.await
.unwrap_or_default();
app_pass_rows
.iter()
.any(|row| verify(password, &row.password_hash).unwrap_or(false))
};
if !password_valid {
return (
StatusCode::UNAUTHORIZED,
@@ -353,14 +311,12 @@ pub async fn delete_account(
)
.into_response();
}
let deletion_request = sqlx::query!(
"SELECT did, expires_at FROM account_deletion_requests WHERE token = $1",
token
)
.fetch_optional(&state.db)
.await;
let (token_did, expires_at) = match deletion_request {
Ok(Some(row)) => (row.did, row.expires_at),
Ok(None) => {
@@ -379,7 +335,6 @@ pub async fn delete_account(
.into_response();
}
};
if token_did != did {
return (
StatusCode::BAD_REQUEST,
@@ -387,19 +342,16 @@ pub async fn delete_account(
)
.into_response();
}
if Utc::now() > expires_at {
let _ = sqlx::query!("DELETE FROM account_deletion_requests WHERE token = $1", token)
.execute(&state.db)
.await;
return (
StatusCode::BAD_REQUEST,
Json(json!({"error": "ExpiredToken", "message": "Token has expired"})),
)
.into_response();
}
let mut tx = match state.db.begin().await {
Ok(tx) => tx,
Err(e) => {
@@ -411,44 +363,34 @@ pub async fn delete_account(
.into_response();
}
};
let deletion_result: Result<(), sqlx::Error> = async {
sqlx::query!("DELETE FROM session_tokens WHERE did = $1", did)
.execute(&mut *tx)
.await?;
sqlx::query!("DELETE FROM records WHERE repo_id = $1", user_id)
.execute(&mut *tx)
.await?;
sqlx::query!("DELETE FROM repos WHERE user_id = $1", user_id)
.execute(&mut *tx)
.await?;
sqlx::query!("DELETE FROM blobs WHERE created_by_user = $1", user_id)
.execute(&mut *tx)
.await?;
sqlx::query!("DELETE FROM user_keys WHERE user_id = $1", user_id)
.execute(&mut *tx)
.await?;
sqlx::query!("DELETE FROM app_passwords WHERE user_id = $1", user_id)
.execute(&mut *tx)
.await?;
sqlx::query!("DELETE FROM account_deletion_requests WHERE did = $1", did)
.execute(&mut *tx)
.await?;
sqlx::query!("DELETE FROM users WHERE id = $1", user_id)
.execute(&mut *tx)
.await?;
Ok(())
}
.await;
match deletion_result {
Ok(()) => {
if let Err(e) = tx.commit().await {
-20
View File
@@ -11,7 +11,6 @@ use axum::{
use serde::{Deserialize, Serialize};
use serde_json::json;
use tracing::{error, warn};
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct AppPassword {
@@ -19,12 +18,10 @@ pub struct AppPassword {
pub created_at: String,
pub privileged: bool,
}
#[derive(Serialize)]
pub struct ListAppPasswordsOutput {
pub passwords: Vec<AppPassword>,
}
pub async fn list_app_passwords(
State(state): State<AppState>,
BearerAuth(auth_user): BearerAuth,
@@ -33,7 +30,6 @@ pub async fn list_app_passwords(
Ok(id) => id,
Err(e) => return ApiError::from(e).into_response(),
};
match sqlx::query!(
"SELECT name, created_at, privileged FROM app_passwords WHERE user_id = $1 ORDER BY created_at DESC",
user_id
@@ -50,7 +46,6 @@ pub async fn list_app_passwords(
privileged: row.privileged,
})
.collect();
Json(ListAppPasswordsOutput { passwords }).into_response()
}
Err(e) => {
@@ -59,13 +54,11 @@ pub async fn list_app_passwords(
}
}
}
#[derive(Deserialize)]
pub struct CreateAppPasswordInput {
pub name: String,
pub privileged: Option<bool>,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CreateAppPasswordOutput {
@@ -74,7 +67,6 @@ pub struct CreateAppPasswordOutput {
pub created_at: String,
pub privileged: bool,
}
pub async fn create_app_password(
State(state): State<AppState>,
headers: HeaderMap,
@@ -92,17 +84,14 @@ pub async fn create_app_password(
})),
).into_response();
}
let user_id = match get_user_id_by_did(&state.db, &auth_user.did).await {
Ok(id) => id,
Err(e) => return ApiError::from(e).into_response(),
};
let name = input.name.trim();
if name.is_empty() {
return ApiError::InvalidRequest("name is required".into()).into_response();
}
let existing = sqlx::query!(
"SELECT id FROM app_passwords WHERE user_id = $1 AND name = $2",
user_id,
@@ -110,11 +99,9 @@ pub async fn create_app_password(
)
.fetch_optional(&state.db)
.await;
if let Ok(Some(_)) = existing {
return ApiError::DuplicateAppPassword.into_response();
}
let password: String = (0..4)
.map(|_| {
use rand::Rng;
@@ -126,7 +113,6 @@ pub async fn create_app_password(
})
.collect::<Vec<String>>()
.join("-");
let password_hash = match bcrypt::hash(&password, bcrypt::DEFAULT_COST) {
Ok(h) => h,
Err(e) => {
@@ -134,10 +120,8 @@ pub async fn create_app_password(
return ApiError::InternalError.into_response();
}
};
let privileged = input.privileged.unwrap_or(false);
let created_at = chrono::Utc::now();
match sqlx::query!(
"INSERT INTO app_passwords (user_id, name, password_hash, created_at, privileged) VALUES ($1, $2, $3, $4, $5)",
user_id,
@@ -162,12 +146,10 @@ pub async fn create_app_password(
}
}
}
#[derive(Deserialize)]
pub struct RevokeAppPasswordInput {
pub name: String,
}
pub async fn revoke_app_password(
State(state): State<AppState>,
BearerAuth(auth_user): BearerAuth,
@@ -177,12 +159,10 @@ pub async fn revoke_app_password(
Ok(id) => id,
Err(e) => return ApiError::from(e).into_response(),
};
let name = input.name.trim();
if name.is_empty() {
return ApiError::InvalidRequest("name is required".into()).into_response();
}
match sqlx::query!(
"DELETE FROM app_passwords WHERE user_id = $1 AND name = $2",
user_id,
-48
View File
@@ -10,17 +10,14 @@ use chrono::{Duration, Utc};
use serde::Deserialize;
use serde_json::json;
use tracing::{error, info, warn};
fn generate_confirmation_code() -> String {
crate::util::generate_token_code()
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RequestEmailUpdateInput {
pub email: String,
}
pub async fn request_email_update(
State(state): State<AppState>,
headers: axum::http::HeaderMap,
@@ -37,7 +34,6 @@ pub async fn request_email_update(
})),
).into_response();
}
let token = match crate::auth::extract_bearer_token_from_header(
headers.get("Authorization").and_then(|h| h.to_str().ok())
) {
@@ -50,13 +46,11 @@ pub async fn request_email_update(
.into_response();
}
};
let auth_result = crate::auth::validate_bearer_token(&state.db, &token).await;
let did = match auth_result {
Ok(user) => user.did,
Err(e) => return ApiError::from(e).into_response(),
};
let user = match sqlx::query!("SELECT id, handle FROM users WHERE did = $1", did)
.fetch_optional(&state.db)
.await
@@ -72,7 +66,6 @@ pub async fn request_email_update(
};
let user_id = user.id;
let handle = user.handle;
let email = input.email.trim().to_lowercase();
if !crate::api::validation::is_valid_email(&email) {
return (
@@ -81,11 +74,9 @@ pub async fn request_email_update(
)
.into_response();
}
let exists = sqlx::query!("SELECT 1 as one FROM users WHERE LOWER(email) = $1", email)
.fetch_optional(&state.db)
.await;
if let Ok(Some(_)) = exists {
return (
StatusCode::BAD_REQUEST,
@@ -93,10 +84,8 @@ pub async fn request_email_update(
)
.into_response();
}
let code = generate_confirmation_code();
let expires_at = Utc::now() + Duration::minutes(10);
let update = sqlx::query!(
"UPDATE users SET email_pending_verification = $1, email_confirmation_code = $2, email_confirmation_code_expires_at = $3 WHERE id = $4",
email,
@@ -106,7 +95,6 @@ pub async fn request_email_update(
)
.execute(&state.db)
.await;
if let Err(e) = update {
error!("DB error setting email update code: {:?}", e);
return (
@@ -115,7 +103,6 @@ pub async fn request_email_update(
)
.into_response();
}
let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
if let Err(e) = crate::notifications::enqueue_email_update(
&state.db,
@@ -129,19 +116,15 @@ pub async fn request_email_update(
{
warn!("Failed to enqueue email update notification: {:?}", e);
}
info!("Email update requested for user {}", user_id);
(StatusCode::OK, Json(json!({ "tokenRequired": true }))).into_response()
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ConfirmEmailInput {
pub email: String,
pub token: String,
}
pub async fn confirm_email(
State(state): State<AppState>,
headers: axum::http::HeaderMap,
@@ -158,7 +141,6 @@ pub async fn confirm_email(
})),
).into_response();
}
let token = match crate::auth::extract_bearer_token_from_header(
headers.get("Authorization").and_then(|h| h.to_str().ok())
) {
@@ -171,13 +153,11 @@ pub async fn confirm_email(
.into_response();
}
};
let auth_result = crate::auth::validate_bearer_token(&state.db, &token).await;
let did = match auth_result {
Ok(user) => user.did,
Err(e) => return ApiError::from(e).into_response(),
};
let user = match sqlx::query!(
"SELECT id, email_confirmation_code, email_confirmation_code_expires_at, email_pending_verification FROM users WHERE did = $1",
did
@@ -198,10 +178,8 @@ pub async fn confirm_email(
let stored_code = user.email_confirmation_code;
let expires_at = user.email_confirmation_code_expires_at;
let email_pending_verification = user.email_pending_verification;
let email = input.email.trim().to_lowercase();
let confirmation_code = input.token.trim();
let (pending_email, saved_code, expiry) = match (email_pending_verification, stored_code, expires_at) {
(Some(p), Some(c), Some(e)) => (p, c, e),
_ => {
@@ -212,7 +190,6 @@ pub async fn confirm_email(
.into_response();
}
};
if pending_email != email {
return (
StatusCode::BAD_REQUEST,
@@ -220,7 +197,6 @@ pub async fn confirm_email(
)
.into_response();
}
if saved_code != confirmation_code {
return (
StatusCode::BAD_REQUEST,
@@ -228,7 +204,6 @@ pub async fn confirm_email(
)
.into_response();
}
if Utc::now() > expiry {
return (
StatusCode::BAD_REQUEST,
@@ -236,7 +211,6 @@ pub async fn confirm_email(
)
.into_response();
}
let update = sqlx::query!(
"UPDATE users SET email = $1, email_pending_verification = NULL, email_confirmation_code = NULL, email_confirmation_code_expires_at = NULL WHERE id = $2",
pending_email,
@@ -244,7 +218,6 @@ pub async fn confirm_email(
)
.execute(&state.db)
.await;
if let Err(e) = update {
error!("DB error finalizing email update: {:?}", e);
if e.as_database_error().map(|db_err| db_err.is_unique_violation()).unwrap_or(false) {
@@ -254,19 +227,15 @@ pub async fn confirm_email(
)
.into_response();
}
return (
StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({"error": "InternalError"})),
)
.into_response();
}
info!("Email updated for user {}", user_id);
(StatusCode::OK, Json(json!({}))).into_response()
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct UpdateEmailInput {
@@ -275,7 +244,6 @@ pub struct UpdateEmailInput {
pub email_auth_factor: Option<bool>,
pub token: Option<String>,
}
pub async fn update_email(
State(state): State<AppState>,
headers: axum::http::HeaderMap,
@@ -293,13 +261,11 @@ pub async fn update_email(
.into_response();
}
};
let auth_result = crate::auth::validate_bearer_token(&state.db, &token).await;
let did = match auth_result {
Ok(user) => user.did,
Err(e) => return ApiError::from(e).into_response(),
};
let user = match sqlx::query!(
"SELECT id, email, email_confirmation_code, email_confirmation_code_expires_at, email_pending_verification FROM users WHERE did = $1",
did
@@ -321,7 +287,6 @@ pub async fn update_email(
let stored_code = user.email_confirmation_code;
let expires_at = user.email_confirmation_code_expires_at;
let email_pending_verification = user.email_pending_verification;
let new_email = input.email.trim().to_lowercase();
if !crate::api::validation::is_valid_email(&new_email) {
return (
@@ -330,15 +295,12 @@ pub async fn update_email(
)
.into_response();
}
if let Some(ref current) = current_email {
if new_email == current.to_lowercase() {
return (StatusCode::OK, Json(json!({}))).into_response();
}
}
let email_confirmed = stored_code.is_some() && email_pending_verification.is_some();
if email_confirmed {
let confirmation_token = match &input.token {
Some(t) => t.trim(),
@@ -350,7 +312,6 @@ pub async fn update_email(
.into_response();
}
};
let pending_email = match email_pending_verification {
Some(p) => p,
None => {
@@ -361,7 +322,6 @@ pub async fn update_email(
.into_response();
}
};
if pending_email.to_lowercase() != new_email {
return (
StatusCode::BAD_REQUEST,
@@ -369,7 +329,6 @@ pub async fn update_email(
)
.into_response();
}
let saved_code = match stored_code {
Some(c) => c,
None => {
@@ -380,7 +339,6 @@ pub async fn update_email(
.into_response();
}
};
if saved_code != confirmation_token {
return (
StatusCode::BAD_REQUEST,
@@ -388,7 +346,6 @@ pub async fn update_email(
)
.into_response();
}
if let Some(exp) = expires_at {
if Utc::now() > exp {
return (
@@ -399,7 +356,6 @@ pub async fn update_email(
}
}
}
let exists = sqlx::query!(
"SELECT 1 as one FROM users WHERE LOWER(email) = $1 AND id != $2",
new_email,
@@ -407,7 +363,6 @@ pub async fn update_email(
)
.fetch_optional(&state.db)
.await;
if let Ok(Some(_)) = exists {
return (
StatusCode::BAD_REQUEST,
@@ -415,7 +370,6 @@ pub async fn update_email(
)
.into_response();
}
let update = sqlx::query!(
r#"
UPDATE users
@@ -431,7 +385,6 @@ pub async fn update_email(
)
.execute(&state.db)
.await;
match update {
Ok(_) => {
info!("Email updated for user {}", user_id);
@@ -449,7 +402,6 @@ pub async fn update_email(
)
.into_response();
}
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({"error": "InternalError"})),
-35
View File
@@ -10,19 +10,16 @@ use axum::{
use serde::{Deserialize, Serialize};
use tracing::error;
use uuid::Uuid;
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CreateInviteCodeInput {
pub use_count: i32,
pub for_account: Option<String>,
}
#[derive(Serialize)]
pub struct CreateInviteCodeOutput {
pub code: String,
}
pub async fn create_invite_code(
State(state): State<AppState>,
BearerAuth(auth_user): BearerAuth,
@@ -31,12 +28,10 @@ pub async fn create_invite_code(
if input.use_count < 1 {
return ApiError::InvalidRequest("useCount must be at least 1".into()).into_response();
}
let user_id = match get_user_id_by_did(&state.db, &auth_user.did).await {
Ok(id) => id,
Err(e) => return ApiError::from(e).into_response(),
};
let creator_user_id = if let Some(for_account) = &input.for_account {
match sqlx::query!("SELECT id FROM users WHERE did = $1", for_account)
.fetch_optional(&state.db)
@@ -52,7 +47,6 @@ pub async fn create_invite_code(
} else {
user_id
};
let user_invites_disabled = sqlx::query_scalar!(
"SELECT invites_disabled FROM users WHERE did = $1",
auth_user.did
@@ -67,13 +61,10 @@ pub async fn create_invite_code(
.flatten()
.flatten()
.unwrap_or(false);
if user_invites_disabled {
return ApiError::InvitesDisabled.into_response();
}
let code = Uuid::new_v4().to_string();
match sqlx::query!(
"INSERT INTO invite_codes (code, available_uses, created_by_user) VALUES ($1, $2, $3)",
code,
@@ -90,7 +81,6 @@ pub async fn create_invite_code(
}
}
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CreateInviteCodesInput {
@@ -98,18 +88,15 @@ pub struct CreateInviteCodesInput {
pub use_count: i32,
pub for_accounts: Option<Vec<String>>,
}
#[derive(Serialize)]
pub struct CreateInviteCodesOutput {
pub codes: Vec<AccountCodes>,
}
#[derive(Serialize)]
pub struct AccountCodes {
pub account: String,
pub codes: Vec<String>,
}
pub async fn create_invite_codes(
State(state): State<AppState>,
BearerAuth(auth_user): BearerAuth,
@@ -118,22 +105,17 @@ pub async fn create_invite_codes(
if input.use_count < 1 {
return ApiError::InvalidRequest("useCount must be at least 1".into()).into_response();
}
let user_id = match get_user_id_by_did(&state.db, &auth_user.did).await {
Ok(id) => id,
Err(e) => return ApiError::from(e).into_response(),
};
let code_count = input.code_count.unwrap_or(1).max(1);
let for_accounts = input.for_accounts.unwrap_or_default();
let mut result_codes = Vec::new();
if for_accounts.is_empty() {
let mut codes = Vec::new();
for _ in 0..code_count {
let code = Uuid::new_v4().to_string();
if let Err(e) = sqlx::query!(
"INSERT INTO invite_codes (code, available_uses, created_by_user) VALUES ($1, $2, $3)",
code,
@@ -146,10 +128,8 @@ pub async fn create_invite_codes(
error!("DB error creating invite code: {:?}", e);
return ApiError::InternalError.into_response();
}
codes.push(code);
}
result_codes.push(AccountCodes {
account: "admin".to_string(),
codes,
@@ -167,11 +147,9 @@ pub async fn create_invite_codes(
return ApiError::InternalError.into_response();
}
};
let mut codes = Vec::new();
for _ in 0..code_count {
let code = Uuid::new_v4().to_string();
if let Err(e) = sqlx::query!(
"INSERT INTO invite_codes (code, available_uses, created_by_user) VALUES ($1, $2, $3)",
code,
@@ -184,27 +162,22 @@ pub async fn create_invite_codes(
error!("DB error creating invite code: {:?}", e);
return ApiError::InternalError.into_response();
}
codes.push(code);
}
result_codes.push(AccountCodes {
account: account_did,
codes,
});
}
}
Json(CreateInviteCodesOutput { codes: result_codes }).into_response()
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct GetAccountInviteCodesParams {
pub include_used: Option<bool>,
pub create_available: Option<bool>,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct InviteCode {
@@ -216,19 +189,16 @@ pub struct InviteCode {
pub created_at: String,
pub uses: Vec<InviteCodeUse>,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct InviteCodeUse {
pub used_by: String,
pub used_at: String,
}
#[derive(Serialize)]
pub struct GetAccountInviteCodesOutput {
pub codes: Vec<InviteCode>,
}
pub async fn get_account_invite_codes(
State(state): State<AppState>,
BearerAuth(auth_user): BearerAuth,
@@ -238,9 +208,7 @@ pub async fn get_account_invite_codes(
Ok(id) => id,
Err(e) => return ApiError::from(e).into_response(),
};
let include_used = params.include_used.unwrap_or(true);
let codes_rows = match sqlx::query!(
r#"
SELECT code, available_uses, created_at, disabled
@@ -265,7 +233,6 @@ pub async fn get_account_invite_codes(
return ApiError::InternalError.into_response();
}
};
let mut codes = Vec::new();
for row in codes_rows {
let uses = sqlx::query!(
@@ -290,7 +257,6 @@ pub async fn get_account_invite_codes(
.collect()
})
.unwrap_or_default();
codes.push(InviteCode {
code: row.code,
available: row.available_uses,
@@ -301,6 +267,5 @@ pub async fn get_account_invite_codes(
uses,
});
}
Json(GetAccountInviteCodesOutput { codes }).into_response()
}
-6
View File
@@ -1,9 +1,7 @@
use crate::state::AppState;
use axum::{Json, extract::State, http::StatusCode, response::IntoResponse};
use serde_json::json;
use tracing::error;
pub async fn robots_txt() -> impl IntoResponse {
(
StatusCode::OK,
@@ -11,24 +9,20 @@ pub async fn robots_txt() -> impl IntoResponse {
"# Hello!\n\n# Crawling the public API is allowed\nUser-agent: *\nAllow: /\n",
)
}
pub async fn describe_server() -> impl IntoResponse {
let pds_hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
let domains_str =
std::env::var("AVAILABLE_USER_DOMAINS").unwrap_or_else(|_| pds_hostname.clone());
let domains: Vec<&str> = domains_str.split(',').map(|s| s.trim()).collect();
let invite_code_required = std::env::var("INVITE_CODE_REQUIRED")
.map(|v| v == "true" || v == "1")
.unwrap_or(false);
Json(json!({
"availableUserDomains": domains,
"inviteCodeRequired": invite_code_required,
"did": format!("did:web:{}", pds_hostname)
}))
}
pub async fn health(State(state): State<AppState>) -> impl IntoResponse {
match sqlx::query!("SELECT 1 as one").fetch_one(&state.db).await {
Ok(_) => (StatusCode::OK, "OK"),
-1
View File
@@ -7,7 +7,6 @@ pub mod password;
pub mod service_auth;
pub mod session;
pub mod signing_key;
pub use account_status::{
activate_account, check_account_status, deactivate_account, delete_account,
request_account_delete,
-32
View File
@@ -10,11 +10,9 @@ use chrono::{Duration, Utc};
use serde::Deserialize;
use serde_json::json;
use tracing::{error, info, warn};
fn generate_reset_code() -> String {
crate::util::generate_token_code()
}
fn extract_client_ip(headers: &HeaderMap) -> String {
if let Some(forwarded) = headers.get("x-forwarded-for") {
if let Ok(value) = forwarded.to_str() {
@@ -30,12 +28,10 @@ fn extract_client_ip(headers: &HeaderMap) -> String {
}
"unknown".to_string()
}
#[derive(Deserialize)]
pub struct RequestPasswordResetInput {
pub email: String,
}
pub async fn request_password_reset(
State(state): State<AppState>,
headers: HeaderMap,
@@ -53,7 +49,6 @@ pub async fn request_password_reset(
)
.into_response();
}
let email = input.email.trim().to_lowercase();
if email.is_empty() {
return (
@@ -62,11 +57,9 @@ pub async fn request_password_reset(
)
.into_response();
}
let user = sqlx::query!("SELECT id FROM users WHERE LOWER(email) = $1", email)
.fetch_optional(&state.db)
.await;
let user_id = match user {
Ok(Some(row)) => row.id,
Ok(None) => {
@@ -82,10 +75,8 @@ pub async fn request_password_reset(
.into_response();
}
};
let code = generate_reset_code();
let expires_at = Utc::now() + Duration::minutes(10);
let update = sqlx::query!(
"UPDATE users SET password_reset_code = $1, password_reset_code_expires_at = $2 WHERE id = $3",
code,
@@ -94,7 +85,6 @@ pub async fn request_password_reset(
)
.execute(&state.db)
.await;
if let Err(e) = update {
error!("DB error setting reset code: {:?}", e);
return (
@@ -103,25 +93,20 @@ pub async fn request_password_reset(
)
.into_response();
}
let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
if let Err(e) =
crate::notifications::enqueue_password_reset(&state.db, user_id, &code, &hostname).await
{
warn!("Failed to enqueue password reset notification: {:?}", e);
}
info!("Password reset requested for user {}", user_id);
(StatusCode::OK, Json(json!({}))).into_response()
}
#[derive(Deserialize)]
pub struct ResetPasswordInput {
pub token: String,
pub password: String,
}
pub async fn reset_password(
State(state): State<AppState>,
headers: HeaderMap,
@@ -138,10 +123,8 @@ pub async fn reset_password(
})),
).into_response();
}
let token = input.token.trim();
let password = &input.password;
if token.is_empty() {
return (
StatusCode::BAD_REQUEST,
@@ -149,7 +132,6 @@ pub async fn reset_password(
)
.into_response();
}
if password.is_empty() {
return (
StatusCode::BAD_REQUEST,
@@ -157,14 +139,12 @@ pub async fn reset_password(
)
.into_response();
}
let user = sqlx::query!(
"SELECT id, password_reset_code, password_reset_code_expires_at FROM users WHERE password_reset_code = $1",
token
)
.fetch_optional(&state.db)
.await;
let (user_id, expires_at) = match user {
Ok(Some(row)) => {
let expires = row.password_reset_code_expires_at;
@@ -186,7 +166,6 @@ pub async fn reset_password(
.into_response();
}
};
if let Some(exp) = expires_at {
if Utc::now() > exp {
if let Err(e) = sqlx::query!(
@@ -198,7 +177,6 @@ pub async fn reset_password(
{
error!("Failed to clear expired reset code: {:?}", e);
}
return (
StatusCode::BAD_REQUEST,
Json(json!({"error": "ExpiredToken", "message": "Token has expired"})),
@@ -212,7 +190,6 @@ pub async fn reset_password(
)
.into_response();
}
let password_hash = match hash(password, DEFAULT_COST) {
Ok(h) => h,
Err(e) => {
@@ -224,7 +201,6 @@ pub async fn reset_password(
.into_response();
}
};
let mut tx = match state.db.begin().await {
Ok(tx) => tx,
Err(e) => {
@@ -236,7 +212,6 @@ pub async fn reset_password(
.into_response();
}
};
if let Err(e) = sqlx::query!(
"UPDATE users SET password_hash = $1, password_reset_code = NULL, password_reset_code_expires_at = NULL WHERE id = $2",
password_hash,
@@ -252,7 +227,6 @@ pub async fn reset_password(
)
.into_response();
}
let user_did = match sqlx::query_scalar!(
"SELECT did FROM users WHERE id = $1",
user_id
@@ -270,7 +244,6 @@ pub async fn reset_password(
.into_response();
}
};
let session_jtis: Vec<String> = match sqlx::query_scalar!(
"SELECT access_jti FROM session_tokens WHERE did = $1",
user_did
@@ -284,7 +257,6 @@ pub async fn reset_password(
vec![]
}
};
if let Err(e) = sqlx::query!("DELETE FROM session_tokens WHERE did = $1", user_did)
.execute(&mut *tx)
.await
@@ -296,7 +268,6 @@ pub async fn reset_password(
)
.into_response();
}
if let Err(e) = tx.commit().await {
error!("Failed to commit password reset transaction: {:?}", e);
return (
@@ -305,15 +276,12 @@ pub async fn reset_password(
)
.into_response();
}
for jti in session_jtis {
let cache_key = format!("auth:session:{}:{}", user_did, jti);
if let Err(e) = state.cache.delete(&cache_key).await {
warn!("Failed to invalidate session cache for {}: {:?}", cache_key, e);
}
}
info!("Password reset completed for user {}", user_id);
(StatusCode::OK, Json(json!({}))).into_response()
}
-8
View File
@@ -9,19 +9,16 @@ use axum::{
use serde::{Deserialize, Serialize};
use serde_json::json;
use tracing::error;
#[derive(Deserialize)]
pub struct GetServiceAuthParams {
pub aud: String,
pub lxm: Option<String>,
pub exp: Option<i64>,
}
#[derive(Serialize)]
pub struct GetServiceAuthOutput {
pub token: String,
}
pub async fn get_service_auth(
State(state): State<AppState>,
headers: axum::http::HeaderMap,
@@ -33,19 +30,15 @@ pub async fn get_service_auth(
Some(t) => t,
None => return ApiError::AuthenticationRequired.into_response(),
};
let auth_user = match crate::auth::validate_bearer_token(&state.db, &token).await {
Ok(user) => user,
Err(e) => return ApiError::from(e).into_response(),
};
let key_bytes = match auth_user.key_bytes {
Some(kb) => kb,
None => return ApiError::AuthenticationFailedMsg("OAuth tokens cannot create service auth".into()).into_response(),
};
let lxm = params.lxm.as_deref().unwrap_or("*");
let service_token = match crate::auth::create_service_token(&auth_user.did, &params.aud, lxm, &key_bytes)
{
Ok(t) => t,
@@ -58,6 +51,5 @@ pub async fn get_service_auth(
.into_response();
}
};
(StatusCode::OK, Json(GetServiceAuthOutput { token: service_token })).into_response()
}
-62
View File
@@ -12,7 +12,6 @@ use chrono::Utc;
use serde::{Deserialize, Serialize};
use serde_json::json;
use tracing::{error, info, warn};
fn extract_client_ip(headers: &HeaderMap) -> String {
if let Some(forwarded) = headers.get("x-forwarded-for") {
if let Ok(value) = forwarded.to_str() {
@@ -28,13 +27,11 @@ fn extract_client_ip(headers: &HeaderMap) -> String {
}
"unknown".to_string()
}
#[derive(Deserialize)]
pub struct CreateSessionInput {
pub identifier: String,
pub password: String,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CreateSessionOutput {
@@ -43,14 +40,12 @@ pub struct CreateSessionOutput {
pub handle: String,
pub did: String,
}
pub async fn create_session(
State(state): State<AppState>,
headers: HeaderMap,
Json(input): Json<CreateSessionInput>,
) -> Response {
info!("create_session called");
let client_ip = extract_client_ip(&headers);
if !state.check_rate_limit(RateLimitKind::Login, &client_ip).await {
warn!(ip = %client_ip, "Login rate limit exceeded");
@@ -63,7 +58,6 @@ pub async fn create_session(
)
.into_response();
}
let row = match sqlx::query!(
r#"SELECT
u.id, u.did, u.handle, u.password_hash,
@@ -88,7 +82,6 @@ pub async fn create_session(
return ApiError::InternalError.into_response();
}
};
let key_bytes = match crate::config::decrypt_key(&row.key_bytes, row.encryption_version) {
Ok(k) => k,
Err(e) => {
@@ -96,7 +89,6 @@ pub async fn create_session(
return ApiError::InternalError.into_response();
}
};
let password_valid = if verify(&input.password, &row.password_hash).unwrap_or(false) {
true
} else {
@@ -107,20 +99,16 @@ pub async fn create_session(
.fetch_all(&state.db)
.await
.unwrap_or_default();
app_passwords.iter().any(|app| verify(&input.password, &app.password_hash).unwrap_or(false))
};
if !password_valid {
warn!("Password verification failed for login attempt");
return ApiError::AuthenticationFailedMsg("Invalid identifier or password".into()).into_response();
}
let is_verified = row.email_confirmed
|| row.discord_verified
|| row.telegram_verified
|| row.signal_verified;
if !is_verified {
warn!("Login attempt for unverified account: {}", row.did);
return (
@@ -132,7 +120,6 @@ pub async fn create_session(
})),
).into_response();
}
let access_meta = match crate::auth::create_access_token_with_metadata(&row.did, &key_bytes) {
Ok(m) => m,
Err(e) => {
@@ -140,7 +127,6 @@ pub async fn create_session(
return ApiError::InternalError.into_response();
}
};
let refresh_meta = match crate::auth::create_refresh_token_with_metadata(&row.did, &key_bytes) {
Ok(m) => m,
Err(e) => {
@@ -148,7 +134,6 @@ pub async fn create_session(
return ApiError::InternalError.into_response();
}
};
if let Err(e) = sqlx::query!(
"INSERT INTO session_tokens (did, access_jti, refresh_jti, access_expires_at, refresh_expires_at) VALUES ($1, $2, $3, $4, $5)",
row.did,
@@ -163,7 +148,6 @@ pub async fn create_session(
error!("Failed to insert session: {:?}", e);
return ApiError::InternalError.into_response();
}
Json(CreateSessionOutput {
access_jwt: access_meta.token,
refresh_jwt: refresh_meta.token,
@@ -171,7 +155,6 @@ pub async fn create_session(
did: row.did,
}).into_response()
}
pub async fn get_session(
State(state): State<AppState>,
BearerAuth(auth_user): BearerAuth,
@@ -211,7 +194,6 @@ pub async fn get_session(
}
}
}
pub async fn delete_session(
State(state): State<AppState>,
headers: axum::http::HeaderMap,
@@ -222,14 +204,11 @@ pub async fn delete_session(
Some(t) => t,
None => return ApiError::AuthenticationRequired.into_response(),
};
let jti = match crate::auth::get_jti_from_token(&token) {
Ok(jti) => jti,
Err(_) => return ApiError::AuthenticationFailed.into_response(),
};
let did = crate::auth::get_did_from_token(&token).ok();
match sqlx::query!("DELETE FROM session_tokens WHERE access_jti = $1", jti)
.execute(&state.db)
.await
@@ -248,7 +227,6 @@ pub async fn delete_session(
}
}
}
pub async fn refresh_session(
State(state): State<AppState>,
headers: axum::http::HeaderMap,
@@ -264,19 +242,16 @@ pub async fn refresh_session(
})),
).into_response();
}
let refresh_token = match crate::auth::extract_bearer_token_from_header(
headers.get("Authorization").and_then(|h| h.to_str().ok())
) {
Some(t) => t,
None => return ApiError::AuthenticationRequired.into_response(),
};
let refresh_jti = match crate::auth::get_jti_from_token(&refresh_token) {
Ok(jti) => jti,
Err(_) => return ApiError::AuthenticationFailedMsg("Invalid token format".into()).into_response(),
};
let mut tx = match state.db.begin().await {
Ok(tx) => tx,
Err(e) => {
@@ -284,7 +259,6 @@ pub async fn refresh_session(
return ApiError::InternalError.into_response();
}
};
if let Ok(Some(session_id)) = sqlx::query_scalar!(
"SELECT session_id FROM used_refresh_tokens WHERE refresh_jti = $1 FOR UPDATE",
refresh_jti
@@ -299,7 +273,6 @@ pub async fn refresh_session(
let _ = tx.commit().await;
return ApiError::ExpiredTokenMsg("Refresh token has been revoked due to suspected compromise".into()).into_response();
}
let session_row = match sqlx::query!(
r#"SELECT st.id, st.did, k.key_bytes, k.encryption_version
FROM session_tokens st
@@ -319,7 +292,6 @@ pub async fn refresh_session(
return ApiError::InternalError.into_response();
}
};
let key_bytes = match crate::config::decrypt_key(&session_row.key_bytes, session_row.encryption_version) {
Ok(k) => k,
Err(e) => {
@@ -327,11 +299,9 @@ pub async fn refresh_session(
return ApiError::InternalError.into_response();
}
};
if crate::auth::verify_refresh_token(&refresh_token, &key_bytes).is_err() {
return ApiError::AuthenticationFailedMsg("Invalid refresh token".into()).into_response();
}
let new_access_meta = match crate::auth::create_access_token_with_metadata(&session_row.did, &key_bytes) {
Ok(m) => m,
Err(e) => {
@@ -339,7 +309,6 @@ pub async fn refresh_session(
return ApiError::InternalError.into_response();
}
};
let new_refresh_meta = match crate::auth::create_refresh_token_with_metadata(&session_row.did, &key_bytes) {
Ok(m) => m,
Err(e) => {
@@ -347,7 +316,6 @@ pub async fn refresh_session(
return ApiError::InternalError.into_response();
}
};
match sqlx::query!(
"INSERT INTO used_refresh_tokens (refresh_jti, session_id) VALUES ($1, $2) ON CONFLICT (refresh_jti) DO NOTHING",
refresh_jti,
@@ -370,7 +338,6 @@ pub async fn refresh_session(
}
Ok(_) => {}
}
if let Err(e) = sqlx::query!(
"UPDATE session_tokens SET access_jti = $1, refresh_jti = $2, access_expires_at = $3, refresh_expires_at = $4, updated_at = NOW() WHERE id = $5",
new_access_meta.jti,
@@ -385,12 +352,10 @@ pub async fn refresh_session(
error!("Database error updating session: {:?}", e);
return ApiError::InternalError.into_response();
}
if let Err(e) = tx.commit().await {
error!("Failed to commit transaction: {:?}", e);
return ApiError::InternalError.into_response();
}
match sqlx::query!(
r#"SELECT
handle, email, email_confirmed,
@@ -430,14 +395,12 @@ pub async fn refresh_session(
}
}
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ConfirmSignupInput {
pub did: String,
pub verification_code: String,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ConfirmSignupOutput {
@@ -450,13 +413,11 @@ pub struct ConfirmSignupOutput {
pub preferred_channel: String,
pub preferred_channel_verified: bool,
}
pub async fn confirm_signup(
State(state): State<AppState>,
Json(input): Json<ConfirmSignupInput>,
) -> Response {
info!("confirm_signup called for DID: {}", input.did);
let row = match sqlx::query!(
r#"SELECT
u.id, u.did, u.handle, u.email,
@@ -482,7 +443,6 @@ pub async fn confirm_signup(
return ApiError::InternalError.into_response();
}
};
let stored_code = match &row.email_confirmation_code {
Some(code) => code,
None => {
@@ -490,19 +450,16 @@ pub async fn confirm_signup(
return ApiError::InvalidRequest("No pending verification".into()).into_response();
}
};
if stored_code != &input.verification_code {
warn!("Invalid verification code for user: {}", input.did);
return ApiError::InvalidRequest("Invalid verification code".into()).into_response();
}
if let Some(expires_at) = row.email_confirmation_code_expires_at {
if expires_at < Utc::now() {
warn!("Verification code expired for user: {}", input.did);
return ApiError::ExpiredTokenMsg("Verification code has expired".into()).into_response();
}
}
let key_bytes = match crate::config::decrypt_key(&row.key_bytes, row.encryption_version) {
Ok(k) => k,
Err(e) => {
@@ -510,19 +467,16 @@ pub async fn confirm_signup(
return ApiError::InternalError.into_response();
}
};
let verified_column = match row.channel {
crate::notifications::NotificationChannel::Email => "email_confirmed",
crate::notifications::NotificationChannel::Discord => "discord_verified",
crate::notifications::NotificationChannel::Telegram => "telegram_verified",
crate::notifications::NotificationChannel::Signal => "signal_verified",
};
let update_query = format!(
"UPDATE users SET {} = TRUE, email_confirmation_code = NULL, email_confirmation_code_expires_at = NULL WHERE did = $1",
verified_column
);
if let Err(e) = sqlx::query(&update_query)
.bind(&input.did)
.execute(&state.db)
@@ -531,7 +485,6 @@ pub async fn confirm_signup(
error!("Failed to update verification status: {:?}", e);
return ApiError::InternalError.into_response();
}
let access_meta = match crate::auth::create_access_token_with_metadata(&row.did, &key_bytes) {
Ok(m) => m,
Err(e) => {
@@ -539,7 +492,6 @@ pub async fn confirm_signup(
return ApiError::InternalError.into_response();
}
};
let refresh_meta = match crate::auth::create_refresh_token_with_metadata(&row.did, &key_bytes) {
Ok(m) => m,
Err(e) => {
@@ -547,7 +499,6 @@ pub async fn confirm_signup(
return ApiError::InternalError.into_response();
}
};
if let Err(e) = sqlx::query!(
"INSERT INTO session_tokens (did, access_jti, refresh_jti, access_expires_at, refresh_expires_at) VALUES ($1, $2, $3, $4, $5)",
row.did,
@@ -562,12 +513,10 @@ pub async fn confirm_signup(
error!("Failed to insert session: {:?}", e);
return ApiError::InternalError.into_response();
}
let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
if let Err(e) = crate::notifications::enqueue_welcome(&state.db, row.id, &hostname).await {
warn!("Failed to enqueue welcome notification: {:?}", e);
}
let email_confirmed = matches!(row.channel, crate::notifications::NotificationChannel::Email);
let preferred_channel = match row.channel {
crate::notifications::NotificationChannel::Email => "email",
@@ -575,7 +524,6 @@ pub async fn confirm_signup(
crate::notifications::NotificationChannel::Telegram => "telegram",
crate::notifications::NotificationChannel::Signal => "signal",
};
Json(ConfirmSignupOutput {
access_jwt: access_meta.token,
refresh_jwt: refresh_meta.token,
@@ -587,19 +535,16 @@ pub async fn confirm_signup(
preferred_channel_verified: true,
}).into_response()
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ResendVerificationInput {
pub did: String,
}
pub async fn resend_verification(
State(state): State<AppState>,
Json(input): Json<ResendVerificationInput>,
) -> Response {
info!("resend_verification called for DID: {}", input.did);
let row = match sqlx::query!(
r#"SELECT
id, handle, email,
@@ -622,19 +567,15 @@ pub async fn resend_verification(
return ApiError::InternalError.into_response();
}
};
let is_verified = row.email_confirmed
|| row.discord_verified
|| row.telegram_verified
|| row.signal_verified;
if is_verified {
return ApiError::InvalidRequest("Account is already verified".into()).into_response();
}
let verification_code = format!("{:06}", rand::random::<u32>() % 1_000_000);
let code_expires_at = Utc::now() + chrono::Duration::minutes(30);
if let Err(e) = sqlx::query!(
"UPDATE users SET email_confirmation_code = $1, email_confirmation_code_expires_at = $2 WHERE did = $3",
verification_code,
@@ -647,7 +588,6 @@ pub async fn resend_verification(
error!("Failed to update verification code: {:?}", e);
return ApiError::InternalError.into_response();
}
let (channel_str, recipient) = match row.channel {
crate::notifications::NotificationChannel::Email => ("email", row.email.clone().unwrap_or_default()),
crate::notifications::NotificationChannel::Discord => {
@@ -660,7 +600,6 @@ pub async fn resend_verification(
("signal", row.signal_number.unwrap_or_default())
}
};
if let Err(e) = crate::notifications::enqueue_signup_verification(
&state.db,
row.id,
@@ -670,6 +609,5 @@ pub async fn resend_verification(
).await {
warn!("Failed to enqueue verification notification: {:?}", e);
}
Json(json!({"success": true})).into_response()
}
-12
View File
@@ -10,33 +10,25 @@ use k256::ecdsa::SigningKey;
use serde::{Deserialize, Serialize};
use serde_json::json;
use tracing::{error, info};
const SECP256K1_MULTICODEC_PREFIX: [u8; 2] = [0xe7, 0x01];
fn public_key_to_did_key(signing_key: &SigningKey) -> String {
let verifying_key = signing_key.verifying_key();
let compressed_pubkey = verifying_key.to_sec1_bytes();
let mut multicodec_key = Vec::with_capacity(2 + compressed_pubkey.len());
multicodec_key.extend_from_slice(&SECP256K1_MULTICODEC_PREFIX);
multicodec_key.extend_from_slice(&compressed_pubkey);
let encoded = multibase::encode(multibase::Base::Base58Btc, &multicodec_key);
format!("did:key:{}", encoded)
}
#[derive(Deserialize)]
pub struct ReserveSigningKeyInput {
pub did: Option<String>,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ReserveSigningKeyOutput {
pub signing_key: String,
}
pub async fn reserve_signing_key(
State(state): State<AppState>,
Json(input): Json<ReserveSigningKeyInput>,
@@ -44,11 +36,8 @@ pub async fn reserve_signing_key(
let signing_key = SigningKey::random(&mut rand::thread_rng());
let private_key_bytes = signing_key.to_bytes();
let public_key_did_key = public_key_to_did_key(&signing_key);
let expires_at = Utc::now() + Duration::hours(24);
let private_bytes: &[u8] = &private_key_bytes;
let result = sqlx::query!(
r#"
INSERT INTO reserved_signing_keys (did, public_key_did_key, private_key_bytes, expires_at)
@@ -62,7 +51,6 @@ pub async fn reserve_signing_key(
)
.fetch_one(&state.db)
.await;
match result {
Ok(row) => {
info!(