Beginning to rip apart the API
This commit is contained in:
parent
4c752b4a0f
commit
5596724ba9
11 changed files with 205 additions and 122 deletions
8
api/.env
8
api/.env
|
@ -1,4 +1,8 @@
|
|||
HOSTNAME=localhost
|
||||
PORT=42068
|
||||
DOMAIN=http://localhost:42069
|
||||
RP_ID=localhost
|
||||
TOKEN_KEY=k4.local.hWoS2ZulK9xPEATtXH1Dvj_iynzqfUv5ER5_IFTg5-Q
|
||||
DATABASE_URL=postgres://debt-pirate:test@192.168.122.241/debt_pirate
|
||||
DATABASE_URL=postgres://debt-pirate:test@192.168.122.215/debt_pirate
|
||||
ASSETS_DIR=/home/zcdziura/Documents/Projects/debt-pirate/api/assets
|
||||
MAINTENANCE_USER_ACCOUNT=dreadnought_maintenance:HRURqlUmtjIy
|
||||
MAINTENANCE_USER_ACCOUNT=debt_pirate:HRURqlUmtjIy
|
||||
|
|
|
@ -5,22 +5,41 @@ edition = "2021"
|
|||
|
||||
[dependencies]
|
||||
argon2 = "0.5"
|
||||
axum = { version = "0.7", features = ["default", "macros", "multipart", "ws"], default-features = false }
|
||||
axum = { version = "0.7", features = [
|
||||
"default",
|
||||
"macros",
|
||||
"multipart",
|
||||
"ws",
|
||||
], default-features = false }
|
||||
base64 = "0.22"
|
||||
dotenvy = "0.15"
|
||||
futures = "0.3"
|
||||
http = "1.0.0"
|
||||
humantime = "2.1.0"
|
||||
hyper = { version = "1.1", features = ["full"] }
|
||||
lettre = { version = "0.11", default-features = false, features = ["builder", "hostname", "pool", "smtp-transport", "tokio1", "tokio1-rustls-tls"] }
|
||||
lettre = { version = "0.11", default-features = false, features = [
|
||||
"builder",
|
||||
"hostname",
|
||||
"pool",
|
||||
"smtp-transport",
|
||||
"tokio1",
|
||||
"tokio1-rustls-tls",
|
||||
] }
|
||||
log = "0.4"
|
||||
num_cpus = "1.16"
|
||||
once_cell = "1.19"
|
||||
pasetors = "0.6"
|
||||
serde = { version = "1.0", features = ["derive", "rc", "std"] }
|
||||
serde_json = "1.0"
|
||||
sqlx = { version = "0.7", features = ["default", "chrono", "postgres", "runtime-tokio"] }
|
||||
sqlx = { version = "0.8", features = [
|
||||
"default",
|
||||
"chrono",
|
||||
"postgres",
|
||||
"runtime-tokio",
|
||||
] }
|
||||
tokio = { version = "1.35", features = ["full"] }
|
||||
tower = "0.4"
|
||||
tower-http = { version = "0.5", features = ["full"] }
|
||||
uuid = { version = "1.8.0", features = ["v7", "fast-rng"] }
|
||||
tower-sessions = "0.12.3"
|
||||
uuid = { version = "1.10", features = ["serde", "v7"] }
|
||||
webauthn-rs = { version = "0.5.0", features = ["danger-allow-state-serialisation"] }
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
use argon2::password_hash::PasswordHashString;
|
||||
use sqlx::prelude::FromRow;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::error::AppError;
|
||||
|
||||
|
@ -7,24 +7,17 @@ use super::DbPool;
|
|||
|
||||
#[derive(Debug)]
|
||||
pub struct NewUserEntity {
|
||||
pub username: String,
|
||||
pub email: String,
|
||||
pub password: String,
|
||||
pub name: String,
|
||||
pub user_id: Uuid,
|
||||
}
|
||||
|
||||
impl NewUserEntity {
|
||||
pub fn new(
|
||||
username: String,
|
||||
email: String,
|
||||
password: PasswordHashString,
|
||||
name: String,
|
||||
) -> Self {
|
||||
pub fn new(name: String, email: String) -> Self {
|
||||
Self {
|
||||
username,
|
||||
email,
|
||||
password: password.as_str().to_owned(),
|
||||
name,
|
||||
email,
|
||||
user_id: Uuid::now_v7(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -33,43 +26,42 @@ impl NewUserEntity {
|
|||
#[derive(Debug, FromRow)]
|
||||
pub struct UserEntity {
|
||||
pub id: i32,
|
||||
pub username: String,
|
||||
pub email: String,
|
||||
pub name: String,
|
||||
pub email: String,
|
||||
pub user_id: Uuid,
|
||||
pub status_id: i32,
|
||||
}
|
||||
|
||||
pub async fn insert_new_user(
|
||||
pool: &DbPool,
|
||||
new_user: NewUserEntity,
|
||||
) -> Result<UserEntity, AppError> {
|
||||
let NewUserEntity {
|
||||
username,
|
||||
email,
|
||||
password,
|
||||
name,
|
||||
} = new_user;
|
||||
// pub async fn _insert_new_user(
|
||||
// pool: &DbPool,
|
||||
// new_user: NewUserEntity,
|
||||
// ) -> Result<UserEntity, AppError> {
|
||||
// let NewUserEntity {
|
||||
// name,
|
||||
// email,
|
||||
// user_id,
|
||||
// } = new_user;
|
||||
|
||||
sqlx::query_as::<_, UserEntity>("INSERT INTO public.user (username, email, password, name) VALUES ($1, $2, $3, $4) RETURNING id, username, email, name, status_id;")
|
||||
.bind(username)
|
||||
.bind(email)
|
||||
.bind(password)
|
||||
.bind(name)
|
||||
.fetch_one(pool).await
|
||||
.map_err(|err| {
|
||||
eprintln!("Error inserting NewUserEntity {err:?}");
|
||||
AppError::from(err)
|
||||
})
|
||||
}
|
||||
// sqlx::query_as::<_, UserEntity>("INSERT INTO public.user (username, email, password, name) VALUES ($1, $2, $3, $4) RETURNING id, username, email, name, status_id;")
|
||||
// .bind(username)
|
||||
// .bind(email)
|
||||
// .bind(password)
|
||||
// .bind(name)
|
||||
// .fetch_one(pool).await
|
||||
// .map_err(|err| {
|
||||
// eprintln!("Error inserting NewUserEntity {err:?}");
|
||||
// AppError::from(err)
|
||||
// })
|
||||
// }
|
||||
|
||||
pub async fn verify_user(pool: &DbPool, user_id: i32) -> Result<(), AppError> {
|
||||
sqlx::query("UPDATE public.user SET status_id = 1, updated_at = now() WHERE id = $1;")
|
||||
.bind(user_id)
|
||||
.execute(pool)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
eprintln!("Error verifying user with id '{user_id}'.");
|
||||
AppError::from(err)
|
||||
})
|
||||
.map(|_| ())
|
||||
}
|
||||
// pub async fn verify_user(pool: &DbPool, user_id: i32) -> Result<(), AppError> {
|
||||
// sqlx::query("UPDATE public.user SET status_id = 1, updated_at = now() WHERE id = $1;")
|
||||
// .bind(user_id)
|
||||
// .execute(pool)
|
||||
// .await
|
||||
// .map_err(|err| {
|
||||
// eprintln!("Error verifying user with id '{user_id}'.");
|
||||
// AppError::from(err)
|
||||
// })
|
||||
// .map(|_| ())
|
||||
// }
|
||||
|
|
|
@ -8,7 +8,7 @@ mod models;
|
|||
mod requests;
|
||||
mod services;
|
||||
|
||||
use db::{create_connection_pool, run_migrations};
|
||||
use db::create_connection_pool;
|
||||
use requests::start_app;
|
||||
use services::{start_emailer_service, UserConfirmationMessage};
|
||||
use tokio::runtime::Handle;
|
||||
|
@ -26,10 +26,10 @@ async fn main() {
|
|||
};
|
||||
|
||||
let pool = create_connection_pool(env.db_connection_uri()).await;
|
||||
if let Err(err) = run_migrations(&pool).await {
|
||||
eprintln!("{err:?}");
|
||||
process::exit(2);
|
||||
}
|
||||
// if let Err(err) = run_migrations(&pool).await {
|
||||
// eprintln!("{err:?}");
|
||||
// process::exit(2);
|
||||
// }
|
||||
|
||||
start_emailer_service(Handle::current(), env.assets_dir(), rx);
|
||||
|
||||
|
|
|
@ -19,6 +19,10 @@ static REQUIRED_ENV_VARS: Lazy<HashSet<String>> = Lazy::new(|| {
|
|||
|
||||
#[derive(Clone)]
|
||||
pub struct Environment {
|
||||
hostname: String,
|
||||
port: u32,
|
||||
domain: String,
|
||||
rp_id: String,
|
||||
token_key: SymmetricKey<V4>,
|
||||
database_url: String,
|
||||
email_sender: Sender<UserConfirmationMessage>,
|
||||
|
@ -33,19 +37,34 @@ impl Environment {
|
|||
.filter_map(|item| item.ok())
|
||||
.filter(|(key, _)| REQUIRED_ENV_VARS.contains(key))
|
||||
.for_each(|(key, value)| match key.as_str() {
|
||||
"HOSTNAME" => builder.with_hostname(value),
|
||||
"PORT" => builder.with_port(value),
|
||||
"TOKEN_KEY" => builder.with_token_key(value),
|
||||
"DATABASE_URL" => builder.with_database_url(value),
|
||||
"ASSETS_DIR" => builder.with_assets_dir(value),
|
||||
_ => {}
|
||||
});
|
||||
|
||||
if let Some(missing_vars) = builder.uninitialized_variables() {
|
||||
let missing_vars = builder.uninitialized_variables();
|
||||
if let Some(missing_vars) = missing_vars {
|
||||
Err(AppError::missing_environment_variables(missing_vars))
|
||||
} else {
|
||||
Ok(Environment::from(builder))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn hostname(&self) -> &str {
|
||||
self.hostname.as_str()
|
||||
}
|
||||
|
||||
pub fn port(&self) -> u32 {
|
||||
self.port
|
||||
}
|
||||
|
||||
pub fn domain(&self) -> &str {
|
||||
self.domain.as_str()
|
||||
}
|
||||
|
||||
pub fn token_key(&self) -> &SymmetricKey<V4> {
|
||||
&self.token_key
|
||||
}
|
||||
|
@ -66,6 +85,10 @@ impl Environment {
|
|||
impl From<EnvironmentObjectBuilder> for Environment {
|
||||
fn from(builder: EnvironmentObjectBuilder) -> Self {
|
||||
let EnvironmentObjectBuilder {
|
||||
hostname,
|
||||
port,
|
||||
domain,
|
||||
rp_id,
|
||||
token_key,
|
||||
database_url,
|
||||
email_sender,
|
||||
|
@ -73,6 +96,10 @@ impl From<EnvironmentObjectBuilder> for Environment {
|
|||
} = builder;
|
||||
|
||||
Self {
|
||||
hostname: hostname.unwrap(),
|
||||
port: port.unwrap(),
|
||||
domain: domain.unwrap(),
|
||||
rp_id: rp_id.unwrap(),
|
||||
token_key: token_key.unwrap(),
|
||||
database_url: database_url.unwrap(),
|
||||
email_sender: email_sender.unwrap(),
|
||||
|
@ -83,6 +110,10 @@ impl From<EnvironmentObjectBuilder> for Environment {
|
|||
|
||||
#[derive(Default)]
|
||||
pub struct EnvironmentObjectBuilder {
|
||||
pub hostname: Option<String>,
|
||||
pub port: Option<u32>,
|
||||
pub domain: Option<String>,
|
||||
pub rp_id: Option<String>,
|
||||
pub token_key: Option<SymmetricKey<V4>>,
|
||||
pub database_url: Option<String>,
|
||||
pub email_sender: Option<Sender<UserConfirmationMessage>>,
|
||||
|
@ -98,15 +129,20 @@ impl EnvironmentObjectBuilder {
|
|||
}
|
||||
|
||||
pub fn uninitialized_variables(&self) -> Option<Vec<&'static str>> {
|
||||
let mut missing_vars = Vec::with_capacity(3);
|
||||
let mut missing_vars = [
|
||||
("HOSTNAME", self.hostname.as_deref()),
|
||||
("DOMAIN", self.domain.as_deref()),
|
||||
("RP_ID", self.rp_id.as_deref()),
|
||||
("DATABASE_URL", self.database_url.as_deref()),
|
||||
]
|
||||
.into_iter()
|
||||
.filter_map(|(key, value)| value.xor(Some(key)))
|
||||
.collect::<Vec<&'static str>>();
|
||||
|
||||
if self.token_key.is_none() {
|
||||
missing_vars.push("TOKEN_KEY");
|
||||
}
|
||||
|
||||
if self.database_url.is_none() {
|
||||
missing_vars.push("DATABASE_URL");
|
||||
}
|
||||
|
||||
if self.assets_dir.is_none() {
|
||||
missing_vars.push("ASSETS_DIR");
|
||||
}
|
||||
|
@ -118,6 +154,27 @@ impl EnvironmentObjectBuilder {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn with_hostname(&mut self, hostname: String) {
|
||||
self.hostname = Some(hostname);
|
||||
}
|
||||
|
||||
pub fn with_port(&mut self, port: String) {
|
||||
let port = port
|
||||
.parse::<u32>()
|
||||
.inspect_err(|err| eprintln!("Not a valid port, defaulting to '42069': {err}"))
|
||||
.ok();
|
||||
|
||||
self.port = port;
|
||||
}
|
||||
|
||||
pub fn with_domain(&mut self, domain: String) {
|
||||
self.domain = Some(domain);
|
||||
}
|
||||
|
||||
pub fn with_rp_id(&mut self, rp_id: String) {
|
||||
self.rp_id = Some(rp_id);
|
||||
}
|
||||
|
||||
pub fn with_token_key(&mut self, key: String) {
|
||||
match SymmetricKey::<V4>::try_from(key.as_str()).map_err(|_| AppError::token_key()) {
|
||||
Ok(key) => self.token_key = Some(key),
|
||||
|
|
|
@ -1,7 +1,10 @@
|
|||
mod user;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use axum::Router;
|
||||
use tokio::net::TcpListener;
|
||||
use webauthn_rs::Webauthn;
|
||||
|
||||
use crate::{db::DbPool, error::AppError, models::Environment};
|
||||
|
||||
|
@ -9,10 +12,13 @@ use crate::{db::DbPool, error::AppError, models::Environment};
|
|||
pub struct AppState {
|
||||
pool: DbPool,
|
||||
env: Environment,
|
||||
webauthn: Arc<Webauthn>,
|
||||
}
|
||||
|
||||
impl AppState {
|
||||
pub fn new(pool: DbPool, env: Environment) -> Self {
|
||||
let rp_id =
|
||||
|
||||
Self { pool, env }
|
||||
}
|
||||
|
||||
|
@ -26,8 +32,8 @@ impl AppState {
|
|||
}
|
||||
|
||||
pub async fn start_app(pool: DbPool, env: Environment) -> Result<(), AppError> {
|
||||
let address = "localhost";
|
||||
let port = "42069";
|
||||
let address = env.hostname();
|
||||
let port = env.port();
|
||||
let listener = TcpListener::bind(format!("{address}:{port}"))
|
||||
.await
|
||||
.unwrap();
|
||||
|
|
|
@ -1,15 +1,29 @@
|
|||
use axum::Router;
|
||||
use tower::ServiceBuilder;
|
||||
use tower_sessions::{
|
||||
cookie::{time::Duration, SameSite},
|
||||
Expiry, MemoryStore, SessionManagerLayer,
|
||||
};
|
||||
|
||||
use super::AppState;
|
||||
|
||||
pub mod new_user;
|
||||
pub mod verify;
|
||||
// pub mod verify;
|
||||
|
||||
pub fn requests(app_state: AppState) -> Router {
|
||||
Router::new().nest(
|
||||
"/user",
|
||||
let domain = app_state.env().domain().to_owned();
|
||||
let user_requests_middleware = ServiceBuilder::new().layer(
|
||||
SessionManagerLayer::new(MemoryStore::default())
|
||||
.with_domain(domain)
|
||||
.with_secure(false)
|
||||
.with_same_site(SameSite::Strict)
|
||||
.with_expiry(Expiry::OnInactivity(Duration::seconds(300))),
|
||||
);
|
||||
|
||||
Router::new()
|
||||
.merge(new_user::request(app_state.clone()))
|
||||
.merge(verify::request(app_state.clone())),
|
||||
.nest(
|
||||
"/user",
|
||||
Router::new().merge(new_user::request(app_state.clone())), // .merge(verify::request(app_state.clone())),
|
||||
)
|
||||
.layer(user_requests_middleware)
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
mod post_request;
|
||||
mod post_response;
|
||||
mod registration_request;
|
||||
mod registration_response;
|
||||
|
||||
pub use post_request::*;
|
||||
pub use post_response::*;
|
||||
pub use registration_request::*;
|
||||
pub use registration_response::*;
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
use serde::Deserialize;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct UserPostRequest {
|
||||
pub username: String,
|
||||
pub password: String,
|
||||
pub email: String,
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct UserRegistrationRequest {
|
||||
pub name: String,
|
||||
pub email: String,
|
||||
}
|
|
@ -5,12 +5,12 @@ use serde::Serialize;
|
|||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct UserPostResponse {
|
||||
pub struct UserRegistrationResponse {
|
||||
user_id: i32,
|
||||
auth: UserPostResponseToken,
|
||||
}
|
||||
|
||||
impl UserPostResponse {
|
||||
impl UserRegistrationResponse {
|
||||
pub fn new(user_id: i32, token: String, expiration: SystemTime) -> Self {
|
||||
let auth = UserPostResponseToken {
|
||||
token,
|
|
@ -7,69 +7,61 @@ use axum::{
|
|||
Json, Router,
|
||||
};
|
||||
use http::StatusCode;
|
||||
use tower_sessions::Session;
|
||||
|
||||
use crate::{
|
||||
db::{insert_new_user, NewUserEntity, UserEntity},
|
||||
db::NewUserEntity,
|
||||
models::ApiResponse,
|
||||
requests::AppState,
|
||||
services::{auth_token::generate_token, hash_string, UserConfirmationMessage},
|
||||
services::{auth_token::generate_token, UserConfirmationMessage},
|
||||
};
|
||||
|
||||
use super::models::{UserPostRequest, UserPostResponse};
|
||||
use super::models::{UserRegistrationRequest, UserRegistrationResponse};
|
||||
|
||||
static FIFTEEN_MINUTES: u64 = 60 * 15;
|
||||
|
||||
pub fn request(app_state: AppState) -> Router {
|
||||
Router::new()
|
||||
.route("/", post(user_post_handler))
|
||||
.route("/", post(user_registration_post_handler))
|
||||
.with_state(app_state)
|
||||
}
|
||||
|
||||
async fn user_post_handler(
|
||||
async fn user_registration_post_handler(
|
||||
State(app_state): State<AppState>,
|
||||
Json(request): Json<UserPostRequest>,
|
||||
session: Session,
|
||||
Json(request): Json<UserRegistrationRequest>,
|
||||
) -> Result<Response, Response> {
|
||||
let UserPostRequest {
|
||||
username,
|
||||
password,
|
||||
email,
|
||||
name,
|
||||
} = request;
|
||||
session.remove_value("reg_state");
|
||||
|
||||
let hashed_password = hash_string(password.as_str());
|
||||
let new_user = NewUserEntity::new(username, email.clone(), hashed_password, name.clone());
|
||||
let UserEntity { id: user_id, name, email , ..} = insert_new_user(app_state.pool(), new_user).await
|
||||
.map_err(|err| {
|
||||
if err.is_duplicate_record() {
|
||||
(StatusCode::CONFLICT, ApiResponse::error("There is already an account associated with this username or email address.").into_json_response()).into_response()
|
||||
} else {
|
||||
(StatusCode::INTERNAL_SERVER_ERROR, ApiResponse::error("An error occurred while creating your new user account. Please try again later.").into_json_response()).into_response()
|
||||
}
|
||||
})?;
|
||||
let UserRegistrationRequest { name, email } = request;
|
||||
// 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| {
|
||||
// if err.is_duplicate_record() {
|
||||
// (StatusCode::CONFLICT, ApiResponse::error("There is already an account associated with this username or email address.").into_json_response()).into_response()
|
||||
// } else {
|
||||
// (StatusCode::INTERNAL_SERVER_ERROR, ApiResponse::error("An error occurred while creating your new user account. Please try again later.").into_json_response()).into_response()
|
||||
// }
|
||||
// })?;
|
||||
|
||||
let (auth_token, expiration) = generate_token(
|
||||
app_state.env().token_key(),
|
||||
user_id,
|
||||
Some(Duration::from_secs(FIFTEEN_MINUTES)),
|
||||
Some(format!("/user/{user_id}/verify").as_str()),
|
||||
);
|
||||
// let new_user_confirmation_message =
|
||||
// UserConfirmationMessage::new(email.as_str(), name.as_str(), auth_token.as_str());
|
||||
|
||||
let new_user_confirmation_message =
|
||||
UserConfirmationMessage::new(email.as_str(), name.as_str(), auth_token.as_str());
|
||||
// let _ = app_state
|
||||
// .env()
|
||||
// .email_sender()
|
||||
// .send(new_user_confirmation_message)
|
||||
// .inspect_err(|err| {
|
||||
// eprintln!("Got the rollowing error while sending across the channel: {err}");
|
||||
// });
|
||||
|
||||
let _ = app_state
|
||||
.env()
|
||||
.email_sender()
|
||||
.send(new_user_confirmation_message)
|
||||
.inspect_err(|err| {
|
||||
eprintln!("Got the rollowing error while sending across the channel: {err}");
|
||||
});
|
||||
let new_user_entity = NewUserEntity::new(name, email);
|
||||
|
||||
let response = (
|
||||
StatusCode::CREATED,
|
||||
Json(ApiResponse::<UserPostResponse>::new(UserPostResponse::new(
|
||||
user_id, auth_token, expiration,
|
||||
))),
|
||||
Json(ApiResponse::<UserRegistrationResponse>::new(
|
||||
UserRegistrationResponse::new(user_id, auth_token, expiration),
|
||||
)),
|
||||
);
|
||||
|
||||
Ok(response.into_response())
|
||||
|
|
Loading…
Add table
Reference in a new issue