diff --git a/Cargo.lock b/Cargo.lock index 9ec27ea..d1c8b64 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -65,30 +65,6 @@ version = "1.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" -[[package]] -name = "api" -version = "0.1.0" -dependencies = [ - "anyhow", - "argon2", - "async-trait", - "axum", - "axum-extra", - "bytes", - "chrono", - "ciborium", - "futures", - "leptos", - "log", - "rand 0.9.2", - "serde", - "serde_json", - "sqlx", - "thiserror 2.0.17", - "uuid", - "werewolves-proto", -] - [[package]] name = "argon2" version = "0.5.3" @@ -463,11 +439,11 @@ checksum = "2550f75b8cfac212855f6b1885455df8eaee8fe8e246b647d69146142e016084" [[package]] name = "colored" -version = "3.0.0" +version = "3.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fde0e0ec90c9dfb3b4b1a0891a7dcd0e2bffde2f7efed5fe7c9bb00e5bfb915e" +checksum = "faf9468729b8cbcea668e36183cb69d317348c2e08e994829fb56ebfdfbaac34" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -4053,7 +4029,7 @@ name = "werewolves" version = "0.1.0" dependencies = [ "anyhow", - "api", + "argon2", "axum", "axum-extra", "bytes", @@ -4102,14 +4078,24 @@ dependencies = [ name = "werewolves-proto" version = "0.1.0" dependencies = [ + "anyhow", + "argon2", + "async-trait", + "axum", + "axum-extra", + "bytes", "chrono", + "ciborium", "colored", + "futures", + "leptos", "log", "pretty_assertions", "pretty_env_logger", "rand 0.9.2", "serde", "serde_json", + "sqlx", "thiserror 2.0.17", "uuid", "werewolves-macros", @@ -4202,15 +4188,6 @@ dependencies = [ "windows-targets 0.48.5", ] -[[package]] -name = "windows-sys" -version = "0.59.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" -dependencies = [ - "windows-targets 0.52.6", -] - [[package]] name = "windows-sys" version = "0.60.2" @@ -4244,22 +4221,6 @@ dependencies = [ "windows_x86_64_msvc 0.48.5", ] -[[package]] -name = "windows-targets" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" -dependencies = [ - "windows_aarch64_gnullvm 0.52.6", - "windows_aarch64_msvc 0.52.6", - "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm 0.52.6", - "windows_i686_msvc 0.52.6", - "windows_x86_64_gnu 0.52.6", - "windows_x86_64_gnullvm 0.52.6", - "windows_x86_64_msvc 0.52.6", -] - [[package]] name = "windows-targets" version = "0.53.5" @@ -4270,7 +4231,7 @@ dependencies = [ "windows_aarch64_gnullvm 0.53.1", "windows_aarch64_msvc 0.53.1", "windows_i686_gnu 0.53.1", - "windows_i686_gnullvm 0.53.1", + "windows_i686_gnullvm", "windows_i686_msvc 0.53.1", "windows_x86_64_gnu 0.53.1", "windows_x86_64_gnullvm 0.53.1", @@ -4283,12 +4244,6 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" - [[package]] name = "windows_aarch64_gnullvm" version = "0.53.1" @@ -4301,12 +4256,6 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" -[[package]] -name = "windows_aarch64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" - [[package]] name = "windows_aarch64_msvc" version = "0.53.1" @@ -4319,24 +4268,12 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" -[[package]] -name = "windows_i686_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" - [[package]] name = "windows_i686_gnu" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" -[[package]] -name = "windows_i686_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" - [[package]] name = "windows_i686_gnullvm" version = "0.53.1" @@ -4349,12 +4286,6 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" -[[package]] -name = "windows_i686_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" - [[package]] name = "windows_i686_msvc" version = "0.53.1" @@ -4367,12 +4298,6 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" -[[package]] -name = "windows_x86_64_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" - [[package]] name = "windows_x86_64_gnu" version = "0.53.1" @@ -4385,12 +4310,6 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" - [[package]] name = "windows_x86_64_gnullvm" version = "0.53.1" @@ -4403,12 +4322,6 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" -[[package]] -name = "windows_x86_64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" - [[package]] name = "windows_x86_64_msvc" version = "0.53.1" diff --git a/Cargo.toml b/Cargo.toml index bcc8151..7cb7101 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,7 +6,7 @@ members = [ "werewolves-proto", # "werewolves-server", "werewolves", - "api", + # "api", ] [[workspace.metadata.leptos]] @@ -120,8 +120,10 @@ serde = { version = "1.0.228" } tokio = { version = "1.45.0", features = ["full"] } tower = { version = "0.5.2", features = ["full"] } tower-http = { version = "0.6.4", features = ["full"] } -api = { path = "api" } ciborium = { version = "0.2" } +pretty_assertions = { version = "1.4" } +colored = { version = "3.1" } +pretty_env_logger = { version = "0.5" } [profile.dev] opt-level = 0 diff --git a/api/Cargo.toml b/api/Cargo.toml deleted file mode 100644 index adee27e..0000000 --- a/api/Cargo.toml +++ /dev/null @@ -1,37 +0,0 @@ -[package] -name = "api" -version = "0.1.0" -edition = "2024" - -[dependencies] -bytes = { version = "1.10.1", features = ["serde"] } -rand = { version = "0.9", optional = true } -serde = { workspace = true, features = ["derive"] } -uuid = { workspace = true, features = ["serde", "v4"] } -thiserror = { workspace = true } -chrono = { workspace = true, features = ["serde"] } -sqlx = { workspace = true, optional = true } -axum = { workspace = true, optional = true, features = ["macros"] } -axum-extra = { workspace = true, optional = true, features = ["typed-header"] } -argon2 = { workspace = true, optional = true } -ciborium = { workspace = true } -async-trait = { workspace = true, optional = true } -werewolves-proto = { workspace = true } -serde_json = { workspace = true, optional = true } -futures = { workspace = true, optional = true } - -log.workspace = true -leptos.workspace = true -anyhow.workspace = true - -[features] -ssr = [ - "dep:sqlx", - "dep:axum", - "dep:axum-extra", - "dep:argon2", - "dep:rand", - "dep:async-trait", - "dep:serde_json", - "dep:futures", -] diff --git a/api/src/error.rs b/api/src/error.rs deleted file mode 100644 index 3c552cf..0000000 --- a/api/src/error.rs +++ /dev/null @@ -1,193 +0,0 @@ -use leptos::prelude::{ServerFnError, ServerFnErrorErr}; -use serde::{Deserialize, Serialize}; -use thiserror::Error; -use werewolves_proto::error::GameError; - -use crate::game::GameId; - -#[derive(Debug, Clone, PartialEq, Error, Serialize, Deserialize)] -pub enum ServerError { - #[error("internal server error")] - InternalServerError, - #[error("not found")] - NotFound, - #[error("user already exists")] - UserAlreadyExists, - #[error("invalid credentials")] - InvalidCredentials, - #[error("token expired")] - ExpiredToken, - #[error("connection error")] - ConnectionError, - #[error("invalid request: {0}")] - InvalidRequest(String), - #[error("you're already in an active game: {0}")] - AlreadyInActiveGame(GameId), - #[error("{0}")] - GameError(#[from] GameError), -} - -impl leptos::prelude::FromServerFnError for ServerError { - type Encoder = leptos::server_fn::codec::JsonEncoding; - - fn from_server_fn_error(value: ServerFnErrorErr) -> Self { - match value { - ServerFnErrorErr::ServerError(err) => { - log::error!("server error: {err}; truncating to ServerError::InternalServerError"); - ServerError::InternalServerError - } - ServerFnErrorErr::MiddlewareError(err) => { - log::error!( - "middleware error: {err}; truncating to ServerError::InternalServerError" - ); - ServerError::InternalServerError - } - ServerFnErrorErr::Request(err) => { - const CONN_ERR: &str = "TypeError: NetworkError when attempting to fetch resource."; - if err == CONN_ERR { - Self::ConnectionError - } else { - Self::InvalidRequest(err) - } - } - err => { - let t = match &err { - ServerFnErrorErr::Registration(_) => "Registration", - ServerFnErrorErr::UnsupportedRequestMethod(_) => "UnsupportedRequestMethod", - ServerFnErrorErr::Request(_) => "Request", - ServerFnErrorErr::ServerError(_) => "ServerError", - ServerFnErrorErr::MiddlewareError(_) => "MiddlewareError", - ServerFnErrorErr::Deserialization(_) => "Deserialization", - ServerFnErrorErr::Serialization(_) => "Serialization", - ServerFnErrorErr::Args(_) => "Args", - ServerFnErrorErr::MissingArg(_) => "MissingArg", - ServerFnErrorErr::Response(_) => "Response", - }; - Self::InvalidRequest(format!("[{t}]: {err}")) - } - } - } -} - -// impl core::str::FromStr for ServerError { -// type Err = core::convert::Infallible; - -// fn from_str(s: &str) -> Result { -// panic!("ServerError::FromStr({s})") -// } -// } -// impl From for ServerError { -// fn from(err: ServerFnError) -> Self { -// match err { -// ServerFnError::ServerError(err) => { -// log::error!("server error: {err}; truncating to ServerError::InternalServerError"); -// ServerError::InternalServerError -// } -// ServerFnError::MiddlewareError(err) => { -// log::error!( -// "middleware error: {err}; truncating to ServerError::InternalServerError" -// ); -// ServerError::InternalServerError -// } -// ServerFnError::Request(err) => { -// log::error!("[{err}]"); -// Self::ConnectionError -// } -// err => { -// let t = match &err { -// ServerFnError::Registration(_) => "Registration", -// ServerFnError::Request(_) => "Request", -// ServerFnError::ServerError(_) => "ServerError", -// ServerFnError::MiddlewareError(_) => "MiddlewareError", -// ServerFnError::Deserialization(_) => "Deserialization", -// ServerFnError::Serialization(_) => "Serialization", -// ServerFnError::Args(_) => "Args", -// ServerFnError::MissingArg(_) => "MissingArg", -// ServerFnError::Response(_) => "Response", -// ServerFnError::WrappedServerError(_) => "WrappedServerError", -// }; -// Self::InvalidRequest(format!("[{t}]: {err}")) -// } -// } -// } -// } - -impl From for ServerError { - fn from(err: DatabaseError) -> Self { - match err { - DatabaseError::NotFound => ServerError::NotFound, - DatabaseError::UserAlreadyExists => ServerError::UserAlreadyExists, - #[allow(unreachable_patterns)] - _ => { - log::error!( - "converting database error into ServerError::InternalServerError: {err}" - ); - ServerError::InternalServerError - } - } - } -} - -#[derive(Debug, Clone, PartialEq, Error)] -pub enum DatabaseError { - #[error("user already exists")] - UserAlreadyExists, - #[error("password hashing error: {0}")] - PasswordHashError(String), - #[error("sqlx error: {0}")] - SqlxError(String), - #[error("not found")] - NotFound, - #[error("(de)serialization error: {0}")] - Serialization(String), -} - -#[cfg(feature = "ssr")] -impl From for DatabaseError { - fn from(value: serde_json::Error) -> Self { - Self::Serialization(value.to_string()) - } -} - -#[cfg(feature = "ssr")] -impl axum::response::IntoResponse for ServerError { - fn into_response(self) -> axum::response::Response { - use axum::{Json, http::StatusCode}; - - use crate::cbor::Cbor; - - ( - match self { - ServerError::AlreadyInActiveGame(_) - | ServerError::GameError(_) - | ServerError::InvalidCredentials - | ServerError::InvalidRequest(_) - | ServerError::UserAlreadyExists => StatusCode::BAD_REQUEST, - ServerError::NotFound => StatusCode::NOT_FOUND, - ServerError::ConnectionError | ServerError::InternalServerError => { - StatusCode::INTERNAL_SERVER_ERROR - } - ServerError::ExpiredToken => StatusCode::UNAUTHORIZED, - }, - Json(self), - ) - .into_response() - } -} - -#[cfg(feature = "ssr")] -impl From for DatabaseError { - fn from(err: sqlx::Error) -> Self { - match err { - sqlx::Error::RowNotFound => Self::NotFound, - _ => Self::SqlxError(err.to_string()), - } - } -} - -#[cfg(feature = "ssr")] -impl From for DatabaseError { - fn from(err: argon2::password_hash::Error) -> Self { - Self::PasswordHashError(err.to_string()) - } -} diff --git a/api/src/identity.rs b/api/src/identity.rs deleted file mode 100644 index 1964e13..0000000 --- a/api/src/identity.rs +++ /dev/null @@ -1,9 +0,0 @@ -use serde::{Deserialize, Serialize}; -use werewolves_proto::{character::CharacterId, message::PublicIdentity, player::PlayerId}; - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct PlayerIdentity { - pub player_id: PlayerId, - pub character_id: CharacterId, - pub public: PublicIdentity, -} diff --git a/api/src/lib.rs b/api/src/lib.rs deleted file mode 100644 index 377fa95..0000000 --- a/api/src/lib.rs +++ /dev/null @@ -1,111 +0,0 @@ -#[cfg(feature = "ssr")] -pub mod cbor; -pub mod cbor_leptos; -#[cfg(feature = "ssr")] -pub mod db; -pub mod error; -pub mod limited; -// pub mod routes; -pub mod game; -#[cfg(feature = "ssr")] -pub mod identity; -pub mod message; -#[cfg(feature = "ssr")] -pub mod state; -pub mod token; -pub mod user; - -pub type ServerResult = core::result::Result; - -#[macro_export] -macro_rules! id_impl { - ($name:ident) => { - #[derive( - Debug, Clone, Copy, PartialEq, Eq, Hash, ::serde::Serialize, ::serde::Deserialize, - )] - pub struct $name(uuid::Uuid); - - #[cfg(feature = "ssr")] - impl sqlx::TypeInfo for $name { - fn is_null(&self) -> bool { - self.0 == uuid::Uuid::nil() - } - - fn name(&self) -> &str { - "uuid" - } - } - - #[cfg(feature = "ssr")] - impl sqlx::Type for $name { - fn type_info() -> ::TypeInfo { - >::type_info() - } - } - - #[cfg(feature = "ssr")] - impl<'q> sqlx::Encode<'q, sqlx::Postgres> for $name { - fn encode_by_ref( - &self, - buf: &mut ::ArgumentBuffer<'q>, - ) -> Result { - self.0.encode_by_ref(buf) - } - } - - #[cfg(feature = "ssr")] - impl<'r> sqlx::Decode<'r, sqlx::Postgres> for $name { - fn decode( - value: ::ValueRef<'r>, - ) -> Result { - Ok(Self(uuid::Uuid::decode(value)?)) - } - } - - impl From for $name { - fn from(value: uuid::Uuid) -> Self { - Self::from_uuid(value) - } - } - - impl From<$name> for uuid::Uuid { - fn from(value: $name) -> Self { - value.into_uuid() - } - } - - impl Default for $name { - fn default() -> Self { - Self::new() - } - } - - impl $name { - pub fn new() -> Self { - Self(uuid::Uuid::new_v4()) - } - - pub const fn from_uuid(uuid: uuid::Uuid) -> Self { - Self(uuid) - } - - pub const fn into_uuid(self) -> uuid::Uuid { - self.0 - } - } - - impl core::fmt::Display for $name { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - self.0.fmt(f) - } - } - - impl core::str::FromStr for $name { - type Err = uuid::Error; - - fn from_str(s: &str) -> Result { - Ok(Self(uuid::Uuid::from_str(s)?)) - } - } - }; -} diff --git a/api/src/message.rs b/api/src/message.rs deleted file mode 100644 index 390a57e..0000000 --- a/api/src/message.rs +++ /dev/null @@ -1,38 +0,0 @@ -use serde::{Deserialize, Serialize}; -use werewolves_proto::message::{ - ClientMessage, ServerToClientMessage, - host::{HostMessage, ServerToHostMessage}, -}; - -use crate::token::TokenString; - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub enum WrappedServerMessage { - Authentication(TokenString), - HostMessage(HostMessage), - ClientMessage(ClientMessage), -} - -impl From for WrappedServerMessage { - fn from(value: TokenString) -> Self { - Self::Authentication(value) - } -} - -impl From for WrappedServerMessage { - fn from(value: HostMessage) -> Self { - Self::HostMessage(value) - } -} - -impl From for WrappedServerMessage { - fn from(value: ClientMessage) -> Self { - Self::ClientMessage(value) - } -} - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub enum IntoClientResponse { - Player(ServerToClientMessage), - Host(ServerToHostMessage), -} diff --git a/api/src/routes.rs b/api/src/routes.rs deleted file mode 100644 index be75722..0000000 --- a/api/src/routes.rs +++ /dev/null @@ -1,36 +0,0 @@ -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum Method { - Options, - Get, - Post, - Put, - Delete, - Head, - Trace, - Connect, - Patch, -} - -pub struct Route { - pub method: Method, - pub path: &'static str, - pub authenticated: bool, -} - -impl Route { - const fn new(method: Method, path: &'static str, authenticated: bool) -> Self { - Self { - method, - path, - authenticated, - } - } -} - -pub struct Routes { - pub create_user: Route, -} - -pub const ROUTES: Routes = Routes { - create_user: Route::new(Method::Post, "/api/users", false), -}; diff --git a/api/src/state.rs b/api/src/state.rs deleted file mode 100644 index 1fca358..0000000 --- a/api/src/state.rs +++ /dev/null @@ -1,13 +0,0 @@ -use std::{collections::HashMap, sync::Arc}; - -use futures::lock::Mutex; -use leptos::config::LeptosOptions; - -use crate::{db::Database, game::GameId}; - -#[cfg_attr(feature = "ssr", derive(axum::extract::FromRef))] -#[derive(Debug, Clone)] -pub struct AppState { - pub db: Database, - pub leptos_options: LeptosOptions, -} diff --git a/api/src/user.rs b/api/src/user.rs deleted file mode 100644 index 317cb1a..0000000 --- a/api/src/user.rs +++ /dev/null @@ -1,57 +0,0 @@ -use chrono::{DateTime, Utc}; -use serde::{Deserialize, Serialize}; -use werewolves_proto::player::PlayerId; - -use crate::{limited::ClampedString, token::TokenString}; - -pub type Username = ClampedString<1, 0x40>; -pub type Password = ClampedString<6, 0x100>; - -#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] -pub struct UserLogin { - pub username: Username, - pub password: Password, -} -#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] -pub struct ChangePassword { - pub current: Password, - pub new: Password, -} - -#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] -pub struct DeleteUserRequest { - pub password: Password, -} - -#[cfg_attr(feature = "ssr", derive(::sqlx::FromRow))] -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Session { - pub username: String, - pub display_name: Option, - pub pronouns: Option, - - pub user_created_at: DateTime, - pub user_updated_at: DateTime, - pub token_created_at: DateTime, - pub token_expires_at: DateTime, - pub token: TokenString, -} -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ProfileUpdate { - pub display_name: Option, - pub pronouns: Option, -} - -crate::id_impl!(UserId); - -impl From for UserId { - fn from(value: PlayerId) -> Self { - UserId(value.into()) - } -} - -impl From for PlayerId { - fn from(value: UserId) -> Self { - PlayerId::from_uuid(value.0) - } -} diff --git a/werewolves-proto/Cargo.toml b/werewolves-proto/Cargo.toml index ea68606..f1456d7 100644 --- a/werewolves-proto/Cargo.toml +++ b/werewolves-proto/Cargo.toml @@ -4,16 +4,38 @@ version = "0.1.0" edition = "2024" [dependencies] -thiserror = { version = "2" } -log = { version = "0.4" } -serde_json = { version = "1.0" } -serde = { version = "1.0", features = ["derive"] } -uuid = { version = "1.17", features = ["v4", "serde"] } -rand = { version = "0.9", features = ["std_rng"] } -werewolves-macros = { path = "../werewolves-macros" } -chrono = { version = "0.4", features = ["serde"] } +bytes = { version = "1.10.1", features = ["serde"] } +serde = { workspace = true, features = ["derive"] } +uuid = { workspace = true, features = ["serde", "v4"] } +thiserror = { workspace = true } +chrono = { workspace = true, features = ["serde"] } +sqlx = { workspace = true, optional = true } +axum = { workspace = true, optional = true, features = ["macros"] } +axum-extra = { workspace = true, optional = true, features = ["typed-header"] } +argon2 = { workspace = true, optional = true } +ciborium = { workspace = true } +async-trait = { workspace = true, optional = true } +serde_json = { workspace = true, optional = true } +futures = { workspace = true, optional = true } + +log.workspace = true +leptos.workspace = true +anyhow.workspace = true +werewolves-macros.workspace = true +rand.workspace = true [dev-dependencies] -pretty_assertions = { version = "1" } -pretty_env_logger = { version = "0.5" } -colored = { version = "3.0" } +colored.workspace = true +pretty_assertions.workspace = true +pretty_env_logger.workspace = true + +[features] +ssr = [ + "dep:sqlx", + "dep:axum", + "dep:axum-extra", + "dep:argon2", + "dep:async-trait", + "dep:serde_json", + "dep:futures", +] diff --git a/api/src/cbor.rs b/werewolves-proto/src/cbor.rs similarity index 97% rename from api/src/cbor.rs rename to werewolves-proto/src/cbor.rs index 0586e6f..4fd7245 100644 --- a/api/src/cbor.rs +++ b/werewolves-proto/src/cbor.rs @@ -7,10 +7,6 @@ use axum::{ use axum_extra::headers::Mime; use bytes::{BufMut, BytesMut}; use core::fmt::Display; -use leptos::server_fn::{ - ContentType, Decodes, Encodes, Format, FormatType, - codec::{Post, Put}, -}; use serde::{Serialize, de::DeserializeOwned}; const CBOR_CONTENT_TYPE: HeaderValue = HeaderValue::from_static("application/cbor"); diff --git a/api/src/cbor_leptos.rs b/werewolves-proto/src/cbor_leptos.rs similarity index 97% rename from api/src/cbor_leptos.rs rename to werewolves-proto/src/cbor_leptos.rs index 13c53e9..57d6a67 100644 --- a/api/src/cbor_leptos.rs +++ b/werewolves-proto/src/cbor_leptos.rs @@ -1,6 +1,3 @@ -use core::marker::PhantomData; -use std::io::Read; - use bytes::Bytes; use leptos::{ server::codee::{Decoder, Encoder}, diff --git a/werewolves-proto/src/character.rs b/werewolves-proto/src/character.rs index 7ff9348..35c6b36 100644 --- a/werewolves-proto/src/character.rs +++ b/werewolves-proto/src/character.rs @@ -26,6 +26,7 @@ use crate::{ diedto::DiedTo, error::GameError, game::{GameTime, Village, night::changes::NightChange}, + id_impl, message::{CharacterIdentity, Identification, PublicIdentity, night::ActionPrompt}, player::{PlayerId, RoleChange}, role::{ @@ -37,29 +38,7 @@ use crate::{ type Result = core::result::Result; -#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)] -pub struct CharacterId(uuid::Uuid); - -impl CharacterId { - pub fn new() -> Self { - Self(uuid::Uuid::new_v4()) - } - pub const fn from_u128(v: u128) -> Self { - Self(uuid::Uuid::from_u128(v)) - } - pub const fn from_uuid(v: uuid::Uuid) -> Self { - Self(v) - } - pub const fn into_uuid(self) -> uuid::Uuid { - self.0 - } -} - -impl Display for CharacterId { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - self.0.fmt(f) - } -} +id_impl!(CharacterId); #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct Character { diff --git a/werewolves-proto/src/error.rs b/werewolves-proto/src/error.rs index 7e2c7db..fdcb5c5 100644 --- a/werewolves-proto/src/error.rs +++ b/werewolves-proto/src/error.rs @@ -15,7 +15,13 @@ use serde::{Deserialize, Serialize}; use thiserror::Error; -use crate::{game::GameTime, message::PublicIdentity, player::PlayerId, role::RoleTitle}; +use crate::{ + game::{GameId, GameTime}, + message::PublicIdentity, + player::PlayerId, + role::RoleTitle, +}; +use leptos::prelude::ServerFnErrorErr; #[derive(Debug, Clone, PartialEq, Error, Serialize, Deserialize)] pub enum GameError { @@ -121,4 +127,149 @@ pub enum GameError { CannotJoinStartedGame, #[error("game already started")] GameAlreadyStarted, + #[error("you're already in another game: {0}")] + AlreadyInAnotherGame(GameId), +} + +#[derive(Debug, Clone, PartialEq, Error, Serialize, Deserialize)] +pub enum ServerError { + #[error("internal server error")] + InternalServerError, + #[error("not found")] + NotFound, + #[error("user already exists")] + UserAlreadyExists, + #[error("invalid credentials")] + InvalidCredentials, + #[error("token expired")] + ExpiredToken, + #[error("connection error")] + ConnectionError, + #[error("invalid request: {0}")] + InvalidRequest(String), + #[error("you're already in an active game: {0}")] + AlreadyInActiveGame(GameId), + #[error("{0}")] + GameError(#[from] GameError), +} + +impl leptos::prelude::FromServerFnError for ServerError { + type Encoder = leptos::server_fn::codec::JsonEncoding; + + fn from_server_fn_error(value: ServerFnErrorErr) -> Self { + match value { + ServerFnErrorErr::ServerError(err) => { + log::error!("server error: {err}; truncating to ServerError::InternalServerError"); + ServerError::InternalServerError + } + ServerFnErrorErr::MiddlewareError(err) => { + log::error!( + "middleware error: {err}; truncating to ServerError::InternalServerError" + ); + ServerError::InternalServerError + } + ServerFnErrorErr::Request(err) => { + const CONN_ERR: &str = "TypeError: NetworkError when attempting to fetch resource."; + if err == CONN_ERR { + Self::ConnectionError + } else { + Self::InvalidRequest(err) + } + } + err => { + let t = match &err { + ServerFnErrorErr::Registration(_) => "Registration", + ServerFnErrorErr::UnsupportedRequestMethod(_) => "UnsupportedRequestMethod", + ServerFnErrorErr::Request(_) => "Request", + ServerFnErrorErr::ServerError(_) => "ServerError", + ServerFnErrorErr::MiddlewareError(_) => "MiddlewareError", + ServerFnErrorErr::Deserialization(_) => "Deserialization", + ServerFnErrorErr::Serialization(_) => "Serialization", + ServerFnErrorErr::Args(_) => "Args", + ServerFnErrorErr::MissingArg(_) => "MissingArg", + ServerFnErrorErr::Response(_) => "Response", + }; + Self::InvalidRequest(format!("[{t}]: {err}")) + } + } + } +} + +impl From for ServerError { + fn from(err: DatabaseError) -> Self { + match err { + DatabaseError::NotFound => ServerError::NotFound, + DatabaseError::UserAlreadyExists => ServerError::UserAlreadyExists, + #[allow(unreachable_patterns)] + _ => { + log::error!( + "converting database error into ServerError::InternalServerError: {err}" + ); + ServerError::InternalServerError + } + } + } +} + +#[derive(Debug, Clone, PartialEq, Error)] +pub enum DatabaseError { + #[error("user already exists")] + UserAlreadyExists, + #[error("password hashing error: {0}")] + PasswordHashError(String), + #[error("sqlx error: {0}")] + SqlxError(String), + #[error("not found")] + NotFound, + #[error("(de)serialization error: {0}")] + Serialization(String), +} + +impl From for DatabaseError { + fn from(value: leptos::serde_json::Error) -> Self { + Self::Serialization(value.to_string()) + } +} + +#[cfg(feature = "ssr")] +impl axum::response::IntoResponse for ServerError { + fn into_response(self) -> axum::response::Response { + use axum::{Json, http::StatusCode}; + + use crate::cbor::Cbor; + + ( + match self { + ServerError::AlreadyInActiveGame(_) + | ServerError::GameError(_) + | ServerError::InvalidCredentials + | ServerError::InvalidRequest(_) + | ServerError::UserAlreadyExists => StatusCode::BAD_REQUEST, + ServerError::NotFound => StatusCode::NOT_FOUND, + ServerError::ConnectionError | ServerError::InternalServerError => { + StatusCode::INTERNAL_SERVER_ERROR + } + ServerError::ExpiredToken => StatusCode::UNAUTHORIZED, + }, + Json(self), + ) + .into_response() + } +} + +#[cfg(feature = "ssr")] +impl From for DatabaseError { + fn from(err: sqlx::Error) -> Self { + match err { + sqlx::Error::RowNotFound => Self::NotFound, + _ => Self::SqlxError(err.to_string()), + } + } +} + +#[cfg(feature = "ssr")] +impl From for DatabaseError { + fn from(err: argon2::password_hash::Error) -> Self { + Self::PasswordHashError(err.to_string()) + } } diff --git a/werewolves-proto/src/game/mod.rs b/werewolves-proto/src/game/mod.rs index 57dd245..84b7fb1 100644 --- a/werewolves-proto/src/game/mod.rs +++ b/werewolves-proto/src/game/mod.rs @@ -37,6 +37,7 @@ use crate::{ night::{Night, ServerAction}, story::{DayDetail, GameActions, GameStory, NightDetails}, }, + id_impl, message::{ CharacterState, ClientDeadChat, Identification, ServerToClientMessage, dead::{DeadChatContent, DeadChatMessage}, @@ -53,6 +54,8 @@ pub use { type Result = core::result::Result; +id_impl!(GameId); + #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Game { started: DateTime, diff --git a/api/src/game.rs b/werewolves-proto/src/game_record.rs similarity index 74% rename from api/src/game.rs rename to werewolves-proto/src/game_record.rs index c122c88..cc8e747 100644 --- a/api/src/game.rs +++ b/werewolves-proto/src/game_record.rs @@ -1,16 +1,17 @@ +use crate::{ + game::{Game, GameSettings, story::GameStory}, + player::PlayerId, +}; use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; -use werewolves_proto::game::{Game, GameSettings, story::GameStory}; -use crate::{id_impl, user::UserId}; - -id_impl!(GameId); +pub use crate::game::GameId; #[cfg_attr(feature = "ssr", derive(::sqlx::FromRow))] #[derive(Debug)] pub struct GameRecord { pub id: GameId, - pub host: UserId, + pub host: PlayerId, pub created_at: DateTime, pub game_state: GameRecordState, } diff --git a/werewolves-proto/src/lib.rs b/werewolves-proto/src/lib.rs index 5765b4c..3128cf2 100644 --- a/werewolves-proto/src/lib.rs +++ b/werewolves-proto/src/lib.rs @@ -15,14 +15,121 @@ #![allow(clippy::new_without_default)] pub mod aura; pub mod bag; +#[cfg(feature = "ssr")] +pub mod cbor; +pub mod cbor_leptos; pub mod character; pub mod diedto; pub mod error; pub mod game; +pub mod game_record; #[cfg(test)] mod game_test; +pub mod limited; +mod log; pub mod message; pub mod nonzero; pub mod player; pub mod role; pub mod team; +pub mod token; +pub use log::*; + +pub type ServerResult = core::result::Result; + +#[macro_export] +macro_rules! id_impl { + ($name:ident) => { + #[derive( + Debug, Clone, Copy, PartialEq, Eq, Hash, ::serde::Serialize, ::serde::Deserialize, + )] + pub struct $name(uuid::Uuid); + + #[cfg(feature = "ssr")] + impl sqlx::TypeInfo for $name { + fn is_null(&self) -> bool { + self.0 == uuid::Uuid::nil() + } + + fn name(&self) -> &str { + "uuid" + } + } + + #[cfg(feature = "ssr")] + impl sqlx::Type for $name { + fn type_info() -> ::TypeInfo { + >::type_info() + } + } + + #[cfg(feature = "ssr")] + impl<'q> sqlx::Encode<'q, sqlx::Postgres> for $name { + fn encode_by_ref( + &self, + buf: &mut ::ArgumentBuffer<'q>, + ) -> core::result::Result { + self.0.encode_by_ref(buf) + } + } + + #[cfg(feature = "ssr")] + impl<'r> sqlx::Decode<'r, sqlx::Postgres> for $name { + fn decode( + value: ::ValueRef<'r>, + ) -> core::result::Result { + Ok(Self(uuid::Uuid::decode(value)?)) + } + } + + impl From for $name { + fn from(value: uuid::Uuid) -> Self { + Self::from_uuid(value) + } + } + + impl From<$name> for uuid::Uuid { + fn from(value: $name) -> Self { + value.into_uuid() + } + } + + impl Default for $name { + fn default() -> Self { + Self::new() + } + } + + impl $name { + pub fn new() -> Self { + Self(uuid::Uuid::new_v4()) + } + + pub const fn from_uuid(uuid: uuid::Uuid) -> Self { + Self(uuid) + } + + pub const fn into_uuid(self) -> uuid::Uuid { + self.0 + } + + pub const fn from_u128(u: u128) -> Self { + Self(::uuid::Uuid::from_u128(u)) + } + } + + impl core::fmt::Display for $name { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> core::fmt::Result { + core::fmt::Display::fmt(&self.0, f) + } + } + + impl core::str::FromStr for $name { + type Err = uuid::Error; + + fn from_str(s: &str) -> core::result::Result { + Ok(Self(uuid::Uuid::from_str(s)?)) + } + } + }; +} diff --git a/api/src/limited.rs b/werewolves-proto/src/limited.rs similarity index 100% rename from api/src/limited.rs rename to werewolves-proto/src/limited.rs diff --git a/werewolves-proto/src/log.rs b/werewolves-proto/src/log.rs new file mode 100644 index 0000000..16d5d50 --- /dev/null +++ b/werewolves-proto/src/log.rs @@ -0,0 +1,59 @@ +pub trait LogError { + fn log(self, loc: CodePath, level: log::Level); + fn log_warn(self, loc: CodePath); + fn log_err(self, loc: CodePath); + fn log_debug(self, loc: CodePath); +} + +pub struct CodePath { + pub module_path: &'static str, + pub loc: &'static std::panic::Location<'static>, +} + +#[macro_export] +macro_rules! loc { + () => { + (::werewolves_proto::CodePath { + module_path: log::__private_api::module_path!(), + loc: log::__private_api::loc(), + }) + }; +} + +impl LogError for Result +where + E: core::fmt::Display, +{ + fn log(self, loc: CodePath, lvl: log::Level) { + let Err(err) = self else { + return; + }; + if lvl <= log::STATIC_MAX_LEVEL && lvl <= log::max_level() { + log::__private_api::log( + log::__log_logger!(__log_global_logger), + log::__private_api::format_args!("{err}"), + lvl, + &(loc.module_path, loc.module_path, loc.loc), + (), + ); + } + } + + fn log_warn(self, loc: CodePath) { + if self.is_err() { + self.log(loc, log::Level::Warn); + } + } + + fn log_err(self, loc: CodePath) { + if self.is_err() { + self.log(loc, log::Level::Error); + } + } + + fn log_debug(self, loc: CodePath) { + if self.is_err() { + self.log(loc, log::Level::Debug); + } + } +} diff --git a/werewolves-proto/src/message.rs b/werewolves-proto/src/message.rs index b3cf3a0..c26968d 100644 --- a/werewolves-proto/src/message.rs +++ b/werewolves-proto/src/message.rs @@ -17,6 +17,10 @@ pub mod host; mod ident; pub mod night; +use crate::{ + message::host::{HostMessage, ServerToHostMessage}, + token::TokenString, +}; use core::num::NonZeroU8; use chrono::{DateTime, Utc}; @@ -25,11 +29,8 @@ use serde::{Deserialize, Serialize}; use werewolves_macros::Titles; use crate::{ - character::CharacterId, - error::GameError, - game::{GameOver, story::GameStory}, - message::dead::DeadChatMessage, - role::RoleTitle, + character::CharacterId, error::GameError, game::story::GameStory, + message::dead::DeadChatMessage, role::RoleTitle, }; #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] @@ -88,3 +89,34 @@ pub enum ServerToClientMessage { pub enum PlayerUpdate { Number(NonZeroU8), } + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub enum WrappedServerMessage { + Authentication(TokenString), + HostMessage(HostMessage), + ClientMessage(ClientMessage), +} + +impl From for WrappedServerMessage { + fn from(value: TokenString) -> Self { + Self::Authentication(value) + } +} + +impl From for WrappedServerMessage { + fn from(value: HostMessage) -> Self { + Self::HostMessage(value) + } +} + +impl From for WrappedServerMessage { + fn from(value: ClientMessage) -> Self { + Self::ClientMessage(value) + } +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub enum IntoClientResponse { + Player(ServerToClientMessage), + Host(ServerToHostMessage), +} diff --git a/werewolves-proto/src/player.rs b/werewolves-proto/src/player.rs index 9113503..fcd062e 100644 --- a/werewolves-proto/src/player.rs +++ b/werewolves-proto/src/player.rs @@ -12,41 +12,20 @@ // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -use core::fmt::Display; +use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use crate::{ character::CharacterId, + id_impl, + limited::ClampedString, + message::PublicIdentity, role::{Role, RoleTitle}, + token::TokenString, }; -#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)] -pub struct PlayerId(uuid::Uuid); - -impl PlayerId { - pub fn new() -> Self { - Self(uuid::Uuid::new_v4()) - } - pub const fn from_u128(v: u128) -> Self { - Self(uuid::Uuid::from_u128(v)) - } - pub const fn from_uuid(uuid: uuid::Uuid) -> Self { - Self(uuid) - } -} - -impl From for uuid::Uuid { - fn from(value: PlayerId) -> Self { - value.0 - } -} - -impl Display for PlayerId { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - self.0.fmt(f) - } -} +id_impl!(PlayerId); #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Player { @@ -82,3 +61,48 @@ pub struct RoleChange { pub new_role: RoleTitle, pub changed_on_night: u8, } + +pub type Username = ClampedString<1, 0x40>; +pub type Password = ClampedString<6, 0x100>; + +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +pub struct UserLogin { + pub username: Username, + pub password: Password, +} +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +pub struct ChangePassword { + pub current: Password, + pub new: Password, +} + +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +pub struct DeleteUserRequest { + pub password: Password, +} + +#[cfg_attr(feature = "ssr", derive(::sqlx::FromRow))] +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Session { + pub username: String, + pub display_name: Option, + pub pronouns: Option, + + pub user_created_at: DateTime, + pub user_updated_at: DateTime, + pub token_created_at: DateTime, + pub token_expires_at: DateTime, + pub token: TokenString, +} +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ProfileUpdate { + pub display_name: Option, + pub pronouns: Option, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct PlayerIdentity { + pub player_id: PlayerId, + pub character_id: CharacterId, + pub public: PublicIdentity, +} diff --git a/api/src/token.rs b/werewolves-proto/src/token.rs similarity index 60% rename from api/src/token.rs rename to werewolves-proto/src/token.rs index 836532a..28e82af 100644 --- a/api/src/token.rs +++ b/werewolves-proto/src/token.rs @@ -1,4 +1,19 @@ -use crate::{limited::FixedLenString, user::Username}; +// Copyright (C) 2026 Emilis Bliūdžius +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +use crate::{limited::FixedLenString, player::Username}; use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; diff --git a/werewolves/Cargo.toml b/werewolves/Cargo.toml index 0397bcf..72cc22d 100644 --- a/werewolves/Cargo.toml +++ b/werewolves/Cargo.toml @@ -16,8 +16,8 @@ leptos_meta = { workspace = true } tokio = { version = "1", features = ["rt-multi-thread"], optional = true } wasm-bindgen = { workspace = true, optional = true } getrandom = { version = "=0.3.4", optional = true } -colored = { version = "3.0", optional = true } -pretty_env_logger = { version = "0.5", optional = true } +colored = { workspace = true, optional = true } +pretty_env_logger = { workspace = true, optional = true } sqlx = { workspace = true, optional = true } tower-http = { workspace = true, optional = true } mime-sniffer = { version = "0.1", optional = true } @@ -32,8 +32,8 @@ axum-extra = { workspace = true, optional = true } anyhow = { workspace = true, optional = true } bytes = { workspace = true, optional = true } fast_qr = { workspace = true, optional = true } +argon2 = { workspace = true, optional = true } log.workspace = true -api.workspace = true uuid.workspace = true chrono.workspace = true werewolves-macros.workspace = true @@ -53,6 +53,7 @@ hydrate = [ ssr = [ "dep:axum", "dep:tokio", + "dep:argon2", "dep:leptos_axum", "dep:colored", "dep:pretty_env_logger", @@ -70,7 +71,7 @@ ssr = [ "leptos_router/ssr", "leptos-use/ssr", "leptos-use/axum", - "api/ssr", + "werewolves-proto/ssr", ] # Defines a size-optimized profile for the WASM bundle in release mode diff --git a/werewolves/src/app/components/error.rs b/werewolves/src/app/components/error.rs index 7800c3a..95a634d 100644 --- a/werewolves/src/app/components/error.rs +++ b/werewolves/src/app/components/error.rs @@ -1,4 +1,3 @@ -use api::error::ServerError; use leptos::{html::Div, prelude::*}; use leptos_use::{ UseDraggableOptions, UseDraggableReturn, core::Position, use_draggable_with_options, diff --git a/werewolves/src/app/components/nav.rs b/werewolves/src/app/components/nav.rs index bc4dd40..afb7409 100644 --- a/werewolves/src/app/components/nav.rs +++ b/werewolves/src/app/components/nav.rs @@ -1,25 +1,31 @@ use core::ops::Not; -use api::{error::ServerError, game::GameId, token::TokenString}; use leptos::{ev::MouseEvent, prelude::*}; use leptos_router::hooks::use_url; use reactive_stores::Store; +use werewolves_proto::{error::ServerError, game::GameId, token::TokenString}; -use crate::app::{ - components::{DialogModal, LinkButton}, - storage::user::{AuthContext, AuthContextStoreFields}, +use crate::{ + app::{ + components::LinkButton, + storage::user::{AuthContext, AuthContextStoreFields}, + }, + db::AppState, }; #[server] pub async fn get_active_game(token: TokenString) -> Result, ServerError> { - let db = expect_context::().db; + let db = expect_context::().db; let user = db.user().check_token(&token).await?; - Ok(db.game().get_joined_active_game(user.id).await?) + let Some(hosted) = db.game().get_hosted_active_game(user.id).await? else { + return Ok(db.game().get_joined_active_game(user.id).await?); + }; + Ok(Some(hosted)) } #[server] pub async fn new_game(token: TokenString) -> Result { - let db = expect_context::().db; + let db = expect_context::().db; let user = db.user().check_token(&token).await?; Ok(db.game().new_game(user.id).await?.id) } diff --git a/werewolves/src/app/pages/game.rs b/werewolves/src/app/pages/game.rs index 91be4f6..6ae1bd6 100644 --- a/werewolves/src/app/pages/game.rs +++ b/werewolves/src/app/pages/game.rs @@ -2,10 +2,6 @@ pub mod big; mod host; mod player; -use api::{ - cbor_leptos::CborEncoding, - message::{IntoClientResponse, WrappedServerMessage}, -}; use codee::{HybridEncoder, binary::MsgpackSerdeCodec}; use leptos::prelude::*; use leptos_router::hooks; @@ -19,9 +15,13 @@ use werewolves_proto::message::{ ClientMessage, host::{HostMessage, ServerToHostMessage}, }; +use werewolves_proto::{ + cbor_leptos::CborEncoding, + message::{IntoClientResponse, WrappedServerMessage}, +}; use crate::{ - ConsoleLogError, LogError, + ConsoleLogError, app::{ components::DebugMarker, pages::game::{host::HostGamePage, player::PlayerGamePage}, @@ -32,158 +32,170 @@ use crate::{ #[component] pub fn GamePage() -> impl IntoView { - let params = hooks::use_params_map(); - let auth = expect_context::>(); - let state = expect_context::>(); - Effect::new(move || { - params.read().get("id").unwrap_or_default(); - }); - let UseWebSocketReturn { - ready_state, - message, - send, - open, - close, - .. - } = use_websocket_with_options::< - WrappedServerMessage, - IntoClientResponse, - MsgpackSerdeCodec, - _, - _, - >( - format!( + move || { + let params = hooks::use_params_map(); + let auth = expect_context::>(); + let state = expect_context::>(); + Effect::new(move || { + params.read().get("id").unwrap_or_default(); + }); + let url = RwSignal::new(format!( "/api/games/{}", - params.read_untracked().get("id").unwrap_or_default() - ) - .as_str(), - UseWebSocketOptions::default().reconnect_limit(ReconnectLimit::Infinite), - ); - let opened = RwSignal::new(false); - let host_message = RwSignal::new(None); - let player_message = RwSignal::new(None); - let host_reply: RwSignal> = RwSignal::new(None); - let player_reply: RwSignal> = RwSignal::new(None); - let disconnect = RwSignal::new(false); + params.read().get("id").unwrap_or_default() + )); + Effect::watch( + move || url.get(), + move |_, _, _| { + #[cfg(feature = "hydrate")] + gloo::utils::window().location().reload().console_log_warn() + }, + false, + ); + let UseWebSocketReturn { + ready_state, + message, + send, + close, + .. + } = use_websocket_with_options::< + WrappedServerMessage, + IntoClientResponse, + MsgpackSerdeCodec, + _, + _, + >( + #[cfg(not(feature = "ssr"))] + url.read().as_str(), + #[cfg(feature = "ssr")] + "", + UseWebSocketOptions::default().reconnect_limit(ReconnectLimit::Infinite), + ); + let opened = RwSignal::new(false); + let host_message = RwSignal::new(None); + let player_message = RwSignal::new(None); + let host_reply: RwSignal> = RwSignal::new(None); + let player_reply: RwSignal> = RwSignal::new(None); + let disconnect = RwSignal::new(false); - Effect::new({ - let close = close.clone(); - move || { - if disconnect.get() { - close(); - } - } - }); - - Effect::watch( - move || message.get(), - move |message, prev, _| { - if let Some(prev) = prev - && message == prev - { - return; - } - match message.clone() { - Some(IntoClientResponse::Host(host_msg)) => { - log::debug!("got host message: {:?}", host_msg.title()); - host_message.set(Some(host_msg)); + Effect::new({ + let close = close.clone(); + move || { + if disconnect.get() { + close(); } - Some(IntoClientResponse::Player(player_msg)) => { - log::debug!("got player message: {:?}", player_msg.title()); - player_message.set(Some(player_msg)); - } - None => {} } - }, - true, - ); - Effect::new({ - let send = send.clone(); - move || { - let Some(reply) = host_reply.get() else { - return; - }; - log::debug!("sending host message: {reply:?}"); - send(&WrappedServerMessage::HostMessage(reply.clone())); - host_reply.write_untracked().take(); - } - }); - Effect::new({ - let send = send.clone(); - move || { - let Some(reply) = player_reply.get() else { - return; - }; - log::debug!("sending client message: {reply:?}"); - send(&WrappedServerMessage::ClientMessage(reply.clone())); - player_reply.write_untracked().take(); - } - }); + }); - let status = move || match ready_state.get() { - ConnectionReadyState::Connecting => view! { -
- -

"connecting..."

-
- } - .into_any(), - ConnectionReadyState::Closing => view! { -
- -

"closing"

-
- } - .into_any(), - ConnectionReadyState::Open => { - Effect::new({ - let send = send.clone(); - let close = close.clone(); - move || { - let o = opened.get(); - if o { - return; + Effect::watch( + move || message.get(), + move |message, prev, _| { + if let Some(prev) = prev + && message == prev + { + return; + } + match message.clone() { + Some(IntoClientResponse::Host(host_msg)) => { + log::debug!("got host message: {:?}", host_msg.title()); + host_message.set(Some(host_msg)); } - if let Some(token) = auth.token().get() { - let auth_message = WrappedServerMessage::Authentication(token.token); - log::debug!("sending auth message: {auth_message:?}"); - send(&auth_message); - opened.set(true); - } else { - close(); - if let Ok(href) = gloo::utils::window().location().href() { - state - .redirect_after_signin() - .set(Some(RedirectAfterSignin::new(href))); + Some(IntoClientResponse::Player(player_msg)) => { + log::debug!("got player message: {:?}", player_msg.title()); + player_message.set(Some(player_msg)); + } + None => {} + } + }, + true, + ); + Effect::new({ + let send = send.clone(); + move || { + let Some(reply) = host_reply.get() else { + return; + }; + log::debug!("sending host message: {reply:?}"); + send(&WrappedServerMessage::HostMessage(reply.clone())); + host_reply.write_untracked().take(); + } + }); + Effect::new({ + let send = send.clone(); + move || { + let Some(reply) = player_reply.get() else { + return; + }; + log::debug!("sending client message: {reply:?}"); + send(&WrappedServerMessage::ClientMessage(reply.clone())); + player_reply.write_untracked().take(); + } + }); + + let status = move || match ready_state.get() { + ConnectionReadyState::Connecting => view! { +
+ +

"connecting..."

+
+ } + .into_any(), + ConnectionReadyState::Closing => view! { +
+ +

"closing"

+
+ } + .into_any(), + ConnectionReadyState::Open => { + Effect::new({ + let send = send.clone(); + let close = close.clone(); + move || { + let o = opened.get(); + if o { + return; + } + if let Some(token) = auth.token().get() { + let auth_message = WrappedServerMessage::Authentication(token.token); + log::debug!("sending auth message: {auth_message:?}"); + send(&auth_message); + opened.set(true); + } else { + close(); + if let Ok(href) = gloo::utils::window().location().href() { + state + .redirect_after_signin() + .set(Some(RedirectAfterSignin::new(href))); + } + #[cfg(feature = "hydrate")] + gloo::utils::window() + .location() + .set_href("/") + .console_log_err(); } - #[cfg(feature = "hydrate")] - gloo::utils::window() - .location() - .set_href("/") - .console_log_err(); } - } - }); - ().into_any() - } - ConnectionReadyState::Closed => view! { -
- -

"disconnected"

-
- } - .into_any(), - }; + }); + ().into_any() + } + ConnectionReadyState::Closed => view! { +
+ +

"disconnected"

+
+ } + .into_any(), + }; - let status = option_env!("LOCAL_DEBUG").map(|_| status); + let status = option_env!("LOCAL_DEBUG").map(|_| status); - view! { - {status} - - + view! { + {status} + + + } } } diff --git a/werewolves/src/app/pages/signin.rs b/werewolves/src/app/pages/signin.rs index 423aa4e..51cf0df 100644 --- a/werewolves/src/app/pages/signin.rs +++ b/werewolves/src/app/pages/signin.rs @@ -1,9 +1,9 @@ use core::ops::Not; -use api::user::{Password, Session, Username}; use chrono::Utc; use gloo::history::History; use leptos::{ev::MouseEvent, prelude::*}; +use werewolves_proto::player::{Password, Session, Username}; use crate::{ app::{ diff --git a/werewolves/src/app/pages/signup.rs b/werewolves/src/app/pages/signup.rs index 7a8ca2f..8410f01 100644 --- a/werewolves/src/app/pages/signup.rs +++ b/werewolves/src/app/pages/signup.rs @@ -1,10 +1,5 @@ use core::ops::Not; -use api::{ - cbor_leptos::CborPost, - error::ServerError, - user::{Password, Session, Username}, -}; use chrono::Utc; use gloo::history::History; use leptos::{ @@ -14,6 +9,11 @@ use leptos::{ use leptos_meta::*; use rand::distr::SampleString; use reactive_stores::Store; +use werewolves_proto::{ + cbor_leptos::CborPost, + error::ServerError, + player::{Password, PlayerId, Session, Username}, +}; use crate::{ app::{ @@ -24,6 +24,7 @@ use crate::{ }, }, auth::Signin, + db::AppState, }; #[leptos::server(CreateUser, input = CborPost)] @@ -31,8 +32,8 @@ pub async fn create_user( username: Username, password: Password, pronouns: Option, -) -> core::result::Result { - let user = use_context::() +) -> core::result::Result { + let user = use_context::() .expect("no app state") .db .user() diff --git a/werewolves/src/app/pages/user_settings/change_password.rs b/werewolves/src/app/pages/user_settings/change_password.rs index f1948c7..fb9b08b 100644 --- a/werewolves/src/app/pages/user_settings/change_password.rs +++ b/werewolves/src/app/pages/user_settings/change_password.rs @@ -1,10 +1,13 @@ -use api::{cbor_leptos::CborPost, error::ServerError, user::Password}; use leptos::{ev::MouseEvent, prelude::*}; use reactive_stores::Store; +use werewolves_proto::{cbor_leptos::CborPost, error::ServerError, player::Password}; -use crate::app::{ - components::{DialogModal, ErrorBox}, - storage::user::{AuthContext, AuthContextStoreFields}, +use crate::{ + app::{ + components::{DialogModal, ErrorBox}, + storage::user::{AuthContext, AuthContextStoreFields}, + }, + db::AppState, }; #[server(input = CborPost)] pub async fn change_password( @@ -12,10 +15,7 @@ pub async fn change_password( current: Password, new: Password, ) -> Result<(), ServerError> { - let db = use_context::() - .expect("no app state") - .db - .user(); + let db = use_context::().expect("no app state").db.user(); let user = db.check_token(token.as_str()).await?; db.verify_login(&user.username, ¤t).await?; db.change_password(user.clone(), new).await?; diff --git a/werewolves/src/app/pages/user_settings/update_profile.rs b/werewolves/src/app/pages/user_settings/update_profile.rs index 0a62dc4..98d655f 100644 --- a/werewolves/src/app/pages/user_settings/update_profile.rs +++ b/werewolves/src/app/pages/user_settings/update_profile.rs @@ -1,25 +1,22 @@ use core::ops::Not; -use api::{ - cbor_leptos::CborPost, - error::ServerError, - token::TokenString, - user::{ProfileUpdate, Session}, -}; use leptos::{ev::MouseEvent, prelude::*}; use reactive_stores::Store; +use werewolves_proto::{ + cbor_leptos::CborPost, error::ServerError, player::ProfileUpdate, token::TokenString, +}; -use crate::app::{ - components::{DialogModal, ErrorBox}, - storage::user::{AuthContext, AuthContextStoreFields}, +use crate::{ + app::{ + components::{DialogModal, ErrorBox}, + storage::user::{AuthContext, AuthContextStoreFields}, + }, + db::AppState, }; #[server(input = CborPost)] pub async fn update_profile(token: TokenString, update: ProfileUpdate) -> Result<(), ServerError> { - let db = use_context::() - .expect("no app state") - .db - .user(); + let db = use_context::().expect("no app state").db.user(); let user = db.check_token(&token).await?; db.update_profile(user.id, update).await?; Ok(()) diff --git a/werewolves/src/app/storage/user.rs b/werewolves/src/app/storage/user.rs index 2729ddc..a3ddbf9 100644 --- a/werewolves/src/app/storage/user.rs +++ b/werewolves/src/app/storage/user.rs @@ -1,8 +1,3 @@ -use api::{ - limited::FixedLenString, - token::TOKEN_LEN, - user::{Password, Session, Username}, -}; use chrono::{DateTime, Utc}; use leptos::{ prelude::{Action, ArcRwSignal, Get, GetUntracked, Set}, @@ -10,6 +5,11 @@ use leptos::{ }; use reactive_stores::{Field, Patch, Store}; use serde::{Deserialize, Serialize}; +use werewolves_proto::{ + limited::FixedLenString, + player::{Password, Session, Username}, + token::TOKEN_LEN, +}; use crate::{ app::storage::{LocalStorage, SessionStorage, Stored}, diff --git a/werewolves/src/auth.rs b/werewolves/src/auth.rs index 5cb2b29..8b63064 100644 --- a/werewolves/src/auth.rs +++ b/werewolves/src/auth.rs @@ -1,18 +1,20 @@ -use api::{ - cbor_leptos::CborPost, - error::ServerError, - limited::{ClampedString, FixedLenString}, - token::{TOKEN_LEN, Token, TokenString}, - user::{Password, Session, Username}, -}; use chrono::Utc; use leptos::prelude::*; use reactive_stores::Store; +use werewolves_proto::{ + cbor_leptos::CborPost, + error::ServerError, + limited::{ClampedString, FixedLenString}, + player::{Password, Session, Username}, + token::{TOKEN_LEN, Token, TokenString}, +}; use crate::app::storage::{ LocalStorage, user::{AuthContext, AuthContextStoreFields, PasswordlessUser, UserSession, UserToken}, }; +#[cfg(feature = "ssr")] +use crate::db::{AppState, user::GetUserBy}; pub async fn try_auto_signin(ctx: Store) -> Result<(), ServerError> { if let Some(sess) = ctx.session().read().as_ref() { @@ -82,10 +84,7 @@ pub async fn replace_token( #[leptos::server(Signin, input = CborPost)] pub async fn signin(username: Username, password: Password) -> Result { - let db = use_context::() - .expect("no app state") - .db - .user(); + let db = use_context::().expect("no app state").db.user(); let sess = db.login(&username, &password).await?; log::info!("user logged in: {}", sess.username); @@ -95,7 +94,7 @@ pub async fn signin(username: Username, password: Password) -> Result) -> Result<(), ServerError> { - use_context::() + use_context::() .expect("no app state") .db .user() @@ -106,10 +105,7 @@ pub async fn verify_token(token: FixedLenString) -> Result<(), Server #[server(input = CborPost)] pub async fn get_me(token: FixedLenString) -> Result { - let db = use_context::() - .expect("no app state") - .db - .user(); + let db = use_context::().expect("no app state").db.user(); db.check_token(&token).await?; let sess = db.get_session(&token).await?; @@ -118,13 +114,10 @@ pub async fn get_me(token: FixedLenString) -> Result) -> Result { - let db = use_context::() - .expect("no app state") - .db - .user(); + let db = use_context::().expect("no app state").db.user(); let token = db.replace_token(&token).await?; let user = db - .get_user(api::db::user::GetUserBy::Id(token.user_id)) + .get_user(GetUserBy::Id(token.user_id)) .await .map_err(Into::::into)?; Ok(Token { diff --git a/api/src/db/mod.rs b/werewolves/src/db.rs similarity index 65% rename from api/src/db/mod.rs rename to werewolves/src/db.rs index 6f8eef9..5016b1d 100644 --- a/api/src/db/mod.rs +++ b/werewolves/src/db.rs @@ -1,13 +1,15 @@ +#[cfg(feature = "ssr")] pub mod game; +#[cfg(feature = "ssr")] pub mod user; -use futures::{future::BoxFuture, stream::BoxStream}; -use sqlx::{Executor, Pool, Postgres, Transaction}; +use leptos::config::LeptosOptions; +#[cfg(feature = "ssr")] +use sqlx::{Pool, Postgres}; +use werewolves_proto::error::DatabaseError; -use crate::{ - db::{game::GameDatabase, user::UserDatabase}, - error::DatabaseError, -}; +#[cfg(feature = "ssr")] +use {game::GameDatabase, user::UserDatabase}; pub(crate) type DatabaseResult = core::result::Result; @@ -15,13 +17,15 @@ trait IntoDatabaseResult { fn into_db_result(self) -> DatabaseResult; } +#[cfg(feature = "ssr")] impl IntoDatabaseResult for Result { fn into_db_result(self) -> DatabaseResult { self.map_err(Into::::into) } } -impl IntoDatabaseResult for Result { +#[cfg(feature = "ssr")] +impl IntoDatabaseResult for Result { fn into_db_result(self) -> DatabaseResult { self.map_err(Into::::into) } @@ -29,9 +33,11 @@ impl IntoDatabaseResult for Result { #[derive(Debug, Clone)] pub struct Database { + #[cfg(feature = "ssr")] pool: Pool, } +#[cfg(feature = "ssr")] impl Database { pub const fn new(pool: Pool) -> Self { Self { pool } @@ -58,3 +64,10 @@ impl Database { log::info!("migrations done"); } } + +#[cfg_attr(feature = "ssr", derive(axum::extract::FromRef))] +#[derive(Debug, Clone)] +pub struct AppState { + pub db: Database, + pub leptos_options: LeptosOptions, +} diff --git a/api/src/db/game.rs b/werewolves/src/db/game.rs similarity index 84% rename from api/src/db/game.rs rename to werewolves/src/db/game.rs index 49dfc4c..2b047a8 100644 --- a/api/src/db/game.rs +++ b/werewolves/src/db/game.rs @@ -1,24 +1,18 @@ use core::num::NonZeroU8; use chrono::Utc; -use futures::executor; -use sqlx::{Pool, Postgres, query, query_as}; +use sqlx::{Pool, Postgres, query}; use werewolves_proto::{ + ServerResult, character::CharacterId, - error::GameError, - game::{Game, GameSettings}, - message::{CharacterIdentity, Identification, PublicIdentity, dead::DeadChatMessage}, - player::PlayerId, + error::{DatabaseError, GameError, ServerError}, + game::{Game, GameId, GameSettings}, + game_record::{GameRecord, GameRecordState}, + message::{Identification, PublicIdentity, dead::DeadChatMessage}, + player::{PlayerId, PlayerIdentity}, }; -use crate::{ - ServerResult, - db::{DatabaseResult, IntoDatabaseResult}, - error::{DatabaseError, ServerError}, - game::{GameId, GameRecord, GameRecordState}, - identity::PlayerIdentity, - user::UserId, -}; +use crate::db::{DatabaseResult, IntoDatabaseResult}; #[derive(Debug, Clone)] pub struct GameDatabase { @@ -30,14 +24,14 @@ impl GameDatabase { Self { pool } } - pub async fn new_game(&self, host: UserId) -> DatabaseResult { + pub async fn new_game(&self, host: PlayerId) -> DatabaseResult { let record = GameRecord { host, id: GameId::new(), created_at: Utc::now(), - game_state: crate::game::GameRecordState::Lobby(GameSettings::default()), + game_state: GameRecordState::Lobby(GameSettings::default()), }; - let game_state_json = serde_json::to_value(&record.game_state) + let game_state_json = leptos::serde_json::to_value(&record.game_state) .map_err(|err| DatabaseError::Serialization(err.to_string()))?; query!( r#" @@ -123,9 +117,9 @@ impl GameDatabase { .await?; Ok(GameRecord { id: GameId::from_uuid(r.id), - host: UserId::from_uuid(r.host), + host: PlayerId::from_uuid(r.host), created_at: r.created_at, - game_state: serde_json::from_value(r.game_state)?, + game_state: leptos::serde_json::from_value(r.game_state)?, }) } @@ -141,7 +135,7 @@ impl GameDatabase { where E: ::sqlx::Executor<'a, Database = Postgres>, { - let game_state_json = serde_json::to_value(&record.game_state) + let game_state_json = leptos::serde_json::to_value(&record.game_state) .map_err(|err| DatabaseError::Serialization(err.to_string()))?; let game_status = match &record.game_state { GameRecordState::Lobby(_) => "Lobby", @@ -171,7 +165,7 @@ impl GameDatabase { pub async fn get_player_number( &self, game: GameId, - player: UserId, + player: PlayerId, ) -> DatabaseResult> { Ok(query!( r#" @@ -194,7 +188,7 @@ impl GameDatabase { pub async fn set_player_number( &self, game: GameId, - player: UserId, + player: PlayerId, number: Option, ) -> DatabaseResult<()> { query!( @@ -215,7 +209,23 @@ impl GameDatabase { Ok(()) } - pub async fn get_joined_active_game(&self, player: UserId) -> DatabaseResult> { + pub async fn get_hosted_active_game(&self, player: PlayerId) -> DatabaseResult> { + Ok(query!( + r#" + select + id + from + games + where + host = $1 and game_status in ('Lobby', 'RoleReveal', 'Started')"#, + player.into_uuid(), + ) + .fetch_optional(&self.pool) + .await? + .map(|d| GameId::from_uuid(d.id))) + } + + pub async fn get_joined_active_game(&self, player: PlayerId) -> DatabaseResult> { Ok(query!( r#" select @@ -236,7 +246,7 @@ impl GameDatabase { pub async fn join_game( &self, game: GameId, - player: UserId, + player: PlayerId, number: Option, ) -> ServerResult<()> { let game = self.get_game(game).await?; @@ -246,9 +256,12 @@ impl GameDatabase { if !matches!(game.game_state, GameRecordState::Lobby(_)) { return Err(GameError::CannotJoinStartedGame.into()); } + if let Some(hosted) = self.get_hosted_active_game(player).await? { + return Err(ServerError::AlreadyInActiveGame(hosted)); + } if let Some(active) = self.get_joined_active_game(player).await? { return Err(ServerError::AlreadyInActiveGame(active)); - }; + } query!( r#" insert into @@ -267,7 +280,7 @@ impl GameDatabase { Ok(()) } - pub async fn leave_game(&self, game: GameId, player: UserId) -> ServerResult<()> { + pub async fn leave_game(&self, game: GameId, player: PlayerId) -> ServerResult<()> { query!( r#" delete from @@ -340,7 +353,7 @@ impl GameDatabase { game_id: GameId, message: DeadChatMessage, ) -> ServerResult<()> { - let content = serde_json::to_value(&message.message).into_db_result()?; + let content = leptos::serde_json::to_value(&message.message).into_db_result()?; query!( r#" insert into @@ -381,7 +394,7 @@ impl GameDatabase { Ok(DeadChatMessage { id: r.message_id, timestamp: r.created_at, - message: serde_json::from_value(r.message).into_db_result()?, + message: leptos::serde_json::from_value(r.message).into_db_result()?, }) }) .collect::, _>>()?) diff --git a/api/src/db/user.rs b/werewolves/src/db/user.rs similarity index 96% rename from api/src/db/user.rs rename to werewolves/src/db/user.rs index 72a8685..722c291 100644 --- a/api/src/db/user.rs +++ b/werewolves/src/db/user.rs @@ -5,18 +5,19 @@ use argon2::{ use chrono::{DateTime, TimeDelta, Utc}; use rand::distr::SampleString; -use serde::{Deserialize, Serialize}; use sqlx::{Decode, Encode, Pool, Postgres, prelude::FromRow, query, query_as}; use uuid::Uuid; +use werewolves_proto::player::PlayerId; -use crate::{ - db::DatabaseResult, +use werewolves_proto::{ error::{DatabaseError, ServerError}, limited::FixedLenString, + player::{Password, ProfileUpdate, Session}, token::{self, TOKEN_LEN}, - user::{Password, ProfileUpdate, Session, UserId}, }; +use crate::db::DatabaseResult; + #[derive(Debug, Clone)] pub struct UserDatabase { pub(super) pool: Pool, @@ -25,7 +26,7 @@ pub struct UserDatabase { #[derive(Debug, Clone, FromRow)] pub struct LoginToken { pub token: String, - pub user_id: UserId, + pub user_id: PlayerId, pub created_at: DateTime, pub expires_at: DateTime, @@ -34,7 +35,7 @@ pub struct LoginToken { impl LoginToken { pub const TOKEN_LONGEVITY: TimeDelta = TimeDelta::days(30); - pub fn new(user_id: UserId) -> Self { + pub fn new(user_id: PlayerId) -> Self { let created_at = Utc::now(); let expires_at = created_at .checked_add_signed(Self::TOKEN_LONGEVITY) @@ -58,7 +59,7 @@ impl LoginToken { pub enum GetUserBy<'a> { Username(&'a str), - Id(UserId), + Id(PlayerId), } impl UserDatabase { @@ -99,7 +100,7 @@ impl UserDatabase { created_at, updated_at, password_hash, - id: UserId::from_uuid(id), + id: PlayerId::from_uuid(id), display_name: Some(display_name.to_string()), }) } @@ -120,7 +121,7 @@ impl UserDatabase { let user = User { pronouns, - id: UserId::new(), + id: PlayerId::new(), username: username.into(), password_hash, display_name: None, @@ -173,7 +174,7 @@ impl UserDatabase { pub async fn update_profile( &self, - user_id: UserId, + user_id: PlayerId, update: ProfileUpdate, ) -> DatabaseResult<()> { query!( @@ -421,7 +422,7 @@ impl UserDatabase { #[derive(Debug, Clone, FromRow, Encode, Decode)] pub struct User { - pub id: UserId, + pub id: PlayerId, pub username: String, pub password_hash: String, pub display_name: Option, diff --git a/werewolves/src/lib.rs b/werewolves/src/lib.rs index 3e01d4a..92cb8f4 100644 --- a/werewolves/src/lib.rs +++ b/werewolves/src/lib.rs @@ -3,6 +3,7 @@ use core::{net::SocketAddr, str::FromStr}; pub mod app; pub mod auth; +pub mod db; #[cfg(feature = "ssr")] pub mod server; pub mod state; @@ -32,64 +33,6 @@ pub fn hydrate() { leptos::mount::hydrate_body(App); } -pub trait LogError { - fn log(self, loc: CodePath, level: log::Level); - fn log_warn(self, loc: CodePath); - fn log_err(self, loc: CodePath); - fn log_debug(self, loc: CodePath); -} - -pub struct CodePath { - pub module_path: &'static str, - pub loc: &'static std::panic::Location<'static>, -} - -#[macro_export] -macro_rules! loc { - () => { - (crate::CodePath { - module_path: log::__private_api::module_path!(), - loc: log::__private_api::loc(), - }) - }; -} - -impl LogError for Result -where - E: core::fmt::Display, -{ - fn log(self, loc: CodePath, lvl: log::Level) { - if let Err(err) = self { - if lvl <= log::STATIC_MAX_LEVEL && lvl <= log::max_level() { - log::__private_api::log( - log::__log_logger!(__log_global_logger), - log::__private_api::format_args!("{err}"), - lvl, - &(loc.module_path, loc.module_path, loc.loc), - (), - ); - } - } - } - - fn log_warn(self, loc: CodePath) { - if self.is_err() { - self.log(loc, log::Level::Warn); - } - } - - fn log_err(self, loc: CodePath) { - if self.is_err() { - self.log(loc, log::Level::Error); - } - } - - fn log_debug(self, loc: CodePath) { - if self.is_err() { - self.log(loc, log::Level::Debug); - } - } -} pub trait ConsoleLogError { fn console_log_warn(self); fn console_log_err(self); diff --git a/werewolves/src/main.rs b/werewolves/src/main.rs index dc27233..d416897 100644 --- a/werewolves/src/main.rs +++ b/werewolves/src/main.rs @@ -1,4 +1,5 @@ #![allow(clippy::expect_fun_call)] + #[cfg(feature = "ssr")] mod ssr { pub const DEFAULT_MAX_PG_CONNECTIONS: u32 = 30; @@ -6,7 +7,6 @@ mod ssr { use core::pin::Pin; - use api::state::AppState; use axum::{ body::Body, extract::{FromRef, State}, @@ -22,6 +22,7 @@ mod ssr { tachys::ssr::StreamBuilder, }; use leptos_axum::{LeptosRoutes, ResponseOptions}; + use werewolves::db::AppState; pub async fn server_fn_handler( State(state): State, @@ -113,7 +114,6 @@ mod ssr { #[cfg(feature = "ssr")] #[tokio::main] async fn main() { - use api::{db::Database, state::AppState}; use axum::ServiceExt; use axum::routing::get; use axum::{Router, routing::any}; @@ -125,6 +125,7 @@ async fn main() { use sqlx::postgres::PgPoolOptions; use std::io::Write; use werewolves::app::*; + use werewolves::db::{AppState, Database}; use colored::Colorize; pretty_env_logger::formatted_builder() diff --git a/werewolves/src/server/game.rs b/werewolves/src/server/game.rs index 3fea516..16318fb 100644 --- a/werewolves/src/server/game.rs +++ b/werewolves/src/server/game.rs @@ -14,7 +14,7 @@ // along with this program. If not, see . use crate::server::runner::{ClientUpdate, HostOrClientMessage, IdentifiedClientMessage}; -use api::game::GameId; +use werewolves_proto::game::GameId; use chrono::Utc; use werewolves_proto::{ error::GameError, diff --git a/werewolves/src/server/game_connection.rs b/werewolves/src/server/game_connection.rs index 0bc0c51..511eeee 100644 --- a/werewolves/src/server/game_connection.rs +++ b/werewolves/src/server/game_connection.rs @@ -15,10 +15,6 @@ use core::{net::SocketAddr, str::FromStr}; use anyhow::anyhow; -use api::{ - error::ServerError, game::GameId, message::WrappedServerMessage, state::AppState, - token::TokenString, -}; use axum::{ extract::{ConnectInfo, Path, State, WebSocketUpgrade, ws::WebSocket}, response::Response, @@ -32,8 +28,11 @@ use colored::Colorize; use futures::FutureExt; use uuid::Uuid; use werewolves_proto::{error::GameError, message::ServerToClientMessage}; +use werewolves_proto::{ + error::ServerError, game::GameId, message::WrappedServerMessage, token::TokenString, +}; -use crate::{LogError, server::XForwardedFor}; +use crate::{db::AppState, server::XForwardedFor}; pub async fn handler( ws: WebSocketUpgrade, diff --git a/werewolves/src/server/game_end.rs b/werewolves/src/server/game_end.rs index fa71ca3..958950c 100644 --- a/werewolves/src/server/game_end.rs +++ b/werewolves/src/server/game_end.rs @@ -1,4 +1,4 @@ -use api::game::GameId; +use werewolves_proto::game::GameId; use werewolves_proto::{ game::story::GameStory, message::{ diff --git a/werewolves/src/server/host.rs b/werewolves/src/server/host.rs index 8630df8..a43087f 100644 --- a/werewolves/src/server/host.rs +++ b/werewolves/src/server/host.rs @@ -14,19 +14,20 @@ // along with this program. If not, see . use anyhow::anyhow; -use api::{ - db::{Database, user::User}, - error::ServerError, - game::GameRecord, - message::{IntoClientResponse, WrappedServerMessage}, -}; use axum::extract::ws::{self, Message, WebSocket}; use codee::{HybridDecoder, HybridEncoder}; use colored::Colorize; use tokio::sync::{broadcast, mpsc::UnboundedSender}; +use werewolves_proto::game_record::GameRecord; use werewolves_proto::message::host::{HostMessage, ServerToHostMessage}; +use werewolves_proto::{LogError, loc}; +use werewolves_proto::{ + error::ServerError, + message::{IntoClientResponse, WrappedServerMessage}, +}; -use crate::{LogError, loc}; +use crate::db::Database; +use crate::db::user::User; pub async fn host_handler( mut ws: WebSocket, diff --git a/werewolves/src/server/lobby.rs b/werewolves/src/server/lobby.rs index fbbc95b..55b4dc5 100644 --- a/werewolves/src/server/lobby.rs +++ b/werewolves/src/server/lobby.rs @@ -18,16 +18,14 @@ use core::{ }; use std::collections::HashSet; -use api::{ - db::Database, - error::ServerError, - game::{GameId, GameRecord}, -}; use colored::Colorize; use tokio::sync::broadcast::Sender; use werewolves_proto::{ + LogError, error::GameError, game::GameSettings, + game_record::GameRecord, + loc, message::{ ClientMessage, Identification, PlayerState, PublicIdentity, ServerToClientMessage, UpdateSelf, @@ -35,9 +33,10 @@ use werewolves_proto::{ }, player::PlayerId, }; +use werewolves_proto::{error::ServerError, game::GameId}; use crate::{ - LogError, loc, + db::Database, server::runner::{ClientUpdate, HostOrClientMessage, IdentifiedClientMessage}, }; @@ -150,6 +149,21 @@ impl<'a> Lobby<'a> { .await; None } + Err(( + HostOrClientMessage::Client(IdentifiedClientMessage { + identity: Identification { player_id, .. }, + .. + }), + ServerError::AlreadyInActiveGame(active_game), + )) => { + super::send_player( + self.game_id, + player_id, + ServerToClientMessage::Error(GameError::AlreadyInAnotherGame(active_game)), + ) + .await; + None + } Err(( HostOrClientMessage::Client(IdentifiedClientMessage { identity: Identification { player_id, public }, @@ -285,9 +299,7 @@ impl<'a> Lobby<'a> { .game() .set_player_number(self.game_id, player_id.into(), Some(number)) .await?; - self.send_lobby_info_to_clients() - .await - .log_debug(crate::loc!()); + self.send_lobby_info_to_clients().await.log_debug(loc!()); self.send_lobby_info_to_host().await.log_warn(loc!()); } HostOrClientMessage::Client(IdentifiedClientMessage { diff --git a/werewolves/src/server/mod.rs b/werewolves/src/server/mod.rs index f5c0b70..186adde 100644 --- a/werewolves/src/server/mod.rs +++ b/werewolves/src/server/mod.rs @@ -19,7 +19,6 @@ use std::{ sync::{Arc, LazyLock}, }; -use api::{db::Database, game::GameId}; use axum::http::header; use axum_extra::headers; use colored::Colorize; @@ -33,6 +32,9 @@ use tokio::{ time::Instant, }; use werewolves_proto::{ + LogError, + game::GameId, + loc, message::{ ServerToClientMessage, host::{HostMessage, ServerToHostMessage}, @@ -40,7 +42,7 @@ use werewolves_proto::{ player::PlayerId, }; -use crate::{loc, server::runner::IdentifiedClientMessage}; +use crate::{db::Database, server::runner::IdentifiedClientMessage}; pub struct XForwardedFor(String); @@ -300,8 +302,10 @@ where } } -async fn send_error(err: api::error::ServerError, socket: &mut axum::extract::ws::WebSocket) { - use crate::LogError; +async fn send_error( + err: werewolves_proto::error::ServerError, + socket: &mut axum::extract::ws::WebSocket, +) { use codee::HybridEncoder; socket .send(axum::extract::ws::Message::Binary( diff --git a/werewolves/src/server/player.rs b/werewolves/src/server/player.rs index 0965b89..e42d70a 100644 --- a/werewolves/src/server/player.rs +++ b/werewolves/src/server/player.rs @@ -16,13 +16,6 @@ use core::{net::SocketAddr, time::Duration}; use std::sync::Arc; use anyhow::anyhow; -use api::{ - db::{Database, user::User}, - error::ServerError, - game::{GameId, GameRecord}, - message::{IntoClientResponse, WrappedServerMessage}, - state::AppState, -}; use axum::{ extract::{ ConnectInfo, Path, State, WebSocketUpgrade, @@ -38,12 +31,20 @@ use chrono::Utc; use codee::{HybridDecoder, HybridEncoder}; use colored::Colorize; use tokio::sync::{broadcast::Receiver, mpsc::UnboundedSender}; -use werewolves_proto::message::{ - ClientMessage, Identification, PublicIdentity, ServerToClientMessage, UpdateSelf, +use werewolves_proto::{ + LogError, + error::ServerError, + game::GameId, + loc, + message::{IntoClientResponse, WrappedServerMessage}, +}; +use werewolves_proto::{ + game_record::GameRecord, + message::{ClientMessage, Identification, PublicIdentity, ServerToClientMessage, UpdateSelf}, }; use crate::{ - LogError, loc, + db::{Database, user::User}, server::{ XForwardedFor, runner::{ClientUpdate, HostOrClientMessage, IdentifiedClientMessage}, @@ -100,42 +101,6 @@ pub async fn player_handler( } } -// pub async fn player_handler( -// ws: WebSocketUpgrade, -// who: String, -// db: Database, -// game: GameRecord, -// user: User, -// ) -> Result { -// // finalize the upgrade process by returning upgrade callback. -// // we can customize the callback by sending additional info such as address. -// Ok(ws.on_upgrade(move |mut socket| async move { -// // log::debug!("connected {who} as {ident}"); - -// // state -// // .send -// // .send(IdentifiedClientMessage { -// // identity: ident.clone(), -// // update: ClientUpdate::ConnectStateUpdate, -// // }) -// // .log_debug(loc!()); -// let (send, recv) = super::new_player_connection(game.id, user.id.into(), db).await; - -// Client::new(ident.clone(), socket, who.to_string(), send, recv) -// .run() -// .await; - -// // player_list.disconnect(&connection_id).await; -// // state -// // .send -// // .send(IdentifiedClientMessage { -// // identity: ident.clone(), -// // update: ClientUpdate::ConnectStateUpdate, -// // }) -// // .log_debug(loc!()); -// })) -// } - async fn get_identification( socket: &mut WebSocket, who: &str, @@ -208,8 +173,6 @@ impl Client { } } async fn on_recv(&mut self, msg: Result) -> Result<(), anyhow::Error> { - use crate::LogError; - let msg = match msg { Ok(msg) => msg, Err(err) => return Err(err.into()), diff --git a/werewolves/src/server/role_reveal.rs b/werewolves/src/server/role_reveal.rs index 26e51dc..ef59797 100644 --- a/werewolves/src/server/role_reveal.rs +++ b/werewolves/src/server/role_reveal.rs @@ -1,6 +1,6 @@ use core::ops::Not; -use api::game::GameId; +use werewolves_proto::game::GameId; use werewolves_proto::{ game::Game, message::{ diff --git a/werewolves/src/server/runner.rs b/werewolves/src/server/runner.rs index 887b3f4..54d4b83 100644 --- a/werewolves/src/server/runner.rs +++ b/werewolves/src/server/runner.rs @@ -15,18 +15,14 @@ use core::num::NonZeroU8; -use api::{ - db::Database, - game::{GameId, GameRecord, GameRecordState}, - user::ProfileUpdate, -}; use tokio::sync::mpsc::UnboundedReceiver; +use werewolves_proto::game::GameId; +use werewolves_proto::game_record::{GameRecord, GameRecordState}; use werewolves_proto::message::{ClientMessage, Identification, host::HostMessage}; +use werewolves_proto::{LogError, loc}; -use crate::{ - LogError, loc, - server::{game::GameRunner, game_end::GameEnd, lobby::Lobby, role_reveal::RoleReveal}, -}; +use crate::db::Database; +use crate::server::{game::GameRunner, game_end::GameEnd, lobby::Lobby, role_reveal::RoleReveal}; #[derive(Debug, Clone, PartialEq)] pub enum ClientUpdate {