use argon2::{ password_hash::{rand_core::OsRng, SaltString}, Argon2, PasswordHasher, }; use axum::{ extract::{Path, State}, http::StatusCode, Json, }; use axum_extra::routing::Resource; use serde::{Deserialize, Serialize}; use sqlx::PgPool; use uuid::Uuid; use crate::state::AppState; use super::{error::Error, AccessClaims, RefreshClaims}; #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct Credential { pub id: Uuid, pub password: String, } pub fn router() -> Resource { Resource::named("credentials") .create(create) .destroy(destroy) } pub async fn create( State(pool): State, Json(Credential { id, password }): Json, ) -> Result<(StatusCode, (AccessClaims, RefreshClaims)), Error> { let salt = SaltString::generate(&mut OsRng); let password_hash = Argon2::default().hash_password(password.as_bytes(), &salt)?; let uuid = sqlx::query!( "INSERT INTO credential (id, password_hash) VALUES ($1, $2) RETURNING id", id, password_hash.to_string() ) .fetch_optional(&pool) .await? .ok_or(Error::Registration)? .id; let refresh = RefreshClaims::issue(uuid); let access = refresh.refresh(); Ok((StatusCode::CREATED, (access, refresh))) } pub async fn destroy(State(pool): State, Path(uuid): Path) -> Result<(), Error> { let mut tx = pool.begin().await?; let rows = sqlx::query!("DELETE FROM credential WHERE id = $1", uuid) .execute(&mut *tx) .await? .rows_affected(); if rows == 0 { return Err(Error::UserNotFound); } else if rows > 1 { tracing::warn!("DELETE query affected {rows} rows. This should not happen."); } tx.commit().await?; Ok(()) }