diff --git a/Cargo.lock b/Cargo.lock
index c8fa4fd..3701112 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -97,23 +97,44 @@ checksum = "34ac096ce696dc2fcabef30516bb13c0a68a11d30131d3df6f04711467681b04"
name = "app"
version = "0.1.0"
dependencies = [
+ "argon2",
"cfg-if",
"chrono",
+ "config 0.15.6",
+ "hex",
"http",
"leptos",
"leptos_axum",
"leptos_meta",
"leptos_router",
+ "password-hash",
+ "rand",
+ "secrecy",
"serde",
+ "serde-aux",
"sqlx",
"thiserror 2.0.11",
+ "tokio",
"tracing",
"tracing-bunyan-formatter",
"tracing-log 0.2.0",
"tracing-subscriber",
+ "unicode-segmentation",
"uuid",
]
+[[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]]
name = "async-compression"
version = "0.4.18"
@@ -289,6 +310,15 @@ dependencies = [
"serde",
]
+[[package]]
+name = "blake2"
+version = "0.10.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe"
+dependencies = [
+ "digest",
+]
+
[[package]]
name = "block-buffer"
version = "0.10.4"
@@ -1892,6 +1922,17 @@ dependencies = [
"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",
+ "subtle",
+]
+
[[package]]
name = "paste"
version = "1.0.15"
@@ -2419,21 +2460,13 @@ version = "0.1.0"
dependencies = [
"app",
"axum",
- "config 0.15.6",
"leptos",
"leptos_axum",
- "rand",
- "secrecy",
- "serde",
- "serde-aux",
- "sqlx",
- "thiserror 2.0.11",
"tokio",
"tower",
"tower-http",
"tracing",
"tracing-log 0.2.0",
- "unicode-segmentation",
"uuid",
]
diff --git a/Cargo.toml b/Cargo.toml
index cc15f37..85f2663 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -48,6 +48,9 @@ config = { version = "0.15", features = ["toml"], default-features = false }
serde-aux = "4"
unicode-segmentation = "1"
rand = "0.8"
+argon2 = "0.5"
+password-hash = "0.5"
+hex = "0.4"
# See https://github.com/leptos-rs/cargo-leptos for documentation of all the parameters.
@@ -55,7 +58,7 @@ rand = "0.8"
# that are used together frontend (lib) & server (bin)
[[workspace.metadata.leptos]]
# this name is used for the wasm, js and css file names
-name = "start-axum-workspace"
+name = "echoes-of-ascension"
# the package in the workspace that contains the server binary (binary crate)
bin-package = "server"
diff --git a/app/Cargo.toml b/app/Cargo.toml
index 4e06a8e..687a74f 100644
--- a/app/Cargo.toml
+++ b/app/Cargo.toml
@@ -21,8 +21,17 @@ tracing-bunyan-formatter.workspace = true
tracing-log.workspace = true
sqlx.workspace = true
uuid.workspace = true
+tokio.workspace = true
chrono.workspace = true
serde.workspace = true
+secrecy.workspace = true
+unicode-segmentation.workspace = true
+rand.workspace = true
+config.workspace = true
+serde-aux.workspace = true
+argon2.workspace = true
+password-hash.workspace = true
+hex.workspace = true
[features]
default = []
diff --git a/server/src/config.rs b/app/src/config.rs
similarity index 100%
rename from server/src/config.rs
rename to app/src/config.rs
diff --git a/app/src/db/mod.rs b/app/src/db/mod.rs
new file mode 100644
index 0000000..913bd46
--- /dev/null
+++ b/app/src/db/mod.rs
@@ -0,0 +1 @@
+pub mod users;
diff --git a/app/src/db/users.rs b/app/src/db/users.rs
new file mode 100644
index 0000000..b997637
--- /dev/null
+++ b/app/src/db/users.rs
@@ -0,0 +1,27 @@
+use sqlx::PgPool;
+
+use crate::models::user::{error::UserError, new_user::NewUser};
+
+#[tracing::instrument(name = "Saving new user details in the database", skip(new_user, pool))]
+pub async fn insert_user(pool: &PgPool, new_user: &NewUser) -> Result<(), UserError> {
+ sqlx::query!(
+ r#"
+ INSERT INTO "user" (username, code)
+ VALUES ($1, $2)
+ "#,
+ new_user.username.as_ref(),
+ new_user.code.hash()?
+ )
+ .execute(pool)
+ .await
+ .map_err(|e| {
+ tracing::error!("Failed to execute query: {:?}", e);
+ match e {
+ sqlx::Error::Database(ref dbe) if dbe.constraint() == Some("user_username_key") => {
+ UserError::UsernameTaken(new_user.username.as_ref().to_string())
+ }
+ _ => UserError::Database(e),
+ }
+ })?;
+ Ok(())
+}
diff --git a/app/src/lib.rs b/app/src/lib.rs
index 14327c1..649b365 100644
--- a/app/src/lib.rs
+++ b/app/src/lib.rs
@@ -1,4 +1,8 @@
+pub mod config;
+pub mod db;
pub mod models;
+pub mod server_fn;
+pub mod startup;
pub mod telemetry;
use leptos::prelude::*;
@@ -32,7 +36,7 @@ pub fn App() -> impl IntoView {
provide_meta_context();
view! {
-
+
// sets the document title
diff --git a/app/src/models/mod.rs b/app/src/models/mod.rs
index 34b2ad4..601225e 100644
--- a/app/src/models/mod.rs
+++ b/app/src/models/mod.rs
@@ -1,2 +1,2 @@
+pub mod response;
pub mod user;
-
diff --git a/app/src/models/response.rs b/app/src/models/response.rs
new file mode 100644
index 0000000..90513eb
--- /dev/null
+++ b/app/src/models/response.rs
@@ -0,0 +1,35 @@
+use secrecy::ExposeSecret;
+use serde::{Deserialize, Serialize};
+
+use super::user::new_user::NewUser;
+
+#[derive(Debug, Serialize, Deserialize)]
+pub struct RegisterResponse {
+ pub username: String,
+ pub code: String,
+}
+
+#[derive(Debug, Serialize, Deserialize)]
+pub struct ErrorResponse {
+ pub error: String,
+}
+
+impl From for ErrorResponse
+where
+ T: Into,
+{
+ fn from(value: T) -> Self {
+ Self {
+ error: value.into(),
+ }
+ }
+}
+
+impl From for RegisterResponse {
+ fn from(value: NewUser) -> Self {
+ Self {
+ username: value.username.as_ref().to_string(),
+ code: value.code.expose_secret().to_string(),
+ }
+ }
+}
diff --git a/app/src/models/user.rs b/app/src/models/user.rs
deleted file mode 100644
index 9bde063..0000000
--- a/app/src/models/user.rs
+++ /dev/null
@@ -1,21 +0,0 @@
-use chrono::{DateTime, Utc};
-use serde::{Deserialize, Serialize};
-use uuid::Uuid;
-
-#[derive(Debug, Serialize, Deserialize)]
-pub struct User {
- pub id: Uuid,
- pub username: String,
- pub code: String,
- pub created_at: DateTime,
-}
-
-#[derive(Debug, Serialize, Deserialize)]
-pub struct UserRegistration {
- pub username: String,
-}
-
-#[derive(Debug, Serialize, Deserialize)]
-pub struct UserLogin {
- pub code: String,
-}
diff --git a/app/src/models/user/error.rs b/app/src/models/user/error.rs
new file mode 100644
index 0000000..668d932
--- /dev/null
+++ b/app/src/models/user/error.rs
@@ -0,0 +1,25 @@
+use thiserror::Error;
+
+#[derive(Debug, Error)]
+pub enum UserError {
+ #[error("Username validation failed: {0}")]
+ UsernameValidation(String),
+
+ #[error("Code hashing failed: {0}")]
+ HashingError(String),
+
+ #[error("Database error: {0}")]
+ Database(#[from] sqlx::Error),
+
+ #[error("Username already taken: {0}")]
+ UsernameTaken(String),
+
+ #[error("Invalid code format")]
+ InvalidCode,
+
+ #[error("Authentication failed")]
+ AuthenticationFailed,
+
+ #[error("Internal server error: {0}")]
+ Internal(String),
+}
diff --git a/server/src/domain/mod.rs b/app/src/models/user/mod.rs
similarity index 78%
rename from server/src/domain/mod.rs
rename to app/src/models/user/mod.rs
index 1186991..d4c3e6e 100644
--- a/server/src/domain/mod.rs
+++ b/app/src/models/user/mod.rs
@@ -1,3 +1,4 @@
+pub mod error;
pub mod new_user;
pub mod user_code;
pub mod username;
diff --git a/app/src/models/user/new_user.rs b/app/src/models/user/new_user.rs
new file mode 100644
index 0000000..74d5160
--- /dev/null
+++ b/app/src/models/user/new_user.rs
@@ -0,0 +1,25 @@
+use serde::{Deserialize, Serialize};
+
+use super::{user_code::UserCode, username::Username};
+
+#[derive(Debug, Serialize, Deserialize)]
+pub struct RegisterUserForm {
+ pub username: String,
+}
+
+#[derive(Debug, Default)]
+pub struct NewUser {
+ pub username: Username,
+ pub code: UserCode,
+}
+
+impl TryFrom for NewUser {
+ type Error = String;
+ fn try_from(value: RegisterUserForm) -> Result {
+ let username = value.username.try_into()?;
+ Ok(Self {
+ username,
+ ..Default::default()
+ })
+ }
+}
diff --git a/app/src/models/user/user_code.rs b/app/src/models/user/user_code.rs
new file mode 100644
index 0000000..80c5e52
--- /dev/null
+++ b/app/src/models/user/user_code.rs
@@ -0,0 +1,73 @@
+use argon2::Argon2;
+use password_hash::SaltString;
+use std::ops::Deref;
+
+use rand::{rngs::OsRng, thread_rng, Rng};
+use secrecy::{ExposeSecret, SecretString};
+
+use super::error::UserError;
+
+#[derive(Debug)]
+pub struct UserCode(SecretString);
+
+impl UserCode {
+ pub fn hash(&self) -> Result {
+ let salt = SaltString::generate(&mut OsRng);
+ let argon2 = Argon2::default();
+
+ let mut output_key_material = [0u8; 32];
+ argon2
+ .hash_password_into(
+ self.expose_secret().as_bytes(),
+ salt.as_str().as_bytes(),
+ &mut output_key_material,
+ )
+ .map_err(|e| UserError::HashingError(e.to_string()))?;
+ Ok(format!(
+ "{}${}",
+ salt.as_str(),
+ hex::encode(output_key_material)
+ ))
+ }
+
+ pub fn verify(stored: &str, code: &str) -> Result {
+ let argon2 = Argon2::default();
+
+ // Split stored value into salt and hash
+ let parts: Vec<&str> = stored.split('$').collect();
+ if parts.len() != 2 {
+ return Err(UserError::HashingError("Invalid hash format".to_string()));
+ }
+
+ let salt = parts[0];
+ let stored_hash =
+ hex::decode(parts[1]).map_err(|e| UserError::HashingError(e.to_string()))?;
+
+ let mut output = [0u8; 32];
+ argon2
+ .hash_password_into(code.as_bytes(), salt.as_bytes(), &mut output)
+ .map_err(|e| UserError::HashingError(e.to_string()))?;
+
+ Ok(output.as_slice() == stored_hash.as_slice())
+ }
+}
+
+impl Default for UserCode {
+ fn default() -> Self {
+ let mut rng = thread_rng();
+
+ let code = (0..16)
+ .map(|_| rng.gen_range(0..10).to_string())
+ .collect::();
+
+ Self(code.into())
+ }
+}
+
+impl Deref for UserCode {
+ type Target = SecretString;
+
+ fn deref(&self) -> &Self::Target {
+ &self.0
+ }
+}
diff --git a/server/src/domain/username.rs b/app/src/models/user/username.rs
similarity index 100%
rename from server/src/domain/username.rs
rename to app/src/models/user/username.rs
diff --git a/app/src/server_fn/auth.rs b/app/src/server_fn/auth.rs
new file mode 100644
index 0000000..ebd7b6d
--- /dev/null
+++ b/app/src/server_fn/auth.rs
@@ -0,0 +1,28 @@
+use crate::db::users::insert_user;
+use crate::models::user::error::UserError;
+use crate::models::user::new_user::RegisterUserForm;
+use crate::{models::response::RegisterResponse, startup::AppState};
+use leptos::{prelude::*, server};
+
+#[server(RegisterUser, "/api/v1/users")]
+pub async fn register_user(username: String) -> Result> {
+ let state = use_context::()
+ .ok_or_else(|| ServerFnError::ServerError("AppState not found".into()))?;
+
+ let form = RegisterUserForm { username };
+ let new_user = form.try_into().map_err(|e| ServerFnError::ServerError(e))?;
+
+ match insert_user(&state.pool, &new_user).await {
+ Ok(_) => Ok(RegisterResponse::from(new_user)),
+ Err(UserError::UsernameTaken(username)) => Err(ServerFnError::ServerError(format!(
+ "Username {} is already taken",
+ username
+ ))),
+ Err(e) => {
+ tracing::error!("Failed to register user: {}", e);
+ Err(ServerFnError::ServerError(
+ "Internal server error".to_string(),
+ ))
+ }
+ }
+}
diff --git a/app/src/server_fn/mod.rs b/app/src/server_fn/mod.rs
new file mode 100644
index 0000000..12bc9de
--- /dev/null
+++ b/app/src/server_fn/mod.rs
@@ -0,0 +1 @@
+mod auth;
diff --git a/app/src/startup.rs b/app/src/startup.rs
new file mode 100644
index 0000000..f77ec0a
--- /dev/null
+++ b/app/src/startup.rs
@@ -0,0 +1,34 @@
+use std::sync::Arc;
+
+use leptos::config::{errors::LeptosConfigError, LeptosOptions};
+use sqlx::{postgres::PgPoolOptions, PgPool};
+use thiserror::Error;
+
+use crate::config::DatabaseSettings;
+
+pub type AppState = Arc;
+
+#[derive(Debug, Error)]
+pub enum ApplicationError {
+ #[error("IO error: {0}")]
+ Io(#[from] std::io::Error),
+
+ #[error("Leptos configuration error: {0}")]
+ LeptosConfig(#[from] LeptosConfigError),
+
+ #[error("Database error: {0}")]
+ Database(#[from] sqlx::Error),
+
+ #[error("Server error: {0}")]
+ Server(String),
+}
+
+#[derive(Debug)]
+pub struct App {
+ pub pool: PgPool,
+ pub leptos_options: LeptosOptions,
+}
+
+pub fn get_connection_pool(config: &DatabaseSettings) -> PgPool {
+ PgPoolOptions::new().connect_lazy_with(config.with_db())
+}
diff --git a/justfile b/justfile
index 346798b..8305c08 100644
--- a/justfile
+++ b/justfile
@@ -1,7 +1,5 @@
set dotenv-load
-export RUSTC_WRAPPER:="sccache"
-
# List all available commands
default:
@just --list
diff --git a/server/Cargo.toml b/server/Cargo.toml
index b43b1ad..e5b5910 100644
--- a/server/Cargo.toml
+++ b/server/Cargo.toml
@@ -13,15 +13,7 @@ leptos_axum.workspace = true
axum.workspace = true
tokio.workspace = true
tower.workspace = true
-thiserror.workspace = true
tower-http.workspace = true
tracing.workspace = true
tracing-log.workspace = true
-sqlx.workspace = true
-serde.workspace = true
-serde-aux.workspace = true
-config.workspace = true
-secrecy.workspace = true
uuid.workspace = true
-unicode-segmentation.workspace = true
-rand.workspace = true
diff --git a/server/src/application.rs b/server/src/application.rs
new file mode 100644
index 0000000..1173fea
--- /dev/null
+++ b/server/src/application.rs
@@ -0,0 +1,37 @@
+use app::{
+ config::Settings,
+ startup::{get_connection_pool, App, ApplicationError},
+};
+use leptos::prelude::*;
+use tokio::{net::TcpListener, task::JoinHandle};
+
+use crate::routes::route;
+
+#[derive(Debug)]
+pub struct Server(JoinHandle>);
+
+impl Server {
+ pub async fn build(config: Settings) -> Result {
+ let pool = get_connection_pool(&config.database);
+
+ // Get Leptos configuration but override the address
+ let conf = get_configuration(None)?;
+
+ // Use application's address configuration
+ let addr = conf.leptos_options.site_addr;
+ let listener = TcpListener::bind(addr).await?;
+
+ let app_state = App {
+ pool,
+ leptos_options: conf.leptos_options,
+ }
+ .into();
+ let server = tokio::spawn(async move { axum::serve(listener, route(app_state)).await });
+
+ Ok(Self(server))
+ }
+
+ pub async fn run_until_stopped(self) -> Result<(), std::io::Error> {
+ self.0.await?
+ }
+}
diff --git a/server/src/domain/new_user.rs b/server/src/domain/new_user.rs
deleted file mode 100644
index 6092521..0000000
--- a/server/src/domain/new_user.rs
+++ /dev/null
@@ -1,7 +0,0 @@
-use super::{user_code::UserCode, username::Username};
-
-#[derive(Debug, Default)]
-pub struct NewUser {
- pub username: Username,
- pub code: UserCode,
-}
diff --git a/server/src/domain/user_code.rs b/server/src/domain/user_code.rs
deleted file mode 100644
index 28bf1d9..0000000
--- a/server/src/domain/user_code.rs
+++ /dev/null
@@ -1,33 +0,0 @@
-use std::ops::Deref;
-
-use rand::{thread_rng, Rng};
-use secrecy::SecretString;
-
-#[derive(Debug)]
-pub struct UserCode(SecretString);
-
-impl UserCode {
- pub fn generate() -> Self {
- Self::default()
- }
-}
-
-impl Default for UserCode {
- fn default() -> Self {
- let mut rng = thread_rng();
-
- let code = (0..16)
- .map(|_| rng.gen_range(0..10).to_string())
- .collect::();
-
- Self(code.into())
- }
-}
-
-impl Deref for UserCode {
- type Target = SecretString;
-
- fn deref(&self) -> &Self::Target {
- &self.0
- }
-}
diff --git a/server/src/lib.rs b/server/src/lib.rs
deleted file mode 100644
index d70ca6e..0000000
--- a/server/src/lib.rs
+++ /dev/null
@@ -1,4 +0,0 @@
-pub mod config;
-pub mod domain;
-pub mod routes;
-pub mod startup;
diff --git a/server/src/main.rs b/server/src/main.rs
index 2f14d9c..81415a7 100644
--- a/server/src/main.rs
+++ b/server/src/main.rs
@@ -1,17 +1,23 @@
-use app::telemetry::{get_subscriber, init_subscriber};
+mod application;
+mod routes;
+
+use app::{
+ config::get_config,
+ startup::ApplicationError,
+ telemetry::{get_subscriber, init_subscriber},
+};
+use application::Server;
use leptos::prelude::*;
-use server::config::get_config;
-use server::startup::{Application, ApplicationError};
#[tokio::main]
async fn main() -> Result<(), ApplicationError> {
// Generate the list of routes in your Leptos App
- let subscriber = get_subscriber("echoes-of-ascension-backend", "info", std::io::stdout);
+ let subscriber = get_subscriber("echoes-of-ascension-server", "info", std::io::stdout);
init_subscriber(subscriber);
let config = get_config().expect("Failed to read configuation.");
- let application = Application::build(config).await?;
+ let application = Server::build(config).await?;
application.run_until_stopped().await?;
Ok(())
}
diff --git a/server/src/routes/api/mod.rs b/server/src/routes/api/mod.rs
new file mode 100644
index 0000000..d198c03
--- /dev/null
+++ b/server/src/routes/api/mod.rs
@@ -0,0 +1,8 @@
+mod v1;
+
+use app::startup::AppState;
+use axum::Router;
+
+pub fn routes() -> Router {
+ Router::new().nest("/v1", v1::routes())
+}
diff --git a/server/src/routes/api/v1/auth.rs b/server/src/routes/api/v1/auth.rs
new file mode 100644
index 0000000..a9686bb
--- /dev/null
+++ b/server/src/routes/api/v1/auth.rs
@@ -0,0 +1,41 @@
+use app::db::users::insert_user;
+use app::models::{
+ response::{ErrorResponse, RegisterResponse},
+ user::{error::UserError, new_user::RegisterUserForm},
+};
+use app::startup::AppState;
+
+use axum::{extract::State, http::StatusCode, response::IntoResponse, Json};
+#[tracing::instrument(
+ name = "Creating new user",
+ skip(data, state),
+ fields(
+ username= %data.username,
+ )
+)]
+pub async fn register(
+ State(state): State,
+ Json(data): Json,
+) -> Result {
+ let new_user = data
+ .try_into()
+ .map_err(|e| (StatusCode::BAD_REQUEST, Json(ErrorResponse::from(e))))?;
+
+ match insert_user(&state.pool, &new_user).await {
+ Ok(_) => Ok((StatusCode::CREATED, Json(RegisterResponse::from(new_user)))),
+ Err(UserError::UsernameTaken(username)) => Err((
+ StatusCode::CONFLICT,
+ Json(ErrorResponse::from(format!(
+ "Username {} is already taken.",
+ username
+ ))),
+ )),
+ Err(e) => {
+ tracing::error!("Failed to register user: {}", e);
+ Err((
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(ErrorResponse::from("Internal server error")),
+ ))
+ }
+ }
+}
diff --git a/server/src/routes/api/v1/mod.rs b/server/src/routes/api/v1/mod.rs
new file mode 100644
index 0000000..f9de52f
--- /dev/null
+++ b/server/src/routes/api/v1/mod.rs
@@ -0,0 +1,8 @@
+mod auth;
+
+use app::startup::AppState;
+use axum::{routing::post, Router};
+
+pub fn routes() -> Router {
+ Router::new().route("/register", post(auth::register))
+}
diff --git a/server/src/routes/mod.rs b/server/src/routes/mod.rs
index 6b0c4c3..4d50f15 100644
--- a/server/src/routes/mod.rs
+++ b/server/src/routes/mod.rs
@@ -1,30 +1,39 @@
+mod api;
mod health_check;
-mod user;
-use std::time::Duration;
-use app::{shell, App};
+use app::{shell, startup::AppState, App};
use axum::{
body::Bytes,
extract::MatchedPath,
http::{HeaderMap, Request},
response::Response,
- routing::{get, post},
+ routing::get,
Router,
};
use health_check::health_check;
-
use leptos_axum::{generate_route_list, LeptosRoutes};
+use std::time::Duration;
use tower_http::{classify::ServerErrorsFailureClass, trace::TraceLayer};
use tracing::{info_span, Span};
-use user::register;
use uuid::Uuid;
-use crate::startup::AppState;
-
pub fn route(state: AppState) -> Router {
+ let leptos_options = state.leptos_options.clone();
+ let routes = generate_route_list(App);
+
Router::new()
- .merge(leptos_routes(state.clone()))
- .merge(api_routes(state))
+ .route("/health_check", get(health_check))
+ // API routes with proper nesting
+ .nest("/api", api::routes())
+ .with_state(state)
+ // Leptos setup
+ .leptos_routes(&leptos_options, routes, {
+ let leptos_options = leptos_options.clone();
+ move || shell(leptos_options.clone())
+ })
+ .fallback(leptos_axum::file_and_error_handler(shell))
+ .with_state(leptos_options)
+ // Tracing layer
.layer(
TraceLayer::new_for_http()
.make_span_with(|request: &Request<_>| {
@@ -51,27 +60,3 @@ pub fn route(state: AppState) -> Router {
),
)
}
-
-fn leptos_routes(state: AppState) -> Router {
- let leptos_options = state.leptos_options.clone();
- let routes = generate_route_list(App);
-
- Router::new()
- .leptos_routes(&leptos_options, routes, {
- let leptos_options = leptos_options.clone();
- move || shell(leptos_options.clone())
- })
- .fallback(leptos_axum::file_and_error_handler(shell))
- .with_state(leptos_options)
-}
-
-fn api_routes(state: AppState) -> Router {
- Router::new()
- .nest(
- "/api/v1",
- Router::new()
- .route("/health_check", get(health_check))
- .route("/register", post(register)),
- )
- .with_state(state)
-}
diff --git a/server/src/routes/user/mod.rs b/server/src/routes/user/mod.rs
deleted file mode 100644
index 56d7d42..0000000
--- a/server/src/routes/user/mod.rs
+++ /dev/null
@@ -1,2 +0,0 @@
-mod register;
-pub use register::register;
diff --git a/server/src/routes/user/register.rs b/server/src/routes/user/register.rs
deleted file mode 100644
index dd14345..0000000
--- a/server/src/routes/user/register.rs
+++ /dev/null
@@ -1,56 +0,0 @@
-use axum::{extract::State, http::StatusCode, response::IntoResponse, Form};
-use secrecy::ExposeSecret;
-use serde::Deserialize;
-use sqlx::PgPool;
-use tracing::error;
-
-use crate::{domain::new_user::NewUser, startup::AppState};
-
-#[derive(Deserialize)]
-pub struct FormData {
- username: String,
-}
-
-pub async fn register(
- State(state): State,
- Form(form): Form,
-) -> impl IntoResponse {
- let new_user = match form.try_into() {
- Ok(subscriber) => subscriber,
- Err(_) => return StatusCode::BAD_REQUEST,
- };
- if insert_user(&state.pool, &new_user).await.is_err() {
- return StatusCode::INTERNAL_SERVER_ERROR;
- }
- todo!()
-}
-
-#[tracing::instrument(name = "Saving new user details in the database", skip(new_user, pool))]
-pub async fn insert_user(pool: &PgPool, new_user: &NewUser) -> Result<(), sqlx::Error> {
- sqlx::query!(
- r#"
- INSERT INTO "user" (username, code)
- VALUES ($1, $2)
- "#,
- new_user.username.as_ref(),
- new_user.code.expose_secret()
- )
- .execute(pool)
- .await
- .map_err(|e| {
- error!("Failed to execute query: {:?}", e);
- e
- })?;
- Ok(())
-}
-
-impl TryFrom for NewUser {
- type Error = String;
- fn try_from(value: FormData) -> Result {
- let username = value.username.try_into()?;
- Ok(Self {
- username,
- ..Default::default()
- })
- }
-}
diff --git a/server/src/startup.rs b/server/src/startup.rs
deleted file mode 100644
index 908c6f1..0000000
--- a/server/src/startup.rs
+++ /dev/null
@@ -1,75 +0,0 @@
-use std::sync::Arc;
-
-use leptos::config::{errors::LeptosConfigError, get_configuration, LeptosOptions};
-use sqlx::{postgres::PgPoolOptions, PgPool};
-use thiserror::Error;
-use tokio::{net::TcpListener, task::JoinHandle};
-
-use crate::{
- config::{DatabaseSettings, Settings},
- routes::route,
-};
-
-pub type AppState = Arc;
-
-#[derive(Debug, Error)]
-pub enum ApplicationError {
- #[error("IO error: {0}")]
- Io(#[from] std::io::Error),
-
- #[error("Leptos configuration error: {0}")]
- LeptosConfig(#[from] LeptosConfigError),
-
- #[error("Database error: {0}")]
- Database(#[from] sqlx::Error),
-
- #[error("Server error: {0}")]
- Server(String),
-}
-
-#[derive(Debug)]
-pub struct App {
- pub pool: PgPool,
- pub leptos_options: LeptosOptions,
-}
-
-#[derive(Debug)]
-pub struct Application {
- port: u16,
- server: JoinHandle>,
-}
-
-impl Application {
- pub async fn build(config: Settings) -> Result {
- let pool = get_connection_pool(&config.database);
-
- // Get Leptos configuration but override the address
- let conf = get_configuration(None)?;
-
- // Use application's address configuration
- let addr = conf.leptos_options.site_addr;
- let listener = TcpListener::bind(addr).await?;
- let port = listener.local_addr()?.port();
-
- let app_state = App {
- pool,
- leptos_options: conf.leptos_options,
- }
- .into();
- let server = tokio::spawn(async move { axum::serve(listener, route(app_state)).await });
-
- Ok(Self { port, server })
- }
-
- pub fn port(&self) -> u16 {
- self.port
- }
-
- pub async fn run_until_stopped(self) -> Result<(), std::io::Error> {
- self.server.await?
- }
-}
-
-pub fn get_connection_pool(config: &DatabaseSettings) -> PgPool {
- PgPoolOptions::new().connect_lazy_with(config.with_db())
-}