mirror of
https://github.com/kristoferssolo/echoes-of-ascension.git
synced 2025-10-21 18:50:34 +00:00
refactor: layout
This commit is contained in:
parent
2e9f4035ee
commit
493a636a7d
19
Cargo.lock
generated
19
Cargo.lock
generated
@ -97,29 +97,22 @@ 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",
|
||||
]
|
||||
|
||||
@ -2459,14 +2452,26 @@ name = "server"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"app",
|
||||
"argon2",
|
||||
"axum",
|
||||
"config 0.15.6",
|
||||
"hex",
|
||||
"leptos",
|
||||
"leptos_axum",
|
||||
"leptos_router",
|
||||
"password-hash",
|
||||
"rand",
|
||||
"secrecy",
|
||||
"serde",
|
||||
"serde-aux",
|
||||
"sqlx",
|
||||
"thiserror 2.0.11",
|
||||
"tokio",
|
||||
"tower",
|
||||
"tower-http",
|
||||
"tracing",
|
||||
"tracing-log 0.2.0",
|
||||
"unicode-segmentation",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
|
||||
21
Cargo.toml
21
Cargo.toml
@ -22,23 +22,10 @@ tracing-subscriber = { version = "0.3", features = ["registry", "env-filter"] }
|
||||
tracing-bunyan-formatter = { version = "0.3", default-features = false }
|
||||
tracing-log = "0.2"
|
||||
thiserror = "2.0"
|
||||
tokio = { version = "1.43", features = [
|
||||
"rt",
|
||||
"macros",
|
||||
"tracing",
|
||||
"rt-multi-thread",
|
||||
] }
|
||||
tokio = { version = "1.43", features = ["rt", "macros", "tracing"] }
|
||||
tower = { version = "0.5", features = ["full"] }
|
||||
tower-http = { version = "0.6", features = ["full"] }
|
||||
wasm-bindgen = "=0.2.100"
|
||||
sqlx = { version = "0.8", features = [
|
||||
"runtime-tokio",
|
||||
"macros",
|
||||
"postgres",
|
||||
"uuid",
|
||||
"chrono",
|
||||
"migrate",
|
||||
] }
|
||||
uuid = { version = "1.12", features = ["v4", "serde"] }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
chrono = { version = "0.4", features = ["serde", "clock"] }
|
||||
@ -128,9 +115,3 @@ pedantic = "warn"
|
||||
nursery = "warn"
|
||||
unwrap_used = "warn"
|
||||
expect_used = "warn"
|
||||
|
||||
[workspace.package.metadata.nextest]
|
||||
slow-timeout = { period = "120s", terminate-after = 3 }
|
||||
|
||||
[workspace.profile.dev.package.sqlx-macros]
|
||||
opt-level = 3
|
||||
|
||||
@ -19,19 +19,12 @@ tracing.workspace = true
|
||||
tracing-subscriber.workspace = true
|
||||
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
|
||||
config.workspace = true
|
||||
|
||||
[features]
|
||||
default = []
|
||||
|
||||
@ -5,7 +5,7 @@ use leptos_router::{
|
||||
StaticSegment,
|
||||
};
|
||||
|
||||
use crate::components::homepage::HomePage;
|
||||
use crate::components::{homepage::HomePage, register::RegisterPage};
|
||||
|
||||
#[component]
|
||||
pub fn App() -> impl IntoView {
|
||||
@ -23,6 +23,7 @@ pub fn App() -> impl IntoView {
|
||||
<main>
|
||||
<Routes fallback=|| "Page not found.".into_view()>
|
||||
<Route path=StaticSegment("") view=HomePage />
|
||||
<Route path=StaticSegment("/register") view=RegisterPage />
|
||||
</Routes>
|
||||
</main>
|
||||
</Router>
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
use leptos::prelude::*;
|
||||
|
||||
use crate::server_fn::auth::register_user;
|
||||
use crate::{models::user::form::RegisterUserForm, server_fn::auth::register_user};
|
||||
|
||||
#[component]
|
||||
pub fn RegisterPage() -> impl IntoView {
|
||||
@ -34,14 +34,14 @@ pub fn RegisterPage() -> impl IntoView {
|
||||
}
|
||||
prop:value=username
|
||||
/>
|
||||
<div class="error">
|
||||
{move || {
|
||||
response
|
||||
.get()
|
||||
.and_then(|result| result.err())
|
||||
.map(|err| err.to_string())
|
||||
}}
|
||||
</div>
|
||||
// <div class="error">
|
||||
// {move || {
|
||||
// response
|
||||
// .get()
|
||||
// .and_then(|result| result.err())
|
||||
// .map(|err| err.to_string())
|
||||
// }}
|
||||
// </div>
|
||||
</div>
|
||||
<button type="submit" disabled=pending>
|
||||
{move || {
|
||||
|
||||
@ -1,29 +1,7 @@
|
||||
use secrecy::{ExposeSecret, SecretString};
|
||||
use serde::Deserialize;
|
||||
use serde_aux::field_attributes::deserialize_number_from_string;
|
||||
use sqlx::{
|
||||
postgres::{PgConnectOptions, PgSslMode},
|
||||
ConnectOptions,
|
||||
};
|
||||
use std::{fmt::Display, str::FromStr};
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
pub struct Settings {
|
||||
pub database: DatabaseSettings,
|
||||
pub application: ApplicationSettings,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
pub struct DatabaseSettings {
|
||||
pub username: String,
|
||||
pub password: SecretString,
|
||||
#[serde(deserialize_with = "deserialize_number_from_string")]
|
||||
pub port: u16,
|
||||
pub host: String,
|
||||
pub database_name: String,
|
||||
pub require_ssl: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
pub struct ApplicationSettings {
|
||||
#[serde(deserialize_with = "deserialize_number_from_string")]
|
||||
@ -37,51 +15,6 @@ pub enum Environment {
|
||||
Production,
|
||||
}
|
||||
|
||||
pub fn get_config() -> Result<Settings, config::ConfigError> {
|
||||
let base_path = std::env::current_dir().expect("Failed to determine current directory");
|
||||
let config_directory = base_path.join("config");
|
||||
let env: Environment = std::env::var("APP_ENVIRONMENT")
|
||||
.unwrap_or_else(|_| "local".into())
|
||||
.try_into()
|
||||
.expect("Failed to parse APP_ENVIRONMENT");
|
||||
|
||||
let env_filename = format!("{}.toml", &env);
|
||||
|
||||
let settings = config::Config::builder()
|
||||
.add_source(config::File::from(config_directory.join("base.toml")))
|
||||
.add_source(config::File::from(config_directory.join(env_filename)))
|
||||
.add_source(
|
||||
config::Environment::with_prefix("APP")
|
||||
.prefix_separator("_")
|
||||
.separator("__"),
|
||||
)
|
||||
.build()?;
|
||||
settings.try_deserialize::<Settings>()
|
||||
}
|
||||
|
||||
impl DatabaseSettings {
|
||||
pub fn without_db(&self) -> PgConnectOptions {
|
||||
let ssl_mode = if self.require_ssl {
|
||||
PgSslMode::Require
|
||||
} else {
|
||||
PgSslMode::Prefer
|
||||
};
|
||||
|
||||
PgConnectOptions::new()
|
||||
.host(&self.host)
|
||||
.username(&self.username)
|
||||
.password(self.password.expose_secret())
|
||||
.port(self.port)
|
||||
.ssl_mode(ssl_mode)
|
||||
}
|
||||
|
||||
pub fn with_db(&self) -> PgConnectOptions {
|
||||
self.without_db()
|
||||
.database(&self.database_name)
|
||||
.log_statements(tracing_log::log::LevelFilter::Trace)
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Environment {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
|
||||
@ -1,16 +1,14 @@
|
||||
pub mod components;
|
||||
pub mod config;
|
||||
pub mod db;
|
||||
pub mod models;
|
||||
pub mod server_fn;
|
||||
pub mod startup;
|
||||
pub mod telemetry;
|
||||
pub mod validation;
|
||||
|
||||
pub use components::app::App;
|
||||
|
||||
use leptos::prelude::*;
|
||||
use leptos_meta::{provide_meta_context, MetaTags, Stylesheet, Title};
|
||||
use leptos_router::{
|
||||
components::{Route, Router, Routes},
|
||||
StaticSegment,
|
||||
};
|
||||
use leptos_meta::MetaTags;
|
||||
|
||||
pub fn shell(options: LeptosOptions) -> impl IntoView {
|
||||
view! {
|
||||
@ -29,38 +27,3 @@ pub fn shell(options: LeptosOptions) -> impl IntoView {
|
||||
</html>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn App() -> impl IntoView {
|
||||
// Provides context that manages stylesheets, titles, meta tags, etc.
|
||||
provide_meta_context();
|
||||
|
||||
view! {
|
||||
<Stylesheet id="leptos" href="/pkg/echoes-of-ascension.css" />
|
||||
|
||||
// sets the document title
|
||||
<Title text="Welcome to Leptos" />
|
||||
|
||||
// content for this welcome page
|
||||
<Router>
|
||||
<main>
|
||||
<Routes fallback=|| "Page not found.".into_view()>
|
||||
<Route path=StaticSegment("") view=HomePage />
|
||||
</Routes>
|
||||
</main>
|
||||
</Router>
|
||||
}
|
||||
}
|
||||
|
||||
/// Renders the home page of your application.
|
||||
#[component]
|
||||
fn HomePage() -> impl IntoView {
|
||||
// Creates a reactive value to update the button
|
||||
let count = RwSignal::new(0);
|
||||
let on_click = move |_| *count.write() += 1;
|
||||
|
||||
view! {
|
||||
<h1>"Welcome to Leptos!"</h1>
|
||||
<button on:click=on_click>"Click Me: " {count}</button>
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,8 +1,5 @@
|
||||
use secrecy::ExposeSecret;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::user::new_user::NewUser;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct RegisterResponse {
|
||||
pub username: String,
|
||||
@ -24,12 +21,3 @@ where
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<NewUser> for RegisterResponse {
|
||||
fn from(value: NewUser) -> Self {
|
||||
Self {
|
||||
username: value.username.as_ref().to_string(),
|
||||
code: value.code.expose_secret().to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -8,9 +8,6 @@ pub enum UserError {
|
||||
#[error("Code hashing failed: {0}")]
|
||||
HashingError(String),
|
||||
|
||||
#[error("Database error: {0}")]
|
||||
Database(#[from] sqlx::Error),
|
||||
|
||||
#[error("Username already taken: {0}")]
|
||||
UsernameTaken(String),
|
||||
|
||||
|
||||
6
app/src/models/user/form.rs
Normal file
6
app/src/models/user/form.rs
Normal file
@ -0,0 +1,6 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct RegisterUserForm {
|
||||
pub username: String,
|
||||
}
|
||||
@ -1,4 +1,2 @@
|
||||
pub mod error;
|
||||
pub mod new_user;
|
||||
pub mod user_code;
|
||||
pub mod username;
|
||||
pub mod form;
|
||||
|
||||
@ -1,27 +1,11 @@
|
||||
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(
|
||||
form: RegisterUserForm,
|
||||
) -> Result<RegisterResponse, ServerFnError<String>> {
|
||||
let state = use_context::<AppState>()
|
||||
.ok_or_else(|| ServerFnError::ServerError("AppState not found".into()))?;
|
||||
use crate::models::{response::RegisterResponse, user::form::RegisterUserForm};
|
||||
|
||||
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".into()))
|
||||
}
|
||||
}
|
||||
#[server(RegisterUser, "/register")]
|
||||
pub async fn register_user(form: RegisterUserForm) -> Result<RegisterResponse, ServerFnError> {
|
||||
Ok(RegisterResponse {
|
||||
username: form.username,
|
||||
code: String::new(),
|
||||
})
|
||||
}
|
||||
|
||||
@ -1,34 +0,0 @@
|
||||
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<App>;
|
||||
|
||||
#[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())
|
||||
}
|
||||
20
app/src/validation.rs
Normal file
20
app/src/validation.rs
Normal file
@ -0,0 +1,20 @@
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum ValidationError {
|
||||
#[error("Field cannot be empty: {0}")]
|
||||
Empty(String),
|
||||
|
||||
#[error("Field too short: {0} (minimum {1} characters)")]
|
||||
TooShort(String, usize),
|
||||
|
||||
#[error("Field too long: {0} (maximum {1} characters)")]
|
||||
TooLong(String, usize),
|
||||
|
||||
#[error("Invalid format: {0}")]
|
||||
InvalidFormat(String),
|
||||
}
|
||||
|
||||
pub trait Validate {
|
||||
fn validate(&self) -> Result<(), ValidationError>;
|
||||
}
|
||||
2
justfile
2
justfile
@ -18,7 +18,7 @@ setup:
|
||||
|
||||
# Start development server with hot reload
|
||||
dev: kill-server db-migrate
|
||||
RUST_BACKTRACE=full cargo leptos watch | bunyan
|
||||
cargo leptos watch | bunyan
|
||||
|
||||
# Run cargo check on both native and wasm targets
|
||||
check:
|
||||
|
||||
@ -9,14 +9,33 @@ edition = "2021"
|
||||
app = { path = "../app", default-features = false, features = ["ssr"] }
|
||||
leptos = { workspace = true, features = ["ssr"] }
|
||||
leptos_axum.workspace = true
|
||||
thiserror.workspace = true
|
||||
|
||||
axum.workspace = true
|
||||
tokio.workspace = true
|
||||
tokio = { workspace = true, features = ["rt-multi-thread"] }
|
||||
sqlx = { version = "0.8", features = [
|
||||
"runtime-tokio",
|
||||
"macros",
|
||||
"postgres",
|
||||
"uuid",
|
||||
"chrono",
|
||||
"migrate",
|
||||
] }
|
||||
secrecy.workspace = true
|
||||
serde.workspace = true
|
||||
serde-aux.workspace = true
|
||||
config.workspace = true
|
||||
tower.workspace = true
|
||||
tower-http.workspace = true
|
||||
tracing.workspace = true
|
||||
tracing-log.workspace = true
|
||||
uuid.workspace = true
|
||||
rand.workspace = true
|
||||
argon2.workspace = true
|
||||
unicode-segmentation.workspace = true
|
||||
password-hash.workspace = true
|
||||
hex.workspace = true
|
||||
leptos_router.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
70
server/src/config.rs
Normal file
70
server/src/config.rs
Normal file
@ -0,0 +1,70 @@
|
||||
use app::config::{ApplicationSettings, Environment};
|
||||
use secrecy::{ExposeSecret, SecretString};
|
||||
use serde::Deserialize;
|
||||
use serde_aux::field_attributes::deserialize_number_from_string;
|
||||
use sqlx::{
|
||||
postgres::{PgConnectOptions, PgSslMode},
|
||||
ConnectOptions,
|
||||
};
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
pub struct Settings {
|
||||
pub database: DatabaseSettings,
|
||||
pub application: ApplicationSettings,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
pub struct DatabaseSettings {
|
||||
pub username: String,
|
||||
pub password: SecretString,
|
||||
#[serde(deserialize_with = "deserialize_number_from_string")]
|
||||
pub port: u16,
|
||||
pub host: String,
|
||||
pub database_name: String,
|
||||
pub require_ssl: bool,
|
||||
}
|
||||
|
||||
impl DatabaseSettings {
|
||||
pub fn without_db(&self) -> PgConnectOptions {
|
||||
let ssl_mode = if self.require_ssl {
|
||||
PgSslMode::Require
|
||||
} else {
|
||||
PgSslMode::Prefer
|
||||
};
|
||||
|
||||
PgConnectOptions::new()
|
||||
.host(&self.host)
|
||||
.username(&self.username)
|
||||
.password(self.password.expose_secret())
|
||||
.port(self.port)
|
||||
.ssl_mode(ssl_mode)
|
||||
}
|
||||
|
||||
pub fn with_db(&self) -> PgConnectOptions {
|
||||
self.without_db()
|
||||
.database(&self.database_name)
|
||||
.log_statements(tracing_log::log::LevelFilter::Trace)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_config() -> Result<Settings, config::ConfigError> {
|
||||
let base_path = std::env::current_dir().expect("Failed to determine current directory");
|
||||
let config_directory = base_path.join("config");
|
||||
let env: Environment = std::env::var("APP_ENVIRONMENT")
|
||||
.unwrap_or_else(|_| "local".into())
|
||||
.try_into()
|
||||
.expect("Failed to parse APP_ENVIRONMENT");
|
||||
|
||||
let env_filename = format!("{}.toml", &env);
|
||||
|
||||
let settings = config::Config::builder()
|
||||
.add_source(config::File::from(config_directory.join("base.toml")))
|
||||
.add_source(config::File::from(config_directory.join(env_filename)))
|
||||
.add_source(
|
||||
config::Environment::with_prefix("APP")
|
||||
.prefix_separator("_")
|
||||
.separator("__"),
|
||||
)
|
||||
.build()?;
|
||||
settings.try_deserialize::<Settings>()
|
||||
}
|
||||
@ -1,9 +1,10 @@
|
||||
use app::models::user::error::UserError;
|
||||
use sqlx::PgPool;
|
||||
|
||||
use crate::models::user::{error::UserError, new_user::NewUser};
|
||||
use crate::{domain::user::NewUser, error::user::ServerUserError};
|
||||
|
||||
#[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> {
|
||||
pub async fn insert_user(pool: &PgPool, new_user: &NewUser) -> Result<(), ServerUserError> {
|
||||
sqlx::query!(
|
||||
r#"
|
||||
INSERT INTO "user" (username, code)
|
||||
@ -18,9 +19,9 @@ pub async fn insert_user(pool: &PgPool, new_user: &NewUser) -> Result<(), UserEr
|
||||
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())
|
||||
ServerUserError::User(UserError::UsernameTaken(new_user.username.as_ref().into()))
|
||||
}
|
||||
_ => UserError::Database(e),
|
||||
_ => ServerUserError::Database(e),
|
||||
}
|
||||
})?;
|
||||
Ok(())
|
||||
3
server/src/domain/mod.rs
Normal file
3
server/src/domain/mod.rs
Normal file
@ -0,0 +1,3 @@
|
||||
pub mod user;
|
||||
mod user_code;
|
||||
mod username;
|
||||
@ -1,13 +1,9 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use app::models::{response::RegisterResponse, user::form::RegisterUserForm};
|
||||
use secrecy::ExposeSecret;
|
||||
|
||||
use super::{user_code::UserCode, username::Username};
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct RegisterUserForm {
|
||||
pub username: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct NewUser {
|
||||
pub username: Username,
|
||||
pub code: UserCode,
|
||||
@ -23,3 +19,12 @@ impl TryFrom<RegisterUserForm> for NewUser {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl From<NewUser> for RegisterResponse {
|
||||
fn from(value: NewUser) -> Self {
|
||||
Self {
|
||||
username: value.username.as_ref().into(),
|
||||
code: value.code.expose_secret().into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,3 +1,4 @@
|
||||
use app::models::user::error::UserError;
|
||||
use argon2::Argon2;
|
||||
use password_hash::SaltString;
|
||||
use std::ops::Deref;
|
||||
@ -5,9 +6,7 @@ use std::ops::Deref;
|
||||
use rand::{rngs::OsRng, thread_rng, Rng};
|
||||
use secrecy::{ExposeSecret, SecretString};
|
||||
|
||||
use super::error::UserError;
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct UserCode(SecretString);
|
||||
|
||||
impl UserCode {
|
||||
@ -2,7 +2,7 @@ use rand::{seq::SliceRandom, thread_rng, Rng};
|
||||
use std::str::FromStr;
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Username(String);
|
||||
|
||||
impl TryFrom<String> for Username {
|
||||
4
server/src/error/mod.rs
Normal file
4
server/src/error/mod.rs
Normal file
@ -0,0 +1,4 @@
|
||||
pub mod server;
|
||||
pub mod user;
|
||||
|
||||
pub use server::ServerError;
|
||||
15
server/src/error/server.rs
Normal file
15
server/src/error/server.rs
Normal file
@ -0,0 +1,15 @@
|
||||
use leptos::config::errors::LeptosConfigError;
|
||||
use sqlx;
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum ServerError {
|
||||
#[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),
|
||||
}
|
||||
21
server/src/error/user.rs
Normal file
21
server/src/error/user.rs
Normal file
@ -0,0 +1,21 @@
|
||||
use app::models::user::error::UserError;
|
||||
use sqlx;
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum ServerUserError {
|
||||
#[error("Database error: {0}")]
|
||||
Database(#[from] sqlx::Error),
|
||||
|
||||
#[error("{0}")]
|
||||
User(#[from] UserError),
|
||||
}
|
||||
|
||||
impl From<ServerUserError> for UserError {
|
||||
fn from(error: ServerUserError) -> Self {
|
||||
match error {
|
||||
ServerUserError::Database(e) => Self::Internal(e.to_string()),
|
||||
ServerUserError::User(e) => e,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,16 +1,19 @@
|
||||
mod application;
|
||||
mod config;
|
||||
mod db;
|
||||
pub mod domain;
|
||||
mod error;
|
||||
mod routes;
|
||||
mod server;
|
||||
mod startup;
|
||||
|
||||
use app::{
|
||||
config::get_config,
|
||||
startup::ApplicationError,
|
||||
telemetry::{get_subscriber, init_subscriber},
|
||||
};
|
||||
use application::Server;
|
||||
use app::telemetry::{get_subscriber, init_subscriber};
|
||||
use config::get_config;
|
||||
use error::ServerError;
|
||||
use leptos::prelude::*;
|
||||
use server::Server;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), ApplicationError> {
|
||||
async fn main() -> Result<(), ServerError> {
|
||||
// Generate the list of routes in your Leptos App
|
||||
let subscriber = get_subscriber("echoes-of-ascension-server", "info", std::io::stdout);
|
||||
init_subscriber(subscriber);
|
||||
|
||||
@ -1,8 +1,9 @@
|
||||
mod v1;
|
||||
|
||||
use app::startup::AppState;
|
||||
use axum::Router;
|
||||
|
||||
use crate::startup::AppState;
|
||||
|
||||
pub fn routes() -> Router<AppState> {
|
||||
Router::new().nest("/v1", v1::routes())
|
||||
}
|
||||
|
||||
@ -1,29 +1,29 @@
|
||||
use app::db::users::insert_user;
|
||||
use app::models::{
|
||||
response::{ErrorResponse, RegisterResponse},
|
||||
user::{error::UserError, new_user::RegisterUserForm},
|
||||
user::{error::UserError, form::RegisterUserForm},
|
||||
};
|
||||
use app::startup::AppState;
|
||||
|
||||
use axum::{extract::State, http::StatusCode, response::IntoResponse, Json};
|
||||
|
||||
use crate::{db::users::insert_user, error::user::ServerUserError, startup::AppState};
|
||||
|
||||
#[tracing::instrument(
|
||||
name = "Creating new user",
|
||||
skip(data, state),
|
||||
skip(payload, state),
|
||||
fields(
|
||||
username= %data.username,
|
||||
username= %payload.username,
|
||||
)
|
||||
)]
|
||||
pub async fn register(
|
||||
State(state): State<AppState>,
|
||||
Json(data): Json<RegisterUserForm>,
|
||||
Json(payload): Json<RegisterUserForm>,
|
||||
) -> Result<impl IntoResponse, impl IntoResponse> {
|
||||
let new_user = data
|
||||
let new_user = payload
|
||||
.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((
|
||||
Err(ServerUserError::User(UserError::UsernameTaken(username))) => Err((
|
||||
StatusCode::CONFLICT,
|
||||
Json(ErrorResponse::from(format!(
|
||||
"Username {} is already taken.",
|
||||
|
||||
@ -1,8 +1,9 @@
|
||||
mod auth;
|
||||
|
||||
use app::startup::AppState;
|
||||
use axum::{routing::post, Router};
|
||||
|
||||
use crate::startup::AppState;
|
||||
|
||||
pub fn routes() -> Router<AppState> {
|
||||
Router::new().route("/register", post(auth::register))
|
||||
}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
mod api;
|
||||
mod health_check;
|
||||
|
||||
use app::{shell, startup::AppState, App};
|
||||
use app::{shell, App};
|
||||
use axum::{
|
||||
body::Bytes,
|
||||
extract::MatchedPath,
|
||||
@ -17,19 +17,24 @@ use tower_http::{classify::ServerErrorsFailureClass, trace::TraceLayer};
|
||||
use tracing::{info_span, Span};
|
||||
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);
|
||||
|
||||
let api_router = api::routes().with_state(state.clone());
|
||||
|
||||
Router::new()
|
||||
.route("/health_check", get(health_check))
|
||||
// API routes with proper nesting
|
||||
.nest("/api", api::routes())
|
||||
.with_state(state)
|
||||
.nest("/api", api_router)
|
||||
// Leptos setup
|
||||
.leptos_routes(&leptos_options, routes, {
|
||||
let leptos_options = leptos_options.clone();
|
||||
move || shell(leptos_options.clone())
|
||||
{
|
||||
let leptos_options = leptos_options.clone();
|
||||
move || shell(leptos_options.clone())
|
||||
}
|
||||
})
|
||||
.fallback(leptos_axum::file_and_error_handler(shell))
|
||||
.with_state(leptos_options)
|
||||
|
||||
@ -1,17 +1,18 @@
|
||||
use app::{
|
||||
config::Settings,
|
||||
startup::{get_connection_pool, App, ApplicationError},
|
||||
};
|
||||
use leptos::prelude::*;
|
||||
use tokio::{net::TcpListener, task::JoinHandle};
|
||||
|
||||
use crate::routes::route;
|
||||
use crate::{
|
||||
config::Settings,
|
||||
error::ServerError,
|
||||
routes::route,
|
||||
startup::{get_connection_pool, App},
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Server(JoinHandle<Result<(), std::io::Error>>);
|
||||
|
||||
impl Server {
|
||||
pub async fn build(config: Settings) -> Result<Self, ApplicationError> {
|
||||
pub async fn build(config: Settings) -> Result<Self, ServerError> {
|
||||
let pool = get_connection_pool(&config.database);
|
||||
|
||||
// Get Leptos configuration but override the address
|
||||
17
server/src/startup.rs
Normal file
17
server/src/startup.rs
Normal file
@ -0,0 +1,17 @@
|
||||
use leptos::config::LeptosOptions;
|
||||
use sqlx::{postgres::PgPoolOptions, PgPool};
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::config::DatabaseSettings;
|
||||
|
||||
pub type AppState = Arc<App>;
|
||||
|
||||
#[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())
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user