mirror of
https://tangled.org/tranquil.farm/tranquil-pds
synced 2026-09-20 01:04:14 +00:00
Idk. Code quality in general?
This commit is contained in:
@@ -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
@@ -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
@@ -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
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
|
||||
@@ -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, ¶ms.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
@@ -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, ¶ms.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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user