implement password changes

This commit is contained in:
DSeeLP 2025-04-06 20:07:27 +02:00
parent c7c96a3595
commit 0202985e22
6 changed files with 123 additions and 16 deletions

View File

@ -34,6 +34,31 @@ impl Name {
} }
} }
#[derive(Clone, PartialEq, Eq, Validate, Serialize, Deserialize)]
#[cfg_attr(feature = "schemas", derive(schemars::JsonSchema))]
#[garde(transparent)]
pub struct Password(#[garde(length(min = 8, max = 96))] pub String);
impl std::fmt::Debug for Password {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_tuple("Password").field(&"...").finish()
}
}
impl Deref for Password {
type Target = str;
fn deref(&self) -> &Self::Target {
self.0.as_str()
}
}
impl Password {
pub fn into_inner(self) -> String {
self.0
}
}
#[derive(Debug, Clone, Deserialize, Serialize, Validate, PartialEq, Eq)] #[derive(Debug, Clone, Deserialize, Serialize, Validate, PartialEq, Eq)]
#[cfg_attr(feature = "schemas", derive(schemars::JsonSchema))] #[cfg_attr(feature = "schemas", derive(schemars::JsonSchema))]
#[serde(untagged)] #[serde(untagged)]
@ -42,22 +67,20 @@ pub enum NameOrUuid {
Name(#[garde(dive)] Name), Name(#[garde(dive)] Name),
} }
#[derive(Clone, Deserialize, Serialize, Validate, PartialEq, Eq)] #[derive(Debug, Clone, Deserialize, Serialize, Validate, PartialEq, Eq)]
#[cfg_attr(feature = "schemas", derive(schemars::JsonSchema))]
pub struct ChangePassword {
#[garde(dive)]
pub password: Password,
}
#[derive(Debug, Clone, Deserialize, Serialize, Validate, PartialEq, Eq)]
#[cfg_attr(feature = "schemas", derive(schemars::JsonSchema))] #[cfg_attr(feature = "schemas", derive(schemars::JsonSchema))]
pub struct Credentials { pub struct Credentials {
#[garde(dive)] #[garde(dive)]
pub name: Name, pub name: Name,
#[garde(length(min = 8, max = 96))] #[garde(dive)]
pub password: String, pub password: Password,
}
impl std::fmt::Debug for Credentials {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Credentials")
.field("name", &self.name)
.field("password", &"...")
.finish()
}
} }
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)] #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]

View File

@ -125,6 +125,27 @@ paths:
$ref: '#/components/responses/Unauthorized' $ref: '#/components/responses/Unauthorized'
default: default:
$ref: '#/components/responses/Default' $ref: '#/components/responses/Default'
/api/users/@me/password:
put:
operationId: self-change-password
summary: Change password
tags:
- Users
security:
- bearer: []
responses:
200:
description: Ok
content:
application/json:
schema:
$ref: '#/components/schemas/TokenResponse'
401:
$ref: '#/components/responses/Unauthorized'
422:
$ref: '#/components/responses/InvalidBody'
default:
$ref: '#/components/responses/Default'
/api/users/@me/data: /api/users/@me/data:
get: get:
operationId: self-list-data operationId: self-list-data

View File

@ -52,7 +52,7 @@ async fn login(
else { else {
return Err(invalid_username_or_password().into()); return Err(invalid_username_or_password().into());
}; };
verify_password(credentials.password, &hash).map_err(|err| match err { verify_password(&credentials.password.0, &hash).map_err(|err| match err {
password_auth::VerifyError::Parse(parse_error) => { password_auth::VerifyError::Parse(parse_error) => {
Error::new(InnerError::PHCParse(parse_error)) Error::new(InnerError::PHCParse(parse_error))
} }
@ -63,7 +63,7 @@ async fn login(
} }
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
struct Claims<'a> { pub(crate) struct Claims<'a> {
#[serde(rename = "iss")] #[serde(rename = "iss")]
issuer: Cow<'a, str>, issuer: Cow<'a, str>,
#[serde(rename = "sub")] #[serde(rename = "sub")]

View File

@ -1,11 +1,17 @@
use std::sync::Arc; use std::sync::Arc;
use axum::{Router, extract::Path, routing::get}; use axum::{
Router,
extract::Path,
routing::{get, put},
};
use bank_core::{ use bank_core::{
ChangePassword, TokenResponse,
pagination::Pagination, pagination::Pagination,
transaction::{FullTransaction, TransactionQuery}, transaction::{FullTransaction, TransactionQuery},
user::{User, UserAccounts, UserBalance}, user::{User, UserAccounts, UserBalance},
}; };
use garde::Validate;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use tracing::instrument; use tracing::instrument;
use uuid::Uuid; use uuid::Uuid;
@ -15,7 +21,10 @@ use crate::{
model::{Accounts, Transactions, Users}, model::{Accounts, Transactions, Users},
}; };
use super::{AppState, EState, Error, Json, PaginationQuery, Query, State, auth::Auth}; use super::{
AppState, EState, Error, Json, PaginationQuery, Query, State,
auth::{Auth, Claims},
};
pub(super) fn router() -> Router<Arc<AppState>> { pub(super) fn router() -> Router<Arc<AppState>> {
Router::new() Router::new()
@ -25,6 +34,7 @@ pub(super) fn router() -> Router<Arc<AppState>> {
.route("/@me/data/{key}", get(get_user_data).put(set_user_data)) .route("/@me/data/{key}", get(get_user_data).put(set_user_data))
.route("/@me/accounts", get(user_accounts)) .route("/@me/accounts", get(user_accounts))
.route("/@me/transactions", get(me_transaction_history)) .route("/@me/transactions", get(me_transaction_history))
.route("/@me/password", put(change_password))
.route("/", get(list_users)) .route("/", get(list_users))
} }
@ -141,6 +151,20 @@ pub async fn set_user_data(
Ok(()) Ok(())
} }
#[instrument(skip(state))]
async fn change_password(
EState(state): State,
auth: Auth,
Json(body): Json<ChangePassword>,
) -> Result<Json<TokenResponse>, Error> {
body.validate()?;
let mut conn = state.conn().await?;
let id = auth.user_id();
Users::change_password(&mut conn, id, &body.password).await?;
let token = Claims::new(id).encode(&state.encoding_key).unwrap();
Ok(Json(TokenResponse { token }))
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use uuid::Uuid; use uuid::Uuid;

View File

@ -42,6 +42,7 @@ impl Users {
let result = client.query_one(&stmt, &[&id]).await?; let result = client.query_one(&stmt, &[&id]).await?;
Ok(result.get(0)) Ok(result.get(0))
} }
#[instrument(skip(client, password))] #[instrument(skip(client, password))]
pub async fn create( pub async fn create(
client: &mut impl GenericClient, client: &mut impl GenericClient,
@ -64,6 +65,20 @@ impl Users {
Ok(id) Ok(id)
} }
#[instrument(skip(client, password))]
pub async fn change_password(
client: &mut impl GenericClient,
user: Uuid,
password: &str,
) -> Result<(), tokio_postgres::Error> {
let hash = password_auth::generate_hash(password);
let stmt = client
.prepare_cached("update users set password = $2 where id = $1")
.await?;
client.execute(&stmt, &[&user, &hash]).await?;
Ok(())
}
#[instrument(skip(client))] #[instrument(skip(client))]
pub async fn get_password_and_info_by_username( pub async fn get_password_and_info_by_username(
client: &impl GenericClient, client: &impl GenericClient,

View File

@ -0,0 +1,24 @@
POST {{host}}/api/login
{
"name": "user6",
"password": "this-is-a-password"
}
HTTP 200
[Captures]
token: jsonpath "$.token"
PUT {{host}}/api/users/@me/password
Authorization: Bearer {{token}}
{
"password": "this-is-another-password"
}
HTTP 200
POST {{host}}/api/login
{
"name": "user6",
"password": "this-is-another-password"
}
HTTP 200