diff --git a/.dockerignore b/.dockerignore index 19936a0..3326fa3 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,2 +1,8 @@ -postgres-data -dragonfly-data +target/ +postgres-data/ +dragonfly-data/ +.env +.git/ +.gitignore +README.md +*.log \ No newline at end of file diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..c7bc3b8 --- /dev/null +++ b/.env.example @@ -0,0 +1,21 @@ +# Server Configuration +HOST=0.0.0.0 +PORT=3000 + +# Database Configuration +DATABASE_URL=postgres://postgres:postgres@postgres:5432/postgres +DB_MAX_CONNECTIONS=10 +DB_CONNECT_TIMEOUT=30 + +# Redis Configuration +REDIS_URL=redis://dragonfly:6379 +REDIS_POOL_SIZE=5 +REDIS_CONNECT_TIMEOUT=10 + +# Discord OAuth Configuration +CLIENT_ID=your_discord_client_id +CLIENT_SECRET=your_discord_client_secret +REDIRECT_URI=https://your.domain/auth/discord/callback + +# Logging (optional) +RUST_LOG=info,timezone_db=debug \ No newline at end of file diff --git a/.gitignore b/.gitignore index 830cb98..9941afa 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,4 @@ .sqlx dragonfly-data postgres-data -.env +.env \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 8cb5347..98074df 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -32,6 +32,21 @@ version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "async-compression" version = "0.4.23" @@ -135,12 +150,6 @@ dependencies = [ "windows-targets 0.52.6", ] -[[package]] -name = "base64" -version = "0.21.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" - [[package]] name = "base64" version = "0.22.1" @@ -210,7 +219,13 @@ version = "0.4.41" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", "num-traits", + "serde", + "wasm-bindgen", + "windows-link", ] [[package]] @@ -648,11 +663,11 @@ dependencies = [ [[package]] name = "headers" -version = "0.4.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "322106e6bd0cba2d5ead589ddb8150a13d7c4217cf80d7c4f682ca994ccc6aa9" +checksum = "b3314d5adb5d94bcdf56771f2e50dbbc80bb4bdf88967526706205ac9eff24eb" dependencies = [ - "base64 0.21.7", + "base64", "bytes", "headers-core", "http", @@ -820,7 +835,7 @@ version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b1c293b6b3d21eca78250dc7dbebd6b9210ec5530e038cbfe0661b5c47ab06e8" dependencies = [ - "base64 0.22.1", + "base64", "bytes", "futures-channel", "futures-core", @@ -840,6 +855,30 @@ dependencies = [ "windows-registry", ] +[[package]] +name = "iana-time-zone" +version = "0.1.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "icu_collections" version = "2.0.0" @@ -1048,6 +1087,15 @@ version = "0.4.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" +[[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.8.4" @@ -1496,8 +1544,17 @@ checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" dependencies = [ "aho-corasick", "memchr", - "regex-automata", - "regex-syntax", + "regex-automata 0.4.9", + "regex-syntax 0.8.5", +] + +[[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]] @@ -1508,9 +1565,15 @@ checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" dependencies = [ "aho-corasick", "memchr", - "regex-syntax", + "regex-syntax 0.8.5", ] +[[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.5" @@ -1519,12 +1582,12 @@ checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] name = "reqwest" -version = "0.12.18" +version = "0.12.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e98ff6b0dbbe4d5a37318f433d4fc82babd21631f194d370409ceb2e40b2f0b5" +checksum = "a2f8e5513d63f2e5b386eb5106dc67eaf3f84e95258e210489136b8b92ad6119" dependencies = [ "async-compression", - "base64 0.22.1", + "base64", "bytes", "encoding_rs", "futures-core", @@ -1886,8 +1949,9 @@ version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6" dependencies = [ - "base64 0.22.1", + "base64", "bytes", + "chrono", "crc", "crossbeam-queue", "either", @@ -1959,10 +2023,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526" dependencies = [ "atoi", - "base64 0.22.1", + "base64", "bitflags", "byteorder", "bytes", + "chrono", "crc", "digest", "dotenvy", @@ -2001,9 +2066,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46" dependencies = [ "atoi", - "base64 0.22.1", + "base64", "bitflags", "byteorder", + "chrono", "crc", "dotenvy", "etcetera", @@ -2038,6 +2104,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2d12fe70b2c1b4401038055f90f151b78208de1f9f89a7dbfd41587a10c3eea" dependencies = [ "atoi", + "chrono", "flume", "futures-channel", "futures-core", @@ -2178,6 +2245,7 @@ name = "timezone-db" version = "0.1.0" dependencies = [ "axum", + "chrono", "chrono-tz", "dotenvy", "headers", @@ -2186,6 +2254,7 @@ dependencies = [ "serde", "serde_json", "sqlx", + "thiserror", "tokio", "tower", "tower-http", @@ -2311,12 +2380,13 @@ dependencies = [ [[package]] name = "tower-http" -version = "0.6.4" +version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fdb0c213ca27a9f57ab69ddb290fd80d970922355b83ae380b395d3986b8a2e" +checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" dependencies = [ "bitflags", "bytes", + "futures-core", "futures-util", "http", "http-body", @@ -2398,10 +2468,14 @@ version = "0.3.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" dependencies = [ + "matchers", "nu-ansi-term", + "once_cell", + "regex", "sharded-slab", "smallvec", "thread_local", + "tracing", "tracing-core", "tracing-log", ] @@ -2652,6 +2726,41 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows-core" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings 0.4.2", +] + +[[package]] +name = "windows-implement" +version = "0.60.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "windows-link" version = "0.1.1" @@ -2665,7 +2774,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4286ad90ddb45071efd1a66dfa43eb02dd0dfbae1545ad6cc3c51cf34d7e8ba3" dependencies = [ "windows-result", - "windows-strings", + "windows-strings 0.3.1", "windows-targets 0.53.0", ] @@ -2687,6 +2796,15 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-sys" version = "0.48.0" diff --git a/Cargo.toml b/Cargo.toml index b1b5e97..297ea6d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,18 +5,25 @@ edition = "2021" [dependencies] axum = "0.8.4" -tokio = { version = "1", features = ["full"] } -sqlx = { version = "0.8.6", features = ["postgres", "runtime-tokio", "macros"] } +tokio = { version = "1", features = ["full", "signal"] } +sqlx = { version = "0.8.6", features = [ + "postgres", + "runtime-tokio", + "macros", + "chrono", +] } redis = { version = "0.31", features = ["tokio-comp", "aio"] } uuid = { version = "1", features = ["v7"] } dotenvy = "0.15" tracing = "0.1" -tracing-subscriber = "0.3" +tracing-subscriber = { version = "0.3", features = ["env-filter"] } serde = { version = "1", features = ["derive"] } serde_json = "1.0" reqwest = { version = "0.12", features = ["json", "gzip"] } tower-http = { version = "0.6.4", features = ["cors", "fs"] } headers = "0.4.0" chrono-tz = "0.10.3" +chrono = { version = "0.4", features = ["serde"] } tower = "0.5.2" urlencoding = "2.1.3" +thiserror = "2.0.12" diff --git a/Dockerfile b/Dockerfile index 5a6c5b6..8d083a3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,3 @@ -# Stage 1: Build FROM rustlang/rust:nightly AS builder WORKDIR /app @@ -6,6 +5,7 @@ WORKDIR /app COPY Cargo.toml Cargo.lock ./ COPY src ./src COPY public ./public +COPY migrations ./migrations RUN cargo build --release @@ -17,7 +17,6 @@ WORKDIR /app COPY --from=builder /app/target/release/timezone-db /usr/local/bin/app COPY --from=builder /app/public ./public - -ENV RUST_LOG=info +COPY --from=builder /app/migrations ./migrations CMD ["/usr/local/bin/app"] diff --git a/README.md b/README.md index b067b10..ba4ccb0 100644 --- a/README.md +++ b/README.md @@ -7,8 +7,10 @@ A simple Rust-powered API service for managing and retrieving user timezones. - Store user timezones via `/set` endpoint (requires Discord OAuth) - Retrieve timezones by user ID via `/get` - List all saved timezones -- Cookie-based session handling using Redis +- Cookie-based session handling using Redis connection pooling - Built-in CORS support +- Structured configuration with validation +- Graceful shutdown support - Fully containerized with PostgreSQL and DragonflyDB ## Requirements @@ -21,15 +23,27 @@ A simple Rust-powered API service for managing and retrieving user timezones. Create a `.env` file with the following: ```env +# Server Configuration HOST=0.0.0.0 PORT=3000 +# Database Configuration DATABASE_URL=postgres://postgres:postgres@postgres:5432/postgres -REDIS_URL=redis://dragonfly:6379 +DB_MAX_CONNECTIONS=10 +DB_CONNECT_TIMEOUT=30 +# Redis Configuration +REDIS_URL=redis://dragonfly:6379 +REDIS_POOL_SIZE=5 +REDIS_CONNECT_TIMEOUT=10 + +# Discord OAuth Configuration CLIENT_ID=your_discord_client_id CLIENT_SECRET=your_discord_client_secret REDIRECT_URI=https://your.domain/auth/discord/callback + +# Logging (optional) +RUST_LOG=info,timezone_db=debug ``` ## Setup @@ -40,17 +54,36 @@ REDIRECT_URI=https://your.domain/auth/discord/callback docker compose up --build ``` +### Run Manually + +```bash +# Make sure PostgreSQL and Redis are running +cargo run +``` + ## API Endpoints ### `GET /get?id=` Returns stored timezone and username for the given user ID. -### `GET /set?timezone=` +**Response:** +```json +{ + "user": { + "id": "123456789", + "username": "username" + }, + "timezone": "America/New_York" +} +``` -Stores timezone for the authenticated user. Requires Discord OAuth session. +### `POST /set` -### `GET /delete` +Stores timezone for the authenticated user. Requires Discord OAuth session. +**Body:** `application/x-www-form-urlencoded` with `timezone=` + +### `DELETE /delete` Deletes the authenticated user's timezone entry. Requires Discord OAuth session. @@ -58,18 +91,40 @@ Deletes the authenticated user's timezone entry. Requires Discord OAuth session. Returns a JSON object of all stored timezones by user ID. +**Response:** +```json +{ + "123456789": { + "username": "user1", + "timezone": "America/New_York" + }, + "987654321": { + "username": "user2", + "timezone": "Europe/London" + } +} +``` + ### `GET /me` -Returns Discord profile info for the current session. +Returns Discord profile info and timezone for the current session. ### `GET /auth/discord` -Starts OAuth2 authentication flow. +Starts OAuth2 authentication flow. Supports optional `?redirect=` parameter. ### `GET /auth/discord/callback` Handles OAuth2 redirect and sets a session cookie. -## License +## Configuration -[BSD-3-Clause](LICENSE) +The application uses structured configuration with validation. All required environment variables must be provided, and the app will exit with helpful error messages if configuration is invalid. + +### Optional Configuration Variables + +- `DB_MAX_CONNECTIONS`: Maximum database connections (default: 10) +- `DB_CONNECT_TIMEOUT`: Database connection timeout in seconds (default: 30) +- `REDIS_POOL_SIZE`: Redis connection pool size (default: 5) +- `REDIS_CONNECT_TIMEOUT`: Redis connection timeout in seconds (default: 10) +- `RUST_LOG`: Logging level configuration \ No newline at end of file diff --git a/compose.yml b/compose.yml index c619309..cf8d388 100644 --- a/compose.yml +++ b/compose.yml @@ -15,6 +15,12 @@ services: condition: service_started networks: - timezoneDB-network + healthcheck: + test: ["CMD", "curl", "-f", "http://${HOST:-localhost}:${PORT:-3000}/health"] + interval: 30s + timeout: 3s + retries: 3 + start_period: 10s postgres: image: postgres:16 @@ -23,6 +29,7 @@ services: POSTGRES_USER: postgres POSTGRES_PASSWORD: postgres POSTGRES_DB: postgres + PGUSER: postgres volumes: - ./postgres-data:/var/lib/postgresql/data networks: @@ -45,4 +52,4 @@ services: networks: timezoneDB-network: - driver: bridge + driver: bridge \ No newline at end of file diff --git a/migrations/001_initial.sql b/migrations/001_initial.sql new file mode 100644 index 0000000..fa7fb55 --- /dev/null +++ b/migrations/001_initial.sql @@ -0,0 +1,9 @@ +CREATE TABLE IF NOT EXISTS timezones ( + user_id TEXT PRIMARY KEY, + username TEXT NOT NULL, + timezone TEXT NOT NULL, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); + +CREATE INDEX IF NOT EXISTS idx_timezones_updated_at ON timezones(updated_at); \ No newline at end of file diff --git a/migrations/002_created_at-updated_at.sql b/migrations/002_created_at-updated_at.sql new file mode 100644 index 0000000..a55bea4 --- /dev/null +++ b/migrations/002_created_at-updated_at.sql @@ -0,0 +1,5 @@ +ALTER TABLE timezones +ADD COLUMN IF NOT EXISTS created_at TIMESTAMPTZ DEFAULT NOW(); + +ALTER TABLE timezones +ADD COLUMN IF NOT EXISTS updated_at TIMESTAMPTZ DEFAULT NOW(); \ No newline at end of file diff --git a/public/css/style.css b/public/css/style.css index b116f25..b7c3143 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -164,6 +164,25 @@ button.danger:hover { color: var(--fg); } +.open-source-logo { + width: 2rem; + height: 2rem; + margin: 0; + padding: 0; + cursor: pointer; + + position: fixed; + bottom: 1rem; + right: 0.5rem; + z-index: 1000; + + opacity: 0.5; + transition: opacity 0.3s ease; + + &:hover { + opacity: 1 !important; + } +} .hidden { display: none; diff --git a/public/forgejo_logo.svg b/public/forgejo_logo.svg new file mode 100644 index 0000000..be0b3ce --- /dev/null +++ b/public/forgejo_logo.svg @@ -0,0 +1,36 @@ + + + + + Forgejo logo + Caesar Schinas + + + + + + + + + + + + + \ No newline at end of file diff --git a/public/index.html b/public/index.html index b053d5e..6c33e03 100644 --- a/public/index.html +++ b/public/index.html @@ -14,6 +14,14 @@ +
+ + + +
+