Added basic Users database support

This commit is contained in:
2025-11-30 22:53:06 +01:00
parent 7aa13825fc
commit 0bc113971b
7 changed files with 144 additions and 37 deletions

View File

@@ -15,6 +15,7 @@ regex = "1.12.2"
serde = { version = "1.0.228", features = ["derive", "serde_derive"] } serde = { version = "1.0.228", features = ["derive", "serde_derive"] }
serde_json = "1.0.145" serde_json = "1.0.145"
sqlx = { version = "0.8.6", features = ["runtime-tokio-rustls", "postgres", "uuid", "chrono", "migrate"] } sqlx = { version = "0.8.6", features = ["runtime-tokio-rustls", "postgres", "uuid", "chrono", "migrate"] }
thiserror = "2.0.17"
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 = { version = "0.5.2", features = ["tokio", "tracing"] }
tower-http = { version = "0.6.7", features = ["cors"] } tower-http = { version = "0.6.7", features = ["cors"] }

View File

@@ -0,0 +1,8 @@
CREATE TABLE users (
uuid UUID PRIMARY KEY,
username VARCHAR NOT NULL UNIQUE,
email VARCHAR UNIQUE,
password_hash VARCHAR NOT NULL,
first_name VARCHAR,
last_name VARCHAR
);

View File

@@ -1,7 +1,8 @@
use crate::prelude::*; use crate::{infra::db, prelude::*};
use std::sync::Arc; use std::sync::Arc;
use axum::http::StatusCode; use anyhow::Result;
use axum::{Json, http::StatusCode};
use validator::Validate; use validator::Validate;
use crate::{ use crate::{
@@ -10,14 +11,31 @@ use crate::{
}; };
pub async fn create(state: Arc<AppState>, new_user: NewUser) -> Result<User, StatusCode> { pub async fn create(state: Arc<AppState>, new_user: NewUser) -> Result<User, StatusCode> {
if let Err(_) = new_user.validate() { new_user.validate().map_err(|e| {
return Err(StatusCode::BAD_REQUEST); error!("User validation failed: {e}");
} StatusCode::BAD_REQUEST
})?;
let internal = InternalNewUser::try_from(new_user).map_err(|e| { let internal = InternalNewUser::try_from(new_user).map_err(|e| {
error!("Conversion to InternalUser failed: {e}"); error!("Conversion to InternalUser failed: {e}");
StatusCode::INTERNAL_SERVER_ERROR StatusCode::INTERNAL_SERVER_ERROR
})?; })?;
todo!("Hook up return once db setup ready"); let created_user = db::user::create(&state.db_pool, internal)
.await
.map_err(|e| {
error!("Failed to create new user: {e}");
StatusCode::INTERNAL_SERVER_ERROR
})?;
Ok(User::from(created_user))
}
pub async fn get_all(state: Arc<AppState>) -> Result<Vec<User>, StatusCode> {
let users = db::user::get_safe_all(&state.db_pool).await.map_err(|e| {
error!("Failed to fetch all users: {e}");
StatusCode::INTERNAL_SERVER_ERROR
})?;
Ok(users)
} }

View File

@@ -4,6 +4,7 @@ use axum::response::IntoResponse;
use lazy_static::lazy_static; use lazy_static::lazy_static;
use regex::Regex; use regex::Regex;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use sqlx::prelude::FromRow;
use uuid::Uuid; use uuid::Uuid;
use validator::Validate; use validator::Validate;
@@ -30,25 +31,25 @@ pub struct NewUser {
#[derive(Debug, Clone, Deserialize)] #[derive(Debug, Clone, Deserialize)]
pub struct InternalNewUser { pub struct InternalNewUser {
uuid: Uuid, pub uuid: Uuid,
username: String, pub username: String,
email: Option<String>, pub email: Option<String>,
password_hash: String, pub password_hash: String,
first_name: Option<String>, pub first_name: Option<String>,
last_name: Option<String>, pub last_name: Option<String>,
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone, Deserialize, FromRow)]
pub struct InternalUser { pub struct InternalUser {
uuid: Uuid, pub uuid: Uuid,
username: String, pub username: String,
email: Option<String>, pub email: Option<String>,
password_hash: String, pub password_hash: String,
first_name: Option<String>, pub first_name: Option<String>,
last_name: Option<String>, pub last_name: Option<String>,
} }
#[derive(Debug, Clone, Serialize)] #[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
pub struct User { pub struct User {
uuid: Uuid, uuid: Uuid,
username: String, username: String,

View File

@@ -0,0 +1,81 @@
use crate::{
domain::user::{InternalNewUser, InternalUser, User},
prelude::*,
};
use anyhow::{Ok, Result, anyhow};
use sqlx::PgPool;
use uuid::Uuid;
pub async fn create(pool: &PgPool, new_user: InternalNewUser) -> Result<InternalUser> {
let user = sqlx::query_as::<_, InternalUser>(
r#"
INSERT INTO users (uuid, username, email, password_hash, first_name, last_name)
VALUES ($1, $2, $3, $4, $5, $6)
RETURNING uuid, username, email, password_hash, first_name, last_name
"#,
)
.bind(new_user.uuid)
.bind(&new_user.username)
.bind(&new_user.email)
.bind(&new_user.password_hash)
.bind(&new_user.first_name)
.bind(&new_user.last_name)
.fetch_one(pool)
.await?;
Ok(user)
}
pub async fn get_by_uuid(pool: &PgPool, uuid: Uuid) -> Result<Option<InternalUser>> {
let user = sqlx::query_as::<_, InternalUser>(
r#"
SELECT uuid, username, email, password_hash, first_name, last_name FROM users WHERE uuid = $
"#,
)
.bind(uuid)
.fetch_optional(pool)
.await?;
Ok(user)
}
pub async fn get_safe_by_uuid(pool: &PgPool, uuid: Uuid) -> Result<Option<User>> {
let user = sqlx::query_as::<_, User>(
r#"
SELECT uuid, username, email, first_name, last_name FROM users WHERE uuid = $
"#,
)
.bind(uuid)
.fetch_optional(pool)
.await?;
Ok(user)
}
pub async fn get_all(pool: &PgPool) -> Result<Vec<InternalUser>> {
let users = sqlx::query_as::<_, InternalUser>(
r#"
SELECT uuid, username, email, password_hash, first_name, last_name
FROM users
ORDER BY username ASC
"#,
)
.fetch_all(pool)
.await?;
Ok(users)
}
pub async fn get_safe_all(pool: &PgPool) -> Result<Vec<User>> {
let users = sqlx::query_as::<_, User>(
r#"
SELECT uuid, username, email, first_name, last_name
FROM users
ORDER BY username ASC
"#,
)
.fetch_all(pool)
.await?;
Ok(users)
}

View File

@@ -23,10 +23,13 @@ pub async fn init_router(app_state: Arc<AppState>) -> Router {
), ),
) )
.route( .route(
"/api/user/create", "/api/users",
post(user_routes::create) post(user_routes::create)
.layer(ServiceBuilder::new().layer(middleware::cors())) .layer(ServiceBuilder::new().layer(middleware::cors()))
.with_state(app_state), .with_state(app_state.clone())
.get(user_routes::get_all)
.layer(ServiceBuilder::new().layer(middleware::cors()))
.with_state(app_state.clone()),
) )
} }

View File

@@ -1,27 +1,22 @@
use crate::prelude::*;
use std::sync::Arc; use std::sync::Arc;
use axum::{Json, extract::State, http::StatusCode};
use serde_json::json;
use tracing::{debug, error, info, warn};
use validator::Validate;
use crate::{ use crate::{
core,
domain::user::{InternalNewUser, NewUser, User}, domain::user::{InternalNewUser, NewUser, User},
state::AppState, state::AppState,
}; };
use axum::{Json, extract::State, http::StatusCode};
pub async fn create( pub async fn create(
State(state): State<Arc<AppState>>, State(state): State<Arc<AppState>>,
Json(new_user): Json<NewUser>, Json(new_user): Json<NewUser>,
) -> Result<Json<User>, StatusCode> { ) -> Result<Json<User>, StatusCode> {
if let Err(_) = new_user.validate() { let user = core::user_routines::create(state, new_user).await?;
return Err(StatusCode::BAD_REQUEST); Ok(Json(user))
} }
let internal = InternalNewUser::try_from(new_user).map_err(|e| { pub async fn get_all(State(state): State<Arc<AppState>>) -> Result<Json<Vec<User>>, StatusCode> {
error!("Conversion to InternalUser failed: {e}"); let users = core::user_routines::get_all(state).await?;
StatusCode::INTERNAL_SERVER_ERROR Ok(Json(users))
})?;
todo!("Hook up return once db setup ready");
} }