Idk. Code quality in general?

This commit is contained in:
lewis
2025-12-11 23:02:37 +02:00
parent ea7837fcec
commit e90488fbad
78 changed files with 4412 additions and 4069 deletions
+13 -64
View File
@@ -1,3 +1,4 @@
use crate::api::ApiError;
use crate::state::AppState;
use axum::{
Json,
@@ -34,25 +35,12 @@ pub async fn check_account_status(
headers.get("Authorization").and_then(|h| h.to_str().ok())
) {
Some(t) => t,
None => {
return (
StatusCode::UNAUTHORIZED,
Json(json!({"error": "AuthenticationRequired"})),
)
.into_response();
}
None => return ApiError::AuthenticationRequired.into_response(),
};
let auth_result = crate::auth::validate_bearer_token_allow_deactivated(&state.db, &token).await;
let did = match auth_result {
let did = match crate::auth::validate_bearer_token_allow_deactivated(&state.db, &token).await {
Ok(user) => user.did,
Err(e) => {
return (
StatusCode::UNAUTHORIZED,
Json(json!({"error": e})),
)
.into_response();
}
Err(e) => return ApiError::from(e).into_response(),
};
let user_id = match sqlx::query_scalar!("SELECT id FROM users WHERE did = $1", did)
@@ -127,25 +115,12 @@ pub async fn activate_account(
headers.get("Authorization").and_then(|h| h.to_str().ok())
) {
Some(t) => t,
None => {
return (
StatusCode::UNAUTHORIZED,
Json(json!({"error": "AuthenticationRequired"})),
)
.into_response();
}
None => return ApiError::AuthenticationRequired.into_response(),
};
let auth_result = crate::auth::validate_bearer_token_allow_deactivated(&state.db, &token).await;
let did = match auth_result {
let did = match crate::auth::validate_bearer_token_allow_deactivated(&state.db, &token).await {
Ok(user) => user.did,
Err(e) => {
return (
StatusCode::UNAUTHORIZED,
Json(json!({"error": e})),
)
.into_response();
}
Err(e) => return ApiError::from(e).into_response(),
};
let result = sqlx::query!("UPDATE users SET deactivated_at = NULL WHERE did = $1", did)
@@ -180,25 +155,12 @@ pub async fn deactivate_account(
headers.get("Authorization").and_then(|h| h.to_str().ok())
) {
Some(t) => t,
None => {
return (
StatusCode::UNAUTHORIZED,
Json(json!({"error": "AuthenticationRequired"})),
)
.into_response();
}
None => return ApiError::AuthenticationRequired.into_response(),
};
let auth_result = crate::auth::validate_bearer_token(&state.db, &token).await;
let did = match auth_result {
let did = match crate::auth::validate_bearer_token(&state.db, &token).await {
Ok(user) => user.did,
Err(e) => {
return (
StatusCode::UNAUTHORIZED,
Json(json!({"error": e})),
)
.into_response();
}
Err(e) => return ApiError::from(e).into_response(),
};
let result = sqlx::query!("UPDATE users SET deactivated_at = NOW() WHERE did = $1", did)
@@ -226,25 +188,12 @@ pub async fn request_account_delete(
headers.get("Authorization").and_then(|h| h.to_str().ok())
) {
Some(t) => t,
None => {
return (
StatusCode::UNAUTHORIZED,
Json(json!({"error": "AuthenticationRequired"})),
)
.into_response();
}
None => return ApiError::AuthenticationRequired.into_response(),
};
let auth_result = crate::auth::validate_bearer_token_allow_deactivated(&state.db, &token).await;
let did = match auth_result {
let did = match crate::auth::validate_bearer_token_allow_deactivated(&state.db, &token).await {
Ok(user) => user.did,
Err(e) => {
return (
StatusCode::UNAUTHORIZED,
Json(json!({"error": e})),
)
.into_response();
}
Err(e) => return ApiError::from(e).into_response(),
};
let user_id = match sqlx::query_scalar!("SELECT id FROM users WHERE did = $1", did)
+63 -190
View File
@@ -1,8 +1,10 @@
use crate::api::ApiError;
use crate::auth::BearerAuth;
use crate::state::AppState;
use crate::util::get_user_id_by_did;
use axum::{
Json,
extract::State,
http::StatusCode,
response::{IntoResponse, Response},
};
use serde::{Deserialize, Serialize};
@@ -24,73 +26,35 @@ pub struct ListAppPasswordsOutput {
pub async fn list_app_passwords(
State(state): State<AppState>,
headers: axum::http::HeaderMap,
BearerAuth(auth_user): BearerAuth,
) -> Response {
let token = match crate::auth::extract_bearer_token_from_header(
headers.get("Authorization").and_then(|h| h.to_str().ok())
) {
Some(t) => t,
None => {
return (
StatusCode::UNAUTHORIZED,
Json(json!({"error": "AuthenticationRequired"})),
)
.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 auth_result = crate::auth::validate_bearer_token(&state.db, &token).await;
let did = match auth_result {
Ok(user) => user.did,
Err(e) => {
return (
StatusCode::UNAUTHORIZED,
Json(json!({"error": e})),
)
.into_response();
}
};
let user_id = match sqlx::query_scalar!("SELECT id FROM users WHERE did = $1", did)
.fetch_optional(&state.db)
.await
match sqlx::query!(
"SELECT name, created_at, privileged FROM app_passwords WHERE user_id = $1 ORDER BY created_at DESC",
user_id
)
.fetch_all(&state.db)
.await
{
Ok(Some(id)) => id,
_ => {
return (
StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({"error": "InternalError"})),
)
.into_response();
}
};
let result = sqlx::query!("SELECT name, created_at, privileged FROM app_passwords WHERE user_id = $1 ORDER BY created_at DESC", user_id)
.fetch_all(&state.db)
.await;
match result {
Ok(rows) => {
let passwords: Vec<AppPassword> = rows
.iter()
.map(|row| {
AppPassword {
name: row.name.clone(),
created_at: row.created_at.to_rfc3339(),
privileged: row.privileged,
}
.map(|row| AppPassword {
name: row.name.clone(),
created_at: row.created_at.to_rfc3339(),
privileged: row.privileged,
})
.collect();
(StatusCode::OK, Json(ListAppPasswordsOutput { passwords })).into_response()
Json(ListAppPasswordsOutput { passwords }).into_response()
}
Err(e) => {
error!("DB error listing app passwords: {:?}", e);
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({"error": "InternalError"})),
)
.into_response()
ApiError::InternalError.into_response()
}
}
}
@@ -112,67 +76,29 @@ pub struct CreateAppPasswordOutput {
pub async fn create_app_password(
State(state): State<AppState>,
headers: axum::http::HeaderMap,
BearerAuth(auth_user): BearerAuth,
Json(input): Json<CreateAppPasswordInput>,
) -> Response {
let token = match crate::auth::extract_bearer_token_from_header(
headers.get("Authorization").and_then(|h| h.to_str().ok())
) {
Some(t) => t,
None => {
return (
StatusCode::UNAUTHORIZED,
Json(json!({"error": "AuthenticationRequired"})),
)
.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 (
StatusCode::UNAUTHORIZED,
Json(json!({"error": e})),
)
.into_response();
}
};
let user_id = match sqlx::query_scalar!("SELECT id FROM users WHERE did = $1", did)
.fetch_optional(&state.db)
.await
{
Ok(Some(id)) => id,
_ => {
return (
StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({"error": "InternalError"})),
)
.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 (
StatusCode::BAD_REQUEST,
Json(json!({"error": "InvalidRequest", "message": "name is required"})),
)
.into_response();
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, name)
.fetch_optional(&state.db)
.await;
let existing = sqlx::query!(
"SELECT id FROM app_passwords WHERE user_id = $1 AND name = $2",
user_id,
name
)
.fetch_optional(&state.db)
.await;
if let Ok(Some(_)) = existing {
return (
StatusCode::BAD_REQUEST,
Json(json!({"error": "DuplicateAppPassword", "message": "App password with this name already exists"})),
)
.into_response();
return ApiError::DuplicateAppPassword.into_response();
}
let password: String = (0..4)
@@ -180,7 +106,9 @@ pub async fn create_app_password(
use rand::Rng;
let mut rng = rand::thread_rng();
let chars: Vec<char> = "abcdefghijklmnopqrstuvwxyz234567".chars().collect();
(0..4).map(|_| chars[rng.gen_range(0..chars.len())]).collect::<String>()
(0..4)
.map(|_| chars[rng.gen_range(0..chars.len())])
.collect::<String>()
})
.collect::<Vec<String>>()
.join("-");
@@ -189,18 +117,14 @@ pub async fn create_app_password(
Ok(h) => h,
Err(e) => {
error!("Failed to hash password: {:?}", e);
return (
StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({"error": "InternalError"})),
)
.into_response();
return ApiError::InternalError.into_response();
}
};
let privileged = input.privileged.unwrap_or(false);
let created_at = chrono::Utc::now();
let result = sqlx::query!(
match sqlx::query!(
"INSERT INTO app_passwords (user_id, name, password_hash, created_at, privileged) VALUES ($1, $2, $3, $4, $5)",
user_id,
name,
@@ -209,26 +133,18 @@ pub async fn create_app_password(
privileged
)
.execute(&state.db)
.await;
match result {
Ok(_) => (
StatusCode::OK,
Json(CreateAppPasswordOutput {
name: name.to_string(),
password,
created_at: created_at.to_rfc3339(),
privileged,
}),
)
.into_response(),
.await
{
Ok(_) => Json(CreateAppPasswordOutput {
name: name.to_string(),
password,
created_at: created_at.to_rfc3339(),
privileged,
})
.into_response(),
Err(e) => {
error!("DB error creating app password: {:?}", e);
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({"error": "InternalError"})),
)
.into_response()
ApiError::InternalError.into_response()
}
}
}
@@ -240,79 +156,36 @@ pub struct RevokeAppPasswordInput {
pub async fn revoke_app_password(
State(state): State<AppState>,
headers: axum::http::HeaderMap,
BearerAuth(auth_user): BearerAuth,
Json(input): Json<RevokeAppPasswordInput>,
) -> Response {
let token = match crate::auth::extract_bearer_token_from_header(
headers.get("Authorization").and_then(|h| h.to_str().ok())
) {
Some(t) => t,
None => {
return (
StatusCode::UNAUTHORIZED,
Json(json!({"error": "AuthenticationRequired"})),
)
.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 (
StatusCode::UNAUTHORIZED,
Json(json!({"error": e})),
)
.into_response();
}
};
let user_id = match sqlx::query_scalar!("SELECT id FROM users WHERE did = $1", did)
.fetch_optional(&state.db)
.await
{
Ok(Some(id)) => id,
_ => {
return (
StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({"error": "InternalError"})),
)
.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 (
StatusCode::BAD_REQUEST,
Json(json!({"error": "InvalidRequest", "message": "name is required"})),
)
.into_response();
return ApiError::InvalidRequest("name is required".into()).into_response();
}
let result = sqlx::query!("DELETE FROM app_passwords WHERE user_id = $1 AND name = $2", user_id, name)
.execute(&state.db)
.await;
match result {
match sqlx::query!(
"DELETE FROM app_passwords WHERE user_id = $1 AND name = $2",
user_id,
name
)
.execute(&state.db)
.await
{
Ok(r) => {
if r.rows_affected() == 0 {
return (
StatusCode::NOT_FOUND,
Json(json!({"error": "AppPasswordNotFound", "message": "App password not found"})),
)
.into_response();
return ApiError::AppPasswordNotFound.into_response();
}
(StatusCode::OK, Json(json!({}))).into_response()
Json(json!({})).into_response()
}
Err(e) => {
error!("DB error revoking app password: {:?}", e);
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({"error": "InternalError"})),
)
.into_response()
ApiError::InternalError.into_response()
}
}
}
+47 -54
View File
@@ -1,3 +1,4 @@
use crate::api::ApiError;
use crate::state::AppState;
use axum::{
Json,
@@ -6,17 +7,12 @@ use axum::{
response::{IntoResponse, Response},
};
use chrono::{Duration, Utc};
use rand::Rng;
use serde::Deserialize;
use serde_json::json;
use tracing::{error, info, warn};
fn generate_confirmation_code() -> String {
let mut rng = rand::thread_rng();
let chars: Vec<char> = "abcdefghijklmnopqrstuvwxyz234567".chars().collect();
let part1: String = (0..5).map(|_| chars[rng.gen_range(0..chars.len())]).collect();
let part2: String = (0..5).map(|_| chars[rng.gen_range(0..chars.len())]).collect();
format!("{}-{}", part1, part2)
crate::util::generate_token_code()
}
#[derive(Deserialize)]
@@ -46,13 +42,7 @@ pub async fn request_email_update(
let auth_result = crate::auth::validate_bearer_token(&state.db, &token).await;
let did = match auth_result {
Ok(user) => user.did,
Err(e) => {
return (
StatusCode::UNAUTHORIZED,
Json(json!({"error": e})),
)
.into_response();
}
Err(e) => return ApiError::from(e).into_response(),
};
let user = match sqlx::query!("SELECT id, handle FROM users WHERE did = $1", did)
@@ -72,10 +62,10 @@ pub async fn request_email_update(
let handle = user.handle;
let email = input.email.trim().to_lowercase();
if email.is_empty() {
if !crate::api::validation::is_valid_email(&email) {
return (
StatusCode::BAD_REQUEST,
Json(json!({"error": "InvalidRequest", "message": "email is required"})),
Json(json!({"error": "InvalidEmail", "message": "Invalid email format"})),
)
.into_response();
}
@@ -161,13 +151,7 @@ pub async fn confirm_email(
let auth_result = crate::auth::validate_bearer_token(&state.db, &token).await;
let did = match auth_result {
Ok(user) => user.did,
Err(e) => {
return (
StatusCode::UNAUTHORIZED,
Json(json!({"error": e})),
)
.into_response();
}
Err(e) => return ApiError::from(e).into_response(),
};
let user = match sqlx::query!(
@@ -194,16 +178,18 @@ pub async fn confirm_email(
let email = input.email.trim().to_lowercase();
let confirmation_code = input.token.trim();
if email_pending_verification.is_none() || stored_code.is_none() || expires_at.is_none() {
return (
StatusCode::BAD_REQUEST,
Json(json!({"error": "InvalidRequest", "message": "No pending email update found"})),
)
.into_response();
}
let (pending_email, saved_code, expiry) = match (email_pending_verification, stored_code, expires_at) {
(Some(p), Some(c), Some(e)) => (p, c, e),
_ => {
return (
StatusCode::BAD_REQUEST,
Json(json!({"error": "InvalidRequest", "message": "No pending email update found"})),
)
.into_response();
}
};
let email_pending_verification = email_pending_verification.unwrap();
if email_pending_verification != email {
if pending_email != email {
return (
StatusCode::BAD_REQUEST,
Json(json!({"error": "InvalidRequest", "message": "Email does not match pending update"})),
@@ -211,7 +197,7 @@ pub async fn confirm_email(
.into_response();
}
if stored_code.unwrap() != confirmation_code {
if saved_code != confirmation_code {
return (
StatusCode::BAD_REQUEST,
Json(json!({"error": "InvalidToken", "message": "Invalid token"})),
@@ -219,7 +205,7 @@ pub async fn confirm_email(
.into_response();
}
if Utc::now() > expires_at.unwrap() {
if Utc::now() > expiry {
return (
StatusCode::BAD_REQUEST,
Json(json!({"error": "ExpiredToken", "message": "Token has expired"})),
@@ -229,7 +215,7 @@ pub async fn confirm_email(
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",
email_pending_verification,
pending_email,
user_id
)
.execute(&state.db)
@@ -287,13 +273,7 @@ pub async fn update_email(
let auth_result = crate::auth::validate_bearer_token(&state.db, &token).await;
let did = match auth_result {
Ok(user) => user.did,
Err(e) => {
return (
StatusCode::UNAUTHORIZED,
Json(json!({"error": e})),
)
.into_response();
}
Err(e) => return ApiError::from(e).into_response(),
};
let user = match sqlx::query!(
@@ -319,18 +299,10 @@ pub async fn update_email(
let email_pending_verification = user.email_pending_verification;
let new_email = input.email.trim().to_lowercase();
if new_email.is_empty() {
if !crate::api::validation::is_valid_email(&new_email) {
return (
StatusCode::BAD_REQUEST,
Json(json!({"error": "InvalidRequest", "message": "email is required"})),
)
.into_response();
}
if !new_email.contains('@') || !new_email.contains('.') {
return (
StatusCode::BAD_REQUEST,
Json(json!({"error": "InvalidRequest", "message": "Invalid email format"})),
Json(json!({"error": "InvalidEmail", "message": "Invalid email format"})),
)
.into_response();
}
@@ -353,7 +325,17 @@ pub async fn update_email(
}
};
let pending_email = email_pending_verification.unwrap();
let pending_email = match email_pending_verification {
Some(p) => p,
None => {
return (
StatusCode::BAD_REQUEST,
Json(json!({"error": "InvalidRequest", "message": "No pending email update found"})),
)
.into_response();
}
};
if pending_email.to_lowercase() != new_email {
return (
StatusCode::BAD_REQUEST,
@@ -362,7 +344,18 @@ pub async fn update_email(
.into_response();
}
if stored_code.unwrap() != confirmation_token {
let saved_code = match stored_code {
Some(c) => c,
None => {
return (
StatusCode::BAD_REQUEST,
Json(json!({"error": "InvalidRequest", "message": "No pending email update found"})),
)
.into_response();
}
};
if saved_code != confirmation_token {
return (
StatusCode::BAD_REQUEST,
Json(json!({"error": "InvalidToken", "message": "Invalid token"})),
@@ -415,7 +408,7 @@ pub async fn update_email(
match update {
Ok(_) => {
info!("Email updated to {} for user {}", new_email, user_id);
info!("Email updated for user {}", user_id);
(StatusCode::OK, Json(json!({}))).into_response()
}
Err(e) => {
+61 -209
View File
@@ -1,12 +1,13 @@
use crate::api::ApiError;
use crate::auth::BearerAuth;
use crate::state::AppState;
use crate::util::get_user_id_by_did;
use axum::{
Json,
extract::State,
http::StatusCode,
response::{IntoResponse, Response},
};
use serde::{Deserialize, Serialize};
use serde_json::json;
use tracing::error;
use uuid::Uuid;
@@ -24,77 +25,28 @@ pub struct CreateInviteCodeOutput {
pub async fn create_invite_code(
State(state): State<AppState>,
headers: axum::http::HeaderMap,
BearerAuth(auth_user): BearerAuth,
Json(input): Json<CreateInviteCodeInput>,
) -> Response {
let token = match crate::auth::extract_bearer_token_from_header(
headers.get("Authorization").and_then(|h| h.to_str().ok())
) {
Some(t) => t,
None => {
return (
StatusCode::UNAUTHORIZED,
Json(json!({"error": "AuthenticationRequired"})),
)
.into_response();
}
};
if input.use_count < 1 {
return (
StatusCode::BAD_REQUEST,
Json(json!({"error": "InvalidRequest", "message": "useCount must be at least 1"})),
)
.into_response();
return ApiError::InvalidRequest("useCount must be at least 1".into()).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 (
StatusCode::UNAUTHORIZED,
Json(json!({"error": e})),
)
.into_response();
}
};
let user_id = match sqlx::query_scalar!("SELECT id FROM users WHERE did = $1", did)
.fetch_optional(&state.db)
.await
{
Ok(Some(id)) => id,
_ => {
return (
StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({"error": "InternalError"})),
)
.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 {
let target = sqlx::query!("SELECT id FROM users WHERE did = $1", for_account)
match sqlx::query!("SELECT id FROM users WHERE did = $1", for_account)
.fetch_optional(&state.db)
.await;
match target {
.await
{
Ok(Some(row)) => row.id,
Ok(None) => {
return (
StatusCode::NOT_FOUND,
Json(json!({"error": "AccountNotFound", "message": "Target account not found"})),
)
.into_response();
}
Ok(None) => return ApiError::AccountNotFound.into_response(),
Err(e) => {
error!("DB error looking up target account: {:?}", e);
return (
StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({"error": "InternalError"})),
)
.into_response();
return ApiError::InternalError.into_response();
}
}
} else {
@@ -103,43 +55,38 @@ pub async fn create_invite_code(
let user_invites_disabled = sqlx::query_scalar!(
"SELECT invites_disabled FROM users WHERE did = $1",
did
auth_user.did
)
.fetch_optional(&state.db)
.await
.map_err(|e| {
error!("DB error checking invites_disabled: {:?}", e);
ApiError::InternalError
})
.ok()
.flatten()
.flatten()
.unwrap_or(false);
if user_invites_disabled {
return (
StatusCode::FORBIDDEN,
Json(json!({"error": "InvitesDisabled", "message": "Invites are disabled for this account"})),
)
.into_response();
return ApiError::InvitesDisabled.into_response();
}
let code = Uuid::new_v4().to_string();
let result = sqlx::query!(
match sqlx::query!(
"INSERT INTO invite_codes (code, available_uses, created_by_user) VALUES ($1, $2, $3)",
code,
input.use_count,
creator_user_id
)
.execute(&state.db)
.await;
match result {
Ok(_) => (StatusCode::OK, Json(CreateInviteCodeOutput { code })).into_response(),
.await
{
Ok(_) => Json(CreateInviteCodeOutput { code }).into_response(),
Err(e) => {
error!("DB error creating invite code: {:?}", e);
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({"error": "InternalError"})),
)
.into_response()
ApiError::InternalError.into_response()
}
}
}
@@ -165,54 +112,16 @@ pub struct AccountCodes {
pub async fn create_invite_codes(
State(state): State<AppState>,
headers: axum::http::HeaderMap,
BearerAuth(auth_user): BearerAuth,
Json(input): Json<CreateInviteCodesInput>,
) -> Response {
let token = match crate::auth::extract_bearer_token_from_header(
headers.get("Authorization").and_then(|h| h.to_str().ok())
) {
Some(t) => t,
None => {
return (
StatusCode::UNAUTHORIZED,
Json(json!({"error": "AuthenticationRequired"})),
)
.into_response();
}
};
if input.use_count < 1 {
return (
StatusCode::BAD_REQUEST,
Json(json!({"error": "InvalidRequest", "message": "useCount must be at least 1"})),
)
.into_response();
return ApiError::InvalidRequest("useCount must be at least 1".into()).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 (
StatusCode::UNAUTHORIZED,
Json(json!({"error": e})),
)
.into_response();
}
};
let user_id = match sqlx::query_scalar!("SELECT id FROM users WHERE did = $1", did)
.fetch_optional(&state.db)
.await
{
Ok(Some(id)) => id,
_ => {
return (
StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({"error": "InternalError"})),
)
.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);
@@ -225,22 +134,17 @@ pub async fn create_invite_codes(
for _ in 0..code_count {
let code = Uuid::new_v4().to_string();
let insert = sqlx::query!(
if let Err(e) = sqlx::query!(
"INSERT INTO invite_codes (code, available_uses, created_by_user) VALUES ($1, $2, $3)",
code,
input.use_count,
user_id
)
.execute(&state.db)
.await;
if let Err(e) = insert {
.await
{
error!("DB error creating invite code: {:?}", e);
return (
StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({"error": "InternalError"})),
)
.into_response();
return ApiError::InternalError.into_response();
}
codes.push(code);
@@ -252,22 +156,15 @@ pub async fn create_invite_codes(
});
} else {
for account_did in for_accounts {
let target = sqlx::query!("SELECT id FROM users WHERE did = $1", account_did)
let target_user_id = match sqlx::query!("SELECT id FROM users WHERE did = $1", account_did)
.fetch_optional(&state.db)
.await;
let target_user_id = match target {
.await
{
Ok(Some(row)) => row.id,
Ok(None) => {
continue;
}
Ok(None) => continue,
Err(e) => {
error!("DB error looking up target account: {:?}", e);
return (
StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({"error": "InternalError"})),
)
.into_response();
return ApiError::InternalError.into_response();
}
};
@@ -275,22 +172,17 @@ pub async fn create_invite_codes(
for _ in 0..code_count {
let code = Uuid::new_v4().to_string();
let insert = sqlx::query!(
if let Err(e) = sqlx::query!(
"INSERT INTO invite_codes (code, available_uses, created_by_user) VALUES ($1, $2, $3)",
code,
input.use_count,
target_user_id
)
.execute(&state.db)
.await;
if let Err(e) = insert {
.await
{
error!("DB error creating invite code: {:?}", e);
return (
StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({"error": "InternalError"})),
)
.into_response();
return ApiError::InternalError.into_response();
}
codes.push(code);
@@ -303,7 +195,7 @@ pub async fn create_invite_codes(
}
}
(StatusCode::OK, Json(CreateInviteCodesOutput { codes: result_codes })).into_response()
Json(CreateInviteCodesOutput { codes: result_codes }).into_response()
}
#[derive(Deserialize)]
@@ -339,51 +231,17 @@ pub struct GetAccountInviteCodesOutput {
pub async fn get_account_invite_codes(
State(state): State<AppState>,
headers: axum::http::HeaderMap,
BearerAuth(auth_user): BearerAuth,
axum::extract::Query(params): axum::extract::Query<GetAccountInviteCodesParams>,
) -> Response {
let token = match crate::auth::extract_bearer_token_from_header(
headers.get("Authorization").and_then(|h| h.to_str().ok())
) {
Some(t) => t,
None => {
return (
StatusCode::UNAUTHORIZED,
Json(json!({"error": "AuthenticationRequired"})),
)
.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 (
StatusCode::UNAUTHORIZED,
Json(json!({"error": e})),
)
.into_response();
}
};
let user_id = match sqlx::query_scalar!("SELECT id FROM users WHERE did = $1", did)
.fetch_optional(&state.db)
.await
{
Ok(Some(id)) => id,
_ => {
return (
StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({"error": "InternalError"})),
)
.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 include_used = params.include_used.unwrap_or(true);
let codes_result = sqlx::query!(
let codes_rows = match sqlx::query!(
r#"
SELECT code, available_uses, created_at, disabled
FROM invite_codes
@@ -393,9 +251,8 @@ pub async fn get_account_invite_codes(
user_id
)
.fetch_all(&state.db)
.await;
let codes_rows = match codes_result {
.await
{
Ok(rows) => {
if include_used {
rows
@@ -405,17 +262,13 @@ pub async fn get_account_invite_codes(
}
Err(e) => {
error!("DB error fetching invite codes: {:?}", e);
return (
StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({"error": "InternalError"})),
)
.into_response();
return ApiError::InternalError.into_response();
}
};
let mut codes = Vec::new();
for row in codes_rows {
let uses_result = sqlx::query!(
let uses = sqlx::query!(
r#"
SELECT u.did, icu.used_at
FROM invite_code_uses icu
@@ -426,29 +279,28 @@ pub async fn get_account_invite_codes(
row.code
)
.fetch_all(&state.db)
.await;
let uses = match uses_result {
Ok(use_rows) => use_rows
.await
.map(|use_rows| {
use_rows
.iter()
.map(|u| InviteCodeUse {
used_by: u.did.clone(),
used_at: u.used_at.to_rfc3339(),
})
.collect(),
Err(_) => Vec::new(),
};
.collect()
})
.unwrap_or_default();
codes.push(InviteCode {
code: row.code,
available: row.available_uses,
disabled: row.disabled.unwrap_or(false),
for_account: did.clone(),
created_by: did.clone(),
for_account: auth_user.did.clone(),
created_by: auth_user.did.clone(),
created_at: row.created_at.to_rfc3339(),
uses,
});
}
(StatusCode::OK, Json(GetAccountInviteCodesOutput { codes })).into_response()
Json(GetAccountInviteCodesOutput { codes }).into_response()
}
+3 -3
View File
@@ -4,6 +4,7 @@ pub mod email;
pub mod invite;
pub mod meta;
pub mod password;
pub mod service_auth;
pub mod session;
pub mod signing_key;
@@ -16,7 +17,6 @@ pub use email::{confirm_email, request_email_update, update_email};
pub use invite::{create_invite_code, create_invite_codes, get_account_invite_codes};
pub use meta::{describe_server, health};
pub use password::{request_password_reset, reset_password};
pub use session::{
create_session, delete_session, get_service_auth, get_session, refresh_session,
};
pub use service_auth::get_service_auth;
pub use session::{create_session, delete_session, get_session, refresh_session};
pub use signing_key::reserve_signing_key;
+43 -17
View File
@@ -7,17 +7,12 @@ use axum::{
};
use bcrypt::{hash, DEFAULT_COST};
use chrono::{Duration, Utc};
use rand::Rng;
use serde::Deserialize;
use serde_json::json;
use tracing::{error, info, warn};
fn generate_reset_code() -> String {
let mut rng = rand::thread_rng();
let chars: Vec<char> = "abcdefghijklmnopqrstuvwxyz234567".chars().collect();
let part1: String = (0..5).map(|_| chars[rng.gen_range(0..chars.len())]).collect();
let part2: String = (0..5).map(|_| chars[rng.gen_range(0..chars.len())]).collect();
format!("{}-{}", part1, part2)
crate::util::generate_token_code()
}
#[derive(Deserialize)]
@@ -45,7 +40,7 @@ pub async fn request_password_reset(
let user_id = match user {
Ok(Some(row)) => row.id,
Ok(None) => {
info!("Password reset requested for unknown email: {}", email);
info!("Password reset requested for unknown email");
return (StatusCode::OK, Json(json!({}))).into_response();
}
Err(e) => {
@@ -151,12 +146,15 @@ pub async fn reset_password(
if let Some(exp) = expires_at {
if Utc::now() > exp {
let _ = sqlx::query!(
if let Err(e) = sqlx::query!(
"UPDATE users SET password_reset_code = NULL, password_reset_code_expires_at = NULL WHERE id = $1",
user_id
)
.execute(&state.db)
.await;
.await
{
error!("Failed to clear expired reset code: {:?}", e);
}
return (
StatusCode::BAD_REQUEST,
@@ -184,15 +182,26 @@ pub async fn reset_password(
}
};
let update = sqlx::query!(
let mut tx = match state.db.begin().await {
Ok(tx) => tx,
Err(e) => {
error!("Failed to begin transaction: {:?}", e);
return (
StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({"error": "InternalError"})),
)
.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,
user_id
)
.execute(&state.db)
.await;
if let Err(e) = update {
.execute(&mut *tx)
.await
{
error!("DB error updating password: {:?}", e);
return (
StatusCode::INTERNAL_SERVER_ERROR,
@@ -201,9 +210,26 @@ pub async fn reset_password(
.into_response();
}
let _ = sqlx::query!("DELETE FROM session_tokens WHERE did = (SELECT did FROM users WHERE id = $1)", user_id)
.execute(&state.db)
.await;
if let Err(e) = sqlx::query!("DELETE FROM session_tokens WHERE did = (SELECT did FROM users WHERE id = $1)", user_id)
.execute(&mut *tx)
.await
{
error!("Failed to invalidate sessions after password reset: {:?}", e);
return (
StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({"error": "InternalError"})),
)
.into_response();
}
if let Err(e) = tx.commit().await {
error!("Failed to commit password reset transaction: {:?}", e);
return (
StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({"error": "InternalError"})),
)
.into_response();
}
info!("Password reset completed for user {}", user_id);
+63
View File
@@ -0,0 +1,63 @@
use crate::api::ApiError;
use crate::state::AppState;
use axum::{
Json,
extract::{Query, State},
http::StatusCode,
response::{IntoResponse, Response},
};
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,
Query(params): Query<GetServiceAuthParams>,
) -> Response {
let 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 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,
Err(e) => {
error!("Failed to create service token: {:?}", e);
return (
StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({"error": "InternalError"})),
)
.into_response();
}
};
(StatusCode::OK, Json(GetServiceAuthOutput { token: service_token })).into_response()
}
+213 -526
View File
@@ -1,8 +1,9 @@
use crate::api::ApiError;
use crate::auth::BearerAuth;
use crate::state::AppState;
use axum::{
Json,
extract::{Query, State},
http::StatusCode,
extract::State,
response::{IntoResponse, Response},
};
use bcrypt::verify;
@@ -10,78 +11,6 @@ use serde::{Deserialize, Serialize};
use serde_json::json;
use tracing::{error, info, warn};
#[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,
Query(params): Query<GetServiceAuthParams>,
) -> Response {
let token = match crate::auth::extract_bearer_token_from_header(
headers.get("Authorization").and_then(|h| h.to_str().ok())
) {
Some(t) => t,
None => {
return (
StatusCode::UNAUTHORIZED,
Json(json!({"error": "AuthenticationRequired"})),
)
.into_response();
}
};
let auth_result = crate::auth::validate_bearer_token(&state.db, &token).await;
let (did, key_bytes) = match auth_result {
Ok(user) => {
let kb = match user.key_bytes {
Some(kb) => kb,
None => {
return (
StatusCode::UNAUTHORIZED,
Json(json!({"error": "AuthenticationFailed", "message": "OAuth tokens cannot create service auth"})),
)
.into_response();
}
};
(user.did, kb)
}
Err(e) => {
return (
StatusCode::UNAUTHORIZED,
Json(json!({"error": e})),
)
.into_response();
}
};
let lxm = params.lxm.as_deref().unwrap_or("*");
let service_token = match crate::auth::create_service_token(&did, &params.aud, lxm, &key_bytes)
{
Ok(t) => t,
Err(e) => {
error!("Failed to create service token: {:?}", e);
return (
StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({"error": "InternalError"})),
)
.into_response();
}
};
(StatusCode::OK, Json(GetServiceAuthOutput { token: service_token })).into_response()
}
#[derive(Deserialize)]
pub struct CreateSessionInput {
pub identifier: String,
@@ -101,194 +30,104 @@ pub async fn create_session(
State(state): State<AppState>,
Json(input): Json<CreateSessionInput>,
) -> Response {
info!("create_session: identifier='{}'", input.identifier);
info!("create_session called");
let user_row = sqlx::query!(
let row = match sqlx::query!(
"SELECT u.id, u.did, u.handle, u.password_hash, k.key_bytes, k.encryption_version FROM users u JOIN user_keys k ON u.id = k.user_id WHERE u.handle = $1 OR u.email = $1",
input.identifier
)
.fetch_optional(&state.db)
.await;
match user_row {
Ok(Some(row)) => {
let user_id = row.id;
let stored_hash = &row.password_hash;
let did = &row.did;
let handle = &row.handle;
let key_bytes = match crate::config::decrypt_key(&row.key_bytes, row.encryption_version) {
Ok(k) => k,
Err(e) => {
error!("Failed to decrypt user key: {:?}", e);
return (
StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({"error": "InternalError"})),
)
.into_response();
}
};
let password_valid = if verify(&input.password, stored_hash).unwrap_or(false) {
true
} else {
let app_pass_rows = sqlx::query!("SELECT password_hash FROM app_passwords WHERE user_id = $1", user_id)
.fetch_all(&state.db)
.await
.unwrap_or_default();
app_pass_rows.iter().any(|row| {
verify(&input.password, &row.password_hash).unwrap_or(false)
})
};
if password_valid {
let access_meta = match crate::auth::create_access_token_with_metadata(did, &key_bytes) {
Ok(m) => m,
Err(e) => {
error!("Failed to create access token: {:?}", e);
return (
StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({"error": "InternalError"})),
)
.into_response();
}
};
let refresh_meta = match crate::auth::create_refresh_token_with_metadata(did, &key_bytes) {
Ok(m) => m,
Err(e) => {
error!("Failed to create refresh token: {:?}", e);
return (
StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({"error": "InternalError"})),
)
.into_response();
}
};
let session_insert = sqlx::query!(
"INSERT INTO session_tokens (did, access_jti, refresh_jti, access_expires_at, refresh_expires_at) VALUES ($1, $2, $3, $4, $5)",
did,
access_meta.jti,
refresh_meta.jti,
access_meta.expires_at,
refresh_meta.expires_at
)
.execute(&state.db)
.await;
match session_insert {
Ok(_) => {
return (
StatusCode::OK,
Json(CreateSessionOutput {
access_jwt: access_meta.token,
refresh_jwt: refresh_meta.token,
handle: handle.clone(),
did: did.clone(),
}),
)
.into_response();
}
Err(e) => {
error!("Failed to insert session: {:?}", e);
return (
StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({"error": "InternalError"})),
)
.into_response();
}
}
} else {
warn!(
"Password verification failed for identifier: {}",
input.identifier
);
}
}
.fetch_optional(&state.db)
.await
{
Ok(Some(row)) => row,
Ok(None) => {
warn!("User not found for identifier: {}", input.identifier);
warn!("User not found for login attempt");
return ApiError::AuthenticationFailedMsg("Invalid identifier or password".into()).into_response();
}
Err(e) => {
error!("Database error fetching user: {:?}", e);
return (
StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({"error": "InternalError"})),
)
.into_response();
return ApiError::InternalError.into_response();
}
};
let key_bytes = match crate::config::decrypt_key(&row.key_bytes, row.encryption_version) {
Ok(k) => k,
Err(e) => {
error!("Failed to decrypt user key: {:?}", e);
return ApiError::InternalError.into_response();
}
};
let password_valid = verify(&input.password, &row.password_hash).unwrap_or(false)
|| sqlx::query!("SELECT password_hash FROM app_passwords WHERE user_id = $1", row.id)
.fetch_all(&state.db)
.await
.unwrap_or_default()
.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();
}
(
StatusCode::UNAUTHORIZED,
Json(json!({"error": "AuthenticationFailed", "message": "Invalid identifier or password"})),
let access_meta = match crate::auth::create_access_token_with_metadata(&row.did, &key_bytes) {
Ok(m) => m,
Err(e) => {
error!("Failed to create access token: {:?}", e);
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) => {
error!("Failed to create refresh token: {:?}", e);
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,
access_meta.jti,
refresh_meta.jti,
access_meta.expires_at,
refresh_meta.expires_at
)
.into_response()
.execute(&state.db)
.await
{
error!("Failed to insert session: {:?}", e);
return ApiError::InternalError.into_response();
}
Json(CreateSessionOutput {
access_jwt: access_meta.token,
refresh_jwt: refresh_meta.token,
handle: row.handle,
did: row.did,
}).into_response()
}
pub async fn get_session(
State(state): State<AppState>,
headers: axum::http::HeaderMap,
BearerAuth(auth_user): BearerAuth,
) -> Response {
let token = match crate::auth::extract_bearer_token_from_header(
headers.get("Authorization").and_then(|h| h.to_str().ok())
) {
Some(t) => t,
None => {
return (
StatusCode::UNAUTHORIZED,
Json(json!({"error": "AuthenticationRequired", "message": "Invalid Authorization header format"})),
)
.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 (
StatusCode::UNAUTHORIZED,
Json(json!({"error": e})),
)
.into_response();
}
};
let user = sqlx::query!(
"SELECT handle, email FROM users WHERE did = $1",
did
)
.fetch_optional(&state.db)
.await;
match user {
Ok(Some(row)) => {
return (
StatusCode::OK,
Json(json!({
"handle": row.handle,
"did": did,
"email": row.email,
"didDoc": {}
})),
)
.into_response();
}
Ok(None) => {
return (
StatusCode::UNAUTHORIZED,
Json(json!({"error": "AuthenticationFailed"})),
)
.into_response();
}
match sqlx::query!("SELECT handle, email FROM users WHERE did = $1", auth_user.did)
.fetch_optional(&state.db)
.await
{
Ok(Some(row)) => Json(json!({
"handle": row.handle,
"did": auth_user.did,
"email": row.email,
"didDoc": {}
})).into_response(),
Ok(None) => ApiError::AuthenticationFailed.into_response(),
Err(e) => {
error!("Database error in get_session: {:?}", e);
return (
StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({"error": "InternalError"})),
)
.into_response();
ApiError::InternalError.into_response()
}
}
}
@@ -301,326 +140,174 @@ pub async fn delete_session(
headers.get("Authorization").and_then(|h| h.to_str().ok())
) {
Some(t) => t,
None => {
return (
StatusCode::UNAUTHORIZED,
Json(json!({"error": "AuthenticationRequired"})),
)
.into_response();
}
None => return ApiError::AuthenticationRequired.into_response(),
};
let jti = match crate::auth::get_did_from_token(&token) {
Ok(_) => {
let parts: Vec<&str> = token.split('.').collect();
if parts.len() != 3 {
return (
StatusCode::UNAUTHORIZED,
Json(json!({"error": "AuthenticationFailed"})),
)
.into_response();
}
use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD};
let claims_json = match URL_SAFE_NO_PAD.decode(parts[1]) {
Ok(bytes) => bytes,
Err(_) => {
return (
StatusCode::UNAUTHORIZED,
Json(json!({"error": "AuthenticationFailed"})),
)
.into_response();
}
};
let claims: serde_json::Value = match serde_json::from_slice(&claims_json) {
Ok(c) => c,
Err(_) => {
return (
StatusCode::UNAUTHORIZED,
Json(json!({"error": "AuthenticationFailed"})),
)
.into_response();
}
};
match claims.get("jti").and_then(|j| j.as_str()) {
Some(jti) => jti.to_string(),
None => {
return (
StatusCode::UNAUTHORIZED,
Json(json!({"error": "AuthenticationFailed"})),
)
.into_response();
}
}
}
Err(_) => {
return (
StatusCode::UNAUTHORIZED,
Json(json!({"error": "AuthenticationFailed"})),
)
.into_response();
}
let jti = match crate::auth::get_jti_from_token(&token) {
Ok(jti) => jti,
Err(_) => return ApiError::AuthenticationFailed.into_response(),
};
let result = sqlx::query!("DELETE FROM session_tokens WHERE access_jti = $1", jti)
match sqlx::query!("DELETE FROM session_tokens WHERE access_jti = $1", jti)
.execute(&state.db)
.await;
match result {
Ok(res) => {
if res.rows_affected() > 0 {
return (StatusCode::OK, Json(json!({}))).into_response();
}
}
.await
{
Ok(res) if res.rows_affected() > 0 => Json(json!({})).into_response(),
Ok(_) => ApiError::AuthenticationFailed.into_response(),
Err(e) => {
error!("Database error in delete_session: {:?}", e);
ApiError::AuthenticationFailed.into_response()
}
}
(
StatusCode::UNAUTHORIZED,
Json(json!({"error": "AuthenticationFailed"})),
)
.into_response()
}
pub async fn refresh_session(
State(state): State<AppState>,
headers: axum::http::HeaderMap,
) -> Response {
use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD};
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 (
StatusCode::UNAUTHORIZED,
Json(json!({"error": "AuthenticationRequired"})),
)
.into_response();
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) => {
error!("Failed to begin transaction: {:?}", e);
return ApiError::InternalError.into_response();
}
};
let refresh_jti = {
let parts: Vec<&str> = refresh_token.split('.').collect();
if parts.len() != 3 {
return (
StatusCode::UNAUTHORIZED,
Json(json!({"error": "AuthenticationFailed", "message": "Invalid token format"})),
)
.into_response();
}
let claims_bytes = match URL_SAFE_NO_PAD.decode(parts[1]) {
Ok(b) => b,
Err(_) => {
return (
StatusCode::UNAUTHORIZED,
Json(json!({"error": "AuthenticationFailed"})),
)
.into_response();
}
};
let claims: serde_json::Value = match serde_json::from_slice(&claims_bytes) {
Ok(c) => c,
Err(_) => {
return (
StatusCode::UNAUTHORIZED,
Json(json!({"error": "AuthenticationFailed"})),
)
.into_response();
}
};
match claims.get("jti").and_then(|j| j.as_str()) {
Some(jti) => jti.to_string(),
None => {
return (
StatusCode::UNAUTHORIZED,
Json(json!({"error": "AuthenticationFailed"})),
)
.into_response();
}
}
};
let reuse_check = sqlx::query_scalar!(
"SELECT session_id FROM used_refresh_tokens WHERE refresh_jti = $1",
if let Ok(Some(session_id)) = sqlx::query_scalar!(
"SELECT session_id FROM used_refresh_tokens WHERE refresh_jti = $1 FOR UPDATE",
refresh_jti
)
.fetch_optional(&state.db)
.await;
if let Ok(Some(session_id)) = reuse_check {
.fetch_optional(&mut *tx)
.await
{
warn!("Refresh token reuse detected! Revoking token family for session_id: {}", session_id);
let _ = sqlx::query!("DELETE FROM session_tokens WHERE id = $1", session_id)
.execute(&state.db)
.execute(&mut *tx)
.await;
return (
StatusCode::UNAUTHORIZED,
Json(json!({"error": "ExpiredToken", "message": "Refresh token has been revoked due to suspected compromise"})),
)
.into_response();
let _ = tx.commit().await;
return ApiError::ExpiredTokenMsg("Refresh token has been revoked due to suspected compromise".into()).into_response();
}
let session = sqlx::query!(
let session_row = match sqlx::query!(
r#"SELECT st.id, st.did, k.key_bytes, k.encryption_version
FROM session_tokens st
JOIN users u ON st.did = u.did
JOIN user_keys k ON u.id = k.user_id
WHERE st.refresh_jti = $1 AND st.refresh_expires_at > NOW()"#,
WHERE st.refresh_jti = $1 AND st.refresh_expires_at > NOW()
FOR UPDATE OF st"#,
refresh_jti
)
.fetch_optional(&state.db)
.await;
match session {
Ok(Some(session_row)) => {
let session_id = session_row.id;
let did = &session_row.did;
let key_bytes = match crate::config::decrypt_key(&session_row.key_bytes, session_row.encryption_version) {
Ok(k) => k,
Err(e) => {
error!("Failed to decrypt user key: {:?}", e);
return (
StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({"error": "InternalError"})),
)
.into_response();
}
};
if let Err(_) = crate::auth::verify_refresh_token(&refresh_token, &key_bytes) {
return (StatusCode::UNAUTHORIZED, Json(json!({"error": "AuthenticationFailed", "message": "Invalid refresh token"}))).into_response();
}
let new_access_meta = match crate::auth::create_access_token_with_metadata(did, &key_bytes) {
Ok(m) => m,
Err(e) => {
error!("Failed to create access token: {:?}", e);
return (
StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({"error": "InternalError"})),
)
.into_response();
}
};
let new_refresh_meta = match crate::auth::create_refresh_token_with_metadata(did, &key_bytes) {
Ok(m) => m,
Err(e) => {
error!("Failed to create refresh token: {:?}", e);
return (
StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({"error": "InternalError"})),
)
.into_response();
}
};
let mut tx = match state.db.begin().await {
Ok(tx) => tx,
Err(e) => {
error!("Failed to begin transaction: {:?}", e);
return (
StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({"error": "InternalError"})),
)
.into_response();
}
};
if let Err(e) = sqlx::query!(
"INSERT INTO used_refresh_tokens (refresh_jti, session_id) VALUES ($1, $2)",
refresh_jti,
session_id
)
.execute(&mut *tx)
.await
{
error!("Failed to record used refresh token: {:?}", e);
return (
StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({"error": "InternalError"})),
)
.into_response();
}
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,
new_refresh_meta.jti,
new_access_meta.expires_at,
new_refresh_meta.expires_at,
session_id
)
.execute(&mut *tx)
.await
{
error!("Database error updating session: {:?}", e);
return (
StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({"error": "InternalError"})),
)
.into_response();
}
if let Err(e) = tx.commit().await {
error!("Failed to commit transaction: {:?}", e);
return (
StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({"error": "InternalError"})),
)
.into_response();
}
let user = sqlx::query!("SELECT handle FROM users WHERE did = $1", did)
.fetch_optional(&state.db)
.await;
match user {
Ok(Some(u)) => {
return (
StatusCode::OK,
Json(json!({
"accessJwt": new_access_meta.token,
"refreshJwt": new_refresh_meta.token,
"handle": u.handle,
"did": did
})),
)
.into_response();
}
Ok(None) => {
error!("User not found for existing session: {}", did);
return (
StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({"error": "InternalError"})),
)
.into_response();
}
Err(e) => {
error!("Database error fetching user: {:?}", e);
return (
StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({"error": "InternalError"})),
)
.into_response();
}
}
}
Ok(None) => {
return (
StatusCode::UNAUTHORIZED,
Json(json!({"error": "AuthenticationFailed", "message": "Invalid refresh token"})),
)
.into_response();
}
.fetch_optional(&mut *tx)
.await
{
Ok(Some(row)) => row,
Ok(None) => return ApiError::AuthenticationFailedMsg("Invalid refresh token".into()).into_response(),
Err(e) => {
error!("Database error fetching session: {:?}", e);
return (
StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({"error": "InternalError"})),
)
.into_response();
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) => {
error!("Failed to decrypt user key: {:?}", e);
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) => {
error!("Failed to create access token: {:?}", e);
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) => {
error!("Failed to create refresh token: {:?}", e);
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,
session_row.id
)
.execute(&mut *tx)
.await
{
Ok(result) if result.rows_affected() == 0 => {
warn!("Concurrent refresh token reuse detected for session_id: {}", session_row.id);
let _ = sqlx::query!("DELETE FROM session_tokens WHERE id = $1", session_row.id)
.execute(&mut *tx)
.await;
let _ = tx.commit().await;
return ApiError::ExpiredTokenMsg("Refresh token has been revoked due to suspected compromise".into()).into_response();
}
Err(e) => {
error!("Failed to record used refresh token: {:?}", e);
return ApiError::InternalError.into_response();
}
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,
new_refresh_meta.jti,
new_access_meta.expires_at,
new_refresh_meta.expires_at,
session_row.id
)
.execute(&mut *tx)
.await
{
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!("SELECT handle FROM users WHERE did = $1", session_row.did)
.fetch_optional(&state.db)
.await
{
Ok(Some(u)) => Json(json!({
"accessJwt": new_access_meta.token,
"refreshJwt": new_refresh_meta.token,
"handle": u.handle,
"did": session_row.did
})).into_response(),
Ok(None) => {
error!("User not found for existing session: {}", session_row.did);
ApiError::InternalError.into_response()
}
Err(e) => {
error!("Database error fetching user: {:?}", e);
ApiError::InternalError.into_response()
}
}
}