mirror of
https://git.dirksys.ovh/dirk/bankserver.git
synced 2025-12-20 02:59:20 +01:00
implement password changes
This commit is contained in:
parent
c7c96a3595
commit
0202985e22
@ -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)]
|
||||
#[cfg_attr(feature = "schemas", derive(schemars::JsonSchema))]
|
||||
#[serde(untagged)]
|
||||
@ -42,22 +67,20 @@ pub enum NameOrUuid {
|
||||
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))]
|
||||
pub struct Credentials {
|
||||
#[garde(dive)]
|
||||
pub name: Name,
|
||||
#[garde(length(min = 8, max = 96))]
|
||||
pub password: String,
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
#[garde(dive)]
|
||||
pub password: Password,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
|
||||
|
||||
@ -125,6 +125,27 @@ paths:
|
||||
$ref: '#/components/responses/Unauthorized'
|
||||
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:
|
||||
get:
|
||||
operationId: self-list-data
|
||||
|
||||
@ -52,7 +52,7 @@ async fn login(
|
||||
else {
|
||||
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) => {
|
||||
Error::new(InnerError::PHCParse(parse_error))
|
||||
}
|
||||
@ -63,7 +63,7 @@ async fn login(
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct Claims<'a> {
|
||||
pub(crate) struct Claims<'a> {
|
||||
#[serde(rename = "iss")]
|
||||
issuer: Cow<'a, str>,
|
||||
#[serde(rename = "sub")]
|
||||
|
||||
@ -1,11 +1,17 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use axum::{Router, extract::Path, routing::get};
|
||||
use axum::{
|
||||
Router,
|
||||
extract::Path,
|
||||
routing::{get, put},
|
||||
};
|
||||
use bank_core::{
|
||||
ChangePassword, TokenResponse,
|
||||
pagination::Pagination,
|
||||
transaction::{FullTransaction, TransactionQuery},
|
||||
user::{User, UserAccounts, UserBalance},
|
||||
};
|
||||
use garde::Validate;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tracing::instrument;
|
||||
use uuid::Uuid;
|
||||
@ -15,7 +21,10 @@ use crate::{
|
||||
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>> {
|
||||
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/accounts", get(user_accounts))
|
||||
.route("/@me/transactions", get(me_transaction_history))
|
||||
.route("/@me/password", put(change_password))
|
||||
.route("/", get(list_users))
|
||||
}
|
||||
|
||||
@ -141,6 +151,20 @@ pub async fn set_user_data(
|
||||
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)]
|
||||
mod tests {
|
||||
use uuid::Uuid;
|
||||
|
||||
@ -42,6 +42,7 @@ impl Users {
|
||||
let result = client.query_one(&stmt, &[&id]).await?;
|
||||
Ok(result.get(0))
|
||||
}
|
||||
|
||||
#[instrument(skip(client, password))]
|
||||
pub async fn create(
|
||||
client: &mut impl GenericClient,
|
||||
@ -64,6 +65,20 @@ impl Users {
|
||||
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))]
|
||||
pub async fn get_password_and_info_by_username(
|
||||
client: &impl GenericClient,
|
||||
|
||||
24
tests/integration/user-password.hurl
Normal file
24
tests/integration/user-password.hurl
Normal 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
|
||||
Loading…
x
Reference in New Issue
Block a user