diff --git a/.dev/.env b/.dev/.env new file mode 100644 index 0000000..a9ee94e --- /dev/null +++ b/.dev/.env @@ -0,0 +1,4 @@ +POSTGRES_DB=elo_app +POSTGRES_USER=elo +POSTGRES_PASSWORD=elo +DATABASE_URL=postgres://elo:elo@db:5432/elo_app diff --git a/.dev/docker-compose.yml b/.dev/docker-compose.yml new file mode 100644 index 0000000..0d98468 --- /dev/null +++ b/.dev/docker-compose.yml @@ -0,0 +1,18 @@ +services: + db: + image: postgres:15.3-alpine + restart: unless-stopped + ports: + - 5432:5432 + networks: + - db + volumes: + - postgres-data:/var/lib/postgresql/data + env_file: + - .env + +volumes: + postgres-data: + +networks: + db: diff --git a/Cargo.toml b/Cargo.toml index c49c59c..8845d5a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,7 +18,9 @@ log = "0.4.20" once_cell = "1.18.0" web-sys = "0.3.64" cfg-if = "1.0.0" -chrono = { version = "0.4.31", features = ["serde"] } +chrono = { version = "0.4.38", features = ["serde", "wasm-bindgen"] } +axum-login = "0.15.3" +password-auth = "1.0.0" [target.'cfg(engine)'.dev-dependencies] fantoccini = "0.19" @@ -28,6 +30,13 @@ tokio = { version = "1", features = ["macros", "rt", "rt-multi-thread"] } perseus-axum = { version = "0.4.2" } axum = "0.6" tower-http = { version = "0.3", features = ["fs"] } +futures = "0.3.28" +sea-orm = { version = "0.12.0", features = [ + "sqlx-postgres", + "runtime-tokio-native-tls", + "macros", + "with-chrono", +] } [target.'cfg(client)'.dependencies] wasm-bindgen = "0.2" diff --git a/README.md b/README.md index 4266666..957433f 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # Installing requirements ## 1. Install rust: -### Windows: +### Windows: Download installer from https://www.rust-lang.org/tools/install @@ -11,7 +11,7 @@ Run `curl --proto '=https' --tlsv1.3 https://sh.rustup.rs -sSf | sh` ## 2. Install npm -### Windows: +### Windows: https://nodejs.org/en @@ -25,11 +25,17 @@ https://nodejs.org/en `cargo install perseus-cli` `rustup target add wasm32-unknown-unknown` -## 4. Install tailwindcss, for styling +## 4. Install docker for Postgresql + +## 5. Install SeaORM for database + +`cargo install sea-orm-cli` + +## 5. Install tailwindcss, for styling `npm install -D tailwindcss` -Also take a look at +Also take a look at Website: https://framesurge.sh/perseus/en-US/ @@ -39,6 +45,12 @@ https://blog.logrocket.com/building-rust-app-perseus/ # Building the project +To set up the database, run: +`$env:DATABASE_URL = "postgres://elo:elo@localhost:5432/elo_app"; sea-orm-cli migrate up` + +Updating entities after updating database: +`$env:DATABASE_URL = "postgres://elo:elo@localhost:5432/elo_app"; sea-orm-cli generate entity -o entity/src --with-serde both` + To build CSS run: `npm run build` diff --git a/entity/src/game.rs b/entity/src/game.rs new file mode 100644 index 0000000..a739bc7 --- /dev/null +++ b/entity/src/game.rs @@ -0,0 +1,30 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 1.0.0 + +use super::sea_orm_active_enums::GameType; +use super::sea_orm_active_enums::PlayerSetupType; +use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] +#[sea_orm(table_name = "game")] +pub struct Model { + #[sea_orm(primary_key)] + pub id: i32, + pub player_setup_type: Option, + pub game_type: Option, + pub time: DateTimeWithTimeZone, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm(has_many = "super::one_vs_one::Entity")] + OneVsOne, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::OneVsOne.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/entity/src/mod.rs b/entity/src/mod.rs new file mode 100644 index 0000000..f22ac25 --- /dev/null +++ b/entity/src/mod.rs @@ -0,0 +1,8 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 1.0.0 + +pub mod prelude; + +pub mod game; +pub mod one_vs_one; +pub mod sea_orm_active_enums; +pub mod user; diff --git a/entity/src/one_vs_one.rs b/entity/src/one_vs_one.rs new file mode 100644 index 0000000..550cda4 --- /dev/null +++ b/entity/src/one_vs_one.rs @@ -0,0 +1,50 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 1.0.0 + +use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] +#[sea_orm(table_name = "one_vs_one")] +pub struct Model { + #[sea_orm(primary_key)] + pub id: i32, + pub player_one: i32, + pub player_two: i32, + pub game_id: i32, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm( + belongs_to = "super::game::Entity", + from = "Column::GameId", + to = "super::game::Column::Id", + on_update = "NoAction", + on_delete = "NoAction" + )] + Game, + #[sea_orm( + belongs_to = "super::user::Entity", + from = "Column::PlayerOne", + to = "super::user::Column::Id", + on_update = "NoAction", + on_delete = "NoAction" + )] + User2, + #[sea_orm( + belongs_to = "super::user::Entity", + from = "Column::PlayerTwo", + to = "super::user::Column::Id", + on_update = "NoAction", + on_delete = "NoAction" + )] + User1, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Game.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/entity/src/prelude.rs b/entity/src/prelude.rs new file mode 100644 index 0000000..f412b60 --- /dev/null +++ b/entity/src/prelude.rs @@ -0,0 +1,5 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 1.0.0 + +pub use super::game::Entity as Game; +pub use super::one_vs_one::Entity as OneVsOne; +pub use super::user::Entity as User; diff --git a/entity/src/sea_orm_active_enums.rs b/entity/src/sea_orm_active_enums.rs new file mode 100644 index 0000000..631fc29 --- /dev/null +++ b/entity/src/sea_orm_active_enums.rs @@ -0,0 +1,23 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 1.0.0 + +use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, PartialEq, Eq, EnumIter, DeriveActiveEnum, Serialize, Deserialize)] +#[sea_orm(rs_type = "String", db_type = "Enum", enum_name = "game_type")] +pub enum GameType { + #[sea_orm(string_value = "PickleBall")] + PickleBall, + #[sea_orm(string_value = "Pool")] + Pool, + #[sea_orm(string_value = "TableTennis")] + TableTennis, +} +#[derive(Debug, Clone, PartialEq, Eq, EnumIter, DeriveActiveEnum, Serialize, Deserialize)] +#[sea_orm(rs_type = "String", db_type = "Enum", enum_name = "player_setup_type")] +pub enum PlayerSetupType { + #[sea_orm(string_value = "OneVsOne")] + OneVsOne, + #[sea_orm(string_value = "TwoVsTwo")] + TwoVsTwo, +} diff --git a/entity/src/user.rs b/entity/src/user.rs new file mode 100644 index 0000000..aa72e13 --- /dev/null +++ b/entity/src/user.rs @@ -0,0 +1,18 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 1.0.0 + +use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] +#[sea_orm(table_name = "user")] +pub struct Model { + #[sea_orm(primary_key)] + pub id: i32, + pub username: String, + pub password: String, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation {} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/migration/Cargo.toml b/migration/Cargo.toml new file mode 100644 index 0000000..780de08 --- /dev/null +++ b/migration/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "migration" +version = "0.1.0" +edition = "2021" +publish = false + +[lib] +name = "migration" +path = "src/lib.rs" + +[dependencies] +async-std = { version = "1", features = ["attributes", "tokio1"] } + +[dependencies.sea-orm-migration] +version = "1.0.0" +features = ["sqlx-postgres", "runtime-tokio-native-tls"] diff --git a/migration/README.md b/migration/README.md new file mode 100644 index 0000000..7a7bc0a --- /dev/null +++ b/migration/README.md @@ -0,0 +1,46 @@ +# References + +https://www.sea-ql.org/sea-orm-tutorial/ch01-02-migration-cli.html +https://www.sea-ql.org/SeaORM/docs/migration/writing-migration/ + +# Running Migrator CLI + +- Generate a new migration file + ```sh + cargo run -- generate MIGRATION_NAME + ``` +- Apply all pending migrations + ```sh + cargo run + ``` + ```sh + cargo run -- up + ``` +- Apply first 10 pending migrations + ```sh + cargo run -- up -n 10 + ``` +- Rollback last applied migrations + ```sh + cargo run -- down + ``` +- Rollback last 10 applied migrations + ```sh + cargo run -- down -n 10 + ``` +- Drop all tables from the database, then reapply all migrations + ```sh + cargo run -- fresh + ``` +- Rollback all applied migrations, then reapply all migrations + ```sh + cargo run -- refresh + ``` +- Rollback all applied migrations + ```sh + cargo run -- reset + ``` +- Check the status of all migrations + ```sh + cargo run -- status + ``` diff --git a/migration/src/lib.rs b/migration/src/lib.rs new file mode 100644 index 0000000..03c62c5 --- /dev/null +++ b/migration/src/lib.rs @@ -0,0 +1,16 @@ +pub use sea_orm_migration::prelude::*; + +mod m20240813_000001_create_users; +mod m20240813_000002_create_game; + +pub struct Migrator; + +#[async_trait::async_trait] +impl MigratorTrait for Migrator { + fn migrations() -> Vec> { + vec![ + Box::new(m20240813_000001_create_users::Migration), + Box::new(m20240813_000002_create_game::Migration), + ] + } +} diff --git a/migration/src/m20240813_000001_create_users.rs b/migration/src/m20240813_000001_create_users.rs new file mode 100644 index 0000000..60a3819 --- /dev/null +++ b/migration/src/m20240813_000001_create_users.rs @@ -0,0 +1,42 @@ +use sea_orm_migration::{prelude::*, schema::*}; + +#[derive(DeriveMigrationName)] +pub struct Migration; + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .create_table( + Table::create() + .table(User::Table) + .if_not_exists() + .col( + ColumnDef::new(User::Id) + .integer() + .not_null() + .auto_increment() + .primary_key(), + ) + .col(string(User::Username)) + .col(string(User::Password)) + .to_owned(), + ) + .await + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .drop_table(Table::drop().table(User::Table).to_owned()) + .await + } +} + +#[derive(DeriveIden)] +pub enum User { + Table, + Id, + Username, + // Hash + Password, +} diff --git a/migration/src/m20240813_000002_create_game.rs b/migration/src/m20240813_000002_create_game.rs new file mode 100644 index 0000000..0dc83ee --- /dev/null +++ b/migration/src/m20240813_000002_create_game.rs @@ -0,0 +1,152 @@ +use sea_orm::{DbBackend, DeriveActiveEnum, EnumIter, Schema}; +use sea_orm_migration::sea_orm::ActiveEnum; +use sea_orm_migration::{prelude::*, schema::*, sea_query::extension::postgres::Type}; + +use super::m20240813_000001_create_users::User; + +#[derive(DeriveMigrationName)] +pub struct Migration; + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + let schema = Schema::new(DbBackend::Postgres); + + manager + .create_type(schema.create_enum_from_active_enum::()) + .await?; + manager + .create_type(schema.create_enum_from_active_enum::()) + .await?; + manager + .create_table( + Table::create() + .table(Game::Table) + .if_not_exists() + .col( + ColumnDef::new(Game::Id) + .integer() + .not_null() + .auto_increment() + .primary_key(), + ) + .col(ColumnDef::new(Game::PlayerSetupType).custom(PlayerSetupType::name())) + .col(ColumnDef::new(Game::GameType).custom(GameType::name())) + .col( + ColumnDef::new(Game::Time) + .timestamp_with_time_zone() + .not_null(), + ) + .to_owned(), + ) + .await?; + manager + .create_table( + Table::create() + .table(Game::Table) + .if_not_exists() + .col( + ColumnDef::new(Game::Id) + .integer() + .not_null() + .auto_increment() + .primary_key(), + ) + .col(ColumnDef::new(Game::PlayerSetupType).custom(PlayerSetupType::name())) + .col(ColumnDef::new(Game::GameType).custom(GameType::name())) + .to_owned(), + ) + .await?; + manager + .create_table( + Table::create() + .table(OneVsOne::Table) + .if_not_exists() + .col( + ColumnDef::new(OneVsOne::Id) + .integer() + .not_null() + .auto_increment() + .primary_key(), + ) + .col(ColumnDef::new(OneVsOne::PlayerOne).integer().not_null()) + .col(ColumnDef::new(OneVsOne::PlayerTwo).integer().not_null()) + .col(ColumnDef::new(OneVsOne::GameId).integer().not_null()) + .foreign_key( + ForeignKey::create() + .name("fk-user-one_vs_one-player_one_id") + .from(OneVsOne::Table, OneVsOne::PlayerOne) + .to(User::Table, User::Id), + ) + .foreign_key( + ForeignKey::create() + .name("fk-user-one_vs_one-player_two_id") + .from(OneVsOne::Table, OneVsOne::PlayerTwo) + .to(User::Table, User::Id), + ) + .foreign_key( + ForeignKey::create() + .name("fk-game-one_vs_one-game_id") + .from(OneVsOne::Table, OneVsOne::GameId) + .to(Game::Table, Game::Id), + ) + .to_owned(), + ) + .await + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .drop_type(Type::drop().name(PlayerSetupType::name()).to_owned()) + .await?; + manager + .drop_type(Type::drop().name(GameType::name()).to_owned()) + .await?; + manager + .drop_table(Table::drop().table(Game::Table).to_owned()) + .await?; + manager + .drop_table(Table::drop().table(OneVsOne::Table).to_owned()) + .await + } +} + +// Enums +#[derive(EnumIter, DeriveActiveEnum)] +#[sea_orm(rs_type = "String", db_type = "Enum", enum_name = "player_setup_type")] +enum PlayerSetupType { + #[sea_orm(string_value = "OneVsOne")] + OneVsOne, + #[sea_orm(string_value = "TwoVsTwo")] + TwoVsTwo, +} + +#[derive(EnumIter, DeriveActiveEnum)] +#[sea_orm(rs_type = "String", db_type = "Enum", enum_name = "game_type")] +enum GameType { + #[sea_orm(string_value = "TableTennis")] + TableTennis, + #[sea_orm(string_value = "Pool")] + Pool, + #[sea_orm(string_value = "PickleBall")] + PickleBall, +} + +// Tables +#[derive(DeriveIden)] +enum Game { + Table, + Id, + Time, + PlayerSetupType, + GameType, +} + +#[derive(DeriveIden)] +enum OneVsOne { + Table, + Id, + GameId, + PlayerOne, + PlayerTwo, +} diff --git a/migration/src/main.rs b/migration/src/main.rs new file mode 100644 index 0000000..c6b6e48 --- /dev/null +++ b/migration/src/main.rs @@ -0,0 +1,6 @@ +use sea_orm_migration::prelude::*; + +#[async_std::main] +async fn main() { + cli::run_cli(migration::Migrator).await; +} diff --git a/package.json b/package.json index 66afa07..adef649 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,6 @@ "author": "", "license": "ISC", "devDependencies": { - "tailwindcss": "^3.3.3" + "tailwindcss": "^3.4.9" } } diff --git a/src/data/user.rs b/src/data/user.rs index 9815690..0124a63 100644 --- a/src/data/user.rs +++ b/src/data/user.rs @@ -1,3 +1,38 @@ +use axum_login::{AuthUser, AuthnBackend, UserId}; +use serde::{Deserialize, Serialize}; pub type PlayerId = u32; +// References +// https://github.com/maxcountryman/axum-login/tree/main/examples/sqlite/src +// https://framesurge.sh/perseus/en-US/docs/0.4.x/state/intro + +#[derive(Clone, Serialize, Deserialize)] +pub struct User { + id: u64, + pub username: String, + password: String, +} + +// Override debug to prevent logging password hash +impl std::fmt::Debug for User { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("User") + .field("id", &self.id) + .field("username", &self.username) + .field("password", &"[hidden]") + .finish() + } +} + +impl AuthUser for User { + type Id = u64; + + fn id(&self) -> Self::Id { + self.id + } + + fn session_auth_hash(&self) -> &[u8] { + self.password.as_bytes() + } +} diff --git a/src/main.rs b/src/main.rs index a5c1e32..b7289a1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -18,6 +18,8 @@ cfg_if::cfg_if! { stores::MutableStore, turbine::Turbine, }; + use futures::executor::block_on; + use sea_orm::{Database}; use crate::server::routes::register_routes; } } @@ -35,6 +37,13 @@ pub async fn dflt_server Update to use environment variable + if let Err(err) = block_on(Database::connect( + "postgres://elo:elo@localhost:5432/elo_app", + )) { + panic!("{}", err); + } + axum::Server::bind(&addr) .serve(app.into_make_service()) .await