Added register function

This commit is contained in:
xeovalyte 2025-02-01 16:27:46 +01:00
parent 6322736baf
commit 30bf08986d
Signed by: xeovalyte
SSH Key Fingerprint: SHA256:GWI1hq+MNKR2UOcvk7n9tekASXT8vyazK7vDF9Xyciw
10 changed files with 248 additions and 16 deletions

41
server/Cargo.lock generated
View File

@ -47,6 +47,18 @@ dependencies = [
"libc", "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]] [[package]]
name = "atoi" name = "atoi"
version = "2.0.0" version = "2.0.0"
@ -192,6 +204,15 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "blake2"
version = "0.10.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe"
dependencies = [
"digest",
]
[[package]] [[package]]
name = "block-buffer" name = "block-buffer"
version = "0.10.4" version = "0.10.4"
@ -1191,6 +1212,17 @@ dependencies = [
"windows-targets 0.52.6", "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 0.6.4",
"subtle",
]
[[package]] [[package]]
name = "pem-rfc7468" name = "pem-rfc7468"
version = "0.7.0" version = "0.7.0"
@ -1642,6 +1674,7 @@ dependencies = [
"tokio-stream", "tokio-stream",
"tracing", "tracing",
"url", "url",
"uuid",
] ]
[[package]] [[package]]
@ -1722,6 +1755,7 @@ dependencies = [
"stringprep", "stringprep",
"thiserror", "thiserror",
"tracing", "tracing",
"uuid",
"whoami", "whoami",
] ]
@ -1759,6 +1793,7 @@ dependencies = [
"stringprep", "stringprep",
"thiserror", "thiserror",
"tracing", "tracing",
"uuid",
"whoami", "whoami",
] ]
@ -1783,6 +1818,7 @@ dependencies = [
"sqlx-core", "sqlx-core",
"tracing", "tracing",
"url", "url",
"uuid",
] ]
[[package]] [[package]]
@ -2096,6 +2132,10 @@ name = "uuid"
version = "1.12.0" version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "744018581f9a3454a9e15beb8a33b017183f1e7c0cd170232a2d1453b23a51c4" checksum = "744018581f9a3454a9e15beb8a33b017183f1e7c0cd170232a2d1453b23a51c4"
dependencies = [
"getrandom 0.2.15",
"rand 0.8.5",
]
[[package]] [[package]]
name = "validator" name = "validator"
@ -2426,6 +2466,7 @@ dependencies = [
name = "wrbapp_server" name = "wrbapp_server"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"argon2",
"axum", "axum",
"axum-extra", "axum-extra",
"chrono", "chrono",

View File

@ -8,20 +8,21 @@ edition = "2021"
axum = { version = "0.8", features = [ "macros", "json" ] } axum = { version = "0.8", features = [ "macros", "json" ] }
axum-extra = { version = "0.10.0", features = [ "typed-header" ] } axum-extra = { version = "0.10.0", features = [ "typed-header" ] }
tokio = { version = "1.43", features = [ "rt-multi-thread", "macros" ] } tokio = { version = "1.43", features = [ "rt-multi-thread", "macros" ] }
sqlx = { version = "0.8", features = [ "runtime-tokio", "postgres" ] } sqlx = { version = "0.8", features = [ "runtime-tokio", "postgres", "uuid" ] }
# Secondary crates # Secondary crates
csv = { version = "1.3" } csv = { version = "1.3" }
serde = "1.0" serde = "1.0"
dotenvy = "0.15.7" dotenvy = "0.15.7"
validator = { version = "0.19.0", features = [ "derive" ] } validator = { version = "0.19.0", features = [ "derive" ] }
argon2 = "0.5"
# Tertiary crates # Tertiary crates
tracing = "0.1" tracing = "0.1"
tracing-subscriber = "0.3" tracing-subscriber = "0.3"
chrono = "0.4.39" chrono = "0.4.39"
uuid = "1.12.0" uuid = { version = "1.12.0", features = ["v4", "fast-rng"] }
serde_json = "1.0.137" serde_json = "1.0.137"
rand = "0.9" rand = "0.9"
thiserror = { version = "2.0" } thiserror = { version = "2.0" }

View File

@ -1,5 +1,9 @@
use std::collections::HashSet; use std::collections::HashSet;
use argon2::{
password_hash::{rand_core::OsRng, PasswordHasher, SaltString},
Argon2,
};
use axum::{extract::FromRequestParts, http::request::Parts, RequestPartsExt}; use axum::{extract::FromRequestParts, http::request::Parts, RequestPartsExt};
use axum_extra::{ use axum_extra::{
headers::{authorization::Bearer, Authorization}, headers::{authorization::Bearer, Authorization},
@ -8,6 +12,7 @@ use axum_extra::{
}; };
use bearer::verify_bearer; use bearer::verify_bearer;
pub use error::AuthError; pub use error::AuthError;
use tokio::task;
mod bearer; mod bearer;
mod error; mod error;
@ -46,3 +51,24 @@ where
Err(AuthError::Unexpected.into()) Err(AuthError::Unexpected.into())
} }
} }
pub async fn generate_password_hash(
password: String,
) -> Result<String, argon2::password_hash::Error> {
let password_hash: Result<String, argon2::password_hash::Error> =
task::spawn_blocking(move || {
let salt = SaltString::generate(&mut OsRng);
let argon2 = Argon2::default();
let password_hash = argon2
.hash_password(password.as_bytes(), &salt)?
.to_string();
Ok(password_hash)
})
.await
.unwrap();
Ok(password_hash?)
}

View File

@ -3,3 +3,5 @@ pub mod session;
pub mod user; pub mod user;
pub use member::Member; pub use member::Member;
pub use user::User;
pub use user::UserMember;

View File

@ -2,7 +2,7 @@ use rand::distr::{Alphanumeric, SampleString};
use sqlx::{PgPool, Postgres, QueryBuilder}; use sqlx::{PgPool, Postgres, QueryBuilder};
use validator::Validate; use validator::Validate;
#[derive(Debug, Validate)] #[derive(Debug, Validate, sqlx::FromRow)]
pub struct Member { pub struct Member {
#[validate(length(equal = 7))] #[validate(length(equal = 7))]
pub member_id: String, pub member_id: String,
@ -14,7 +14,40 @@ pub struct Member {
pub groups: Vec<String>, pub groups: Vec<String>,
} }
pub struct SearchMember {
pub registration_tokens: Option<Vec<String>>,
}
impl Member { impl Member {
pub async fn search(
transaction: &PgPool,
search: SearchMember,
) -> Result<Vec<Self>, sqlx::Error> {
if let None = search.registration_tokens {
return Err(sqlx::Error::RowNotFound);
}
let mut query = QueryBuilder::new("SELECT * from members WHERE 1=1");
if let Some(registration_tokens) = search.registration_tokens {
if registration_tokens.len() == 0 {
return Err(sqlx::Error::RowNotFound);
}
query.push(" AND registration_token = ANY(");
query.push_bind(registration_tokens);
query.push(")");
}
let members: Vec<Self> = query.build_query_as().fetch_all(transaction).await?;
if members.len() == 0 {
return Err(sqlx::Error::RowNotFound);
}
Ok(members)
}
pub async fn get_many(transaction: &PgPool, members: Vec<Self>) -> Result<(), sqlx::Error> { pub async fn get_many(transaction: &PgPool, members: Vec<Self>) -> Result<(), sqlx::Error> {
Ok(()) Ok(())
} }

View File

@ -1,8 +1,68 @@
use sqlx::Postgres;
#[derive(validator::Validate)] #[derive(validator::Validate)]
struct User { pub struct User {
pub id: uuid::Uuid, pub user_id: uuid::Uuid,
#[validate(email)] #[validate(email)]
pub email: String, pub email: String,
pub password: String, pub password: String,
pub admin: bool, pub admin: bool,
} }
impl User {
pub async fn insert(
transaction: &mut sqlx::Transaction<'_, Postgres>,
email: &str,
password: &str,
) -> Result<uuid::Uuid, sqlx::Error> {
let user_id = uuid::Uuid::new_v4();
sqlx::query!(
"
INSERT INTO users (
user_id, email, password, admin
)
VALUES (
$1, $2, $3, $4
);
",
&user_id,
email,
password,
false
)
.execute(&mut **transaction)
.await?;
Ok(user_id)
}
}
#[derive(Debug)]
pub struct UserMember {
user_id: uuid::Uuid,
member_id: String,
}
impl UserMember {
pub async fn insert_many(
transaction: &mut sqlx::Transaction<'_, Postgres>,
user_ids: &Vec<uuid::Uuid>,
member_ids: &Vec<String>,
) -> Result<(), sqlx::Error> {
sqlx::query!(
"
INSERT INTO users_members (
user_id, member_id
)
SELECT * FROM UNNEST($1::uuid[], $2::varchar[])
",
&user_ids[..],
&member_ids[..]
)
.execute(&mut **transaction)
.await?;
Ok(())
}
}

View File

@ -38,7 +38,7 @@ async fn main() {
}; };
// Serve app // Serve app
let app = Router::new().merge(routes()).with_state(app_state); let app = Router::new().nest("/v1", routes()).with_state(app_state);
let listener = TcpListener::bind("127.0.0.1:3000") let listener = TcpListener::bind("127.0.0.1:3000")
.await .await

View File

@ -1,12 +1,5 @@
use axum::{
extract::State,
http::StatusCode,
routing::{get, post},
Router,
};
use member::migrate::{migrate_confirm, migrate_request};
use crate::{auth::Permissions, AppState}; use crate::{auth::Permissions, AppState};
use axum::{extract::State, http::StatusCode, routing::get, Router};
pub mod auth; pub mod auth;
pub mod member; pub mod member;
@ -16,8 +9,8 @@ pub fn routes() -> Router<AppState> {
Router::new() Router::new()
.route("/", get(root)) .route("/", get(root))
// .route("/member/:id", get()) // .route("/member/:id", get())
.route("/members/migrate_request", post(migrate_request)) .merge(member::routes())
.route("/members/migrate_confirm", post(migrate_confirm)) .merge(auth::routes())
} }
async fn root( async fn root(

View File

@ -1 +1,67 @@
use axum::{extract::State, routing::post, Json, Router};
use crate::database::model::member::SearchMember;
use crate::database::model::Member as DbMember;
use crate::database::model::User as DbUser;
use crate::database::model::UserMember as DbUserMember;
use crate::{
auth::{generate_password_hash, Permissions},
AppState,
};
pub fn routes() -> Router<AppState> {
Router::new()
.route("/auth/login", post(login))
.route("/auth/register", post(register))
}
pub async fn login<'a>(
State(state): State<AppState>,
permissions: Permissions<'a>,
body: String,
) -> Result<(), crate::Error> {
Ok(())
}
#[derive(serde::Deserialize)]
pub struct AuthRequest {
email: String,
password: String,
registration_tokens: Vec<String>,
}
pub async fn register<'a>(
State(state): State<AppState>,
permissions: Permissions<'a>,
Json(auth_request): Json<AuthRequest>,
) -> Result<(), crate::Error> {
// Get all the members to link with the user
let members = DbMember::search(
&state.pool,
SearchMember {
registration_tokens: Some(auth_request.registration_tokens),
},
)
.await?;
let member_ids: Vec<String> = members.into_iter().map(|m| m.member_id).collect();
// Hash password
let password_hash = match generate_password_hash(auth_request.password).await {
Ok(hash) => hash,
Err(_err) => return Err(crate::Error::Auth(crate::auth::AuthError::InvalidToken)),
};
let mut transaction = state.pool.begin().await?;
// Insert the user to the database
let user_id = DbUser::insert(&mut transaction, &auth_request.email, &password_hash).await?;
// Link the user to the members
let user_ids: Vec<uuid::Uuid> = vec![user_id; member_ids.len()];
DbUserMember::insert_many(&mut transaction, &user_ids, &member_ids).await?;
transaction.commit().await?;
Ok(())
}

View File

@ -1 +1,11 @@
use axum::{routing::post, Router};
use crate::AppState;
pub mod migrate; pub mod migrate;
pub fn routes() -> Router<AppState> {
Router::new()
.route("/members/migrate_request", post(migrate::migrate_request))
.route("/members/migrate_confirm", post(migrate::migrate_confirm))
}