diff --git a/.env.example b/.env.example index da91d0a..90e281c 100644 --- a/.env.example +++ b/.env.example @@ -1,2 +1,3 @@ DATABASE_URL= // the url to the database ( example local postgres: postgres://postgres:postgres@localhost/findthetime ) -EVENT_UID_SIZE= // size of snowflake ids for events ( 20 ) \ No newline at end of file +EVENT_UID_SIZE= // size of snowflake ids for events ( 20 ) +LOG_LEVEL= // see https://docs.rs/tracing-subscriber/0.3.15/tracing_subscriber/struct.EnvFilter.html#. Default is info \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 8344950..f991682 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -426,7 +426,7 @@ checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" [[package]] name = "findtheti-me" -version = "0.1.0" +version = "0.1.2" dependencies = [ "axum", "chrono", @@ -439,6 +439,8 @@ dependencies = [ "sqlx", "tokio", "tower-http", + "tracing", + "tracing-subscriber", ] [[package]] @@ -913,6 +915,15 @@ version = "0.4.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" +[[package]] +name = "matchers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +dependencies = [ + "regex-automata 0.1.10", +] + [[package]] name = "matchit" version = "0.7.3" @@ -998,6 +1009,16 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + [[package]] name = "num-bigint-dig" version = "0.8.4" @@ -1071,6 +1092,12 @@ version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + [[package]] name = "parking_lot" version = "0.12.1" @@ -1245,8 +1272,17 @@ checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343" dependencies = [ "aho-corasick", "memchr", - "regex-automata", - "regex-syntax", + "regex-automata 0.4.3", + "regex-syntax 0.8.2", +] + +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +dependencies = [ + "regex-syntax 0.6.29", ] [[package]] @@ -1257,9 +1293,15 @@ checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f" dependencies = [ "aho-corasick", "memchr", - "regex-syntax", + "regex-syntax 0.8.2", ] +[[package]] +name = "regex-syntax" +version = "0.6.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" + [[package]] name = "regex-syntax" version = "0.8.2" @@ -1398,6 +1440,15 @@ dependencies = [ "digest", ] +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + [[package]] name = "signature" version = "2.2.0" @@ -1757,6 +1808,16 @@ dependencies = [ "syn 2.0.48", ] +[[package]] +name = "thread_local" +version = "1.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152" +dependencies = [ + "cfg-if", + "once_cell", +] + [[package]] name = "tinyvec" version = "1.6.0" @@ -1908,6 +1969,36 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" dependencies = [ "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", ] [[package]] @@ -1975,6 +2066,12 @@ version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" +[[package]] +name = "valuable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" + [[package]] name = "vcpkg" version = "0.2.15" diff --git a/Cargo.toml b/Cargo.toml index 44916d8..0fc5608 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "findtheti-me" -version = "0.1.0" +version = "0.1.2" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -16,4 +16,6 @@ serde = { version = "1.0.195", features = ["derive"] } serde_json = "1.0.111" sqlx = { version = "0.7.3", features = ["runtime-tokio", "postgres", "chrono"] } tokio = {version = "1.35.1", features = ["macros", "rt-multi-thread", "rt", "net"]} -tower-http = { version = "0.5.0", features = ["fs"] } +tower-http = { version = "0.5.0", features = ["fs", "trace"] } +tracing = "0.1.40" +tracing-subscriber = { version = "0.3.18", features = ["env-filter", "std"] } diff --git a/Dockerfile b/Dockerfile index d0f4de1..db2b3bc 100644 --- a/Dockerfile +++ b/Dockerfile @@ -52,7 +52,7 @@ RUN chown -R appuser ./findtheti-me USER appuser -ENV RUST_LOG="findtheti-me=debug,info" +ENV LOG_LEVEL=info ENV EVENT_UID_SIZE=20 WORKDIR ./findtheti-me diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..63b4b68 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) [year] [fullname] + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md index 3449db6..e2dfd73 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,76 @@ -# Setup For Development -## Backend + +![](project-image.png) +A convenient scheduling assistant written in Rust and React + +## Setup + +The simplest way to set this application up is via docker. Its images can be found at https://hub.docker.com/r/mvv97/findthetime. +Also, it is only compatible with PostgreSQL at the moment. It is required to have a PostgreSQL database already setup and running. + +### Simple (With Docker) + +To use `findtheti.me` with docker, simply run +```sh +docker run + -e DATABASE_URL='postgresql://{postgres user}:{postgres password}@{postgres host}/{postgres database}' + mvv97/findthetime +``` + +#### Example docker-compose.yml +```yml +version: "3.4" + +services: + + postgresql: + image: "docker.io/library/postgres:16-alpine" + restart: unless-stopped + volumes: + - '/data/findtheti-me/postgres_data:/var/lib/postgresql/data' + environment: + POSTGRES_PASSWORD: ${PG_PASS:?database password required} + POSTGRES_USER: ${PG_USER:-findthetime} + POSTGRES_DB: ${PG_DB:-findthetime} + + findthetime: + image: "docker.io/mvv97/findthetime:latest" + restart: unless-stopped + environment: + DATABASE_URL: "postgres://${PG_USER:-findthetime}:${PG_PASS}@postgresql/${PG_DB:-findthetime}" + ports: + - '8080:8080' +``` + +### Advanced (Without Docker) + +1. Compile Backend (`cargo build --release`) +2. Build Frontend (`cd frontend && yarn install --production && yarn build`) +3. Copy the `findtheti-me` file from `target/release` and place it into your desired installation folder +4. Copy the `frontend/dist` folder and place it into the same installation folder, maintaining the directory tree. + +In the end, your folder structure should be as follows: +``` +installationDir/ +| |-frontend/ +| |-dist/ +|-findtheti-me +``` +5. Next, create a `.env` file in the root of the installation directory, and look at `.env.example` for what should be in there + +Finally, run `./findtheti-me` in the root, and the application should start. + +## Setup For Development +### Backend 1. Create a PostgreSQL database 2. Configure a `.env` in the project root directory ( following `.env.example` ) 3. Run `cargo sqlx migrate run` to run all migrations ( ensure you've created the database beforehand ) -4. `cargo run` +4. `cargo run` to run the backend ( or `cargo build` to compile it, with the `--release` flag for an optimized build ) -## Frontend +### Frontend 1. `yarn install` 2. `yarn dev` ( or `yarn build`/`yarn preview` ) -## Docker Build Image +### Docker Build Image 1. Do Backend and Frontend setups first 2. Run `cargo sqlx prepare` ( ensure .sqlx directory has been created. The one included in this git repo may be out of date. ) 3. `docker build .` ( or `podman build .` ) in root directory \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..a728a24 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,21 @@ +version: "3.4" + +services: + + postgresql: + image: "docker.io/library/postgres:16-alpine" + restart: unless-stopped + volumes: + - '/data/findtheti-me/postgres_data:/var/lib/postgresql/data' + environment: + POSTGRES_PASSWORD: ${PG_PASS:?database password required} + POSTGRES_USER: ${PG_USER:-findthetime} + POSTGRES_DB: ${PG_DB:-findthetime} + + findthetime: + image: "docker.io/mvv97/findthetime:latest" + restart: unless-stopped + environment: + DATABASE_URL: "postgres://${PG_USER:-findthetime}:${PG_PASS}@postgresql/${PG_DB:-findthetime}" + ports: + - '8080:8080' \ No newline at end of file diff --git a/project-image.png b/project-image.png new file mode 100644 index 0000000..94dcff8 Binary files /dev/null and b/project-image.png differ diff --git a/src/api.rs b/src/api.rs index cf621c1..6e1cf88 100644 --- a/src/api.rs +++ b/src/api.rs @@ -46,7 +46,9 @@ pub(crate) fn error(e: ApplicationError) -> UniversalResponseDto for ApplicationError { fn from(value: sqlx::Error) -> Self { Self { msg: value.to_string(), - status: StatusCode::INTERNAL_SERVER_ERROR + status: StatusCode::INTERNAL_SERVER_ERROR, } } } @@ -137,7 +139,7 @@ impl From for ApplicationError { fn from(value: MigrateError) -> Self { Self { msg: value.to_string(), - status: StatusCode::INTERNAL_SERVER_ERROR + status: StatusCode::INTERNAL_SERVER_ERROR, } } } @@ -146,7 +148,7 @@ impl From for ApplicationError { fn from(value: VarError) -> Self { Self { msg: value.to_string(), - status: StatusCode::INTERNAL_SERVER_ERROR + status: StatusCode::INTERNAL_SERVER_ERROR, } } } diff --git a/src/endpoints.rs b/src/endpoints.rs index 64d4e9f..5239cdf 100644 --- a/src/endpoints.rs +++ b/src/endpoints.rs @@ -1,8 +1,9 @@ use std::net::SocketAddr; use axum::{ - extract::{Path, State, ConnectInfo}, - Json, http::StatusCode, + extract::{ConnectInfo, Path, State}, + http::StatusCode, + Json, }; use chrono::{DateTime, TimeZone, Utc}; use rand::{distributions::Alphanumeric, Rng}; @@ -15,7 +16,7 @@ use crate::{ entity::{ availability::Availability, event::{Event, EventType}, - } + }, }; #[derive(Deserialize)] @@ -25,7 +26,7 @@ pub struct CreateEventDto { name: String, description: Option, event_type: String, - duration: i32 + duration: i32, } #[derive(Serialize)] @@ -36,7 +37,7 @@ pub struct EventDto { name: String, description: Option, event_type: String, - duration: i32 + duration: i32, } #[derive(Deserialize)] @@ -164,7 +165,7 @@ pub async fn fetch_event( name: event.name, description: event.description, event_type: event.event_type.to_string(), - duration: event.duration + duration: event.duration, }) }) }) @@ -194,7 +195,9 @@ pub async fn create_availabilities( let current_availabilities = db::fetch_event_availabilities(txn, event.id).await?; let already_submitted = current_availabilities.iter().any(|a| { - (dto.user_email.is_some() && a.user_email.is_some() && a.user_email == dto.user_email) + (dto.user_email.is_some() + && a.user_email.is_some() + && a.user_email == dto.user_email) || a.user_ip == user_ip || a.user_name == dto.user_name }); @@ -202,7 +205,7 @@ pub async fn create_availabilities( if already_submitted { return Err(ApplicationError::new( "Availability already submitted".to_string(), - StatusCode::UNPROCESSABLE_ENTITY + StatusCode::UNPROCESSABLE_ENTITY, )); } diff --git a/src/entity/event.rs b/src/entity/event.rs index 3fc4fec..756c5d3 100644 --- a/src/entity/event.rs +++ b/src/entity/event.rs @@ -10,7 +10,7 @@ pub(crate) struct Event { pub from_date: Option, pub to_date: Option, pub event_type: EventType, - pub duration: i32 + pub duration: i32, } #[derive(Debug)] diff --git a/src/main.rs b/src/main.rs index 9bb6e46..4b77f83 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,7 +3,10 @@ use std::net::SocketAddr; use axum::Router; use dotenv::dotenv; use tokio::net::TcpListener; -use tower_http::services::ServeDir; +use tower_http::trace::TraceLayer; +use tower_http::{services::ServeDir, trace}; +use tracing::Level; +use tracing_subscriber::{fmt, layer::SubscriberExt, util::SubscriberInitExt, EnvFilter}; mod api; mod db; @@ -16,29 +19,42 @@ async fn main() { dotenv().ok(); + tracing_subscriber::registry() + .with(fmt::layer()) + .with(EnvFilter::from_env("LOG_LEVEL")) + .init(); + let api_routes = api::routes().await.expect("Unable to create api routes"); - let mut routes = Router::new() - .nest("/api", api_routes); - + let mut routes = Router::new().nest("/api", api_routes); // If in release mod, serve static files if !cfg!(debug_assertions) { println!("Initializing frontend routes..."); - - routes = routes.nest_service("/", ServeDir::new("./frontend/dist")) + + routes = routes + .nest_service("/", ServeDir::new("./frontend/dist")) .fallback_service(ServeDir::new("./frontend/dist")); } println!("Routes initialized..."); - let addr = SocketAddr::from(([127, 0, 0, 1], 8080)); + let addr = SocketAddr::from(([0, 0, 0, 0], 8080)); let listener = TcpListener::bind(addr).await.unwrap(); println!("Starting server..."); - axum::serve(listener, routes.into_make_service_with_connect_info::()) - .await - .unwrap(); -} \ No newline at end of file + axum::serve( + listener, + routes + .layer( + TraceLayer::new_for_http() + .make_span_with(trace::DefaultMakeSpan::new().level(Level::INFO)) + .on_response(trace::DefaultOnResponse::new().level(Level::INFO)), + ) + .into_make_service_with_connect_info::(), + ) + .await + .unwrap(); +}