Alot of user management

This commit is contained in:
2025-11-28 23:28:58 +01:00
parent 504b5b2b27
commit d7374504f8
10 changed files with 190 additions and 16 deletions

View File

@@ -5,9 +5,15 @@ edition = "2024"
[dependencies] [dependencies]
anyhow = "1.0.100" anyhow = "1.0.100"
argon2 = "0.5.3"
axum = "0.8.7" axum = "0.8.7"
serde = "1.0.228" password-hash = "0.5.0"
rand_core = { version = "0.6", features = ["getrandom"] }
serde = { version = "1.0.228", features = ["derive", "serde_derive"] }
serde_json = "1.0.145" serde_json = "1.0.145"
tokio = { version = "1.48.0", features = ["macros", "rt-multi-thread"] } tokio = { version = "1.48.0", features = ["macros", "rt-multi-thread"] }
tower = { version = "0.5.2", features = ["tokio", "tracing"] }
tower-http = { version = "0.6.7", features = ["cors"] }
tracing = { version = "0.1.43", features = ["max_level_debug"] } tracing = { version = "0.1.43", features = ["max_level_debug"] }
tracing-subscriber = "0.3.22" tracing-subscriber = "0.3.22"
validator = { version = "0.20.0", features = ["derive"] }

View File

@@ -0,0 +1,15 @@
use argon2::{
Argon2,
password_hash::{PasswordHash, PasswordHasher, PasswordVerifier, SaltString, rand_core::OsRng},
};
use tracing::debug;
pub fn hash_password(password: &str) -> Result<String, password_hash::Error> {
let salt = SaltString::generate(&mut OsRng);
let argon2 = Argon2::default();
let hash = argon2
.hash_password(password.as_bytes(), &salt)?
.to_string();
debug!("Hashed password {}", hash);
Ok(hash)
}

View File

@@ -0,0 +1 @@
pub mod user;

View File

@@ -0,0 +1,71 @@
use std::fmt::Display;
use axum::response::IntoResponse;
use serde::{Deserialize, Serialize};
use crate::auth;
#[derive(Debug, Clone, Deserialize)]
pub struct NewUser {
username: String,
email: Option<String>,
password: String,
first_name: Option<String>,
last_name: Option<String>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct InternalUser {
username: String,
email: Option<String>,
password_hash: String,
first_name: Option<String>,
last_name: Option<String>,
}
#[derive(Debug, Clone, Serialize)]
pub struct User {
username: String,
email: Option<String>,
first_name: Option<String>,
last_name: Option<String>,
}
#[derive(Debug)]
pub enum UserConversionError {
HashFailed(password_hash::Error),
}
impl TryFrom<NewUser> for InternalUser {
type Error = UserConversionError;
fn try_from(value: NewUser) -> Result<Self, Self::Error> {
let password_hash =
auth::hash_password(&value.password).map_err(|e| UserConversionError::HashFailed(e))?;
Ok(Self {
username: value.username,
email: value.email,
password_hash: password_hash,
first_name: value.first_name,
last_name: value.last_name,
})
}
}
impl From<InternalUser> for User {
fn from(value: InternalUser) -> Self {
Self {
username: value.username,
email: value.email,
first_name: value.first_name,
last_name: value.last_name,
}
}
}
impl Display for UserConversionError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
UserConversionError::HashFailed(e) => write!(f, "failed to hash password: {e}"),
}
}
}

View File

@@ -1 +1,4 @@
pub mod auth;
pub mod domain;
pub mod router; pub mod router;
pub mod state;

View File

@@ -1,5 +1,7 @@
use std::sync::Arc;
use anyhow::{Ok, Result}; use anyhow::{Ok, Result};
use rustymine_daemon::router; use rustymine_daemon::{router, state::AppState};
use tracing::Level; use tracing::Level;
#[tokio::main] #[tokio::main]
@@ -10,9 +12,12 @@ async fn main() -> Result<()> {
tracing::subscriber::set_global_default(subscriber)?; tracing::subscriber::set_global_default(subscriber)?;
let app_result = router::init_router().await?; let state = Arc::new(AppState::new());
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap(); let app_result = router::init_router(state.clone()).await;
axum::serve(listener, app_result).await.unwrap();
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await?;
axum::serve(listener, app_result).await?;
Ok(()) Ok(())
} }

View File

@@ -1,9 +1,24 @@
use axum::{extract::Request, middleware::Next, response::IntoResponse}; use axum::{
Router,
extract::Request,
http::{Method, header::AUTHORIZATION},
middleware::Next,
response::IntoResponse,
};
use tower_http::cors::{Any, CorsLayer};
use tracing::{debug, error, info, warn}; use tracing::{debug, error, info, warn};
pub async fn auth_middleware(request: Request, next: Next) -> impl IntoResponse { pub async fn auth(request: Request, next: Next) -> impl IntoResponse {
debug!("auth_middleware entry"); debug!("auth_middleware entry");
let response = next.run(request).await; let response = next.run(request).await;
debug!("auth_middleware exit"); debug!("auth_middleware exit");
response response
} }
pub fn cors() -> CorsLayer {
debug!("Generating CorsLayer");
CorsLayer::new()
.allow_origin(Any)
.allow_methods([Method::GET, Method::POST])
.allow_headers([AUTHORIZATION])
}

View File

@@ -1,14 +1,33 @@
pub mod middleware; pub mod middleware;
use anyhow::Result; pub mod user_routes;
use axum::{Json, Router, http::StatusCode, middleware::from_fn, routing::get};
use serde_json::{Value, json};
pub async fn init_router() -> Result<Router> { use axum::{
let router = Router::new().route( Json, Router,
"/api/ping", http::StatusCode,
get(ping).layer(from_fn(middleware::auth_middleware)), routing::{get, post},
); };
Ok(router) use serde_json::{Value, json};
use std::sync::Arc;
use tower::{Layer, ServiceBuilder};
use crate::state::AppState;
pub async fn init_router(app_state: Arc<AppState>) -> Router {
Router::new()
.route(
"/api/ping",
get(ping).layer(
ServiceBuilder::new()
.layer(middleware::cors())
.layer(axum::middleware::from_fn(middleware::auth)),
),
)
.route(
"/api/user/create",
post(user_routes::create)
.layer(ServiceBuilder::new().layer(middleware::cors()))
.with_state(app_state),
)
} }
async fn ping() -> Result<Json<Value>, StatusCode> { async fn ping() -> Result<Json<Value>, StatusCode> {

View File

@@ -0,0 +1,24 @@
use std::sync::Arc;
use axum::{Json, extract::State, http::StatusCode};
use serde_json::json;
use tracing::{debug, error, info, warn};
use crate::{
domain::user::{InternalUser, NewUser, User},
state::AppState,
};
pub async fn create(
State(state): State<Arc<AppState>>,
Json(new_user): Json<NewUser>,
) -> Result<Json<User>, StatusCode> {
let internal = InternalUser::try_from(new_user).map_err(|e| {
error!("Conversion to InternalUser failed: {e}");
StatusCode::INTERNAL_SERVER_ERROR
})?;
let user = User::from(internal);
Ok(Json(user))
}

15
src/backend/src/state.rs Normal file
View File

@@ -0,0 +1,15 @@
use std::collections::HashMap;
use crate::domain::user::InternalUser;
pub struct AppState {
pub users: HashMap<String, InternalUser>,
}
impl AppState {
pub fn new() -> Self {
Self {
users: HashMap::new(),
}
}
}