feat: Set up user backend

users
Miroito 3 years ago
parent 569e3753f0
commit 0ba58d90e3

251
server/Cargo.lock generated

@ -14,6 +14,41 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
[[package]]
name = "aead"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0"
dependencies = [
"crypto-common",
"generic-array",
]
[[package]]
name = "aes"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "433cfd6710c9986c576a25ca913c39d66a6474107b406f34f91d4a8923395241"
dependencies = [
"cfg-if",
"cipher",
"cpufeatures",
]
[[package]]
name = "aes-gcm"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "209b47e8954a928e1d72e86eca7000ebb6655fe1436d33eefc2201cad027e237"
dependencies = [
"aead",
"aes",
"cipher",
"ctr",
"ghash",
"subtle",
]
[[package]]
name = "ahash"
version = "0.7.6"
@ -51,6 +86,12 @@ dependencies = [
"libc",
]
[[package]]
name = "arrayref"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6b4930d2cb77ce62f89ee5d5289b4ac049559b1c45539271f5ed4fdc7db34545"
[[package]]
name = "arrayvec"
version = "0.7.2"
@ -292,6 +333,12 @@ version = "0.2.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4cbbc9d0964165b47557570cce6c952866c2678457aca742aafc9fb771d30270"
[[package]]
name = "base64"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8"
[[package]]
name = "base64"
version = "0.21.0"
@ -321,6 +368,17 @@ version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "blake2b_simd"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c2f0dc9a68c6317d884f97cc36cf5a3d20ba14ce404227df55e1af708ab04bc"
dependencies = [
"arrayref",
"arrayvec",
"constant_time_eq 0.2.5",
]
[[package]]
name = "block-buffer"
version = "0.10.4"
@ -461,6 +519,16 @@ dependencies = [
"winapi",
]
[[package]]
name = "cipher"
version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
dependencies = [
"crypto-common",
"inout",
]
[[package]]
name = "clap"
version = "3.2.24"
@ -528,6 +596,36 @@ version = "0.4.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fbdcdcb6d86f71c5e97409ad45898af11cbc995b4ee8112d59095a28d376c935"
[[package]]
name = "constant_time_eq"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc"
[[package]]
name = "constant_time_eq"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13418e745008f7349ec7e449155f419a61b92b58a99cc3616942b926825ec76b"
[[package]]
name = "cookie"
version = "0.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7efb37c3e1ccb1ff97164ad95ac1606e8ccd35b3fa0a7d99a304c7f4a428cc24"
dependencies = [
"aes-gcm",
"base64 0.21.0",
"hkdf",
"hmac",
"percent-encoding",
"rand",
"sha2",
"subtle",
"time 0.3.20",
"version_check",
]
[[package]]
name = "core-foundation"
version = "0.9.3"
@ -598,6 +696,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
dependencies = [
"generic-array",
"rand_core",
"typenum",
]
@ -611,6 +710,15 @@ dependencies = [
"syn 1.0.109",
]
[[package]]
name = "ctr"
version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835"
dependencies = [
"cipher",
]
[[package]]
name = "cxx"
version = "1.0.94"
@ -674,12 +782,13 @@ checksum = "850878694b7933ca4c9569d30a34b55031b9b139ee1fc7b94a527c4ef960d690"
[[package]]
name = "digest"
version = "0.10.6"
version = "0.10.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8168378f4e5023e7218c89c891c0fd8ecdb5e5e4f18cb78f38cf245dd021e76f"
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
dependencies = [
"block-buffer",
"crypto-common",
"subtle",
]
[[package]]
@ -994,6 +1103,16 @@ dependencies = [
"wasi 0.11.0+wasi-snapshot-preview1",
]
[[package]]
name = "ghash"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d930750de5717d2dd0b8c0d42c076c0e884c81a73e6cab859bbd2339c71e3e40"
dependencies = [
"opaque-debug",
"polyval",
]
[[package]]
name = "gloo-timers"
version = "0.2.6"
@ -1091,6 +1210,24 @@ version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
[[package]]
name = "hkdf"
version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "791a029f6b9fc27657f6f188ec6e5e43f6911f6f878e0dc5501396e09809d437"
dependencies = [
"hmac",
]
[[package]]
name = "hmac"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
dependencies = [
"digest",
]
[[package]]
name = "http"
version = "0.2.9"
@ -1225,6 +1362,15 @@ dependencies = [
"hashbrown 0.12.3",
]
[[package]]
name = "inout"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5"
dependencies = [
"generic-array",
]
[[package]]
name = "instant"
version = "0.1.12"
@ -1275,6 +1421,20 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "jsonwebtoken"
version = "8.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6971da4d9c3aa03c3d8f3ff0f4155b534aad021292003895a469716b2a230378"
dependencies = [
"base64 0.21.0",
"pem",
"ring",
"serde",
"serde_json",
"simple_asn1",
]
[[package]]
name = "kv-log-macro"
version = "1.0.7"
@ -1538,6 +1698,12 @@ version = "1.17.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3"
[[package]]
name = "opaque-debug"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5"
[[package]]
name = "openssl"
version = "0.10.52"
@ -1677,6 +1843,15 @@ version = "1.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f746c4065a8fa3fe23974dd82f15431cc8d40779821001404d10d2e79ca7d79"
[[package]]
name = "pem"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8835c273a76a90455d7344889b0964598e3316e2a79ede8e36f16bdcf2228b8"
dependencies = [
"base64 0.13.1",
]
[[package]]
name = "pem-rfc7468"
version = "0.3.1"
@ -1768,6 +1943,18 @@ dependencies = [
"windows-sys 0.48.0",
]
[[package]]
name = "polyval"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ef234e08c11dfcb2e56f79fd70f6f2eb7f025c0ce2333e82f4f0518ecad30c6"
dependencies = [
"cfg-if",
"cpufeatures",
"opaque-debug",
"universal-hash",
]
[[package]]
name = "pom"
version = "3.2.0"
@ -1950,7 +2137,7 @@ version = "0.11.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "27b71749df584b7f4cac2c426c127a7c785a5106cc98f7a8feb044115f0fa254"
dependencies = [
"base64",
"base64 0.21.0",
"bytes",
"encoding_rs",
"futures-core",
@ -2046,6 +2233,18 @@ dependencies = [
"zeroize",
]
[[package]]
name = "rust-argon2"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b50162d19404029c1ceca6f6980fe40d45c8b369f6f44446fa14bb39573b5bb9"
dependencies = [
"base64 0.13.1",
"blake2b_simd",
"constant_time_eq 0.1.5",
"crossbeam-utils",
]
[[package]]
name = "rust_decimal"
version = "1.29.1"
@ -2105,7 +2304,7 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d194b56d58803a43635bdc398cd17e383d6f71f9182b9a192c127ca42494a59b"
dependencies = [
"base64",
"base64 0.21.0",
]
[[package]]
@ -2418,15 +2617,19 @@ dependencies = [
"axum",
"bytes",
"chrono",
"cookie",
"dotenvy",
"envy",
"futures",
"hyper",
"jsonwebtoken",
"lazy_static",
"log",
"lopdf",
"migration",
"rand",
"reqwest",
"rust-argon2",
"sea-orm",
"serde",
"serde_json",
@ -2434,6 +2637,7 @@ dependencies = [
"thiserror",
"tokio",
"tower",
"tower-cookies",
"tower-http",
"tracing",
"tracing-subscriber",
@ -2500,6 +2704,18 @@ version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f27f6278552951f1f2b8cf9da965d10969b2efdea95a6ec47987ab46edfe263a"
[[package]]
name = "simple_asn1"
version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "adc4e5204eb1910f40f9cfa375f6f05b68c3abac4b6fd879c8ff5e7ae8a0a085"
dependencies = [
"num-bigint",
"num-traits",
"thiserror",
"time 0.3.20",
]
[[package]]
name = "slab"
version = "0.4.8"
@ -3008,6 +3224,23 @@ dependencies = [
"tracing",
]
[[package]]
name = "tower-cookies"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40f38d941a2ffd8402b36e02ae407637a9caceb693aaf2edc910437db0f36984"
dependencies = [
"async-trait",
"axum-core",
"cookie",
"futures-util",
"http",
"parking_lot 0.12.1",
"pin-project-lite",
"tower-layer",
"tower-service",
]
[[package]]
name = "tower-http"
version = "0.4.0"
@ -3152,6 +3385,16 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e"
[[package]]
name = "universal-hash"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea"
dependencies = [
"crypto-common",
"subtle",
]
[[package]]
name = "untrusted"
version = "0.7.1"

@ -17,6 +17,7 @@ reqwest = { version = "0.11", features = ["json", "rustls-tls"] }
axum = "0.6.12"
hyper = { version = "0.14.25", features = ["full"] }
tower = "0.4"
tower-cookies = "0.9"
sea-orm = { version = "0.11.0", features = [
"runtime-tokio-rustls",
"macros",
@ -33,3 +34,7 @@ slug = "0.1.4"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
tower-http = { version = "0.4", features = ["trace", "cors"] }
tracing = "0.1"
rust-argon2 = "1"
rand = "0.8"
jsonwebtoken = "8"
cookie = { version = "0.17", features = [ "secure" ] }

@ -18,8 +18,8 @@ impl MigrationTrait for Migration {
.auto_increment()
.primary_key(),
)
.col(ColumnDef::new(User::Email).string().not_null())
.col(ColumnDef::new(User::Name).string().not_null())
.col(ColumnDef::new(User::Email).string().not_null().unique_key())
.col(ColumnDef::new(User::Name).string().not_null().unique_key())
.col(ColumnDef::new(User::Password).string().not_null())
.to_owned(),
)

@ -8,6 +8,7 @@ pub struct Env {
pub host: String,
#[serde(default = "port_default")]
pub port: String,
pub api_url: String,
pub mysql_user: String,
pub mysql_password: String,
pub mysql_host: String,
@ -107,6 +108,7 @@ impl Env {
#[derive(Debug)]
pub struct Config {
pub server_address: SocketAddr,
pub server_domain: String,
pub database_url: String,
pub max_connections: u32,
pub min_connections: u32,
@ -134,6 +136,7 @@ impl Config {
let mut config = Config {
server_address,
server_domain: env.api_url,
database_url,
max_connections: env.max_connections,
min_connections: env.min_connections,

@ -12,6 +12,9 @@ pub enum AppError {
DbErr(DbErr),
InProcessTransaction(InProcessTransactionError),
NotFound(String),
InternalServerError(String),
Unauthorized,
Conflict(String),
}
impl From<DbErr> for AppError {
@ -38,6 +41,9 @@ impl IntoResponse for AppError {
format!("Error in the in process transaction repo: {}", e),
),
AppError::NotFound(e) => (StatusCode::NOT_FOUND, format!("Not found error: {}", e)),
AppError::InternalServerError(e) => (StatusCode::INTERNAL_SERVER_ERROR, e),
AppError::Unauthorized => (StatusCode::UNAUTHORIZED, "Unauthorized".to_string()),
AppError::Conflict(e) => (StatusCode::CONFLICT, e),
};
let body = Json(json!({

@ -6,15 +6,18 @@ extern crate lazy_static;
#[macro_use]
extern crate log;
extern crate argon2;
// External crates
use axum::{
extract::MatchedPath,
http::{HeaderValue, Method, Request, StatusCode},
response::Response,
routing::get,
routing::{get, post},
Router,
};
use sea_orm::DatabaseConnection;
use tower_cookies::CookieManagerLayer;
use std::time::Duration;
use tokio::signal;
use tower_http::{classify::ServerErrorsFailureClass, cors::CorsLayer, trace::TraceLayer};
@ -33,7 +36,7 @@ mod model;
mod repo;
mod route;
mod task;
use crate::task::run_tasks;
use crate::{route::user, task::run_tasks};
// Module imports
use env::Config;
@ -96,6 +99,14 @@ pub async fn main() -> Result<(), Box<dyn std::error::Error>> {
"/in_process_transaction/retry_all",
get(in_process_transaction::retry_all),
)
.route(
"/user/login",
post(user::login),
)
.route(
"/user/register",
post(user::register),
)
.with_state(shared_state.clone())
.fallback(fallback)
.layer(
@ -129,6 +140,7 @@ pub async fn main() -> Result<(), Box<dyn std::error::Error>> {
},
),
)
.layer(CookieManagerLayer::new())
.layer(
CorsLayer::new()
.allow_origin("*".parse::<HeaderValue>().unwrap())

@ -1,3 +1,4 @@
pub mod company;
pub mod in_process_transaction;
pub mod transaction;
pub mod user;

@ -0,0 +1,23 @@
use crate::model::user::ActiveModel;
use sea_orm::{ActiveModelTrait, ConnectionTrait, DbErr, DeriveIntoActiveModel, IntoActiveModel};
use serde::{Deserialize, Serialize};
use crate::model;
#[derive(Debug, PartialEq, Clone, DeriveIntoActiveModel, Serialize, Deserialize)]
pub struct NewUser {
pub email: String,
pub name: String,
pub password: String,
}
impl NewUser {
pub async fn create<C>(&self, db: &C) -> Result<model::user::Model, DbErr>
where
C: ConnectionTrait,
{
let res = self.clone().into_active_model().insert(db).await?;
Ok(res)
}
}

@ -5,6 +5,7 @@ use serde::{de, Deserialize, Deserializer};
pub mod company;
pub mod in_process_transaction;
pub mod transaction;
pub mod user;
/// Struct to deserialize paginated routes query parameters
#[derive(Deserialize)]

@ -0,0 +1,133 @@
use axum::{extract::State, Json};
use cookie::{time::Duration, Cookie, SameSite};
use jsonwebtoken::{encode, EncodingKey, Header};
use rand::RngCore;
use sea_orm::{ColumnTrait, EntityTrait, QueryFilter};
use serde::{Deserialize, Serialize};
use tower_cookies::Cookies;
use crate::{error::AppError, model, repo::user::NewUser, AppState, CONFIG};
#[derive(Deserialize)]
pub struct UserLoginBody {
pub name: String,
pub password: String,
}
#[derive(Serialize, Deserialize)]
pub struct JWTClaim {
pub username: String,
}
pub async fn login(
cookies: Cookies,
State(state): State<AppState>,
Json(payload): Json<UserLoginBody>,
) -> Result<(), AppError> {
let db = &state.db;
let user_opt = model::user::Entity::find()
.filter(model::user::Column::Name.eq(payload.name))
.one(db)
.await?;
if user_opt.is_none() {
// To prevent timing attacks, we use the same verify function on a known password
argon2::verify_encoded("$argon2i$v=19$m=4096,t=3,p=1$CXr/AgSDawghR+GmOhM0wQ$4k2TCyoqkh/YaK9mh6uEa0eRZ/CIx3bfzJs5UnCcKjw", b"1234").unwrap();
return Err(AppError::NotFound("User not found".to_string()));
}
let user = user_opt.unwrap();
let valid =
argon2::verify_encoded(&user.password, payload.password.as_bytes()).map_err(|e| {
error!("Error verifying the password for user {}: {}", user.name, e);
AppError::InternalServerError("There was an error verifying authentication".to_string())
})?;
if !valid {
return Err(AppError::Unauthorized);
}
// Generate a JWT and store it as a same site cookie
let claim = JWTClaim {
username: user.name,
};
let token_str = encode(
&Header::default(),
&claim,
&EncodingKey::from_secret(b"some-secret"),
)
.map_err(|e| {
error!("Failed to encode a JWT: {}", e);
AppError::InternalServerError("There was an error verifying authentication".to_string())
})?;
let cookie = Cookie::build("auth", token_str)
.domain(CONFIG.server_domain.clone())
.path("/")
.same_site(SameSite::Strict)
.secure(true)
.http_only(true)
.max_age(Duration::days(4))
.finish();
cookies.add(cookie);
Ok(())
}
#[derive(Deserialize)]
pub struct UserRegisterBody {
pub name: String,
pub email: String,
pub password: String,
}
pub async fn register(
State(state): State<AppState>,
Json(payload): Json<UserRegisterBody>,
) -> Result<(), AppError> {
let db = &state.db;
let user_opt = model::user::Entity::find()
.filter(
model::user::Column::Name
.eq(&payload.name)
.or(model::user::Column::Email.eq(&payload.email)),
)
.one(db)
.await?;
if user_opt.is_some() {
return Err(AppError::Conflict(
"The username or email is already in use".to_string(),
));
}
let salt = generate_salt();
let pass_hash =
argon2::hash_encoded(payload.password.as_ref(), &salt, &argon2::Config::default())
.map_err(|e| {
error!("Failed to hash a password: {}", e);
AppError::InternalServerError(
"There was an error in the registration process".to_string(),
)
})?;
let new_user = NewUser {
email: payload.email,
name: payload.name,
password: pass_hash,
};
new_user.create(db).await?;
Ok(())
}
fn generate_salt() -> Vec<u8> {
let mut salt = [0u8; 16]; // 16 bytes salt length (adjust as needed)
rand::thread_rng().fill_bytes(&mut salt);
salt.into()
}
Loading…
Cancel
Save