Added basic Users database support
This commit is contained in:
@@ -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"] }
|
||||||
|
|||||||
8
src/backend/migrations/0002_create_users.sql
Normal file
8
src/backend/migrations/0002_create_users.sql
Normal 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
|
||||||
|
);
|
||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
|||||||
@@ -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()),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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");
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user