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
PORT=42068
DOMAIN=http://localhost:42069
RP_ID=localhost
DOMAIN=http://api.debtpirate.app
RP_ID=debtpirate.app
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
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};
pub use user::*;
use crate::error::AppError;
use crate::models::AppError;
pub type DbPool = Pool<Postgres>;

View file

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

View file

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

View file

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

View file

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

View file

@ -4,9 +4,12 @@ use std::sync::Arc;
use axum::Router;
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)]
pub struct AppState {
@ -17,9 +20,25 @@ pub struct AppState {
impl AppState {
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 {
@ -29,6 +48,10 @@ impl AppState {
pub fn env(&self) -> &Environment {
&self.env
}
pub fn webauthn(&self) -> Arc<Webauthn> {
Arc::clone(&self.webauthn)
}
}
pub async fn start_app(pool: DbPool, env: Environment) -> Result<(), AppError> {

View file

@ -1,5 +1,3 @@
mod registration_request;
mod registration_response;
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 tower_sessions::Session;
use uuid::Uuid;
use webauthn_rs::prelude::CreationChallengeResponse;
use crate::{
db::NewUserEntity,
@ -16,7 +18,7 @@ use crate::{
services::{auth_token::generate_token, UserConfirmationMessage},
};
use super::models::{UserRegistrationRequest, UserRegistrationResponse};
use super::models::UserRegistrationRequest;
static FIFTEEN_MINUTES: u64 = 60 * 15;
@ -31,9 +33,17 @@ async fn user_registration_post_handler(
session: Session,
Json(request): Json<UserRegistrationRequest>,
) -> Result<Response, Response> {
session.remove_value("reg_state");
let _ = session.remove_value("reg_state").await;
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 UserEntity { id: user_id, name, email , ..} = insert_new_user(app_state.pool(), new_user).await
// .map_err(|err| {
@ -55,12 +65,12 @@ async fn user_registration_post_handler(
// 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 = (
StatusCode::CREATED,
Json(ApiResponse::<UserRegistrationResponse>::new(
UserRegistrationResponse::new(user_id, auth_token, expiration),
StatusCode::OK,
Json(ApiResponse::<CreationChallengeResponse>::new(
creation_challenge_response,
)),
);

View file

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