Refactor step one of user registration

This commit is contained in:
Z. Charles Dziura 2024-08-24 13:22:51 -04:00
parent 5596724ba9
commit 29dca09ac7
13 changed files with 119 additions and 59 deletions

View file

@ -1,8 +1,8 @@
HOSTNAME=localhost HOSTNAME=localhost
PORT=42068 PORT=42068
DOMAIN=http://localhost:42069 DOMAIN=http://api.debtpirate.app
RP_ID=localhost RP_ID=debtpirate.app
TOKEN_KEY=k4.local.hWoS2ZulK9xPEATtXH1Dvj_iynzqfUv5ER5_IFTg5-Q TOKEN_KEY=k4.local.hWoS2ZulK9xPEATtXH1Dvj_iynzqfUv5ER5_IFTg5-Q
DATABASE_URL=postgres://debt-pirate:test@192.168.122.215/debt_pirate DATABASE_URL=postgres://debt_pirate:HRURqlUmtjIy@192.168.122.251/debt_pirate
ASSETS_DIR=/home/zcdziura/Documents/Projects/debt-pirate/api/assets ASSETS_DIR=/home/zcdziura/Documents/Projects/debt-pirate/api/assets
MAINTENANCE_USER_ACCOUNT=debt_pirate:HRURqlUmtjIy MAINTENANCE_USER_ACCOUNT=debt_pirate:HRURqlUmtjIy

45
api/.vscode/launch.json vendored Normal file
View file

@ -0,0 +1,45 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "lldb",
"request": "launch",
"name": "Debug executable 'auth-test'",
"cargo": {
"args": [
"build",
"--bin=auth-test",
"--package=auth-test"
],
"filter": {
"name": "auth-test",
"kind": "bin"
}
},
"args": [],
"cwd": "${workspaceFolder}"
},
{
"type": "lldb",
"request": "launch",
"name": "Debug unit tests in executable 'auth-test'",
"cargo": {
"args": [
"test",
"--no-run",
"--bin=auth-test",
"--package=auth-test"
],
"filter": {
"name": "auth-test",
"kind": "bin"
}
},
"args": [],
"cwd": "${workspaceFolder}"
}
]
}

View file

@ -3,7 +3,7 @@ mod user;
use sqlx::{self, postgres::PgPoolOptions, Pool, Postgres}; use sqlx::{self, postgres::PgPoolOptions, Pool, Postgres};
pub use user::*; pub use user::*;
use crate::error::AppError; use crate::models::AppError;
pub type DbPool = Pool<Postgres>; pub type DbPool = Pool<Postgres>;

View file

@ -1,8 +1,6 @@
use sqlx::prelude::FromRow; use sqlx::prelude::FromRow;
use uuid::Uuid; use uuid::Uuid;
use crate::error::AppError;
use super::DbPool; use super::DbPool;
#[derive(Debug)] #[derive(Debug)]

View file

@ -3,7 +3,6 @@ use std::{process, sync::mpsc::channel};
use models::Environment; use models::Environment;
mod db; mod db;
mod error;
mod models; mod models;
mod requests; mod requests;
mod services; mod services;

View file

@ -7,14 +7,22 @@ use std::{
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use pasetors::{keys::SymmetricKey, version4::V4}; use pasetors::{keys::SymmetricKey, version4::V4};
use crate::{error::AppError, services::UserConfirmationMessage}; use crate::services::UserConfirmationMessage;
static REQUIRED_ENV_VARS: Lazy<HashSet<String>> = Lazy::new(|| { use super::AppError;
HashSet::<String>::from_iter(
["TOKEN_KEY", "DATABASE_URL", "ASSETS_DIR"] static REQUIRED_ENV_VARS: Lazy<HashSet<&'static str>> = Lazy::new(|| {
.into_iter() [
.map(ToString::to_string), "HOSTNAME",
) "PORT",
"DOMAIN",
"RP_ID",
"TOKEN_KEY",
"DATABASE_URL",
"ASSETS_DIR",
]
.into_iter()
.collect()
}); });
#[derive(Clone)] #[derive(Clone)]
@ -35,10 +43,12 @@ impl Environment {
dotenvy::dotenv_iter() dotenvy::dotenv_iter()
.expect("Missing .env file") .expect("Missing .env file")
.filter_map(|item| item.ok()) .filter_map(|item| item.ok())
.filter(|(key, _)| REQUIRED_ENV_VARS.contains(key)) .filter(|(key, _)| REQUIRED_ENV_VARS.contains(key.as_str()))
.for_each(|(key, value)| match key.as_str() { .for_each(|(key, value)| match key.as_str() {
"HOSTNAME" => builder.with_hostname(value), "HOSTNAME" => builder.with_hostname(value),
"PORT" => builder.with_port(value), "PORT" => builder.with_port(value),
"DOMAIN" => builder.with_domain(value),
"RP_ID" => builder.with_rp_id(value),
"TOKEN_KEY" => builder.with_token_key(value), "TOKEN_KEY" => builder.with_token_key(value),
"DATABASE_URL" => builder.with_database_url(value), "DATABASE_URL" => builder.with_database_url(value),
"ASSETS_DIR" => builder.with_assets_dir(value), "ASSETS_DIR" => builder.with_assets_dir(value),
@ -65,6 +75,10 @@ impl Environment {
self.domain.as_str() self.domain.as_str()
} }
pub fn rp_id(&self) -> &str {
self.rp_id.as_str()
}
pub fn token_key(&self) -> &SymmetricKey<V4> { pub fn token_key(&self) -> &SymmetricKey<V4> {
&self.token_key &self.token_key
} }
@ -108,7 +122,7 @@ impl From<EnvironmentObjectBuilder> for Environment {
} }
} }
#[derive(Default)] #[derive(Debug, Default)]
pub struct EnvironmentObjectBuilder { pub struct EnvironmentObjectBuilder {
pub hostname: Option<String>, pub hostname: Option<String>,
pub port: Option<u32>, pub port: Option<u32>,
@ -136,7 +150,7 @@ impl EnvironmentObjectBuilder {
("DATABASE_URL", self.database_url.as_deref()), ("DATABASE_URL", self.database_url.as_deref()),
] ]
.into_iter() .into_iter()
.filter_map(|(key, value)| value.xor(Some(key))) .filter_map(|(key, value)| value.map(|_| key).xor(Some(key)))
.collect::<Vec<&'static str>>(); .collect::<Vec<&'static str>>();
if self.token_key.is_none() { if self.token_key.is_none() {

View file

@ -1,5 +1,7 @@
mod api_response; mod api_response;
mod environment; mod environment;
mod error;
pub use api_response::*; pub use api_response::*;
pub use environment::*; pub use environment::*;
pub use error::*;

View file

@ -4,9 +4,12 @@ use std::sync::Arc;
use axum::Router; use axum::Router;
use tokio::net::TcpListener; use tokio::net::TcpListener;
use webauthn_rs::Webauthn; use webauthn_rs::prelude::*;
use crate::{db::DbPool, error::AppError, models::Environment}; use crate::{
db::DbPool,
models::{AppError, Environment},
};
#[derive(Clone)] #[derive(Clone)]
pub struct AppState { pub struct AppState {
@ -17,9 +20,25 @@ pub struct AppState {
impl AppState { impl AppState {
pub fn new(pool: DbPool, env: Environment) -> Self { pub fn new(pool: DbPool, env: Environment) -> Self {
let rp_id = let rp_id = env.rp_id();
let rp_origin = env
.domain()
.parse::<Url>()
.expect("RP_ORIGIN must be in a valid domain name format");
Self { pool, env } let webauthn = Arc::new(
WebauthnBuilder::new(rp_id, &rp_origin)
.map(|builder| builder.allow_any_port(true))
.and_then(WebauthnBuilder::build)
.inspect_err(|err| eprintln!("{err}"))
.expect("Unable to build authenticator"),
);
Self {
pool,
env,
webauthn,
}
} }
pub fn pool(&self) -> &DbPool { pub fn pool(&self) -> &DbPool {
@ -29,6 +48,10 @@ impl AppState {
pub fn env(&self) -> &Environment { pub fn env(&self) -> &Environment {
&self.env &self.env
} }
pub fn webauthn(&self) -> Arc<Webauthn> {
Arc::clone(&self.webauthn)
}
} }
pub async fn start_app(pool: DbPool, env: Environment) -> Result<(), AppError> { pub async fn start_app(pool: DbPool, env: Environment) -> Result<(), AppError> {

View file

@ -1,5 +1,3 @@
mod registration_request; mod registration_request;
mod registration_response;
pub use registration_request::*; pub use registration_request::*;
pub use registration_response::*;

View file

@ -1,29 +0,0 @@
use std::time::SystemTime;
use humantime::format_rfc3339_seconds;
use serde::Serialize;
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct UserRegistrationResponse {
user_id: i32,
auth: UserPostResponseToken,
}
impl UserRegistrationResponse {
pub fn new(user_id: i32, token: String, expiration: SystemTime) -> Self {
let auth = UserPostResponseToken {
token,
expiration: format_rfc3339_seconds(expiration).to_string(),
};
Self { user_id, auth }
}
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct UserPostResponseToken {
pub token: String,
pub expiration: String,
}

View file

@ -8,6 +8,8 @@ use axum::{
}; };
use http::StatusCode; use http::StatusCode;
use tower_sessions::Session; use tower_sessions::Session;
use uuid::Uuid;
use webauthn_rs::prelude::CreationChallengeResponse;
use crate::{ use crate::{
db::NewUserEntity, db::NewUserEntity,
@ -16,7 +18,7 @@ use crate::{
services::{auth_token::generate_token, UserConfirmationMessage}, services::{auth_token::generate_token, UserConfirmationMessage},
}; };
use super::models::{UserRegistrationRequest, UserRegistrationResponse}; use super::models::UserRegistrationRequest;
static FIFTEEN_MINUTES: u64 = 60 * 15; static FIFTEEN_MINUTES: u64 = 60 * 15;
@ -31,9 +33,17 @@ async fn user_registration_post_handler(
session: Session, session: Session,
Json(request): Json<UserRegistrationRequest>, Json(request): Json<UserRegistrationRequest>,
) -> Result<Response, Response> { ) -> Result<Response, Response> {
session.remove_value("reg_state"); let _ = session.remove_value("reg_state").await;
let UserRegistrationRequest { name, email } = request; let UserRegistrationRequest { name, email } = request;
let user_uuid = Uuid::now_v7();
// TODO: Fetch any already saved credentials for the given user for exclusion
let (creation_challenge_response, reg) = app_state
.webauthn()
.start_passkey_registration(user_uuid, email.as_str(), name.as_str(), None)
.expect("Invalid passkey registration");
// let new_user = NewUserEntity::new(email.clone(), name.clone()); // let new_user = NewUserEntity::new(email.clone(), name.clone());
// let UserEntity { id: user_id, name, email , ..} = insert_new_user(app_state.pool(), new_user).await // let UserEntity { id: user_id, name, email , ..} = insert_new_user(app_state.pool(), new_user).await
// .map_err(|err| { // .map_err(|err| {
@ -55,12 +65,12 @@ async fn user_registration_post_handler(
// eprintln!("Got the rollowing error while sending across the channel: {err}"); // eprintln!("Got the rollowing error while sending across the channel: {err}");
// }); // });
let new_user_entity = NewUserEntity::new(name, email); // let new_user_entity = NewUserEntity::new(name, email);
let response = ( let response = (
StatusCode::CREATED, StatusCode::OK,
Json(ApiResponse::<UserRegistrationResponse>::new( Json(ApiResponse::<CreationChallengeResponse>::new(
UserRegistrationResponse::new(user_id, auth_token, expiration), creation_challenge_response,
)), )),
); );

View file

@ -10,7 +10,7 @@ use pasetors::{
}; };
use uuid::Uuid; use uuid::Uuid;
use crate::error::AppError; use crate::models::AppError;
static FOURTY_FIVE_DAYS: u64 = 3_888_000; // 60 * 60 * 24 * 45 static FOURTY_FIVE_DAYS: u64 = 3_888_000; // 60 * 60 * 24 * 45
static ONE_HOUR: u64 = 3_600; static ONE_HOUR: u64 = 3_600;