diff --git a/.env.example b/.env.example index 1390efc..04a1dd8 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,6 @@ TURN_URLS=turn:turn.example.com:3478?transport=udp,turn:turn.example.com:3478?tr TURN_USERNAME=replace-me TURN_PASSWORD=replace-me -# 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 +# 32+ random chars; used to sign session cookies +SESSION_SECRET=replace-with-long-random-secret +COOKIE_SECURE=false diff --git a/.forgejo/workflows/pipeline.yaml b/.forgejo/workflows/pipeline.yaml index c76ae86..30aa0d7 100644 --- a/.forgejo/workflows/pipeline.yaml +++ b/.forgejo/workflows/pipeline.yaml @@ -11,14 +11,6 @@ jobs: with: node-version: 24 - uses: actions/checkout@v6 - with: - fetch-depth: 0 - - name: Update version from tag - run: | - # Extract version from tag (e.g. v1.2.3 -> 1.2.3) - VERSION=${GITHUB_REF_NAME#v} - echo "Bumping version to $VERSION" - npm version $VERSION --no-git-tag-version - run: npm ci - run: npm run dist:linux - name: Build Windows installer with 32-bit Wine prefix @@ -39,23 +31,11 @@ jobs: cp "$file" "static/installers/$out" fi } - # Copy versioned artifacts for electron-updater - cp dist/*.AppImage static/installers/ || true - cp dist/*.exe static/installers/ || true - cp dist/*.rpm static/installers/ || true - cp dist/*.deb static/installers/ || true - cp dist/*.msi static/installers/ || true - - # Maintain generic names for stable website links copy_first '*.rpm' 'chattz-linux.rpm' copy_first '*.deb' 'chattz-linux.deb' copy_first '*.AppImage' 'chattz-linux.AppImage' copy_first '*.exe' 'chattz-windows.exe' copy_first '*.msi' 'chattz-windows.msi' - - # Metadata files for electron-updater - copy_first 'latest-linux.yml' 'latest-linux.yml' - copy_first 'latest.yml' 'latest.yml' - name: Cache Cargo registry uses: actions/cache@v4 with: diff --git a/Cargo.lock b/Cargo.lock index c44764d..190bd84 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -92,418 +92,6 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" -[[package]] -name = "aws-config" -version = "1.8.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a8fc176d53d6fe85017f230405e3255cedb4a02221cb55ed6d76dccbbb099b2" -dependencies = [ - "aws-credential-types", - "aws-runtime", - "aws-sdk-sts", - "aws-smithy-async", - "aws-smithy-http 0.63.5", - "aws-smithy-json 0.62.4", - "aws-smithy-runtime", - "aws-smithy-runtime-api", - "aws-smithy-types", - "aws-types", - "bytes", - "fastrand", - "http 1.4.0", - "time", - "tokio", - "tracing", - "url", -] - -[[package]] -name = "aws-credential-types" -version = "1.2.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d203b0bf2626dcba8665f5cd0871d7c2c0930223d6b6be9097592fea21242d0" -dependencies = [ - "aws-smithy-async", - "aws-smithy-runtime-api", - "aws-smithy-types", - "zeroize", -] - -[[package]] -name = "aws-lc-rs" -version = "1.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9a7b350e3bb1767102698302bc37256cbd48422809984b98d292c40e2579aa9" -dependencies = [ - "aws-lc-sys", - "zeroize", -] - -[[package]] -name = "aws-lc-sys" -version = "0.37.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b092fe214090261288111db7a2b2c2118e5a7f30dc2569f1732c4069a6840549" -dependencies = [ - "cc", - "cmake", - "dunce", - "fs_extra", -] - -[[package]] -name = "aws-runtime" -version = "1.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ede2ddc593e6c8acc6ce3358c28d6677a6dc49b65ba4b37a2befe14a11297e75" -dependencies = [ - "aws-credential-types", - "aws-sigv4", - "aws-smithy-async", - "aws-smithy-eventstream", - "aws-smithy-http 0.63.5", - "aws-smithy-runtime", - "aws-smithy-runtime-api", - "aws-smithy-types", - "aws-types", - "bytes", - "bytes-utils", - "fastrand", - "http 0.2.12", - "http 1.4.0", - "http-body 0.4.6", - "http-body 1.0.1", - "percent-encoding", - "pin-project-lite", - "tracing", - "uuid", -] - -[[package]] -name = "aws-sdk-s3" -version = "1.119.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d65fddc3844f902dfe1864acb8494db5f9342015ee3ab7890270d36fbd2e01c" -dependencies = [ - "aws-credential-types", - "aws-runtime", - "aws-sigv4", - "aws-smithy-async", - "aws-smithy-checksums", - "aws-smithy-eventstream", - "aws-smithy-http 0.62.6", - "aws-smithy-json 0.61.9", - "aws-smithy-runtime", - "aws-smithy-runtime-api", - "aws-smithy-types", - "aws-smithy-xml", - "aws-types", - "bytes", - "fastrand", - "hex", - "hmac", - "http 0.2.12", - "http 1.4.0", - "http-body 0.4.6", - "lru", - "percent-encoding", - "regex-lite", - "sha2", - "tracing", - "url", -] - -[[package]] -name = "aws-sdk-sts" -version = "1.99.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9acba7c62f3d4e2408fa998a3a8caacd8b9a5b5549cf36e2372fbdae329d5449" -dependencies = [ - "aws-credential-types", - "aws-runtime", - "aws-smithy-async", - "aws-smithy-http 0.63.5", - "aws-smithy-json 0.62.4", - "aws-smithy-observability", - "aws-smithy-query", - "aws-smithy-runtime", - "aws-smithy-runtime-api", - "aws-smithy-types", - "aws-smithy-xml", - "aws-types", - "fastrand", - "http 0.2.12", - "http 1.4.0", - "regex-lite", - "tracing", -] - -[[package]] -name = "aws-sigv4" -version = "1.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37411f8e0f4bea0c3ca0958ce7f18f6439db24d555dbd809787262cd00926aa9" -dependencies = [ - "aws-credential-types", - "aws-smithy-eventstream", - "aws-smithy-http 0.63.5", - "aws-smithy-runtime-api", - "aws-smithy-types", - "bytes", - "form_urlencoded", - "hex", - "hmac", - "http 0.2.12", - "http 1.4.0", - "percent-encoding", - "sha2", - "time", - "tracing", -] - -[[package]] -name = "aws-smithy-async" -version = "1.2.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cc50d0f63e714784b84223abd7abbc8577de8c35d699e0edd19f0a88a08ae13" -dependencies = [ - "futures-util", - "pin-project-lite", - "tokio", -] - -[[package]] -name = "aws-smithy-checksums" -version = "0.63.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87294a084b43d649d967efe58aa1f9e0adc260e13a6938eb904c0ae9b45824ae" -dependencies = [ - "aws-smithy-http 0.62.6", - "aws-smithy-types", - "bytes", - "crc-fast", - "hex", - "http 0.2.12", - "http-body 0.4.6", - "md-5", - "pin-project-lite", - "sha1", - "sha2", - "tracing", -] - -[[package]] -name = "aws-smithy-eventstream" -version = "0.60.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c0b3e587fbaa5d7f7e870544508af8ce82ea47cd30376e69e1e37c4ac746f79" -dependencies = [ - "aws-smithy-types", - "bytes", - "crc32fast", -] - -[[package]] -name = "aws-smithy-http" -version = "0.62.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "826141069295752372f8203c17f28e30c464d22899a43a0c9fd9c458d469c88b" -dependencies = [ - "aws-smithy-eventstream", - "aws-smithy-runtime-api", - "aws-smithy-types", - "bytes", - "bytes-utils", - "futures-core", - "futures-util", - "http 0.2.12", - "http 1.4.0", - "http-body 0.4.6", - "percent-encoding", - "pin-project-lite", - "pin-utils", - "tracing", -] - -[[package]] -name = "aws-smithy-http" -version = "0.63.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d619373d490ad70966994801bc126846afaa0d1ee920697a031f0cf63f2568e7" -dependencies = [ - "aws-smithy-runtime-api", - "aws-smithy-types", - "bytes", - "bytes-utils", - "futures-core", - "futures-util", - "http 1.4.0", - "http-body 1.0.1", - "http-body-util", - "percent-encoding", - "pin-project-lite", - "pin-utils", - "tracing", -] - -[[package]] -name = "aws-smithy-http-client" -version = "1.1.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00ccbb08c10f6bcf912f398188e42ee2eab5f1767ce215a02a73bc5df1bbdd95" -dependencies = [ - "aws-smithy-async", - "aws-smithy-runtime-api", - "aws-smithy-types", - "h2 0.3.27", - "h2 0.4.13", - "http 0.2.12", - "http 1.4.0", - "http-body 0.4.6", - "hyper 0.14.32", - "hyper 1.8.1", - "hyper-rustls 0.24.2", - "hyper-rustls 0.27.7", - "hyper-util", - "pin-project-lite", - "rustls 0.21.12", - "rustls 0.23.36", - "rustls-native-certs", - "rustls-pki-types", - "tokio", - "tokio-rustls 0.26.4", - "tower", - "tracing", -] - -[[package]] -name = "aws-smithy-json" -version = "0.61.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49fa1213db31ac95288d981476f78d05d9cbb0353d22cdf3472cc05bb02f6551" -dependencies = [ - "aws-smithy-types", -] - -[[package]] -name = "aws-smithy-json" -version = "0.62.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27b3a779093e18cad88bbae08dc4261e1d95018c4c5b9356a52bcae7c0b6e9bb" -dependencies = [ - "aws-smithy-types", -] - -[[package]] -name = "aws-smithy-observability" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d3f39d5bb871aaf461d59144557f16d5927a5248a983a40654d9cf3b9ba183b" -dependencies = [ - "aws-smithy-runtime-api", -] - -[[package]] -name = "aws-smithy-query" -version = "0.60.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f76a580e3d8f8961e5d48763214025a2af65c2fa4cd1fb7f270a0e107a71b0" -dependencies = [ - "aws-smithy-types", - "urlencoding", -] - -[[package]] -name = "aws-smithy-runtime" -version = "1.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22ccf7f6eba8b2dcf8ce9b74806c6c185659c311665c4bf8d6e71ebd454db6bf" -dependencies = [ - "aws-smithy-async", - "aws-smithy-http 0.63.5", - "aws-smithy-http-client", - "aws-smithy-observability", - "aws-smithy-runtime-api", - "aws-smithy-types", - "bytes", - "fastrand", - "http 0.2.12", - "http 1.4.0", - "http-body 0.4.6", - "http-body 1.0.1", - "http-body-util", - "pin-project-lite", - "pin-utils", - "tokio", - "tracing", -] - -[[package]] -name = "aws-smithy-runtime-api" -version = "1.11.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4af6e5def28be846479bbeac55aa4603d6f7986fc5da4601ba324dd5d377516" -dependencies = [ - "aws-smithy-async", - "aws-smithy-types", - "bytes", - "http 0.2.12", - "http 1.4.0", - "pin-project-lite", - "tokio", - "tracing", - "zeroize", -] - -[[package]] -name = "aws-smithy-types" -version = "1.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ca2734c16913a45343b37313605d84e7d8b34a4611598ce1d25b35860a2bed3" -dependencies = [ - "base64-simd", - "bytes", - "bytes-utils", - "futures-core", - "http 0.2.12", - "http 1.4.0", - "http-body 0.4.6", - "http-body 1.0.1", - "http-body-util", - "itoa", - "num-integer", - "pin-project-lite", - "pin-utils", - "ryu", - "serde", - "time", - "tokio", - "tokio-util", -] - -[[package]] -name = "aws-smithy-xml" -version = "0.60.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b53543b4b86ed43f051644f704a98c7291b3618b67adf057ee77a366fa52fcaa" -dependencies = [ - "xmlparser", -] - -[[package]] -name = "aws-types" -version = "1.3.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0470cc047657c6e286346bdf10a8719d26efd6a91626992e0e64481e44323e96" -dependencies = [ - "aws-credential-types", - "aws-smithy-async", - "aws-smithy-runtime-api", - "aws-smithy-types", - "rustc_version", - "tracing", -] - [[package]] name = "axum" version = "0.8.8" @@ -516,10 +104,10 @@ dependencies = [ "bytes", "form_urlencoded", "futures-util", - "http 1.4.0", - "http-body 1.0.1", + "http", + "http-body", "http-body-util", - "hyper 1.8.1", + "hyper", "hyper-util", "itoa", "matchit", @@ -550,8 +138,8 @@ checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" dependencies = [ "bytes", "futures-core", - "http 1.4.0", - "http-body 1.0.1", + "http", + "http-body", "http-body-util", "mime", "pin-project-lite", @@ -578,16 +166,6 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" -[[package]] -name = "base64-simd" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "339abbe78e73178762e23bea9dfd08e697eb3f3301cd4be981c0f78ba5859195" -dependencies = [ - "outref", - "vsimd", -] - [[package]] name = "base64ct" version = "1.8.3" @@ -630,16 +208,6 @@ version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" -[[package]] -name = "bytes-utils" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dafe3a8757b027e2be6e4e5601ed563c55989fcf1546e933c66c8eb3a058d35" -dependencies = [ - "bytes", - "either", -] - [[package]] name = "cc" version = "1.2.56" @@ -647,8 +215,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" dependencies = [ "find-msvc-tools", - "jobserver", - "libc", "shlex", ] @@ -669,20 +235,17 @@ name = "chattz" version = "0.1.0" dependencies = [ "anyhow", - "aws-config", - "aws-sdk-s3", "axum", "chrono", "dotenvy", "futures-util", + "jsonwebtoken", "reqwest", "sea-orm", "sea-orm-migration", "serde", "serde_json", - "sha2", "tokio", - "tower", "tower-http", "tracing", "tracing-subscriber", @@ -704,15 +267,6 @@ dependencies = [ "windows-link", ] -[[package]] -name = "cmake" -version = "0.1.57" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75443c44cd6b379beb8c5b45d85d0773baf31cce901fe7bb252f4eff3008ef7d" -dependencies = [ - "cc", -] - [[package]] name = "concurrent-queue" version = "2.5.0" @@ -728,16 +282,6 @@ version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" -[[package]] -name = "core-foundation" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" -dependencies = [ - "core-foundation-sys", - "libc", -] - [[package]] name = "core-foundation-sys" version = "0.8.7" @@ -768,28 +312,6 @@ version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" -[[package]] -name = "crc-fast" -version = "1.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ddc2d09feefeee8bd78101665bd8645637828fa9317f9f292496dbbd8c65ff3" -dependencies = [ - "crc", - "digest", - "rand 0.9.2", - "regex", - "rustversion", -] - -[[package]] -name = "crc32fast" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" -dependencies = [ - "cfg-if", -] - [[package]] name = "crossbeam-queue" version = "0.3.12" @@ -926,12 +448,6 @@ version = "0.15.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" -[[package]] -name = "dunce" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" - [[package]] name = "either" version = "1.15.0" @@ -978,12 +494,6 @@ dependencies = [ "pin-project-lite", ] -[[package]] -name = "fastrand" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" - [[package]] name = "find-msvc-tools" version = "0.1.9" @@ -1022,12 +532,6 @@ dependencies = [ "percent-encoding", ] -[[package]] -name = "fs_extra" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" - [[package]] name = "futures" version = "0.3.31" @@ -1169,44 +673,6 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" -[[package]] -name = "h2" -version = "0.3.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0beca50380b1fc32983fc1cb4587bfa4bb9e78fc259aad4a0032d2080309222d" -dependencies = [ - "bytes", - "fnv", - "futures-core", - "futures-sink", - "futures-util", - "http 0.2.12", - "indexmap", - "slab", - "tokio", - "tokio-util", - "tracing", -] - -[[package]] -name = "h2" -version = "0.4.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" -dependencies = [ - "atomic-waker", - "bytes", - "fnv", - "futures-core", - "futures-sink", - "http 1.4.0", - "indexmap", - "slab", - "tokio", - "tokio-util", - "tracing", -] - [[package]] name = "hashbrown" version = "0.15.5" @@ -1278,17 +744,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "http" -version = "0.2.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" -dependencies = [ - "bytes", - "fnv", - "itoa", -] - [[package]] name = "http" version = "1.4.0" @@ -1299,17 +754,6 @@ dependencies = [ "itoa", ] -[[package]] -name = "http-body" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" -dependencies = [ - "bytes", - "http 0.2.12", - "pin-project-lite", -] - [[package]] name = "http-body" version = "1.0.1" @@ -1317,7 +761,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" dependencies = [ "bytes", - "http 1.4.0", + "http", ] [[package]] @@ -1328,8 +772,8 @@ checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" dependencies = [ "bytes", "futures-core", - "http 1.4.0", - "http-body 1.0.1", + "http", + "http-body", "pin-project-lite", ] @@ -1351,30 +795,6 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" -[[package]] -name = "hyper" -version = "0.14.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7" -dependencies = [ - "bytes", - "futures-channel", - "futures-core", - "futures-util", - "h2 0.3.27", - "http 0.2.12", - "http-body 0.4.6", - "httparse", - "httpdate", - "itoa", - "pin-project-lite", - "socket2 0.5.10", - "tokio", - "tower-service", - "tracing", - "want", -] - [[package]] name = "hyper" version = "1.8.1" @@ -1385,9 +805,8 @@ dependencies = [ "bytes", "futures-channel", "futures-core", - "h2 0.4.13", - "http 1.4.0", - "http-body 1.0.1", + "http", + "http-body", "httparse", "httpdate", "itoa", @@ -1398,35 +817,19 @@ dependencies = [ "want", ] -[[package]] -name = "hyper-rustls" -version = "0.24.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" -dependencies = [ - "futures-util", - "http 0.2.12", - "hyper 0.14.32", - "log", - "rustls 0.21.12", - "tokio", - "tokio-rustls 0.24.1", -] - [[package]] name = "hyper-rustls" version = "0.27.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" dependencies = [ - "http 1.4.0", - "hyper 1.8.1", + "http", + "hyper", "hyper-util", - "rustls 0.23.36", - "rustls-native-certs", + "rustls", "rustls-pki-types", "tokio", - "tokio-rustls 0.26.4", + "tokio-rustls", "tower-service", "webpki-roots 1.0.6", ] @@ -1441,14 +844,14 @@ dependencies = [ "bytes", "futures-channel", "futures-util", - "http 1.4.0", - "http-body 1.0.1", - "hyper 1.8.1", + "http", + "http-body", + "hyper", "ipnet", "libc", "percent-encoding", "pin-project-lite", - "socket2 0.6.2", + "socket2", "tokio", "tower-service", "tracing", @@ -1629,16 +1032,6 @@ version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" -[[package]] -name = "jobserver" -version = "0.1.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" -dependencies = [ - "getrandom 0.3.4", - "libc", -] - [[package]] name = "js-sys" version = "0.3.85" @@ -1649,6 +1042,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" @@ -1712,15 +1120,6 @@ version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" -[[package]] -name = "lru" -version = "0.12.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" -dependencies = [ - "hashbrown 0.15.5", -] - [[package]] name = "lru-slab" version = "0.1.2" @@ -1794,7 +1193,7 @@ dependencies = [ "bytes", "encoding_rs", "futures-util", - "http 1.4.0", + "http", "httparse", "memchr", "mime", @@ -1811,6 +1210,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" @@ -1869,12 +1278,6 @@ version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" -[[package]] -name = "openssl-probe" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" - [[package]] name = "ordered-float" version = "4.6.0" @@ -1908,12 +1311,6 @@ dependencies = [ "syn", ] -[[package]] -name = "outref" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e" - [[package]] name = "parking" version = "2.2.1" @@ -1943,6 +1340,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" @@ -2077,8 +1484,8 @@ dependencies = [ "quinn-proto", "quinn-udp", "rustc-hash", - "rustls 0.23.36", - "socket2 0.6.2", + "rustls", + "socket2", "thiserror", "tokio", "tracing", @@ -2097,7 +1504,7 @@ dependencies = [ "rand 0.9.2", "ring", "rustc-hash", - "rustls 0.23.36", + "rustls", "rustls-pki-types", "slab", "thiserror", @@ -2115,7 +1522,7 @@ dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2 0.6.2", + "socket2", "tracing", "windows-sys 0.60.2", ] @@ -2235,12 +1642,6 @@ dependencies = [ "regex-syntax", ] -[[package]] -name = "regex-lite" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cab834c73d247e67f4fae452806d17d3c7501756d98c8808d7c9c7aa7d18f973" - [[package]] name = "regex-syntax" version = "0.8.9" @@ -2256,25 +1657,25 @@ dependencies = [ "base64", "bytes", "futures-core", - "http 1.4.0", - "http-body 1.0.1", + "http", + "http-body", "http-body-util", - "hyper 1.8.1", - "hyper-rustls 0.27.7", + "hyper", + "hyper-rustls", "hyper-util", "js-sys", "log", "percent-encoding", "pin-project-lite", "quinn", - "rustls 0.23.36", + "rustls", "rustls-pki-types", "serde", "serde_json", "serde_urlencoded", "sync_wrapper", "tokio", - "tokio-rustls 0.26.4", + "tokio-rustls", "tower", "tower-http", "tower-service", @@ -2334,45 +1735,20 @@ dependencies = [ "semver", ] -[[package]] -name = "rustls" -version = "0.21.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" -dependencies = [ - "log", - "ring", - "rustls-webpki 0.101.7", - "sct", -] - [[package]] name = "rustls" version = "0.23.36" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c665f33d38cea657d9614f766881e4d510e0eda4239891eea56b4cadcf01801b" dependencies = [ - "aws-lc-rs", "once_cell", "ring", "rustls-pki-types", - "rustls-webpki 0.103.9", + "rustls-webpki", "subtle", "zeroize", ] -[[package]] -name = "rustls-native-certs" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" -dependencies = [ - "openssl-probe", - "rustls-pki-types", - "schannel", - "security-framework", -] - [[package]] name = "rustls-pki-types" version = "1.14.0" @@ -2383,23 +1759,12 @@ dependencies = [ "zeroize", ] -[[package]] -name = "rustls-webpki" -version = "0.101.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" -dependencies = [ - "ring", - "untrusted", -] - [[package]] name = "rustls-webpki" version = "0.103.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" dependencies = [ - "aws-lc-rs", "ring", "rustls-pki-types", "untrusted", @@ -2417,31 +1782,12 @@ version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" -[[package]] -name = "schannel" -version = "0.1.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" -dependencies = [ - "windows-sys 0.61.2", -] - [[package]] name = "scopeguard" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" -[[package]] -name = "sct" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" -dependencies = [ - "ring", - "untrusted", -] - [[package]] name = "sea-bae" version = "0.2.1" @@ -2589,29 +1935,6 @@ dependencies = [ "syn", ] -[[package]] -name = "security-framework" -version = "3.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d17b898a6d6948c3a8ee4372c17cb384f90d2e6e912ef00895b14fd7ab54ec38" -dependencies = [ - "bitflags", - "core-foundation", - "core-foundation-sys", - "libc", - "security-framework-sys", -] - -[[package]] -name = "security-framework-sys" -version = "2.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" -dependencies = [ - "core-foundation-sys", - "libc", -] - [[package]] name = "semver" version = "1.0.27" @@ -2731,6 +2054,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" @@ -2746,16 +2081,6 @@ dependencies = [ "serde", ] -[[package]] -name = "socket2" -version = "0.5.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" -dependencies = [ - "libc", - "windows-sys 0.52.0", -] - [[package]] name = "socket2" version = "0.6.2" @@ -2822,7 +2147,7 @@ dependencies = [ "memchr", "once_cell", "percent-encoding", - "rustls 0.23.36", + "rustls", "serde", "serde_json", "sha2", @@ -3085,6 +2410,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" dependencies = [ "deranged", + "itoa", "num-conv", "powerfmt", "serde_core", @@ -3143,7 +2469,7 @@ dependencies = [ "libc", "mio", "pin-project-lite", - "socket2 0.6.2", + "socket2", "tokio-macros", "windows-sys 0.61.2", ] @@ -3159,23 +2485,13 @@ dependencies = [ "syn", ] -[[package]] -name = "tokio-rustls" -version = "0.24.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" -dependencies = [ - "rustls 0.21.12", - "tokio", -] - [[package]] name = "tokio-rustls" version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" dependencies = [ - "rustls 0.23.36", + "rustls", "tokio", ] @@ -3241,8 +2557,8 @@ dependencies = [ "bytes", "futures-core", "futures-util", - "http 1.4.0", - "http-body 1.0.1", + "http", + "http-body", "http-body-util", "http-range-header", "httpdate", @@ -3347,7 +2663,7 @@ checksum = "8628dcc84e5a09eb3d8423d6cb682965dea9133204e8fb3efee74c2a0c259442" dependencies = [ "bytes", "data-encoding", - "http 1.4.0", + "http", "httparse", "log", "rand 0.9.2", @@ -3467,12 +2783,6 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" -[[package]] -name = "vsimd" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c3082ca00d5a5ef149bb8b555a72ae84c9c59f7250f013ac822ac2e49b19c64" - [[package]] name = "want" version = "0.3.1" @@ -3903,12 +3213,6 @@ version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" -[[package]] -name = "xmlparser" -version = "0.13.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4" - [[package]] name = "yansi" version = "1.0.1" diff --git a/Cargo.toml b/Cargo.toml index 6dbd768..5ab0c14 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,20 +5,17 @@ edition = "2024" [dependencies] anyhow = "1" -aws-config = { version = "1", default-features = false, features = ["behavior-version-latest", "rt-tokio", "rustls"] } -aws-sdk-s3 = { version = "1", default-features = false, features = ["rt-tokio", "rustls"] } 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/build/icon.png b/build/icon.png deleted file mode 100644 index 8169e48..0000000 Binary files a/build/icon.png and /dev/null differ diff --git a/desktop/app.js b/desktop/app.js index e0b9fa7..490f680 100644 --- a/desktop/app.js +++ b/desktop/app.js @@ -1 +1,1647 @@ -import "../static/shared/app-core.js"; +const state = { + me: null, + guilds: [], + channels: [], + dmConversations: [], + members: [], + voicePresence: new Map(), + selectedGuildId: null, + selectedTextChannelId: null, + selectedDmUserId: null, + selectedDmDisplayName: null, + selectedVoiceChannelId: null, + voice: { + ws: null, + joinedChannelId: null, + localStream: null, + rawStream: null, + videoStream: null, + screenStream: null, + peerConnections: new Map(), + videoSenders: new Map(), + screenSenders: new Map(), + muted: false, + sharingVideo: false, + sharingScreen: false, + viewMode: 'chat', // 'chat' or 'video' + iceServers: [{ urls: "stun:stun.l.google.com:19302" }], + }, + voicePresencePollId: null, + chatWs: null, + lastMessageId: null, + onlineUsers: new Set(), +}; + +// --- Desktop Backend Configuration --- +let API_BASE_URL = ''; // Will be initialized via IPC + +async function initializeConfig() { + try { + const config = await window.electronAPI.getConfig(); + API_BASE_URL = config.backendUrl; + console.log(`Backend initialized: ${API_BASE_URL}`); + + // Start the app now that config is ready + init(); + } catch (err) { + console.error("Failed to fetch config via IPC", err); + // Fallback to URL search params if IPC fails (unlikely in desktop app) + const urlParams = new URLSearchParams(window.location.search); + API_BASE_URL = urlParams.get('backend') || ''; + } +} + +// Start config fetch immediately +initializeConfig(); + +function getWsUrl(path) { + let urlStr; + if (!API_BASE_URL) { + const proto = location.protocol === "https:" ? "wss" : "ws"; + urlStr = `${proto}://${location.host}${path}`; + } else { + const url = new URL(API_BASE_URL); + const proto = url.protocol === "https:" ? "wss" : "ws"; + const normalizedPath = path.startsWith('/') ? path : `/${path}`; + urlStr = `${proto}://${url.host}${normalizedPath}`; + } + + const token = localStorage.getItem("chattz_token"); + if (token) { + urlStr += (urlStr.includes('?') ? '&' : '?') + `token=${token}`; + } + return urlStr; +} + + + + + + +const el = { + authScreen: document.getElementById("auth-screen"), + loginBtn: document.getElementById("login-btn"), + main: document.getElementById("main"), + status: document.getElementById("status"), + + // Guilds + guildList: document.getElementById("guild-list"), + addGuildBtn: document.getElementById("add-guild-btn"), + guildTitle: document.getElementById("guild-title"), + createInviteBtn: document.getElementById("create-invite-btn"), + inviteCopiedBadge: document.getElementById("invite-copied-badge"), + + // Channels + channelList: document.getElementById("channel-list"), + voiceChannelList: document.getElementById("voice-channel-list"), + addTextBtn: document.getElementById("add-text-btn"), + addVoiceBtn: document.getElementById("add-voice-btn"), + dmList: document.getElementById("dm-list"), + channelTitle: document.getElementById("channel-title"), + + // Messages + messageList: document.getElementById("message-list"), + messageForm: document.getElementById("message-form"), + messageBody: document.getElementById("message-body"), + + // User Panel + userName: document.getElementById("user-name"), + userAvatar: document.getElementById("user-avatar"), + logoutBtn: document.getElementById("logout-btn"), + + // Voice Connection + voiceConnection: document.getElementById("voice-connection"), + vcChannelName: document.getElementById("vc-channel-name"), + voiceVideoBtn: document.getElementById("voice-video-btn"), + voiceScreenBtn: document.getElementById("voice-screen-btn"), + voiceMuteBtn: document.getElementById("voice-mute-btn"), + voiceLeaveBtn: document.getElementById("voice-leave-btn"), + videoGrid: document.getElementById("video-grid"), + messageForm: document.getElementById("message-form"), + messageInputWrapper: document.querySelector(".chat-input-wrapper"), + + // Members + memberList: document.getElementById("member-list"), + + // Modals + modalContainer: document.getElementById("modal-container"), + guildForm: document.getElementById("guild-form"), + guildName: document.getElementById("guild-name"), + modalCancel: document.getElementById("modal-cancel"), + + channelModal: document.getElementById("channel-modal"), + channelForm: document.getElementById("channel-form"), + channelName: document.getElementById("channel-name"), + channelModalCancel: document.getElementById("channel-modal-cancel"), + + // Sound Board + soundboard: document.getElementById('soundboard'), + soundboardGrid: document.getElementById('soundboard-grid'), + addSoundBtn: document.getElementById('add-sound-btn'), + soundModal: document.getElementById('sound-modal'), + soundForm: document.getElementById('sound-form'), + soundName: document.getElementById('sound-name'), + soundIcon: document.getElementById('sound-icon'), + soundFile: document.getElementById('sound-file'), + soundModalCancel: document.getElementById('sound-modal-cancel'), + soundSubmitBtn: document.getElementById('sound-submit-btn'), + + // Mobile + mobileMenuBtn: document.getElementById("mobile-menu-btn"), + mobileMembersBtn: document.getElementById("mobile-members-btn"), + overlay: document.getElementById("overlay"), + utilitySidebar: document.getElementById("utility-sidebar"), + + // GIF Picker + gifBtn: document.getElementById("gif-btn"), + gifPicker: document.getElementById("gif-picker"), + gifPickerClose: document.getElementById("gif-picker-close"), + gifSearchInput: document.getElementById("gif-search-input"), + gifResults: document.getElementById("gif-results"), +}; + +// --- API Helpers --- + +async function api(path, options = {}) { + const fullUrl = API_BASE_URL ? (path.startsWith('http') ? path : `${API_BASE_URL}${path}`) : path; + + const headers = { + "content-type": "application/json", + ...(options.headers || {}), + }; + + const token = localStorage.getItem("chattz_token"); + if (token) { + headers["Authorization"] = `Bearer ${token}`; + } + + let res = await fetch(fullUrl, { + ...options, + headers, + }); + + // Handle 401 Unauthorized via Refresh Token + if (res.status === 401 && !path.includes('/auth/refresh')) { + const refreshToken = localStorage.getItem("chattz_refresh_token"); + if (refreshToken) { + try { + const refreshUrl = API_BASE_URL ? `${API_BASE_URL}/auth/refresh` : '/auth/refresh'; + const refreshRes = await fetch(refreshUrl, { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify({ refresh_token: refreshToken }) + }); + + if (refreshRes.ok) { + const newTokens = await refreshRes.json(); + localStorage.setItem("chattz_token", newTokens.access_token); + localStorage.setItem("chattz_refresh_token", newTokens.refresh_token); + + // Retry original request with new token + headers["Authorization"] = `Bearer ${newTokens.access_token}`; + res = await fetch(fullUrl, { ...options, headers }); + } else { + // Both tokens invalid/expired, wipe out to force clear state + throw new Error("Refresh token expired or invalid"); + } + } catch (err) { + localStorage.removeItem("chattz_token"); + localStorage.removeItem("chattz_refresh_token"); + location.href = API_BASE_URL ? `${API_BASE_URL}/auth/login` : "/auth/login"; + throw new Error("Session expired, please log in again"); + } + } + } + + if (!res.ok) { + let detail = "request failed"; + try { + const body = await res.json(); + detail = body.error || detail; + } catch { } + throw new Error(`${res.status}: ${detail}`); + } + + if (res.status === 204) return null; + return res.json(); +} + +// --- Utils --- + +function shortName(name) { + if (!name || typeof name !== 'string') return "?"; + const trimmed = name.trim(); + if (!trimmed) return "?"; + const words = trimmed.split(/\s+/).filter(w => w.length > 0).slice(0, 2); + if (words.length === 0) return "?"; + if (words.length === 1) return words[0].substring(0, 2).toUpperCase(); + return words.map((w) => w[0]?.toUpperCase() || "").join(""); +} + +function escapeHtml(s) { + if (s === null || s === undefined) return ""; + return String(s) + .replaceAll("&", "&") + .replaceAll("<", "<") + .replaceAll(">", ">") + .replaceAll('"', """) + .replaceAll("'", "'"); +} + +function formatMessageBody(body) { + const escaped = escapeHtml(body); + // Detect GIF URLs (simple regex for demo) + const urlRegex = /(https?:\/\/[^\s]+(?:\.gif|\/giphy\.gif)[^\s]*)/gi; + if (urlRegex.test(body)) { + return escaped.replace(urlRegex, (url) => { + return `