feat: basic register and login

This commit is contained in:
Matthieu Bessat 2024-10-21 00:05:20 +02:00
parent 98be8dd574
commit 327f0cd5b9
39 changed files with 990 additions and 66 deletions

View file

@ -1,8 +1,8 @@
use argh::FromArgs;
use anyhow::{anyhow, Context, Result};
use anyhow::{Context, Result};
use log::info;
use crate::{get_app_context, server::{start_http_server, ServerConfig}};
use crate::{get_app_context, server::{start_http_server, ServerConfig}, DEFAULT_ASSETS_PATH};
#[derive(Debug, FromArgs)]
/// Autotasker daemon
@ -14,7 +14,7 @@ struct ServerCliFlags {
/// path to the Sqlite3 DB file to use
#[argh(option)]
database: Option<String>,
/// path to the static assets dir
#[argh(option)]
static_assets: Option<String>,
@ -27,12 +27,11 @@ struct ServerCliFlags {
listen_port: u32
}
const DEFAULT_ASSETS_PATH: &'static str = &"/usr/local/lib/autotasker/assets";
/// handle CLI arguments to start process daemon
pub async fn start_server_cli() -> Result<()> {
info!("Starting minauth");
let flags: ServerCliFlags = argh::from_env();
let (config, db_pool) = get_app_context(crate::StartAppConfig {
let (config, secrets, db_pool) = get_app_context(crate::StartAppConfig {
config_path: flags.config,
database_path: flags.database
}).await.context("Getting app context")?;
@ -43,6 +42,7 @@ pub async fn start_server_cli() -> Result<()> {
listen_port: flags.listen_port
},
config,
secrets,
db_pool
).await
}

View file

@ -0,0 +1,19 @@
use axum::{extract::State, http::HeaderMap, response::{Html, IntoResponse}};
use minijinja::context;
use crate::server::AppState;
pub async fn authorize_form(
State(app_state): State<AppState>
) -> impl IntoResponse {
// 1. Verify if login
Html(
app_state.templating_env.get_template("pages/authorize.html").unwrap()
.render(context!())
.unwrap()
)
}

View file

@ -1,26 +1,99 @@
use axum::{extract::State, response::{Html, IntoResponse}};
use chrono::{Duration, SecondsFormat, Utc};
use log::info;
use serde::{Deserialize, Serialize};
use axum::{extract::State, http::{HeaderMap, HeaderValue, StatusCode}, response::{Html, IntoResponse}, Form};
use fully_pub::fully_pub;
use minijinja::context;
use crate::server::AppState;
use crate::{
models::user::{User, UserStatus},
server::AppState,
services::{password::verify_password_hash, session::create_token}
};
pub async fn login_form(
State(app_state): State<AppState>
) -> impl IntoResponse {
Html(
app_state.templating_env.get_template("pages/home.html").unwrap()
app_state.templating_env.get_template("pages/login.html").unwrap()
.render(context!())
.unwrap()
)
}
#[derive(Debug, Deserialize)]
#[fully_pub]
struct LoginForm {
/// handle or email or user_id
login: String,
password: String
}
const DUMMY_PASSWORD_HASH: &str = "$argon2id$v=19$m=19456,t=2,p=1$+06ud2g4uVTI7kUIXjWM4g$6XqwuHt/+xl0d5J4BYKuIbg2acBp6udxMCnmJ6QfceY";
pub async fn perform_login(
State(app_state): State<AppState>
State(app_state): State<AppState>,
Form(login): Form<LoginForm>
) -> impl IntoResponse {
Html(
app_state.templating_env.get_template("pages/home.html").unwrap()
.render(context!())
.unwrap()
)
// get user from db
let user_res = sqlx::query_as::<_, User>("SELECT * FROM users WHERE handle = $1 OR email = $2")
.bind(&login.login)
.bind(&login.login)
.fetch_one(&app_state.db)
.await;
let password_hash = match &user_res {
Ok(u) => u
.password_hash
.clone()
.unwrap_or(DUMMY_PASSWORD_HASH.into()),
Err(_e) => DUMMY_PASSWORD_HASH.into()
};
let templ = app_state.templating_env.get_template("pages/login.html").unwrap();
if verify_password_hash(password_hash, login.password).is_err() {
return (
StatusCode::BAD_REQUEST,
Html(
templ.render(context!(
error => Some("Invalid login or password.".to_string())
)).unwrap()
)
).into_response();
}
let user = user_res.expect("Expected User to be found.");
if user.status == UserStatus::Disabled {
return (
StatusCode::BAD_REQUEST,
Html(
templ.render(context!(
error => Some("This account is disabled.".to_string())
)).unwrap()
)
).into_response();
}
info!("User {:?} {:?} logged in", &user.handle, &user.email);
let _result = sqlx::query("UPDATE users SET last_login_at = $2 WHERE id = $1")
.bind(user.id.clone())
.bind(Utc::now().to_rfc3339_opts(SecondsFormat::Millis, true))
.execute(&app_state.db)
.await.unwrap();
let jwt = create_token(&app_state.secrets, &user);
// TODO: handle keep_session boolean from form and specify cookie max age only if this setting
// is true
let cookie_max_age = Duration::days(7).num_seconds();
let jwt_cookie = format!("minauth_jwt={jwt}; SameSite=Lax; Max-Age={cookie_max_age}");
let mut headers = HeaderMap::new();
headers.insert("Set-Cookie", HeaderValue::from_str(&jwt_cookie).unwrap());
headers.insert("Location", HeaderValue::from_str(&format!("/me")).unwrap());
(StatusCode::FOUND, headers, Html(
templ.render(context!()).unwrap()
)).into_response()
}

View file

@ -0,0 +1,12 @@
use axum::{http::StatusCode, response::Redirect};
use axum_extra::extract::CookieJar;
pub async fn perform_logout(
cookies: CookieJar
) -> Result<(CookieJar, Redirect), StatusCode> {
Ok((
cookies.remove("minauth_jwt"),
Redirect::to("/")
))
}

18
src/controllers/ui/me.rs Normal file
View file

@ -0,0 +1,18 @@
use axum::{extract::State, response::{Html, IntoResponse}, Extension};
use minijinja::context;
use crate::{server::AppState, services::session::TokenClaims};
pub async fn me_page(
State(app_state): State<AppState>,
Extension(token_claims): Extension<TokenClaims>
) -> impl IntoResponse {
Html(
app_state.templating_env.get_template("pages/me.html").unwrap()
.render(context!(
token_claims => token_claims
))
.unwrap()
)
}

View file

@ -2,3 +2,5 @@ pub mod home;
pub mod authorize;
pub mod login;
pub mod register;
pub mod me;
pub mod logout;

View file

@ -0,0 +1,86 @@
use axum::{extract::State, response::{Html, IntoResponse}, Form};
use chrono::{SecondsFormat, Utc};
use serde::{Deserialize, Serialize};
use minijinja::context;
use fully_pub::fully_pub;
use uuid::Uuid;
use crate::{models::user::{User, UserStatus}, server::AppState, services::password::get_password_hash};
pub async fn register_form(
State(app_state): State<AppState>
) -> impl IntoResponse {
Html(
app_state.templating_env.get_template("pages/register.html").unwrap()
.render(context!())
.unwrap()
)
}
#[derive(Debug, Deserialize)]
#[fully_pub]
struct RegisterForm {
handle: String,
email: String,
password: String,
}
pub async fn perform_register(
State(app_state): State<AppState>,
Form(register): Form<RegisterForm>
) -> impl IntoResponse {
let templ = app_state.templating_env.get_template("pages/register.html").unwrap();
let user_res = sqlx::query_as::<_, User>("SELECT * FROM users WHERE handle = $1 OR email = $2")
.bind(&register.handle)
.bind(&register.email)
.fetch_one(&app_state.db)
.await;
if user_res.is_ok() {
// user already exists
return Html(
templ.render(context!(
success => true
)).unwrap()
);
}
let password_hash = Some(
get_password_hash(register.password)
.expect("To process password").1
);
let user = User {
id: Uuid::new_v4().to_string(),
email: Some(register.email),
handle: register.handle,
full_name: None,
picture: None,
password_hash,
activation_token: None,
status: UserStatus::Active,
created_at: Utc::now(),
website: None,
last_login_at: None
};
// save in DB
let _result = sqlx::query("INSERT INTO users (id, handle, email, status, password_hash, created_at) VALUES ($1, $2, $3, $4, $5, $6)")
.bind(user.id)
.bind(user.handle)
.bind(user.email)
.bind(user.status.to_string())
.bind(user.password_hash)
.bind(user.created_at.to_rfc3339_opts(SecondsFormat::Millis, true))
.execute(&app_state.db)
.await.unwrap();
Html(
templ
.render(context!(
success => true
))
.unwrap()
)
}

View file

@ -4,20 +4,21 @@ pub mod router;
pub mod server;
pub mod database;
pub mod cli;
pub mod utils;
pub mod services;
pub mod middlewares;
use std::fs;
use std::{env, fs};
use anyhow::{Result, Context, anyhow};
use database::prepare_database;
use log::info;
use sqlx::sqlite::{SqliteConnectOptions, SqlitePoolOptions};
use sqlx::{ConnectOptions, Pool, Sqlite};
use models::config::Config;
use minijinja::{context, Environment};
use sqlx::{Pool, Sqlite};
use models::config::{AppSecrets, Config};
const DEFAULT_DB_PATH: &'static str = &"/var/lib/autotasker/autotasker.db";
const DEFAULT_ASSETS_PATH: &'static str = &"/usr/local/lib/autotasker/assets";
const DEFAULT_CONFIG_PATH: &'static str = &"/etc/autotasker/config.yaml";
pub const DEFAULT_DB_PATH: &'static str = &"/var/lib/autotasker/autotasker.db";
pub const DEFAULT_ASSETS_PATH: &'static str = &"/usr/local/lib/autotasker/assets";
pub const DEFAULT_CONFIG_PATH: &'static str = &"/etc/autotasker/config.yaml";
fn get_config(path: String) -> Result<Config> {
let inp_def_yaml = fs::read_to_string(path)
@ -37,15 +38,23 @@ async fn main() -> Result<()> {
cli::start_server_cli().await
}
async fn get_app_context(start_app_config: StartAppConfig) -> Result<(Config, Pool<Sqlite>)> {
async fn get_app_context(start_app_config: StartAppConfig) -> Result<(Config, AppSecrets, Pool<Sqlite>)> {
env_logger::init();
let pool = prepare_database(
&start_app_config.database_path.unwrap_or(DEFAULT_DB_PATH.to_string())
).await.context("Could not prepare db")?;
let _ = dotenvy::dotenv();
let database_path = &start_app_config.database_path.unwrap_or(DEFAULT_DB_PATH.to_string());
info!("Using database file at {}", database_path);
let pool = prepare_database(&database_path).await.context("Could not prepare db.")?;
let config_path = start_app_config.config_path.unwrap_or(DEFAULT_CONFIG_PATH.to_string());
info!("Using config file at {}", &config_path);
let config: Config = get_config(config_path).expect("Cannot get config");
let config: Config = get_config(config_path)
.expect("Cannot get config.");
Ok((config, pool))
dotenvy::dotenv().context("loading .env")?;
let secrets = AppSecrets {
jwt_secret: env::var("APP_JWT_SECRET").context("Expecting APP_JWT_SECRET env var.")?
};
Ok((config, secrets, pool))
}

28
src/middlewares/auth.rs Normal file
View file

@ -0,0 +1,28 @@
use axum::{extract::{Request, State}, http::{HeaderMap, StatusCode}, middleware::Next, response::{Response, Html, IntoResponse}};
use axum_extra::extract::CookieJar;
use crate::{server::AppState, services::session::verify_token};
pub async fn auth_middleware(
State(app_state): State<AppState>,
cookies: CookieJar,
mut req: Request,
next: Next,
) -> Result<Response, StatusCode> {
let jwt = match cookies.get("minauth_jwt") {
Some(cookie) => cookie.value(),
None => {
// return Err((StatusCode::UNAUTHORIZED, Html("Did not found header")));
return Err(StatusCode::UNAUTHORIZED);
}
};
let token_claims = match verify_token(&app_state.secrets, &jwt) {
Ok(val) => val,
Err(_e) => {
return Err(StatusCode::UNAUTHORIZED);
}
};
req.extensions_mut().insert(token_claims);
Ok(next.run(req).await)
}

1
src/middlewares/mod.rs Normal file
View file

@ -0,0 +1 @@
pub mod auth;

View file

View file

@ -10,15 +10,30 @@ use uuid::Uuid;
/// Instance branding/customization config
struct InstanceConfig {
name: String,
logo_uri: String
logo_uri: Option<String>
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[fully_pub]
struct Application {
slug: String,
name: String,
client_id: String,
client_secret: String
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[fully_pub]
/// Configuration of this minauthator instance
struct Config {
/// configure current autotasker instance
instance: Option<InstanceConfig>,
instance: InstanceConfig,
applications: Vec<Application>
}
#[derive(Debug, Clone)]
#[fully_pub]
pub struct AppSecrets {
jwt_secret: String
}

View file

@ -1,3 +1,2 @@
pub mod app;
pub mod config;
pub mod user;

View file

@ -0,0 +1,30 @@
use fully_pub::fully_pub;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
#[derive(sqlx::Type, Clone, Debug, Serialize, Deserialize, PartialEq)]
#[derive(strum_macros::Display)]
#[fully_pub]
enum UserStatus {
Active,
Disabled
}
#[derive(sqlx::FromRow, Deserialize, Serialize, Debug)]
#[fully_pub]
struct User {
/// uuid
id: String,
handle: String,
full_name: Option<String>,
email: Option<String>,
website: Option<String>,
picture: Option<String>, // embeded blob to store profile pic
password_hash: Option<String>, // argon2 password hash
status: UserStatus,
activation_token: Option<String>,
last_login_at: Option<DateTime<Utc>>,
created_at: DateTime<Utc>
}

View file

@ -1,10 +1,27 @@
use axum::{routing::{get, post}, Router};
use axum::{middleware, routing::{get, post}, Router};
use tower_http::services::ServeDir;
use crate::{controllers::ui, server::AppState};
use crate::{controllers::ui, middlewares::auth::auth_middleware, server::{AppState, ServerConfig}};
pub fn build_router() -> Router<AppState> {
Router::new()
pub fn build_router(server_config: &ServerConfig, app_state: AppState) -> Router<AppState> {
let public_routes = Router::new()
.route("/", get(ui::home::home))
.route("/register", get(ui::register::register_form))
.route("/register", post(ui::register::perform_register))
.route("/login", get(ui::login::login_form))
.route("/login", post(ui::login::perform_login))
.route("/login", post(ui::login::perform_login));
let user_routes = Router::new()
.route("/me", get(ui::me::me_page))
.route("/logout", get(ui::logout::perform_logout))
.route("/authorize", get(ui::authorize::authorize_form))
.layer(middleware::from_fn_with_state(app_state, auth_middleware));
Router::new()
.merge(public_routes)
.merge(user_routes)
.nest_service(
"/assets",
ServeDir::new(server_config.assets_path.clone())
)
}

View file

@ -4,23 +4,17 @@ use anyhow::{Result, Context, anyhow};
use log::info;
use minijinja::{context, Environment};
use sqlx::{Pool, Sqlite};
use crate::{models::config::Config, router::build_router};
use crate::{models::config::{AppSecrets, Config}, router::build_router};
fn build_templating_env(config: &Config) -> Environment<'static> {
let mut templating_env = Environment::new();
let mut env = Environment::new();
let _ = templating_env
.add_template("layouts/base.html", include_str!("./templates/layouts/base.html"));
let _ = templating_env
.add_template("pages/home.html", include_str!("./templates/pages/home.html"));
minijinja_embed::load_templates!(&mut env);
// TODO: better loading with embed https://docs.rs/minijinja-embed/latest/minijinja_embed/
templating_env.add_global("gl", context! {
instance => context! {
version => "1.243".to_string()
}
env.add_global("gl", context! {
instance => config.instance
});
templating_env
env
}
#[derive(Debug)]
@ -35,25 +29,31 @@ pub struct ServerConfig {
#[derive(Debug, Clone)]
#[fully_pub]
pub struct AppState {
secrets: AppSecrets,
config: Config,
db: Pool<Sqlite>,
templating_env: Environment<'static>
}
pub async fn start_http_server(server_config: ServerConfig, config: Config, db_pool: Pool<Sqlite>) -> Result<()> {
pub async fn start_http_server(
server_config: ServerConfig,
config: Config,
secrets: AppSecrets,
db_pool: Pool<Sqlite>
) -> Result<()> {
// build state
let state = AppState {
templating_env: build_templating_env(&config),
config,
secrets,
db: db_pool
};
// build routes
let services = build_router()
.nest_service(
"/assets",
ServeDir::new(server_config.assets_path)
)
let services = build_router(
&server_config,
state.clone()
)
.with_state(state);
let listen_addr = format!("{}:{}", server_config.listen_host, server_config.listen_port);

2
src/services/mod.rs Normal file
View file

@ -0,0 +1,2 @@
pub mod password;
pub mod session;

35
src/services/password.rs Normal file
View file

@ -0,0 +1,35 @@
use anyhow::{anyhow, Result};
use argon2::{
password_hash::{
rand_core::OsRng,
PasswordHash, PasswordHasher, PasswordVerifier, SaltString
},
Argon2
};
pub fn get_password_hash(password: String) -> Result<(String, String)> {
let salt = SaltString::generate(&mut OsRng);
// Argon2 with default params (Argon2id v19)
let argon2 = Argon2::default();
// Hash password to PHC string ($argon2id$v=19$...)
match argon2.hash_password(password.as_bytes(), &salt) {
Ok(val) => Ok((salt.to_string(), val.to_string())),
Err(_) => Err(anyhow!("Failed to process password."))
}
}
pub fn verify_password_hash(password_hash: String, password: String) -> Result<()> {
let parsed_hash = match PasswordHash::new(&password_hash) {
Ok(val) => val,
Err(_) => {
return Err(anyhow!("Failed to parse password hash"));
}
};
match Argon2::default().verify_password(password.as_bytes(), &parsed_hash) {
Ok(()) => Ok(()),
Err(_) => Err(anyhow!("Failed to verify password."))
}
}

38
src/services/session.rs Normal file
View file

@ -0,0 +1,38 @@
use fully_pub::fully_pub;
use anyhow::Result;
use serde::{Deserialize, Serialize};
use jsonwebtoken::{encode, decode, get_current_timestamp, Header, Algorithm, Validation, EncodingKey, DecodingKey};
use crate::models::{config::AppSecrets, user::User};
#[derive(Serialize, Deserialize, Clone)]
#[fully_pub]
struct TokenClaims {
sub: String, // user id
exp: u64
}
pub fn create_token(secrets: &AppSecrets, user: &User) -> String {
let claims = TokenClaims {
sub: user.id.clone(),
exp: get_current_timestamp() + 86_400
};
let token = encode(
&Header::default(),
&claims,
&EncodingKey::from_secret(&secrets.jwt_secret.as_bytes())
).expect("Create token");
return token;
}
pub fn verify_token(secrets: &AppSecrets, jwt: &str) -> Result<TokenClaims> {
let token_data = decode::<TokenClaims>(
&jwt,
&DecodingKey::from_secret(&secrets.jwt_secret.as_bytes()),
&Validation::new(Algorithm::HS256)
)?;
Ok(token_data.claims)
}

View file

@ -0,0 +1,15 @@
<footer class="d-flex flex-wrap justify-content-between align-items-center py-3 my-4 border-top">
<p class="col-md-4 mb-0 text-muted">© 2022 Company, Inc</p>
<a href="/" class="col-md-4 d-flex align-items-center justify-content-center mb-3 mb-md-0 me-md-auto link-dark text-decoration-none">
<svg class="bi me-2" width="40" height="32"><use xlink:href="#bootstrap"></use></svg>
</a>
<ul class="nav col-md-4 justify-content-end">
<li class="nav-item"><a href="#" class="nav-link px-2 text-muted">Home</a></li>
<li class="nav-item"><a href="#" class="nav-link px-2 text-muted">Features</a></li>
<li class="nav-item"><a href="#" class="nav-link px-2 text-muted">Pricing</a></li>
<li class="nav-item"><a href="#" class="nav-link px-2 text-muted">FAQs</a></li>
<li class="nav-item"><a href="#" class="nav-link px-2 text-muted">About</a></li>
</ul>
</footer>

View file

@ -0,0 +1,26 @@
<header>
<nav class="navbar navbar-expand-lg bg-body-tertiary">
<div class="container-fluid">
<a class="navbar-brand" href="#">Minauth</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav">
<li class="nav-item">
<a class="nav-link active" aria-current="page" href="#">Home</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/login">Login</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/register">Register</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/logout">Logout</a>
</li>
</ul>
</div>
</div>
</nav>
</header>

View file

@ -4,19 +4,14 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Minauth</title>
<link href="/assets/styles/simple.css" rel="stylesheet">
<link href="/assets/styles/app.css" rel="stylesheet">
<link href="/assets/node_modules/bootstrap/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<header>
Minauth
</header>
{% include "components/header.html" %}
<main class="container">
{% block body %}{% endblock %}
</main>
<footer>
Minauth {{ gl.instance.version }}
</footer>
{% include "components/footer.html" %}
</body>
</html>

View file

@ -0,0 +1,34 @@
{% extends "layouts/base.html" %}
{% block body %}
<h1>Login</h1>
<!-- Login form -->
{% if error %}
<div>
Error: {{ error }}
</div>
{% endif %}
<form id="login-form" method="post">
<div class="mb-3">
<label for="login" class="form-label">Email or username</label>
<input
id="login" name="login" type="text"
required
class="form-control"
/>
</div>
<div class="mb-3">
<label for="password" class="form-label">Password</label>
<input
id="password" name="password" type="password"
required
class="form-control"
/>
</div>
<div class="mb-3 form-check">
<input id="keep_session" type="checkbox" class="form-check-input">
<label class="form-check-label" for="keep_session">Check me out</label>
</div>
<button type="submit" class="btn btn-primary">Submit</button>
</form>
{% endblock %}

View file

@ -0,0 +1,9 @@
{% extends "layouts/base.html" %}
{% block body %}
<h1>Me page</h1>
<p>
{{ token_claims.sub }}
</p>
{% endblock %}

View file

@ -0,0 +1,47 @@
{% extends "layouts/base.html" %}
{% block body %}
<!-- Register form -->
<h1>Register</h1>
{% if error %}
<div class="alert alert-danger">
Error: {{ error }}
</div>
{% endif %}
{% if success %}
<div class="alert alert-success">
If all the information you submitted are valid and unique, you're account
has been created and we've sent you a confirmation email.
</div>
{% endif %}
<form id="register-form" method="post">
<div class="mb-3">
<label for="handle" class="form-label">Handle</label>
<input
id="handle" name="handle" type="text"
minlength="2"
maxlength="255"
required
class="form-control"
/>
</div>
<div>
<label for="email">Email</label>
<input
id="email" name="email" type="email"
required
class="form-control"
/>
</div>
<div>
<label for="password">Password</label>
<input
id="password" name="password" type="password"
required
class="form-control"
/>
</div>
<button type="submit" class="btn btn-primary">
Register
</button>
</form>
{% endblock %}

35
src/utils.rs Normal file
View file

@ -0,0 +1,35 @@
use anyhow::{anyhow, Result};
use argon2::{
password_hash::{
rand_core::OsRng,
PasswordHash, PasswordHasher, PasswordVerifier, SaltString
},
Argon2
};
pub fn get_password_hash(password: String) -> Result<(String, String)> {
let salt = SaltString::generate(&mut OsRng);
// Argon2 with default params (Argon2id v19)
let argon2 = Argon2::default();
// Hash password to PHC string ($argon2id$v=19$...)
match argon2.hash_password(password.as_bytes(), &salt) {
Ok(val) => Ok((salt.to_string(), val.to_string())),
Err(_) => Err(anyhow!("Failed to process password."))
}
}
pub fn verify_password_hash(password_hash: String, password: String) -> Result<()> {
let parsed_hash = match PasswordHash::new(&password_hash) {
Ok(val) => val,
Err(_) => {
return Err(anyhow!("Failed to parse password hash"));
}
};
match Argon2::default().verify_password(password.as_bytes(), &parsed_hash) {
Ok(()) => Ok(()),
Err(_) => Err(anyhow!("Failed to verify password."))
}
}