diff --git a/Cargo.lock b/Cargo.lock index 44d0f87..0549b43 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -47,6 +47,18 @@ dependencies = [ "libc", ] +[[package]] +name = "argon2" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072" +dependencies = [ + "base64ct", + "blake2", + "cpufeatures", + "password-hash", +] + [[package]] name = "atoi" version = "2.0.0" @@ -158,6 +170,15 @@ dependencies = [ "serde", ] +[[package]] +name = "blake2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" +dependencies = [ + "digest", +] + [[package]] name = "block-buffer" version = "0.10.4" @@ -208,7 +229,9 @@ checksum = "1a7964611d71df112cb1730f2ee67324fcf4d0fc6606acbbe9bfe06df124637c" dependencies = [ "android-tzdata", "iana-time-zone", + "js-sys", "num-traits", + "wasm-bindgen", "windows-link", ] @@ -574,7 +597,10 @@ checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" name = "goathacks-backend" version = "0.1.0" dependencies = [ + "argon2", "axum", + "chrono", + "kankyo", "reqwest", "serde", "serde_json", @@ -585,6 +611,7 @@ dependencies = [ "tracing", "tracing-appender", "tracing-subscriber", + "uuid", ] [[package]] @@ -978,6 +1005,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "kankyo" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "325a11231fa70c1d1b562655db757cefb6022876d62f173831f35bd670ae0c40" + [[package]] name = "lazy_static" version = "1.5.0" @@ -1268,6 +1301,17 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "password-hash" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" +dependencies = [ + "base64ct", + "rand_core", + "subtle", +] + [[package]] name = "pem-rfc7468" version = "0.7.0" @@ -2440,6 +2484,10 @@ name = "uuid" version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "458f7a779bf54acc9f347480ac654f68407d3aab21269a6e3c9f922acd9e2da9" +dependencies = [ + "getrandom 0.3.2", + "serde", +] [[package]] name = "valuable" diff --git a/Cargo.toml b/Cargo.toml index da59111..362ca0b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,10 +9,20 @@ thiserror="2" tracing="0.1" tracing-subscriber={version="0.3", features=["env-filter"]} tracing-appender="0.2" -axum = "0.8" serde_json="1" +kankyo = "0.3" + +chrono = "0.4" + +argon2 = "0.5" + +[dependencies.axum] +version = "0.8" +features = [ + "json" + ] [dependencies.reqwest] version="0.12" @@ -26,6 +36,13 @@ features=["trace"] version="1" features=["derive"] +[dependencies.uuid] +version = "1" +features = [ + "v4", + "serde" +] + # Async runtime [dependencies.tokio] version="1" diff --git a/src/errors.rs b/src/errors.rs new file mode 100644 index 0000000..0ae391a --- /dev/null +++ b/src/errors.rs @@ -0,0 +1,22 @@ +use axum::response::IntoResponse; +use axum::http::StatusCode; +use axum::Json; +use serde_json::json; +use thiserror::Error as thisError; + +#[derive(thisError, Debug)] +pub enum Error { + #[error("Invalid value provided: {0}")] + ValueError(String) +} + +pub type EmptyResult = Result<()>; +pub type Result = std::result::Result; + +impl IntoResponse for Error { + fn into_response(self) -> axum::response::Response { + let body = json!({"status": "error", "message": format!("{}", self)}); + + (StatusCode::INTERNAL_SERVER_ERROR, Json(body)).into_response() + } +} diff --git a/src/main.rs b/src/main.rs index 21b1d33..c757bc6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,22 +1,26 @@ use std::time::Duration; -use axum::{routing::get, Router}; +use axum::{Router, routing::get}; use sqlx::postgres::PgPoolOptions; use tokio::net::TcpListener; use tracing::info; use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; - +mod models; +mod errors; #[tokio::main] async fn main() { tracing_subscriber::registry() - .with(tracing_subscriber::EnvFilter::try_from_default_env().unwrap_or_else(|_| "goathacks-backend=debug".into())) + .with( + tracing_subscriber::EnvFilter::try_from_default_env() + .unwrap_or_else(|_| "goathacks-backend=debug".into()), + ) .with(tracing_subscriber::fmt::layer()) .init(); - let db_connection_str = std::env::var("DATABASE_URL") - .unwrap_or_else(|_| "postgres://localhost/goathacks".into()); + let db_connection_str = + std::env::var("DATABASE_URL").unwrap_or_else(|_| "postgres://localhost/goathacks".into()); let pool = PgPoolOptions::new() .max_connections(5) diff --git a/src/models/mod.rs b/src/models/mod.rs new file mode 100644 index 0000000..5975988 --- /dev/null +++ b/src/models/mod.rs @@ -0,0 +1 @@ +mod users; diff --git a/src/models/users.rs b/src/models/users.rs new file mode 100644 index 0000000..dba9aa2 --- /dev/null +++ b/src/models/users.rs @@ -0,0 +1,62 @@ +/*! Users are entities that interact with the system. + * +* They are notably separate from any API tokens. +*/ + +use std::fmt::{Debug, Display}; + +use argon2::{password_hash::{rand_core::OsRng, SaltString}, Argon2}; +use chrono::{DateTime, Utc}; + +use crate::errors::{EmptyResult, Result}; + +/** A single User with access to the system + * + * The specific User object contains profile and biographic data that's either useful to ACM or + * required by external partners (see: MLH). + */ +#[derive(Clone)] +struct User { + /// Unique user identifier, a V4 UUID + id: uuid::Uuid, + /// The user's name. Not guaranteed to be anything in particular, should be + /// treated as a standard UTF8 string + name: String, + /// User's (hashed) password + password_hash: String, + /// User's email. This must be validated for correct format! + email: String, + /// Whether the user is allowed to log in. + is_active: bool, + /// UTC date/time of creation + created: chrono::DateTime, + /// UTC date/time of last login + last_login: Option>, +} + +impl Display for User { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{} ({})", self.email, self.id) + } +} + +impl Debug for User { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{} ({})", self.email, self.id) + } +} + +impl User { + pub fn new_user() -> Self { + Self { id: uuid::Uuid::new_v4(), name: String::new(), password_hash: String::new(), email: String::new(), is_active: true, created: Utc::now(), last_login: None } + } + pub fn set_password(new_pw: String) -> EmptyResult { + + let argon = Argon2::default(); + let salt = SaltString::generate(&mut OsRng); + + let mut out: Vec = Vec::new(); + + Ok(()) + } +}