diff --git a/.env.example b/.env.example index 1390efc..c062b55 100644 --- a/.env.example +++ b/.env.example @@ -1,6 +1,5 @@ DATABASE_URL=postgres://postgres:postgres@localhost:5432/chattz PORT=3000 -APP_BASE_URL=http://localhost:3000 # Authentik OIDC app values OIDC_CLIENT_ID=replace-me @@ -17,9 +16,13 @@ TURN_URLS=turn:turn.example.com:3478?transport=udp,turn:turn.example.com:3478?tr TURN_USERNAME=replace-me TURN_PASSWORD=replace-me +# 32+ random chars; used to sign session cookies +SESSION_SECRET=replace-with-long-random-secret +COOKIE_SECURE=false + # Cloudflare R2 media uploads R2_ACCOUNT_ID=replace-me R2_ACCESS_KEY_ID=replace-me R2_SECRET_ACCESS_KEY=replace-me R2_BUCKET=chattz-media -MEDIA_BASE_URL=https://media.example.com +R2_PUBLIC_BASE_URL=https://media.example.com diff --git a/Cargo.lock b/Cargo.lock index c44764d..b496651 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -675,14 +675,13 @@ dependencies = [ "chrono", "dotenvy", "futures-util", + "jsonwebtoken", "reqwest", "sea-orm", "sea-orm-migration", "serde", "serde_json", - "sha2", "tokio", - "tower", "tower-http", "tracing", "tracing-subscriber", @@ -1649,6 +1648,21 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "jsonwebtoken" +version = "9.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a87cc7a48537badeae96744432de36f4be2b4a34a05a5ef32e9dd8a1c169dde" +dependencies = [ + "base64", + "js-sys", + "pem", + "ring", + "serde", + "serde_json", + "simple_asn1", +] + [[package]] name = "lazy_static" version = "1.5.0" @@ -1811,6 +1825,16 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + [[package]] name = "num-bigint-dig" version = "0.8.6" @@ -1943,6 +1967,16 @@ dependencies = [ "windows-link", ] +[[package]] +name = "pem" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be" +dependencies = [ + "base64", + "serde_core", +] + [[package]] name = "pem-rfc7468" version = "0.7.0" @@ -2731,6 +2765,18 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "simple_asn1" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d585997b0ac10be3c5ee635f1bab02d512760d14b7c468801ac8a01d9ae5f1d" +dependencies = [ + "num-bigint", + "num-traits", + "thiserror", + "time", +] + [[package]] name = "slab" version = "0.4.12" @@ -3085,6 +3131,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" dependencies = [ "deranged", + "itoa", "num-conv", "powerfmt", "serde_core", diff --git a/Cargo.toml b/Cargo.toml index 6dbd768..d7ece96 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,15 +10,14 @@ aws-sdk-s3 = { version = "1", default-features = false, features = ["rt-tokio", axum = { version = "0.8", features = ["macros", "ws", "multipart"] } chrono = { version = "0.4", features = ["serde"] } dotenvy = "0.15" +jsonwebtoken = "9" reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] } -sea-orm = { version = "1.1", default-features = false, features = ["sqlx-postgres", "runtime-tokio-rustls", "macros", "with-chrono", "with-uuid", "mock"] } +sea-orm = { version = "1.1", default-features = false, features = ["sqlx-postgres", "runtime-tokio-rustls", "macros", "with-chrono", "with-uuid"] } sea-orm-migration = { version = "1.1", default-features = false, features = ["sqlx-postgres", "runtime-tokio-rustls"] } serde = { version = "1", features = ["derive"] } serde_json = "1" -sha2 = "0.10" futures-util = "0.3" -tokio = { version = "1", features = ["fs", "io-util", "macros", "rt-multi-thread"] } -tower = { version = "0.5", features = ["util"] } +tokio = { version = "1", features = ["macros", "rt-multi-thread"] } tower-http = { version = "0.6", features = ["trace", "fs"] } tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt"] } diff --git a/README.md b/README.md index 2708a60..af5e8f0 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ A simple single-instance Discord-style monolith in Rust using: ## What this includes - OIDC login flow (`/auth/login`, `/auth/callback`, `/auth/logout`) -- HttpOnly session cookie auth +- Signed session cookie auth - Channel voice chat over WebRTC (P2P mesh) with server WebSocket signaling - Guild invite codes (create + join) - Direct messages (DM) between users @@ -41,11 +41,6 @@ For voice reliability on restrictive networks, configure TURN in `.env`: - `TURN_USERNAME` - `TURN_PASSWORD` -For production deployments: -- `APP_BASE_URL` must be your public app origin and should use `https` -- `MEDIA_BASE_URL` should be a separate media origin for user uploads -- uploads and soundboard require R2/object storage to be configured - 3. Run app: ```bash @@ -60,7 +55,7 @@ Web UI is available at `http://localhost:${PORT}/`. ## Authentik setup notes Create an Authentik OAuth2/OIDC provider + application and set: -- Redirect URI: `${APP_BASE_URL}/auth/callback` +- Redirect URI: `http://localhost:3000/auth/callback` - Scopes including at least: `openid profile email` If you change `PORT`, update `OIDC_REDIRECT_URL` and this redirect URI to match. @@ -96,7 +91,6 @@ For Authentik these are commonly under `/application/o/...` for the app slug. - `GET /channels/:channel_id/voice/ws` (WebSocket signaling) All endpoints except health and auth flow require the session cookie from successful login. -Authenticated WebSocket connections (`/ws`, `/channels/:channel_id/voice/ws`) also use the same cookie session. ## Notes @@ -104,7 +98,6 @@ This is intentionally minimal and monolithic (single process, single Postgres in Voice is implemented as browser-to-browser WebRTC audio with signaling in this server. For two users behind strict NAT/firewall, you may need TURN for reliable connectivity. The web UI remembers the last selected guild in browser local storage and auto-selects it on reload. -User uploads are served from the configured media origin, not from `/static`. Mic filter modes in the UI: - `NSNet2 (Compat)`: always-on denoising mode (implemented using DeepFilterNet3 with lighter suppression preset) diff --git a/desktop/index.html b/desktop/index.html index 1684077..ed754ec 100644 --- a/desktop/index.html +++ b/desktop/index.html @@ -1,4 +1,3 @@ - @@ -6,9 +5,12 @@