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)]
|
#[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)]
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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")]
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
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