This is an automated email from the ASF dual-hosted git repository. piotr pushed a commit to branch iggy_web_embedded in repository https://gitbox.apache.org/repos/asf/iggy.git
commit e54f0bc69155e3cd8abab2ab2cbc7c1350895ef6 Author: spetz <[email protected]> AuthorDate: Fri Dec 12 20:54:21 2025 +0100 feat(server,web): embed Web UI into server behind iggy-web feature --- Cargo.lock | 258 +++++++++------------ Cargo.toml | 2 +- DEPENDENCIES.md | 38 ++- bdd/rust/Cargo.toml | 2 +- core/bench/dashboard/server/Cargo.toml | 2 +- core/configs/server.toml | 2 +- core/server/Cargo.toml | 3 + core/server/src/http/http_server.rs | 6 + core/server/src/http/jwt/middleware.rs | 1 + core/server/src/http/mod.rs | 2 + core/server/src/http/web.rs | 82 +++++++ web/package-lock.json | 11 + web/package.json | 2 + web/src/hooks.client.ts | 11 +- web/src/hooks.server.ts | 73 ------ web/src/lib/api/clientApi.ts | 106 +++++++++ web/src/lib/api/fetchRouteApi.ts | 36 ++- web/src/lib/api/handleFetchErrors.ts | 6 +- web/src/lib/auth/authStore.svelte.ts | 125 ++++++++++ web/src/lib/components/Breadcrumbs.svelte | 2 +- web/src/lib/components/Header.svelte | 21 +- .../lib/components/Layouts/SettingsLayout.svelte | 10 +- web/src/lib/components/Logo/Logo.svelte | 5 +- web/src/lib/components/Navbar.svelte | 15 +- web/src/lib/types/appRoutes.ts | 10 +- web/src/routes/+layout.ts | 64 +++++ web/src/routes/api/proxy/+server.ts | 43 ---- .../auth/logout/+page.svelte} | 19 +- .../auth/logout/{+page.server.ts => +page.ts} | 27 +-- web/src/routes/auth/sign-in/+page.server.ts | 77 ------ web/src/routes/auth/sign-in/+page.svelte | 100 +++++--- .../{logout/+page.server.ts => sign-in/+page.ts} | 26 +-- .../dashboard/{+layout.server.ts => +layout.ts} | 45 +++- .../overview/{+page.server.ts => +page.ts} | 13 +- .../dashboard/settings/server/+page.server.ts | 39 ---- .../settings/server/+page.ts} | 13 +- .../settings/users/{+page.server.ts => +page.ts} | 31 +-- web/src/routes/dashboard/streams/+layout.server.ts | 37 --- web/src/routes/dashboard/streams/+layout.svelte | 5 +- .../+page.server.ts => streams/+layout.ts} | 17 +- .../streams/[streamId=i32]/+page.server.ts | 40 ---- .../dashboard/streams/[streamId=i32]/+page.svelte | 3 +- .../streams/[streamId=i32]/+page.ts} | 13 +- .../topics/[topicId=i32]/+page.server.ts | 40 ---- .../topics/[topicId=i32]/+page.svelte | 6 +- .../[streamId=i32]/topics/[topicId=i32]/+page.ts} | 17 +- .../messages/{+page.server.ts => +page.ts} | 29 +-- web/svelte.config.js | 21 +- 48 files changed, 844 insertions(+), 712 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8e8525388..a03e0847e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -27,7 +27,7 @@ checksum = "daa239b93927be1ff123eebada5a3ff23e89f0124ccb8609234e5103d5a5ae6d" dependencies = [ "actix-utils", "actix-web", - "derive_more 2.1.0", + "derive_more", "futures-util", "log", "once_cell", @@ -46,7 +46,7 @@ dependencies = [ "actix-web", "bitflags 2.10.0", "bytes", - "derive_more 2.1.0", + "derive_more", "futures-core", "http-range", "log", @@ -72,7 +72,7 @@ dependencies = [ "brotli", "bytes", "bytestring", - "derive_more 2.1.0", + "derive_more", "encoding_rs", "flate2", "foldhash 0.1.5", @@ -187,7 +187,7 @@ dependencies = [ "bytestring", "cfg-if", "cookie", - "derive_more 2.1.0", + "derive_more", "encoding_rs", "foldhash 0.1.5", "futures-core", @@ -621,7 +621,7 @@ dependencies = [ "memchr", "num", "regex", - "regex-syntax 0.8.8", + "regex-syntax", ] [[package]] @@ -1050,7 +1050,7 @@ dependencies = [ "charming", "colored", "derive-new", - "derive_more 2.1.0", + "derive_more", "human-repr", "rand 0.9.2", "serde", @@ -1619,7 +1619,7 @@ version = "4.5.49" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671" dependencies = [ - "heck 0.5.0", + "heck", "proc-macro2", "quote", "syn 2.0.111", @@ -1930,15 +1930,15 @@ dependencies = [ [[package]] name = "console" -version = "0.15.11" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "054ccb5b10f9f2cbf51eb355ca1d05c2d279ce1804688d0db74b4733a5aeafd8" +checksum = "b430743a6eb14e9764d4260d4c0d8123087d504eeb9c48f2b2a5e810dd369df4" dependencies = [ "encode_unicode", "libc", "once_cell", "unicode-width 0.2.2", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -2242,28 +2242,26 @@ dependencies = [ [[package]] name = "cucumber" -version = "0.21.1" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6cd12917efc3a8b069a4975ef3cb2f2d835d42d04b3814d90838488f9dd9bf69" +checksum = "18c09939b8de21501b829a3839fa8a01ef6cc226e6bc1f5f163f7104bd5e847d" dependencies = [ "anyhow", "clap", "console", "cucumber-codegen", "cucumber-expressions", - "derive_more 0.99.20", - "drain_filter_polyfill", + "derive_more", "either", "futures", "gherkin", "globwalk", "humantime", "inventory", - "itertools 0.13.0", - "lazy-regex", + "itertools 0.14.0", "linked-hash-map", - "once_cell", "pin-project", + "ref-cast", "regex", "sealed", "smart-default", @@ -2271,13 +2269,13 @@ dependencies = [ [[package]] name = "cucumber-codegen" -version = "0.21.1" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e19cd9e8e7cfd79fbf844eb6a7334117973c01f6bad35571262b00891e60f1c" +checksum = "1f5afe541b5147a7b986816153ccfd502622bb37789420cfff412685f27c0a95" dependencies = [ "cucumber-expressions", "inflections", - "itertools 0.13.0", + "itertools 0.14.0", "proc-macro2", "quote", "regex", @@ -2287,16 +2285,16 @@ dependencies = [ [[package]] name = "cucumber-expressions" -version = "0.3.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d794fed319eea24246fb5f57632f7ae38d61195817b7eb659455aa5bdd7c1810" +checksum = "6401038de3af44fe74e6fccdb8a5b7db7ba418f480c8e9ad584c6f65c05a27a6" dependencies = [ - "derive_more 0.99.20", + "derive_more", "either", "nom", "nom_locate", "regex", - "regex-syntax 0.7.5", + "regex-syntax", ] [[package]] @@ -2584,17 +2582,6 @@ dependencies = [ "syn 2.0.111", ] -[[package]] -name = "derive_more" -version = "0.99.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6edb4b64a43d977b8e99788fe3a04d483834fba1215a7e02caa415b626497f7f" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.111", -] - [[package]] name = "derive_more" version = "2.1.0" @@ -2665,7 +2652,7 @@ dependencies = [ "libc", "option-ext", "redox_users", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -2761,12 +2748,6 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1435fa1053d8b2fbbe9be7e97eca7f33d37b28409959813daefc1446a14247f1" -[[package]] -name = "drain_filter_polyfill" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "669a445ee724c5c69b1b06fe0b63e70a1c84bc9bb7d9696cd4f4e3ec45050408" - [[package]] name = "dtoa" version = "1.0.10" @@ -2972,7 +2953,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -3154,9 +3135,9 @@ dependencies = [ [[package]] name = "file-operation" -version = "0.8.6" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e820507a8045dbe4861c925a91ca6989cce6c7c09b4c3d90c4941df84ffc0aeb" +checksum = "be50c359f22a9fc2e61d5c3bf78a097a449d5c03b5d0f0b0c7a2a89c870b3cd3" dependencies = [ "tokio", ] @@ -3491,19 +3472,19 @@ dependencies = [ [[package]] name = "gherkin" -version = "0.14.0" +version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20b79820c0df536d1f3a089a2fa958f61cb96ce9e0f3f8f507f5a31179567755" +checksum = "70197ce7751bfe8bc828e3a855502d3a869a1e9416b58b10c4bde5cf8a0a3cb3" dependencies = [ - "heck 0.4.1", + "heck", "peg", "quote", "serde", "serde_json", "syn 2.0.111", "textwrap", - "thiserror 1.0.69", - "typed-builder 0.15.2", + "thiserror 2.0.17", + "typed-builder 0.23.2", ] [[package]] @@ -3529,7 +3510,7 @@ dependencies = [ "bstr", "log", "regex-automata", - "regex-syntax 0.8.8", + "regex-syntax", ] [[package]] @@ -3912,12 +3893,6 @@ dependencies = [ "stable_deref_trait", ] -[[package]] -name = "heck" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" - [[package]] name = "heck" version = "0.5.0" @@ -4575,7 +4550,7 @@ dependencies = [ "compio-tls", "compio-ws", "crossbeam", - "derive_more 2.1.0", + "derive_more", "err_trail", "fast-async-mutex", "figment", @@ -4905,7 +4880,7 @@ dependencies = [ "chrono", "compio", "ctor", - "derive_more 2.1.0", + "derive_more", "env_logger", "futures", "humantime", @@ -5152,29 +5127,6 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4345964bb142484797b161f473a503a434de77149dd8c7427788c6e13379388" -[[package]] -name = "lazy-regex" -version = "3.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "191898e17ddee19e60bccb3945aa02339e81edd4a8c50e21fd4d48cdecda7b29" -dependencies = [ - "lazy-regex-proc_macros", - "once_cell", - "regex", -] - -[[package]] -name = "lazy-regex-proc_macros" -version = "3.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c35dc8b0da83d1a9507e12122c80dea71a9c7c613014347392483a83ea593e04" -dependencies = [ - "proc-macro2", - "quote", - "regex", - "syn 2.0.111", -] - [[package]] name = "lazy_static" version = "1.5.0" @@ -5478,7 +5430,7 @@ dependencies = [ "lazy_static", "proc-macro2", "quote", - "regex-syntax 0.8.8", + "regex-syntax", "rustc_version", "syn 2.0.111", ] @@ -5629,12 +5581,6 @@ dependencies = [ "unicase", ] -[[package]] -name = "minimal-lexical" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" - [[package]] name = "miniz_oxide" version = "0.8.9" @@ -5730,19 +5676,18 @@ dependencies = [ [[package]] name = "nom" -version = "7.1.3" +version = "8.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" dependencies = [ "memchr", - "minimal-lexical", ] [[package]] name = "nom_locate" -version = "4.2.0" +version = "5.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e3c83c053b0713da60c5b8de47fe8e494fe3ece5267b2f23090a07a053ba8f3" +checksum = "0b577e2d69827c4740cba2b52efaad1c4cc7c73042860b199710b3575c68438d" dependencies = [ "bytecount", "memchr", @@ -5827,7 +5772,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -6272,7 +6217,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.45.0", ] [[package]] @@ -6381,7 +6326,7 @@ checksum = "914a1c2265c98e2446911282c6ac86d8524f495792c38c5bd884f80499c7538a" dependencies = [ "parse-display-derive", "regex", - "regex-syntax 0.8.8", + "regex-syntax", ] [[package]] @@ -6393,7 +6338,7 @@ dependencies = [ "proc-macro2", "quote", "regex", - "regex-syntax 0.8.8", + "regex-syntax", "structmeta", "syn 2.0.111", ] @@ -6873,7 +6818,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9120690fafc389a67ba3803df527d0ec9cbbc9cc45e4cc20b332996dfb672425" dependencies = [ "anyhow", - "itertools 0.14.0", + "itertools 0.13.0", "proc-macro2", "quote", "syn 2.0.111", @@ -7042,7 +6987,7 @@ dependencies = [ "once_cell", "socket2 0.6.1", "tracing", - "windows-sys 0.60.2", + "windows-sys 0.52.0", ] [[package]] @@ -7225,7 +7170,7 @@ dependencies = [ "aho-corasick", "memchr", "regex-automata", - "regex-syntax 0.8.8", + "regex-syntax", ] [[package]] @@ -7236,7 +7181,7 @@ checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" dependencies = [ "aho-corasick", "memchr", - "regex-syntax 0.8.8", + "regex-syntax", ] [[package]] @@ -7245,12 +7190,6 @@ version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8d942b98df5e658f56f20d592c7f868833fe38115e65c33003d8cd224b0155da" -[[package]] -name = "regex-syntax" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da" - [[package]] name = "regex-syntax" version = "0.8.8" @@ -7544,6 +7483,40 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rust-embed" +version = "8.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "947d7f3fad52b283d261c4c99a084937e2fe492248cb9a68a8435a861b8798ca" +dependencies = [ + "rust-embed-impl", + "rust-embed-utils", + "walkdir", +] + +[[package]] +name = "rust-embed-impl" +version = "8.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fa2c8c9e8711e10f9c4fd2d64317ef13feaab820a4c51541f1a8c8e2e851ab2" +dependencies = [ + "proc-macro2", + "quote", + "rust-embed-utils", + "syn 2.0.111", + "walkdir", +] + +[[package]] +name = "rust-embed-utils" +version = "8.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60b161f275cb337fe0a44d924a5f4df0ed69c2c39519858f931ce61c779d3475" +dependencies = [ + "sha2", + "walkdir", +] + [[package]] name = "rust-ini" version = "0.21.3" @@ -7595,7 +7568,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -7663,7 +7636,7 @@ dependencies = [ "security-framework", "security-framework-sys", "webpki-root-certs", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -7787,11 +7760,10 @@ checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" [[package]] name = "sealed" -version = "0.5.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4a8caec23b7800fb97971a1c6ae365b6239aaeddfb934d6265f8505e795699d" +checksum = "22f968c5ea23d555e670b449c1c5e7b2fc399fdaec1d304a17cd48e288abc107" dependencies = [ - "heck 0.4.1", "proc-macro2", "quote", "syn 2.0.111", @@ -7850,7 +7822,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b55fb86dfd3a2f5f76ea78310a88f96c4ea21a3031f8d212443d56123fd0521" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -8100,7 +8072,7 @@ dependencies = [ "cyper", "cyper-axum", "dashmap", - "derive_more 2.1.0", + "derive_more", "dotenvy", "enum_dispatch", "err_trail", @@ -8115,6 +8087,7 @@ dependencies = [ "jsonwebtoken", "lending-iterator", "mimalloc", + "mime_guess", "moka", "nix", "opentelemetry", @@ -8128,6 +8101,7 @@ dependencies = [ "reqwest", "ring", "ringbuffer", + "rust-embed", "rustls", "rustls-pemfile", "send_wrapper", @@ -8301,7 +8275,7 @@ version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c1c97747dbf44bb1ca44a561ece23508e99cb592e862f22222dcf42f51d1e451" dependencies = [ - "heck 0.5.0", + "heck", "proc-macro2", "quote", "syn 2.0.111", @@ -8433,7 +8407,7 @@ checksum = "19a9c1841124ac5a61741f96e1d9e2ec77424bf323962dd894bdb93f37d5219b" dependencies = [ "dotenvy", "either", - "heck 0.5.0", + "heck", "hex", "once_cell", "proc-macro2", @@ -8653,7 +8627,7 @@ version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" dependencies = [ - "heck 0.5.0", + "heck", "proc-macro2", "quote", "rustversion", @@ -8666,7 +8640,7 @@ version = "0.27.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" dependencies = [ - "heck 0.5.0", + "heck", "proc-macro2", "quote", "syn 2.0.111", @@ -8722,9 +8696,9 @@ dependencies = [ [[package]] name = "synthez" -version = "0.3.1" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3d2c2202510a1e186e63e596d9318c91a8cbe85cd1a56a7be0c333e5f59ec8d" +checksum = "6d8a928f38f1bc873f28e0d2ba8298ad65374a6ac2241dabd297271531a736cd" dependencies = [ "syn 2.0.111", "synthez-codegen", @@ -8733,9 +8707,9 @@ dependencies = [ [[package]] name = "synthez-codegen" -version = "0.3.1" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f724aa6d44b7162f3158a57bccd871a77b39a4aef737e01bcdff41f4772c7746" +checksum = "8fb83b8df4238e11746984dfb3819b155cd270de0e25847f45abad56b3671047" dependencies = [ "syn 2.0.111", "synthez-core", @@ -8743,9 +8717,9 @@ dependencies = [ [[package]] name = "synthez-core" -version = "0.3.1" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78bfa6ec52465e2425fd43ce5bbbe0f0b623964f7c63feb6b10980e816c654ea" +checksum = "906fba967105d822e7c7ed60477b5e76116724d33de68a585681fb253fc30d5c" dependencies = [ "proc-macro2", "quote", @@ -8802,7 +8776,7 @@ dependencies = [ "getrandom 0.3.4", "once_cell", "rustix", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -9485,15 +9459,6 @@ dependencies = [ "rand 0.9.2", ] -[[package]] -name = "typed-builder" -version = "0.15.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fe83c85a85875e8c4cb9ce4a890f05b23d38cd0d47647db7895d3d2a79566d2" -dependencies = [ - "typed-builder-macro 0.15.2", -] - [[package]] name = "typed-builder" version = "0.19.1" @@ -9513,14 +9478,12 @@ dependencies = [ ] [[package]] -name = "typed-builder-macro" -version = "0.15.2" +name = "typed-builder" +version = "0.23.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29a3151c41d0b13e3d011f98adc24434560ef06673a155a6c7f66b9879eecce2" +checksum = "31aa81521b70f94402501d848ccc0ecaa8f93c8eb6999eb9747e72287757ffda" dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.111", + "typed-builder-macro 0.23.2", ] [[package]] @@ -9545,6 +9508,17 @@ dependencies = [ "syn 2.0.111", ] +[[package]] +name = "typed-builder-macro" +version = "0.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "076a02dc54dd46795c2e9c8282ed40bcfb1e22747e955de9389a1de28190fb26" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + [[package]] name = "typenum" version = "1.19.0" @@ -10062,7 +10036,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.48.0", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 68679eada..87ecac08c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -161,7 +161,7 @@ reqwest = { version = "0.12.25", default-features = false, features = [ reqwest-middleware = { version = "0.4.2", features = ["json"] } reqwest-retry = "0.8.0" reqwest-tracing = "0.5.8" -rust-s3 = { version = "0.37.0", default-features = false, features = [ +rust-s3 = { version = "0.37.1", default-features = false, features = [ "tokio-rustls-tls", "tags", ] } diff --git a/DEPENDENCIES.md b/DEPENDENCIES.md index c81f89d2d..0e99b3c04 100644 --- a/DEPENDENCIES.md +++ b/DEPENDENCIES.md @@ -162,7 +162,7 @@ compression-codecs: 0.4.33, "Apache-2.0 OR MIT", compression-core: 0.4.31, "Apache-2.0 OR MIT", concurrent-queue: 2.5.0, "Apache-2.0 OR MIT", consensus: 0.1.0, "Apache-2.0", -console: 0.15.11, "MIT", +console: 0.16.1, "MIT", console_error_panic_hook: 0.1.7, "Apache-2.0 OR MIT", const-oid: 0.9.6, "Apache-2.0 OR MIT", const-random: 0.1.18, "Apache-2.0 OR MIT", @@ -196,9 +196,9 @@ ctor: 0.6.3, "Apache-2.0 OR MIT", ctor-proc-macro: 0.0.7, "Apache-2.0 OR MIT", ctr: 0.9.2, "Apache-2.0 OR MIT", ctrlc: 3.5.1, "Apache-2.0 OR MIT", -cucumber: 0.21.1, "Apache-2.0 OR MIT", -cucumber-codegen: 0.21.1, "Apache-2.0 OR MIT", -cucumber-expressions: 0.3.0, "Apache-2.0 OR MIT", +cucumber: 0.22.0, "Apache-2.0 OR MIT", +cucumber-codegen: 0.22.0, "Apache-2.0 OR MIT", +cucumber-expressions: 0.5.0, "Apache-2.0 OR MIT", curve25519-dalek: 4.1.3, "BSD-3-Clause", curve25519-dalek-derive: 0.1.1, "Apache-2.0 OR MIT", cyper: 0.7.1, "MIT", @@ -223,7 +223,6 @@ derive_arbitrary: 1.4.2, "Apache-2.0 OR MIT", derive_builder: 0.20.2, "Apache-2.0 OR MIT", derive_builder_core: 0.20.2, "Apache-2.0 OR MIT", derive_builder_macro: 0.20.2, "Apache-2.0 OR MIT", -derive_more: 0.99.20, "MIT", derive_more: 2.1.0, "MIT", derive_more-impl: 2.1.0, "MIT", difflib: 0.4.0, "MIT", @@ -241,7 +240,6 @@ docker_credential: 1.3.2, "Apache-2.0 OR MIT", document-features: 0.2.12, "Apache-2.0 OR MIT", dotenvy: 0.15.7, "MIT", downcast: 0.11.0, "MIT", -drain_filter_polyfill: 0.1.3, "Apache-2.0 OR MIT", dtoa: 1.0.10, "Apache-2.0 OR MIT", dtor: 0.1.1, "Apache-2.0 OR MIT", dtor-proc-macro: 0.0.6, "Apache-2.0 OR MIT", @@ -281,7 +279,7 @@ ff: 0.13.1, "Apache-2.0 OR MIT", fiat-crypto: 0.2.9, "Apache-2.0 OR BSD-1-Clause OR MIT", figlet-rs: 0.1.5, "Apache-2.0", figment: 0.10.19, "Apache-2.0 OR MIT", -file-operation: 0.8.6, "MIT", +file-operation: 0.8.7, "MIT", filetime: 0.2.26, "Apache-2.0 OR MIT", find-msvc-tools: 0.1.5, "Apache-2.0 OR MIT", flatbuffers: 25.9.23, "Apache-2.0", @@ -317,7 +315,7 @@ generic-array: 0.14.7, "MIT", getrandom: 0.2.16, "Apache-2.0 OR MIT", getrandom: 0.3.4, "Apache-2.0 OR MIT", ghash: 0.5.1, "Apache-2.0 OR MIT", -gherkin: 0.14.0, "Apache-2.0 OR MIT", +gherkin: 0.15.0, "Apache-2.0 OR MIT", git2: 0.20.3, "Apache-2.0 OR MIT", globset: 0.4.18, "MIT OR Unlicense", globwalk: 0.9.1, "MIT", @@ -349,7 +347,6 @@ hashbrown: 0.15.5, "Apache-2.0 OR MIT", hashbrown: 0.16.1, "Apache-2.0 OR MIT", hashlink: 0.10.0, "Apache-2.0 OR MIT", heapless: 0.7.17, "Apache-2.0 OR MIT", -heck: 0.4.1, "Apache-2.0 OR MIT", heck: 0.5.0, "Apache-2.0 OR MIT", hermit-abi: 0.5.2, "Apache-2.0 OR MIT", hex: 0.4.3, "Apache-2.0 OR MIT", @@ -439,8 +436,6 @@ keyring: 3.6.3, "Apache-2.0 OR MIT", kqueue: 1.1.1, "MIT", kqueue-sys: 1.0.4, "MIT", language-tags: 0.3.2, "Apache-2.0 OR MIT", -lazy-regex: 3.4.2, "MIT", -lazy-regex-proc_macros: 3.4.2, "MIT", lazy_static: 1.5.0, "Apache-2.0 OR MIT", lending-iterator: 0.1.7, "Apache-2.0 OR MIT OR Zlib", lending-iterator-proc_macros: 0.1.7, "Apache-2.0 OR MIT OR Zlib", @@ -491,7 +486,6 @@ miette-derive: 7.6.0, "Apache-2.0", mimalloc: 0.1.48, "MIT", mime: 0.3.17, "Apache-2.0 OR MIT", mime_guess: 2.0.5, "MIT", -minimal-lexical: 0.2.1, "Apache-2.0 OR MIT", miniz_oxide: 0.8.9, "Apache-2.0 OR MIT OR Zlib", mio: 1.1.0, "MIT", mockall: 0.14.0, "Apache-2.0 OR MIT", @@ -500,8 +494,8 @@ moka: 0.12.11, "(Apache-2.0 OR MIT) AND Apache-2.0", murmur3: 0.5.2, "Apache-2.0 OR MIT", never-say-never: 6.6.666, "Apache-2.0 OR MIT OR Zlib", nix: 0.30.1, "MIT", -nom: 7.1.3, "MIT", -nom_locate: 4.2.0, "MIT", +nom: 8.0.0, "MIT", +nom_locate: 5.0.0, "MIT", nonzero_ext: 0.3.0, "Apache-2.0", nonzero_lit: 0.1.2, "Apache-2.0 OR CC0-1.0 OR MIT", normalize-line-endings: 0.3.0, "Apache-2.0", @@ -643,7 +637,6 @@ ref-cast-impl: 1.0.25, "Apache-2.0 OR MIT", regex: 1.12.2, "Apache-2.0 OR MIT", regex-automata: 0.4.13, "Apache-2.0 OR MIT", regex-lite: 0.1.8, "Apache-2.0 OR MIT", -regex-syntax: 0.7.5, "Apache-2.0 OR MIT", regex-syntax: 0.8.8, "Apache-2.0 OR MIT", rend: 0.4.2, "MIT", reqsign: 0.16.5, "Apache-2.0", @@ -663,6 +656,9 @@ rmcp-macros: 0.11.0, "MIT", roaring: 0.10.12, "Apache-2.0 OR MIT", route-recognizer: 0.3.1, "MIT", rsa: 0.9.9, "Apache-2.0 OR MIT", +rust-embed: 8.9.0, "MIT", +rust-embed-impl: 8.9.0, "MIT", +rust-embed-utils: 8.9.0, "MIT", rust-ini: 0.21.3, "MIT", rust_decimal: 1.39.0, "MIT", rustc-hash: 2.1.1, "Apache-2.0 OR MIT", @@ -687,7 +683,7 @@ scoped-tls: 1.0.1, "Apache-2.0 OR MIT", scopeguard: 1.2.0, "Apache-2.0 OR MIT", sdd: 3.0.10, "Apache-2.0", seahash: 4.1.0, "MIT", -sealed: 0.5.0, "Apache-2.0 OR MIT", +sealed: 0.6.0, "Apache-2.0 OR MIT", sec1: 0.7.3, "Apache-2.0 OR MIT", secrecy: 0.10.3, "Apache-2.0 OR MIT", security-framework: 3.5.1, "Apache-2.0 OR MIT", @@ -760,9 +756,9 @@ syn: 1.0.109, "Apache-2.0 OR MIT", syn: 2.0.111, "Apache-2.0 OR MIT", sync_wrapper: 1.0.2, "Apache-2.0", synstructure: 0.13.2, "MIT", -synthez: 0.3.1, "BlueOak-1.0.0", -synthez-codegen: 0.3.1, "BlueOak-1.0.0", -synthez-core: 0.3.1, "BlueOak-1.0.0", +synthez: 0.4.0, "BlueOak-1.0.0", +synthez-codegen: 0.4.0, "BlueOak-1.0.0", +synthez-core: 0.4.0, "BlueOak-1.0.0", sysinfo: 0.34.2, "MIT", sysinfo: 0.37.2, "MIT", tagptr: 0.2.0, "Apache-2.0 OR MIT", @@ -825,12 +821,12 @@ trait-variant: 0.1.2, "Apache-2.0 OR MIT", try-lock: 0.2.5, "MIT", tungstenite: 0.28.0, "Apache-2.0 OR MIT", twox-hash: 2.1.2, "MIT", -typed-builder: 0.15.2, "Apache-2.0 OR MIT", typed-builder: 0.19.1, "Apache-2.0 OR MIT", typed-builder: 0.20.1, "Apache-2.0 OR MIT", -typed-builder-macro: 0.15.2, "Apache-2.0 OR MIT", +typed-builder: 0.23.2, "Apache-2.0 OR MIT", typed-builder-macro: 0.19.1, "Apache-2.0 OR MIT", typed-builder-macro: 0.20.1, "Apache-2.0 OR MIT", +typed-builder-macro: 0.23.2, "Apache-2.0 OR MIT", typenum: 1.19.0, "Apache-2.0 OR MIT", ucd-trie: 0.1.7, "Apache-2.0 OR MIT", ulid: 1.2.1, "MIT", diff --git a/bdd/rust/Cargo.toml b/bdd/rust/Cargo.toml index 4182a31d9..5f8fe779b 100644 --- a/bdd/rust/Cargo.toml +++ b/bdd/rust/Cargo.toml @@ -27,7 +27,7 @@ bdd = [] [dev-dependencies] bytes = { workspace = true } -cucumber = "0.21" +cucumber = "0.22" iggy = { workspace = true } integration = { workspace = true } tokio = { workspace = true } diff --git a/core/bench/dashboard/server/Cargo.toml b/core/bench/dashboard/server/Cargo.toml index 217b8de00..c3afed877 100644 --- a/core/bench/dashboard/server/Cargo.toml +++ b/core/bench/dashboard/server/Cargo.toml @@ -30,7 +30,7 @@ bench-report = { workspace = true } chrono = { workspace = true, features = ["serde"] } clap = { workspace = true } dashmap = { workspace = true } -file-operation = "0.8.6" +file-operation = "0.8.7" notify = "8.2.0" octocrab = "0.48.1" serde = { workspace = true, features = ["derive"] } diff --git a/core/configs/server.toml b/core/configs/server.toml index 581763acf..b1a25b61d 100644 --- a/core/configs/server.toml +++ b/core/configs/server.toml @@ -53,7 +53,7 @@ allowed_origins = ["*"] # Lists allowed headers that can be used in CORS requests. # For example, ["content-type"] permits only the content-type header. -allowed_headers = ["content-type"] +allowed_headers = ["content-type", "authorization"] # Headers that browsers are allowed to access in CORS responses. # An empty array means no additional headers are exposed to browsers. diff --git a/core/server/Cargo.toml b/core/server/Cargo.toml index 3688a4fb0..bb375bdf5 100644 --- a/core/server/Cargo.toml +++ b/core/server/Cargo.toml @@ -35,6 +35,7 @@ path = "src/main.rs" default = ["mimalloc"] disable-mimalloc = [] mimalloc = ["dep:mimalloc"] +iggy-web = ["dep:rust-embed", "dep:mime_guess"] [dependencies] ahash = { workspace = true } @@ -74,6 +75,7 @@ iggy_common = { workspace = true } jsonwebtoken = { version = "10.2.0", features = ["rust_crypto"] } lending-iterator = "0.1.7" mimalloc = { workspace = true, optional = true } +mime_guess = { version = "2.0", optional = true } moka = { version = "0.12.11", features = ["future"] } nix = { version = "0.30", features = ["fs", "resource"] } opentelemetry = { version = "0.31.0", features = ["trace", "logs"] } @@ -100,6 +102,7 @@ rand = { workspace = true } reqwest = { workspace = true, features = ["rustls-tls-no-provider"] } ring = "0.17.14" ringbuffer = "0.16.0" +rust-embed = { version = "8.9.0", optional = true } rustls = { workspace = true } rustls-pemfile = "2.2.0" send_wrapper = "0.6.0" diff --git a/core/server/src/http/http_server.rs b/core/server/src/http/http_server.rs index 21e7cbcdb..e83c2191d 100644 --- a/core/server/src/http/http_server.rs +++ b/core/server/src/http/http_server.rs @@ -117,6 +117,12 @@ pub async fn start_http_server( app = app.layer(middleware::from_fn(request_diagnostics)); + #[cfg(feature = "iggy-web")] + { + app = app.merge(web::router()); + info!("Web UI enabled at /ui"); + } + if !config.tls.enabled { let listener = TcpListener::bind(config.address.clone()) .await diff --git a/core/server/src/http/jwt/middleware.rs b/core/server/src/http/jwt/middleware.rs index b2ed614d5..a47d83ad6 100644 --- a/core/server/src/http/jwt/middleware.rs +++ b/core/server/src/http/jwt/middleware.rs @@ -41,6 +41,7 @@ const PUBLIC_PATHS: &[&str] = &[ "/users/login", "/users/refresh-token", "/personal-access-tokens/login", + "/ui", ]; pub async fn jwt_auth( diff --git a/core/server/src/http/mod.rs b/core/server/src/http/mod.rs index 4cf663d36..1d6046ebb 100644 --- a/core/server/src/http/mod.rs +++ b/core/server/src/http/mod.rs @@ -34,5 +34,7 @@ pub mod streams; pub mod system; pub mod topics; pub mod users; +#[cfg(feature = "iggy-web")] +pub mod web; pub const COMPONENT: &str = "HTTP"; diff --git a/core/server/src/http/web.rs b/core/server/src/http/web.rs new file mode 100644 index 000000000..8de59f219 --- /dev/null +++ b/core/server/src/http/web.rs @@ -0,0 +1,82 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +use axum::Router; +use axum::body::Body; +use axum::extract::Path; +use axum::http::{Response, StatusCode, header}; +use axum::response::IntoResponse; +use axum::routing::get; +use rust_embed::{Embed, EmbeddedFile}; + +#[derive(Embed)] +#[folder = "../../web/build/static/"] +struct WebAssets; + +impl WebAssets { + fn get_file(path: &str) -> Option<EmbeddedFile> { + <Self as Embed>::get(path) + } +} + +pub fn router() -> Router { + Router::new() + .route("/ui/{*wildcard}", get(serve_web_asset)) + .route("/ui", get(serve_index)) + .route("/ui/", get(serve_index)) +} + +async fn serve_index() -> impl IntoResponse { + serve_file("index.html") +} + +async fn serve_web_asset(Path(wildcard): Path<String>) -> impl IntoResponse { + if let Some(response) = try_serve_file(&wildcard) { + return response; + } + + if !wildcard.contains('.') { + return serve_file("index.html"); + } + + Response::builder() + .status(StatusCode::NOT_FOUND) + .body(Body::from("Not Found")) + .unwrap() +} + +fn try_serve_file(path: &str) -> Option<Response<Body>> { + let asset = WebAssets::get_file(path)?; + let mime = mime_guess::from_path(path).first_or_octet_stream(); + + Some( + Response::builder() + .status(StatusCode::OK) + .header(header::CONTENT_TYPE, mime.as_ref()) + .body(Body::from(asset.data.into_owned())) + .unwrap(), + ) +} + +fn serve_file(path: &str) -> Response<Body> { + try_serve_file(path).unwrap_or_else(|| { + Response::builder() + .status(StatusCode::NOT_FOUND) + .body(Body::from("Not Found")) + .unwrap() + }) +} diff --git a/web/package-lock.json b/web/package-lock.json index d8f1a6e49..8dd77898c 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -25,6 +25,7 @@ "@eslint/compat": "^2.0.0", "@eslint/js": "^9.39.1", "@sveltejs/adapter-node": "^5.4.0", + "@sveltejs/adapter-static": "^3.0.0", "@sveltejs/kit": "^2.48.5", "@sveltejs/vite-plugin-svelte": "^6.2.1", "@types/d3-interpolate": "^3.0.4", @@ -1461,6 +1462,16 @@ "@sveltejs/kit": "^2.4.0" } }, + "node_modules/@sveltejs/adapter-static": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@sveltejs/adapter-static/-/adapter-static-3.0.10.tgz", + "integrity": "sha512-7D9lYFWJmB7zxZyTE/qxjksvMqzMuYrrsyh1f4AlZqeZeACPRySjbC3aFiY55wb1tWUaKOQG9PVbm74JcN2Iew==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@sveltejs/kit": "^2.0.0" + } + }, "node_modules/@sveltejs/kit": { "version": "2.49.0", "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.49.0.tgz", diff --git a/web/package.json b/web/package.json index 63800cb17..cd91929f0 100644 --- a/web/package.json +++ b/web/package.json @@ -5,6 +5,7 @@ "scripts": { "dev": "vite dev --port 3050", "build": "vite build", + "build:static": "STATIC_BUILD=true PUBLIC_IGGY_API_URL= vite build", "preview": "vite preview", "server": "vite server", "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", @@ -16,6 +17,7 @@ "@eslint/compat": "^2.0.0", "@eslint/js": "^9.39.1", "@sveltejs/adapter-node": "^5.4.0", + "@sveltejs/adapter-static": "^3.0.0", "@sveltejs/kit": "^2.48.5", "@sveltejs/vite-plugin-svelte": "^6.2.1", "@types/d3-interpolate": "^3.0.4", diff --git a/web/src/hooks.client.ts b/web/src/hooks.client.ts index d3f825db2..e5fb6b240 100644 --- a/web/src/hooks.client.ts +++ b/web/src/hooks.client.ts @@ -17,12 +17,7 @@ * under the License. */ -import type { HandleClientError } from '@sveltejs/kit'; +import { authStore } from '$lib/auth/authStore.svelte'; -export const handleError: HandleClientError = async ({ error }) => { - console.log('client error handler', error); - return { - message: 'Whoops!', - errorId: 1 - }; -}; +// Initialize auth store when the app loads +authStore.initialize(); diff --git a/web/src/hooks.server.ts b/web/src/hooks.server.ts deleted file mode 100644 index 6285ec07a..000000000 --- a/web/src/hooks.server.ts +++ /dev/null @@ -1,73 +0,0 @@ -/** - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { env } from '$env/dynamic/public'; -import { checkIfPathnameIsPublic, typedRoute } from '$lib/types/appRoutes'; -import { tokens } from '$lib/utils/constants/tokens'; -import type { Handle } from '@sveltejs/kit'; -import { sequence } from '@sveltejs/kit/hooks'; -import { redirect } from '@sveltejs/kit'; - -console.log(`Iggy API URL: ${env.PUBLIC_IGGY_API_URL}`); - -const accessTokenOkRedirects = [ - { - from: '/dashboard/', - to: typedRoute('/dashboard/overview') - }, - { - from: '/dashboard', - to: typedRoute('/dashboard/overview') - }, - { - from: '/', - to: typedRoute('/dashboard/overview') - }, - { - from: '/auth', - to: typedRoute('/dashboard/overview') - }, - { - from: typedRoute('/auth/sign-in'), - to: typedRoute('/dashboard/overview') - } -]; - -const handleAuth: Handle = async ({ event, resolve }) => { - const cookies = event.cookies; - const isPublicPath = checkIfPathnameIsPublic(event.url.pathname); - const accessToken = cookies.get(tokens.accessToken); - - if (!accessToken) { - if (isPublicPath) { - return resolve(event); - } else { - redirect(302, typedRoute('/auth/sign-in')); - } - } - - const invalidPathRedirect = accessTokenOkRedirects.find((r) => r.from === event.url.pathname); - if (invalidPathRedirect) { - redirect(302, invalidPathRedirect.to); - } - - return resolve(event); -}; - -export const handle = sequence(handleAuth); diff --git a/web/src/lib/api/clientApi.ts b/web/src/lib/api/clientApi.ts new file mode 100644 index 000000000..fb1372371 --- /dev/null +++ b/web/src/lib/api/clientApi.ts @@ -0,0 +1,106 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { browser } from '$app/environment'; +import { goto } from '$app/navigation'; +import { resolve } from '$app/paths'; +import { env } from '$env/dynamic/public'; +import { authStore } from '$lib/auth/authStore.svelte'; +import { typedRoute } from '$lib/types/appRoutes'; +import { error } from '@sveltejs/kit'; +import { getJson } from './getJson'; + +export interface ApiRequest { + path: string; + method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'; + body?: unknown; + queryParams?: Record<string, string>; +} + +export async function clientApi<T = unknown>(args: ApiRequest): Promise<T> { + const { path, method, queryParams, body } = args; + + const headers = new Headers(); + headers.set('Content-Type', 'application/json'); + + const token = authStore.getAccessToken(); + if (token) { + headers.set('Authorization', `Bearer ${token}`); + } + + // Use PUBLIC_IGGY_API_URL if set, otherwise use relative path (for embedded mode) + const baseUrl = env.PUBLIC_IGGY_API_URL || ''; + let fullUrl = `${baseUrl}${path}`; + + if (queryParams) { + const query = new URLSearchParams(Object.entries(queryParams)); + fullUrl += '?' + query.toString(); + } + + const response = await fetch(fullUrl, { + headers, + method, + ...(body ? { body: JSON.stringify(body) } : {}) + }); + + const data = await getJson(response); + + if (response.ok) { + return data as T; + } + + // Handle errors + if (response.status === 401 || response.status === 403) { + if (browser) { + authStore.logout(); + } + error(401, { message: 'Unauthorized' }); + } + + if (response.status === 404) { + error(404, { message: 'Not Found' }); + } + + if (response.status === 400) { + // Return the error data for form validation + throw { status: 400, data }; + } + + error(500, { message: 'Internal server error' }); +} + +/** + * Wrapper that handles errors gracefully for load functions + */ +export async function clientApiSafe<T = unknown>( + args: ApiRequest +): Promise<{ data: T | null; error: string | null }> { + try { + const data = await clientApi<T>(args); + return { data, error: null }; + } catch (e: any) { + if (e?.status === 401 || e?.status === 403) { + if (browser) { + goto(resolve(typedRoute('/auth/sign-in'))); + } + return { data: null, error: 'Unauthorized' }; + } + return { data: null, error: e?.message || 'Unknown error' }; + } +} diff --git a/web/src/lib/api/fetchRouteApi.ts b/web/src/lib/api/fetchRouteApi.ts index 894c094e2..aa36c3359 100644 --- a/web/src/lib/api/fetchRouteApi.ts +++ b/web/src/lib/api/fetchRouteApi.ts @@ -17,15 +17,45 @@ * under the License. */ +import { env } from '$env/dynamic/public'; +import { authStore } from '$lib/auth/authStore.svelte'; import type { ApiSchema } from './ApiSchema'; +import { convertBigIntsToStrings } from './convertBigIntsToStrings'; import { getJson } from './getJson'; export const fetchRouteApi = async ( - arg: ApiSchema + arg: ApiSchema & { queryParams?: Record<string, string> } ): Promise<{ data: any; status: number; ok: boolean }> => { try { - const res = await fetch('/api/proxy', { body: JSON.stringify(arg), method: 'POST' }); - return (await getJson(res)) as any; + const { path, method, queryParams } = arg; + + const headers = new Headers(); + headers.set('Content-Type', 'application/json'); + + const token = authStore.getAccessToken(); + if (token) { + headers.set('Authorization', `Bearer ${token}`); + } + + // Use PUBLIC_IGGY_API_URL if set, otherwise use relative path (for embedded mode) + const baseUrl = env.PUBLIC_IGGY_API_URL || ''; + let fullUrl = `${baseUrl}${path}`; + + if (queryParams) { + const query = new URLSearchParams(Object.entries(queryParams)); + fullUrl += '?' + query.toString(); + } + + const res = await fetch(fullUrl, { + headers, + method, + ...('body' in arg && arg.body ? { body: JSON.stringify(arg.body) } : {}) + }); + + const data = await getJson(res); + const safeData = convertBigIntsToStrings(data); + + return { data: safeData, status: res.status, ok: res.ok }; } catch (err) { throw new Error('fetchRouteApi error'); } diff --git a/web/src/lib/api/handleFetchErrors.ts b/web/src/lib/api/handleFetchErrors.ts index b3bb75f66..3ef5559fc 100644 --- a/web/src/lib/api/handleFetchErrors.ts +++ b/web/src/lib/api/handleFetchErrors.ts @@ -18,6 +18,7 @@ */ import { error, type Cookies, redirect } from '@sveltejs/kit'; +import { base } from '$app/paths'; import { getJson } from './getJson'; import { tokens } from '$lib/utils/constants/tokens'; import { typedRoute } from '$lib/types/appRoutes'; @@ -57,15 +58,14 @@ export const handleFetchErrors = async ( }; }, 401: () => { - // TODO: Refresh token console.log(`handleErrorStatus: 401 ${response.url}`); removeCookies(); - redirect(302, typedRoute('/auth/sign-in')); + redirect(302, `${base}${typedRoute('/auth/sign-in')}`); }, 403: () => { console.log(`handleErrorStatus: 403 ${response.url}`); removeCookies(); - redirect(302, typedRoute('/auth/sign-in')); + redirect(302, `${base}${typedRoute('/auth/sign-in')}`); }, 404: () => { console.log(`handleErrorStatus: 404 ${response.url}`); diff --git a/web/src/lib/auth/authStore.svelte.ts b/web/src/lib/auth/authStore.svelte.ts new file mode 100644 index 000000000..fdd5511e0 --- /dev/null +++ b/web/src/lib/auth/authStore.svelte.ts @@ -0,0 +1,125 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { browser } from '$app/environment'; +import { goto } from '$app/navigation'; +import { resolve } from '$app/paths'; +import { typedRoute } from '$lib/types/appRoutes'; +import { tokens } from '$lib/utils/constants/tokens'; +import { jwtDecode } from 'jwt-decode'; + +export interface AuthState { + isAuthenticated: boolean; + accessToken: string | null; + userId: number | null; +} + +function createAuthStore() { + let state = $state<AuthState>({ + isAuthenticated: false, + accessToken: null, + userId: null + }); + + function getTokenFromCookie(): string | null { + if (!browser) return null; + const match = document.cookie.match(new RegExp(`(^| )${tokens.accessToken}=([^;]+)`)); + return match ? match[2] : null; + } + + function setTokenCookie(token: string, expiry: number): void { + if (!browser) return; + const expires = new Date(expiry * 1000); + document.cookie = `${tokens.accessToken}=${token}; path=/; expires=${expires.toUTCString()}; SameSite=Lax`; + } + + function clearTokenCookie(): void { + if (!browser) return; + document.cookie = `${tokens.accessToken}=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT`; + } + + function initialize(): void { + const token = getTokenFromCookie(); + if (token) { + try { + const decoded = jwtDecode(token); + const userId = decoded.sub ? parseInt(decoded.sub, 10) : null; + state = { + isAuthenticated: true, + accessToken: token, + userId + }; + } catch { + clearTokenCookie(); + state = { isAuthenticated: false, accessToken: null, userId: null }; + } + } + } + + function login(token: string, expiry: number): void { + setTokenCookie(token, expiry); + try { + const decoded = jwtDecode(token); + const userId = decoded.sub ? parseInt(decoded.sub, 10) : null; + state = { + isAuthenticated: true, + accessToken: token, + userId + }; + } catch { + state = { isAuthenticated: true, accessToken: token, userId: null }; + } + } + + function logout(): void { + clearTokenCookie(); + state = { isAuthenticated: false, accessToken: null, userId: null }; + goto(resolve(typedRoute('/auth/sign-in'))); + } + + function getAccessToken(): string | null { + return state.accessToken || getTokenFromCookie(); + } + + // Initialize on creation if in browser + if (browser) { + initialize(); + } + + return { + get state() { + return state; + }, + get isAuthenticated() { + return state.isAuthenticated; + }, + get accessToken() { + return state.accessToken; + }, + get userId() { + return state.userId; + }, + initialize, + login, + logout, + getAccessToken + }; +} + +export const authStore = createAuthStore(); diff --git a/web/src/lib/components/Breadcrumbs.svelte b/web/src/lib/components/Breadcrumbs.svelte index b90f70e2d..d83adef32 100644 --- a/web/src/lib/components/Breadcrumbs.svelte +++ b/web/src/lib/components/Breadcrumbs.svelte @@ -65,7 +65,7 @@ return { path, label: segment }; } - let parts = $derived(page.url.pathname.split('/').filter(Boolean).slice(1)); + let parts = $derived(page.url.pathname.replace(/^\/ui/, '').split('/').filter(Boolean).slice(1)); let crumbs = $derived(parts.map((segment, index) => formatPathSegment(segment, index, parts))); </script> diff --git a/web/src/lib/components/Header.svelte b/web/src/lib/components/Header.svelte index 4f4388565..4906aba8f 100644 --- a/web/src/lib/components/Header.svelte +++ b/web/src/lib/components/Header.svelte @@ -26,11 +26,12 @@ import DropdownMenu from './DropdownMenu/DropdownMenu.svelte'; import { theme } from './ThemeController.svelte'; import { typedRoute } from '$lib/types/appRoutes'; + import { resolve } from '$app/paths'; import Button from './Button.svelte'; import StopPropagation from './StopPropagation.svelte'; interface Props { - user: User; + user: User | null; } let { user }: Props = $props(); @@ -63,7 +64,7 @@ {#snippet children({ close: _close })} <div class="p-1 min-w-[150px] transition-all duration-200 flex flex-col text-sm text-color"> <span class="flex items-center justify-between gap-2 border-b px-2 py-3"> - <span>{user.username}</span> + <span>{user?.username ?? 'User'}</span> <Icon name="user" class="w-[24px] h-[24px] stroke-black dark:stroke-white" @@ -98,15 +99,13 @@ /> </label> </div> - <form class="w-full" method="POST" action={typedRoute('/auth/logout')}> - <button - class="flex w-full items-center justify-between gap-2 px-2 py-2 hover:bg-shade-l300 dark:hover:bg-shade-d1000 rounded-md my-1 dark:hover:text-white" - > - <span>Log Out</span> - - <Icon name="logout" /> - </button> - </form> + <a + href={resolve(typedRoute('/auth/logout'))} + class="flex w-full items-center justify-between gap-2 px-2 py-2 hover:bg-shade-l300 dark:hover:bg-shade-d1000 rounded-md my-1 dark:hover:text-white" + > + <span>Log Out</span> + <Icon name="logout" /> + </a> </div> {/snippet} </DropdownMenu> diff --git a/web/src/lib/components/Layouts/SettingsLayout.svelte b/web/src/lib/components/Layouts/SettingsLayout.svelte index a2b28ba4a..ee7fd6912 100644 --- a/web/src/lib/components/Layouts/SettingsLayout.svelte +++ b/web/src/lib/components/Layouts/SettingsLayout.svelte @@ -41,25 +41,25 @@ tab: 'server', icon: 'adjustments', name: 'Server', - href: typedRoute('/dashboard/settings/server') + href: resolve(typedRoute('/dashboard/settings/server')) }, { tab: 'webUI', icon: 'settings', name: 'Web UI', - href: typedRoute('/dashboard/settings/webUI') + href: resolve(typedRoute('/dashboard/settings/webUI')) }, { tab: 'users', icon: 'usersGroup', name: 'Users', - href: typedRoute('/dashboard/settings/users') + href: resolve(typedRoute('/dashboard/settings/users')) } // { // name: 'Terminal', // icon: 'terminal', // tab: 'terminal', - // href: typedRoute('/dashboard/settings/terminal') + // href: resolve(typedRoute('/dashboard/settings/terminal')) // } ] satisfies { tab: Tabs; name: string; icon: iconType; href: string }[]; </script> @@ -74,7 +74,7 @@ {#each tabs as { icon, name, href }, idx (idx)} {@const isActive = activeTab === href.split('/').slice(-1)[0]} <a - href={resolve(href)} + {href} class={twMerge('pb-3 relative group flex items-center justify-start gap-2 text-color')} > <Icon name={icon} class="w-[15px] h-[15px]" /> diff --git a/web/src/lib/components/Logo/Logo.svelte b/web/src/lib/components/Logo/Logo.svelte index dd561ef0a..002456b2d 100644 --- a/web/src/lib/components/Logo/Logo.svelte +++ b/web/src/lib/components/Logo/Logo.svelte @@ -18,10 +18,11 @@ --> <script lang="ts"> + import { base } from '$app/paths'; let className = ''; export { className as class }; - const lightLogo = '/iggy-apache-lightbg.svg'; - const darkLogo = '/iggy-apache-darkbg.svg'; + const lightLogo = `${base}/iggy-apache-lightbg.svg`; + const darkLogo = `${base}/iggy-apache-darkbg.svg`; </script> <img src={lightLogo} class="{className} block dark:hidden" alt="Apache Iggy" /> diff --git a/web/src/lib/components/Navbar.svelte b/web/src/lib/components/Navbar.svelte index fd0c1c307..55524b703 100644 --- a/web/src/lib/components/Navbar.svelte +++ b/web/src/lib/components/Navbar.svelte @@ -21,42 +21,41 @@ import Icon from './Icon.svelte'; import type { iconType } from './Icon.svelte'; import { page } from '$app/state'; + import { resolve } from '$app/paths'; import { twMerge } from 'tailwind-merge'; import { tooltip } from '$lib/actions/tooltip'; import { typedRoute } from '$lib/types/appRoutes'; import LogoType from '$lib/components/Logo/LogoType.svelte'; import LogoMark from '$lib/components/Logo/LogoMark.svelte'; - import { resolve } from '$app/paths'; - let navItems = $derived([ { name: 'Overview', icon: 'home', - href: typedRoute('/dashboard/overview'), + href: resolve(typedRoute('/dashboard/overview')), active: page.url.pathname.includes(typedRoute('/dashboard/overview')) }, { name: 'Streams', icon: 'stream', - href: typedRoute('/dashboard/streams'), + href: resolve(typedRoute('/dashboard/streams')), active: page.url.pathname.includes(typedRoute('/dashboard/streams')) }, // { // name: 'Clients', // icon: 'clients', - // href: typedRoute('/dashboard/clients'), + // href: resolve(typedRoute('/dashboard/clients')), // active: page.url.pathname.includes(typedRoute('/dashboard/clients')) // }, // { // name: 'Logs', // icon: 'logs', - // href: typedRoute('/dashboard/logs'), + // href: resolve(typedRoute('/dashboard/logs')), // active: page.url.pathname.includes(typedRoute('/dashboard/logs')) // }, { name: 'Settings', icon: 'settings', - href: typedRoute('/dashboard/settings/webUI'), + href: resolve(typedRoute('/dashboard/settings/webUI')), active: page.url.pathname.includes('/dashboard/settings') } ] satisfies { name: string; icon: iconType; href: string; active: boolean }[]); @@ -78,7 +77,7 @@ <li> <div use:tooltip={{ placement: 'right' }}> <a - href={resolve(href)} + {href} data-trigger class={twMerge( 'p-2 block rounded-xl transition-colors ring-2 ring-transparent', diff --git a/web/src/lib/types/appRoutes.ts b/web/src/lib/types/appRoutes.ts index 17823b40a..2078467db 100644 --- a/web/src/lib/types/appRoutes.ts +++ b/web/src/lib/types/appRoutes.ts @@ -32,7 +32,11 @@ type DashboardRoutes = `/dashboard/${ type AuthRoutes = `/auth/${'sign-in' | 'logout'}`; export const typedRoute = <const T extends DashboardRoutes | AuthRoutes>(route: T) => route; -export const publicRoutes = [typedRoute('/auth/sign-in'), '/auth/test'] as const; +export const publicRoutes = ['/auth/sign-in', '/auth/logout', '/auth/test'] as const; -export const checkIfPathnameIsPublic = (pathname: string) => - (publicRoutes as ReadonlyArray<string>).includes(pathname); +export const checkIfPathnameIsPublic = (pathname: string) => { + const pathWithoutBase = pathname.replace(/^\/ui/, '') || '/'; + return (publicRoutes as ReadonlyArray<string>).some( + (route) => pathWithoutBase === route || pathWithoutBase.startsWith(route + '/') + ); +}; diff --git a/web/src/routes/+layout.ts b/web/src/routes/+layout.ts new file mode 100644 index 000000000..7a0657895 --- /dev/null +++ b/web/src/routes/+layout.ts @@ -0,0 +1,64 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { browser } from '$app/environment'; +import { goto } from '$app/navigation'; +import { base, resolve } from '$app/paths'; +import { authStore } from '$lib/auth/authStore.svelte'; +import { checkIfPathnameIsPublic, typedRoute } from '$lib/types/appRoutes'; +import type { LayoutLoad } from './$types'; + +// Enable client-side rendering for SPA mode +export const ssr = false; +export const prerender = false; + +export const load: LayoutLoad = async ({ url }) => { + if (browser) { + authStore.initialize(); + } + + const pathname = url.pathname; + const isPublicPath = checkIfPathnameIsPublic(pathname); + const isAuthenticated = authStore.getAccessToken() !== null; + + const authRedirects = [ + base, + `${base}/`, + `${base}/dashboard`, + `${base}/dashboard/`, + `${base}/auth`, + `${base}/auth/sign-in` + ]; + + if (browser) { + if (!isAuthenticated && !isPublicPath) { + goto(resolve(typedRoute('/auth/sign-in'))); + return { isAuthenticated: false }; + } + + if (isAuthenticated && authRedirects.includes(pathname)) { + goto(resolve(typedRoute('/dashboard/overview'))); + return { isAuthenticated: true }; + } + } + + return { + isAuthenticated + }; +}; diff --git a/web/src/routes/api/proxy/+server.ts b/web/src/routes/api/proxy/+server.ts deleted file mode 100644 index 70d091b0e..000000000 --- a/web/src/routes/api/proxy/+server.ts +++ /dev/null @@ -1,43 +0,0 @@ -/** - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { fetchIggyApi } from '$lib/api/fetchApi'; -import { handleFetchErrors } from '$lib/api/handleFetchErrors'; -import { error, json, type RequestHandler } from '@sveltejs/kit'; -import { convertBigIntsToStrings } from '$lib/api/convertBigIntsToStrings'; - -export const POST: RequestHandler = async ({ request, cookies }) => { - const { path, body, method, queryParams } = await request.json(); - - if (!path || !method) { - const message = `routes/api/proxy/+server.ts no path or body or method provided`; - console.error(message); - error(500, { - message - }); - } - - const result = await fetchIggyApi({ body, path, method, cookies, queryParams }); - - const { data, response } = await handleFetchErrors(result, cookies); - - const safeData = convertBigIntsToStrings(data); - - return json({ data: safeData, ok: response.ok, status: response.status }); -}; diff --git a/web/src/lib/components/Logo/Logo.svelte b/web/src/routes/auth/logout/+page.svelte similarity index 64% copy from web/src/lib/components/Logo/Logo.svelte copy to web/src/routes/auth/logout/+page.svelte index dd561ef0a..6401055af 100644 --- a/web/src/lib/components/Logo/Logo.svelte +++ b/web/src/routes/auth/logout/+page.svelte @@ -18,11 +18,18 @@ --> <script lang="ts"> - let className = ''; - export { className as class }; - const lightLogo = '/iggy-apache-lightbg.svg'; - const darkLogo = '/iggy-apache-darkbg.svg'; + import { onMount } from 'svelte'; + import { goto } from '$app/navigation'; + import { resolve } from '$app/paths'; + import { authStore } from '$lib/auth/authStore.svelte'; + import { typedRoute } from '$lib/types/appRoutes'; + + onMount(() => { + authStore.logout(); + goto(resolve(typedRoute('/auth/sign-in'))); + }); </script> -<img src={lightLogo} class="{className} block dark:hidden" alt="Apache Iggy" /> -<img src={darkLogo} class="{className} hidden dark:block" alt="Apache Iggy" /> +<div class="flex items-center justify-center h-full"> + <span class="text-color">Logging out...</span> +</div> diff --git a/web/src/routes/auth/logout/+page.server.ts b/web/src/routes/auth/logout/+page.ts similarity index 67% copy from web/src/routes/auth/logout/+page.server.ts copy to web/src/routes/auth/logout/+page.ts index e2d7ac8af..5de770f46 100644 --- a/web/src/routes/auth/logout/+page.server.ts +++ b/web/src/routes/auth/logout/+page.ts @@ -17,21 +17,18 @@ * under the License. */ +import { browser } from '$app/environment'; +import { goto } from '$app/navigation'; +import { resolve } from '$app/paths'; +import { authStore } from '$lib/auth/authStore.svelte'; import { typedRoute } from '$lib/types/appRoutes'; -import { tokens } from '$lib/utils/constants/tokens.js'; -import { redirect, type Actions } from '@sveltejs/kit'; +import type { PageLoad } from './$types'; -export const actions = { - default({ cookies }) { - // eat the cookie - cookies.set(tokens.accessToken, '', { - path: '/', - expires: new Date(0) - }); - - console.log('deleting cookie'); - - // redirect the user - redirect(302, typedRoute('/auth/sign-in')); +export const load: PageLoad = async () => { + if (browser) { + authStore.logout(); + goto(resolve(typedRoute('/auth/sign-in'))); } -} satisfies Actions; + + return {}; +}; diff --git a/web/src/routes/auth/sign-in/+page.server.ts b/web/src/routes/auth/sign-in/+page.server.ts deleted file mode 100644 index f840df92f..000000000 --- a/web/src/routes/auth/sign-in/+page.server.ts +++ /dev/null @@ -1,77 +0,0 @@ -/** - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { fail, redirect } from '@sveltejs/kit'; -import type { Actions } from './$types'; -import { message, superValidate } from 'sveltekit-superforms/server'; -import { zod4 } from 'sveltekit-superforms/adapters'; -import { z } from 'zod'; -import { typedRoute } from '$lib/types/appRoutes'; -import { fetchIggyApi } from '$lib/api/fetchApi'; -import { tokens } from '$lib/utils/constants/tokens'; -import { getJson } from '$lib/api/getJson'; - -const schema = z.object({ - username: z.string().min(1), - password: z.string().min(4) -}); - -type FormSchema = z.infer<typeof schema>; - -export const load = async () => { - const form = await superValidate(zod4(schema)); - - return { form }; -}; - -export const actions = { - default: async ({ request, cookies }) => { - const form = await superValidate(request, zod4(schema)); - - if (!form.valid) { - return fail(400, { form }); - } - - const { password, username } = form.data; - - const result = await fetchIggyApi({ - method: 'POST', - path: '/users/login', - body: { username, password } - }); - - console.log(result); - - if (!(result instanceof Response) || !result.ok) { - return message(form, 'Username or password is not valid', { status: 403 }); - } - - const { access_token } = (await getJson(result)) as any; - - cookies.set(tokens.accessToken, access_token.token, { - path: '/', - httpOnly: true, - sameSite: 'lax', - secure: true, - expires: new Date(1000 * access_token.expiry) - }); - - redirect(302, typedRoute('/dashboard/overview')); - } -} satisfies Actions; diff --git a/web/src/routes/auth/sign-in/+page.svelte b/web/src/routes/auth/sign-in/+page.svelte index 0183cf15e..31e0aca1b 100644 --- a/web/src/routes/auth/sign-in/+page.svelte +++ b/web/src/routes/auth/sign-in/+page.svelte @@ -18,40 +18,87 @@ --> <script lang="ts"> + import { goto } from '$app/navigation'; + import { resolve } from '$app/paths'; + import { env } from '$env/dynamic/public'; + import { authStore } from '$lib/auth/authStore.svelte'; import Button from '$lib/components/Button.svelte'; import Checkbox from '$lib/components/Checkbox.svelte'; import Icon from '$lib/components/Icon.svelte'; import Input from '$lib/components/Input.svelte'; import PasswordInput from '$lib/components/PasswordInput.svelte'; + import { typedRoute } from '$lib/types/appRoutes'; import { persistedStore } from '$lib/utils/persistedStore.js'; import { onMount } from 'svelte'; - import { superForm } from 'sveltekit-superforms/client'; - interface Props { - data: any; - } - - let { data }: Props = $props(); - const { form, constraints, errors, message } = superForm(data.form, {}); + let username = $state(''); + let password = $state(''); + let errorMessage = $state(''); + let isLoading = $state(false); + let usernameError = $state(''); + let passwordError = $state(''); const remember = persistedStore('rememberMe', { rememberMe: true, username: '' }); onMount(() => { if ($remember.rememberMe) { - $form.username = $remember.username; + username = $remember.username; } }); -</script> -<form - method="POST" - onsubmit={() => { + async function handleSubmit(event: SubmitEvent) { + event.preventDefault(); + errorMessage = ''; + usernameError = ''; + passwordError = ''; + + // Validate + if (!username) { + usernameError = 'Username is required'; + return; + } + if (!password || password.length < 4) { + passwordError = 'Password must be at least 4 characters'; + return; + } + + // Remember username if checked if ($remember.rememberMe) { - $remember.username = $form.username; + $remember.username = username; } else { $remember.username = ''; } - }} + + isLoading = true; + + try { + const baseUrl = env.PUBLIC_IGGY_API_URL || ''; + const response = await fetch(`${baseUrl}/users/login`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ username, password }) + }); + + if (!response.ok) { + errorMessage = 'Username or password is not valid'; + isLoading = false; + return; + } + + const data = await response.json(); + const { access_token } = data; + + authStore.login(access_token.token, access_token.expiry); + goto(resolve(typedRoute('/dashboard/overview'))); + } catch (e) { + errorMessage = 'Failed to connect to server'; + isLoading = false; + } + } +</script> + +<form + onsubmit={handleSubmit} class="min-w-[350px] max-w-[400px] bg-white dark:bg-shade-d700 border text-color p-5 rounded-2xl card-shadow dark:shadow-lg flex flex-col gap-5" > <span class="mx-auto font-semibold">Admin sign in</span> @@ -60,26 +107,23 @@ label="Username" name="username" autocomplete="username" - errorMessage={Array.isArray($errors?.username) - ? $errors.username.join(',') - : String($errors?.username || '')} - bind:value={$form.username} - {...$constraints.username} + errorMessage={usernameError} + bind:value={username} + required /> <PasswordInput label="Password" name="password" autocomplete="current-password" - errorMessage={Array.isArray($errors?.password) - ? $errors.password.join(',') - : String($errors?.password || '')} - bind:value={$form.password} - {...$constraints.password} + errorMessage={passwordError} + bind:value={password} + required + minlength={4} /> - {#if $message} - <span class="text-sm mx-auto text-red">{$message}</span> + {#if errorMessage} + <span class="text-sm mx-auto text-red-500">{errorMessage}</span> {/if} <div class="flex justify-between items-center"> @@ -89,8 +133,8 @@ </label> </div> - <Button variant="contained" size="lg" class="mt-7" type="submit"> - <span> Login </span> + <Button variant="contained" size="lg" class="mt-7" type="submit" disabled={isLoading}> + <span>{isLoading ? 'Logging in...' : 'Login'}</span> <Icon name="login" class="w-[23px] h-[23px]" /> </Button> diff --git a/web/src/routes/auth/logout/+page.server.ts b/web/src/routes/auth/sign-in/+page.ts similarity index 67% rename from web/src/routes/auth/logout/+page.server.ts rename to web/src/routes/auth/sign-in/+page.ts index e2d7ac8af..5ca57c99b 100644 --- a/web/src/routes/auth/logout/+page.server.ts +++ b/web/src/routes/auth/sign-in/+page.ts @@ -17,21 +17,17 @@ * under the License. */ +import { browser } from '$app/environment'; +import { goto } from '$app/navigation'; +import { resolve } from '$app/paths'; +import { authStore } from '$lib/auth/authStore.svelte'; import { typedRoute } from '$lib/types/appRoutes'; -import { tokens } from '$lib/utils/constants/tokens.js'; -import { redirect, type Actions } from '@sveltejs/kit'; +import type { PageLoad } from './$types'; -export const actions = { - default({ cookies }) { - // eat the cookie - cookies.set(tokens.accessToken, '', { - path: '/', - expires: new Date(0) - }); - - console.log('deleting cookie'); - - // redirect the user - redirect(302, typedRoute('/auth/sign-in')); +export const load: PageLoad = async () => { + if (browser && authStore.getAccessToken()) { + goto(resolve(typedRoute('/dashboard/overview'))); } -} satisfies Actions; + + return {}; +}; diff --git a/web/src/routes/dashboard/+layout.server.ts b/web/src/routes/dashboard/+layout.ts similarity index 50% rename from web/src/routes/dashboard/+layout.server.ts rename to web/src/routes/dashboard/+layout.ts index 5d0c105f2..30b80d565 100644 --- a/web/src/routes/dashboard/+layout.server.ts +++ b/web/src/routes/dashboard/+layout.ts @@ -17,22 +17,47 @@ * under the License. */ -import { fetchIggyApi } from '$lib/api/fetchApi'; -import { handleFetchErrors } from '$lib/api/handleFetchErrors'; +import { browser } from '$app/environment'; +import { goto } from '$app/navigation'; +import { resolve } from '$app/paths'; +import { clientApi } from '$lib/api/clientApi'; +import { authStore } from '$lib/auth/authStore.svelte'; import { userDetailsMapper } from '$lib/domain/UserDetails'; -import type { LayoutServerLoad } from './$types'; +import { typedRoute } from '$lib/types/appRoutes'; import { jwtDecode } from 'jwt-decode'; +import type { LayoutLoad } from './$types'; + +export const load: LayoutLoad = async () => { + if (browser) { + authStore.initialize(); + } + + const token = authStore.getAccessToken(); + if (browser && !token) { + goto(resolve(typedRoute('/auth/sign-in'))); + return { user: null }; + } -export const load: LayoutServerLoad = async ({ cookies }) => { const getDetailedUser = async () => { - //always available here, auth hook prevents rendering this page without access_token - const accessToken = cookies.get('access_token')!; - const userId = jwtDecode(accessToken).sub!; + let userId = authStore.userId; + + if (!userId && token) { + try { + const decoded = jwtDecode(token); + userId = decoded.sub ? parseInt(decoded.sub, 10) : null; + } catch { + return null; + } + } - const userResult = await fetchIggyApi({ method: 'GET', path: `/users/${+userId}`, cookies }); - const { data } = await handleFetchErrors(userResult, cookies); + if (!userId) return null; - return userDetailsMapper(data); + try { + const data = await clientApi({ method: 'GET', path: `/users/${userId}` }); + return userDetailsMapper(data); + } catch { + return null; + } }; return { diff --git a/web/src/routes/dashboard/overview/+page.server.ts b/web/src/routes/dashboard/overview/+page.ts similarity index 76% copy from web/src/routes/dashboard/overview/+page.server.ts copy to web/src/routes/dashboard/overview/+page.ts index 3eac60a15..5f88bb588 100644 --- a/web/src/routes/dashboard/overview/+page.server.ts +++ b/web/src/routes/dashboard/overview/+page.ts @@ -17,19 +17,16 @@ * under the License. */ -import { fetchIggyApi } from '$lib/api/fetchApi'; -import { handleFetchErrors } from '$lib/api/handleFetchErrors'; +import { clientApi } from '$lib/api/clientApi'; import { statsMapper } from '$lib/domain/Stats'; +import type { PageLoad } from './$types'; -export const load = async ({ cookies }) => { - const result = await fetchIggyApi({ +export const load: PageLoad = async () => { + const data = await clientApi({ path: '/stats', - method: 'GET', - cookies + method: 'GET' }); - const { data } = await handleFetchErrors(result, cookies); - return { stats: statsMapper(data) }; diff --git a/web/src/routes/dashboard/settings/server/+page.server.ts b/web/src/routes/dashboard/settings/server/+page.server.ts deleted file mode 100644 index bff846f2c..000000000 --- a/web/src/routes/dashboard/settings/server/+page.server.ts +++ /dev/null @@ -1,39 +0,0 @@ -/** - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { fetchIggyApi } from '$lib/api/fetchApi'; -import { handleFetchErrors } from '$lib/api/handleFetchErrors'; -import { statsMapper } from '$lib/domain/Stats'; - -export const load = async ({ cookies }) => { - const getStats = async () => { - const result = await fetchIggyApi({ - method: 'GET', - path: '/stats', - cookies - }); - - const { data } = await handleFetchErrors(result, cookies); - return statsMapper(data); - }; - - return { - serverStats: await getStats() - }; -}; diff --git a/web/src/routes/+layout.server.ts b/web/src/routes/dashboard/settings/server/+page.ts similarity index 73% copy from web/src/routes/+layout.server.ts copy to web/src/routes/dashboard/settings/server/+page.ts index 27bb9c06f..5bf4759bc 100644 --- a/web/src/routes/+layout.server.ts +++ b/web/src/routes/dashboard/settings/server/+page.ts @@ -17,10 +17,17 @@ * under the License. */ -import type { LayoutServerLoad } from './$types'; +import { clientApi } from '$lib/api/clientApi'; +import { statsMapper } from '$lib/domain/Stats'; +import type { PageLoad } from './$types'; + +export const load: PageLoad = async () => { + const data = await clientApi({ + method: 'GET', + path: '/stats' + }); -export const load: LayoutServerLoad = async ({ locals }) => { return { - user: locals.user + serverStats: statsMapper(data) }; }; diff --git a/web/src/routes/dashboard/settings/users/+page.server.ts b/web/src/routes/dashboard/settings/users/+page.ts similarity index 63% rename from web/src/routes/dashboard/settings/users/+page.server.ts rename to web/src/routes/dashboard/settings/users/+page.ts index 2f2344585..73fc12615 100644 --- a/web/src/routes/dashboard/settings/users/+page.server.ts +++ b/web/src/routes/dashboard/settings/users/+page.ts @@ -17,33 +17,28 @@ * under the License. */ -import { fetchIggyApi } from '$lib/api/fetchApi'; -import { handleFetchErrors } from '$lib/api/handleFetchErrors'; +import { clientApi } from '$lib/api/clientApi'; import { streamListMapper } from '$lib/domain/Stream'; -import { streamDetailsMapper } from '$lib/domain/StreamDetails.js'; +import { streamDetailsMapper } from '$lib/domain/StreamDetails'; +import { userMapper, type User } from '$lib/domain/User'; +import type { PageLoad } from './$types'; -import { userMapper, type User } from '$lib/domain/User.js'; - -export const load = async ({ cookies }) => { +export const load: PageLoad = async () => { const getUsers = async () => { - const result = await fetchIggyApi({ + const data = await clientApi<any[]>({ method: 'GET', - path: '/users', - cookies + path: '/users' }); - const { data } = await handleFetchErrors(result, cookies); - return (data as any).map((item: any) => userMapper(item)) as User[]; + return data.map((item: any) => userMapper(item)) as User[]; }; const getStreams = async () => { - const result = await fetchIggyApi({ + const data = await clientApi<any[]>({ method: 'GET', - path: '/streams', - cookies + path: '/streams' }); - const { data } = await handleFetchErrors(result, cookies); const streams = streamListMapper(data); if (streams.length === 0) { @@ -53,12 +48,10 @@ export const load = async ({ cookies }) => { }; } - const streamDetailResult = await fetchIggyApi({ + const streamDetailsData = await clientApi({ method: 'GET', - path: `/streams/${streams[0].id}`, - cookies + path: `/streams/${streams[0].id}` }); - const { data: streamDetailsData } = await handleFetchErrors(streamDetailResult, cookies); return { streams, diff --git a/web/src/routes/dashboard/streams/+layout.server.ts b/web/src/routes/dashboard/streams/+layout.server.ts deleted file mode 100644 index 0e0650ea2..000000000 --- a/web/src/routes/dashboard/streams/+layout.server.ts +++ /dev/null @@ -1,37 +0,0 @@ -/** - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { fetchIggyApi } from '$lib/api/fetchApi'; -import { handleFetchErrors } from '$lib/api/handleFetchErrors'; -import { streamMapper, type Stream } from '$lib/domain/Stream'; -import type { LayoutServerLoad } from './$types'; - -export const load: LayoutServerLoad = async ({ cookies }) => { - const result = await fetchIggyApi({ - method: 'GET', - path: '/streams', - cookies - }); - - const { data } = await handleFetchErrors(result, cookies); - - return { - streams: ((data as any).map(streamMapper) as Stream[]).sort((a, b) => b.createdAt - a.createdAt) - }; -}; diff --git a/web/src/routes/dashboard/streams/+layout.svelte b/web/src/routes/dashboard/streams/+layout.svelte index bc66222b4..99b8d91d0 100644 --- a/web/src/routes/dashboard/streams/+layout.svelte +++ b/web/src/routes/dashboard/streams/+layout.svelte @@ -41,7 +41,10 @@ let filteredData = $derived(data.streams.filter((stream) => stream.name.includes(searchQuery))); onMount(() => { - if (data.streams.length > 0 && page.url.pathname === typedRoute('/dashboard/streams')) { + if ( + data.streams.length > 0 && + page.url.pathname === resolve(typedRoute('/dashboard/streams')) + ) { goto(resolve(typedRoute(`/dashboard/streams/${data.streams[0].id}`))); } }); diff --git a/web/src/routes/dashboard/overview/+page.server.ts b/web/src/routes/dashboard/streams/+layout.ts similarity index 69% copy from web/src/routes/dashboard/overview/+page.server.ts copy to web/src/routes/dashboard/streams/+layout.ts index 3eac60a15..711b9977c 100644 --- a/web/src/routes/dashboard/overview/+page.server.ts +++ b/web/src/routes/dashboard/streams/+layout.ts @@ -17,20 +17,17 @@ * under the License. */ -import { fetchIggyApi } from '$lib/api/fetchApi'; -import { handleFetchErrors } from '$lib/api/handleFetchErrors'; -import { statsMapper } from '$lib/domain/Stats'; +import { clientApi } from '$lib/api/clientApi'; +import { streamMapper, type Stream } from '$lib/domain/Stream'; +import type { LayoutLoad } from './$types'; -export const load = async ({ cookies }) => { - const result = await fetchIggyApi({ - path: '/stats', +export const load: LayoutLoad = async () => { + const data = await clientApi<any[]>({ method: 'GET', - cookies + path: '/streams' }); - const { data } = await handleFetchErrors(result, cookies); - return { - stats: statsMapper(data) + streams: (data.map(streamMapper) as Stream[]).sort((a, b) => b.createdAt - a.createdAt) }; }; diff --git a/web/src/routes/dashboard/streams/[streamId=i32]/+page.server.ts b/web/src/routes/dashboard/streams/[streamId=i32]/+page.server.ts deleted file mode 100644 index 3b22b2cbf..000000000 --- a/web/src/routes/dashboard/streams/[streamId=i32]/+page.server.ts +++ /dev/null @@ -1,40 +0,0 @@ -/** - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { fetchIggyApi } from '$lib/api/fetchApi.js'; -import { handleFetchErrors } from '$lib/api/handleFetchErrors'; -import { streamDetailsMapper } from '$lib/domain/StreamDetails'; - -export const load = async ({ params, cookies }) => { - const getStreamDetails = async () => { - const result = await fetchIggyApi({ - method: 'GET', - path: `/streams/${+params.streamId}`, - cookies - }); - - const { data } = await handleFetchErrors(result, cookies); - - return streamDetailsMapper(data); - }; - - return { - streamDetails: await getStreamDetails() - }; -}; diff --git a/web/src/routes/dashboard/streams/[streamId=i32]/+page.svelte b/web/src/routes/dashboard/streams/[streamId=i32]/+page.svelte index ee2da4431..a4869cea5 100644 --- a/web/src/routes/dashboard/streams/[streamId=i32]/+page.svelte +++ b/web/src/routes/dashboard/streams/[streamId=i32]/+page.svelte @@ -19,6 +19,7 @@ <script lang="ts"> import { page } from '$app/state'; + import { resolve } from '$app/paths'; import Button from '$lib/components/Button.svelte'; import Icon from '$lib/components/Icon.svelte'; import { openModal } from '$lib/components/Modals/AppModals.svelte'; @@ -85,7 +86,7 @@ rowClass="grid grid-cols-[150px_3fr_2fr_2fr_2fr_2fr_3fr]" data={stream.topics} hrefBuilder={(topic) => - typedRoute(`/dashboard/streams/${+(page.params.streamId || '')}/topics/${topic.id}`)} + resolve(typedRoute(`/dashboard/streams/${+(page.params.streamId || '')}/topics/${topic.id}`))} colNames={{ id: 'ID', name: 'Name', diff --git a/web/src/routes/+layout.server.ts b/web/src/routes/dashboard/streams/[streamId=i32]/+page.ts similarity index 70% rename from web/src/routes/+layout.server.ts rename to web/src/routes/dashboard/streams/[streamId=i32]/+page.ts index 27bb9c06f..8ac07677d 100644 --- a/web/src/routes/+layout.server.ts +++ b/web/src/routes/dashboard/streams/[streamId=i32]/+page.ts @@ -17,10 +17,17 @@ * under the License. */ -import type { LayoutServerLoad } from './$types'; +import { clientApi } from '$lib/api/clientApi'; +import { streamDetailsMapper } from '$lib/domain/StreamDetails'; +import type { PageLoad } from './$types'; + +export const load: PageLoad = async ({ params }) => { + const data = await clientApi({ + method: 'GET', + path: `/streams/${+params.streamId}` + }); -export const load: LayoutServerLoad = async ({ locals }) => { return { - user: locals.user + streamDetails: streamDetailsMapper(data) }; }; diff --git a/web/src/routes/dashboard/streams/[streamId=i32]/topics/[topicId=i32]/+page.server.ts b/web/src/routes/dashboard/streams/[streamId=i32]/topics/[topicId=i32]/+page.server.ts deleted file mode 100644 index 5b0c49451..000000000 --- a/web/src/routes/dashboard/streams/[streamId=i32]/topics/[topicId=i32]/+page.server.ts +++ /dev/null @@ -1,40 +0,0 @@ -/** - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { fetchIggyApi } from '$lib/api/fetchApi'; -import { handleFetchErrors } from '$lib/api/handleFetchErrors'; -import { topicDetailsMapper } from '$lib/domain/TopicDetails'; - -export const load = async ({ params, cookies }) => { - const getTopic = async () => { - const result = await fetchIggyApi({ - method: 'GET', - path: `/streams/${+params.streamId}/topics/${+params.topicId}`, - cookies - }); - - const { data } = await handleFetchErrors(result, cookies); - - return topicDetailsMapper(data); - }; - - return { - topic: await getTopic() - }; -}; diff --git a/web/src/routes/dashboard/streams/[streamId=i32]/topics/[topicId=i32]/+page.svelte b/web/src/routes/dashboard/streams/[streamId=i32]/topics/[topicId=i32]/+page.svelte index 6cb4c4673..0a4601ede 100644 --- a/web/src/routes/dashboard/streams/[streamId=i32]/topics/[topicId=i32]/+page.svelte +++ b/web/src/routes/dashboard/streams/[streamId=i32]/topics/[topicId=i32]/+page.svelte @@ -92,8 +92,10 @@ rowClass="grid grid-cols-[150px_1fr_1fr_1fr_1fr_1fr]" data={topic.partitions} hrefBuilder={(partition) => - typedRoute( - `/dashboard/streams/${+(page.params.streamId || '')}/topics/${topic.id}/partitions/${partition.id}/messages` + resolve( + typedRoute( + `/dashboard/streams/${+(page.params.streamId || '')}/topics/${topic.id}/partitions/${partition.id}/messages` + ) )} colNames={{ id: 'ID', diff --git a/web/src/routes/dashboard/overview/+page.server.ts b/web/src/routes/dashboard/streams/[streamId=i32]/topics/[topicId=i32]/+page.ts similarity index 69% rename from web/src/routes/dashboard/overview/+page.server.ts rename to web/src/routes/dashboard/streams/[streamId=i32]/topics/[topicId=i32]/+page.ts index 3eac60a15..78626a88a 100644 --- a/web/src/routes/dashboard/overview/+page.server.ts +++ b/web/src/routes/dashboard/streams/[streamId=i32]/topics/[topicId=i32]/+page.ts @@ -17,20 +17,17 @@ * under the License. */ -import { fetchIggyApi } from '$lib/api/fetchApi'; -import { handleFetchErrors } from '$lib/api/handleFetchErrors'; -import { statsMapper } from '$lib/domain/Stats'; +import { clientApi } from '$lib/api/clientApi'; +import { topicDetailsMapper } from '$lib/domain/TopicDetails'; +import type { PageLoad } from './$types'; -export const load = async ({ cookies }) => { - const result = await fetchIggyApi({ - path: '/stats', +export const load: PageLoad = async ({ params }) => { + const data = await clientApi({ method: 'GET', - cookies + path: `/streams/${+params.streamId}/topics/${+params.topicId}` }); - const { data } = await handleFetchErrors(result, cookies); - return { - stats: statsMapper(data) + topic: topicDetailsMapper(data) }; }; diff --git a/web/src/routes/dashboard/streams/[streamId=i32]/topics/[topicId=i32]/partitions/[partitionId=i32]/messages/+page.server.ts b/web/src/routes/dashboard/streams/[streamId=i32]/topics/[topicId=i32]/partitions/[partitionId=i32]/messages/+page.ts similarity index 79% rename from web/src/routes/dashboard/streams/[streamId=i32]/topics/[topicId=i32]/partitions/[partitionId=i32]/messages/+page.server.ts rename to web/src/routes/dashboard/streams/[streamId=i32]/topics/[topicId=i32]/partitions/[partitionId=i32]/messages/+page.ts index 7bf5ba315..b5bdcec87 100644 --- a/web/src/routes/dashboard/streams/[streamId=i32]/topics/[topicId=i32]/partitions/[partitionId=i32]/messages/+page.server.ts +++ b/web/src/routes/dashboard/streams/[streamId=i32]/topics/[topicId=i32]/partitions/[partitionId=i32]/messages/+page.ts @@ -17,22 +17,21 @@ * under the License. */ -import { fetchIggyApi } from '$lib/api/fetchApi'; -import { handleFetchErrors } from '$lib/api/handleFetchErrors'; +import { clientApi } from '$lib/api/clientApi'; import { partitionMessagesDetailsMapper } from '$lib/domain/MessageDetails'; import { topicDetailsMapper } from '$lib/domain/TopicDetails'; +import type { PageLoad } from './$types'; const MESSAGES_PER_PAGE = 20; -export const load = async ({ params, cookies, url }) => { - const offset = url.searchParams.get('offset') || '0'; +export const load: PageLoad = async ({ params, url }) => { const direction = url.searchParams.get('direction') || 'desc'; const getPartitionMessages = async () => { - const initialResult = await fetchIggyApi({ + // First, get the initial offset to determine total messages + const initialData = await clientApi<any>({ method: 'GET', path: `/streams/${+params.streamId}/topics/${+params.topicId}/messages`, - cookies, queryParams: { kind: 'offset', value: '0', @@ -42,18 +41,15 @@ export const load = async ({ params, cookies, url }) => { } }); - const { data: initialData } = await handleFetchErrors(initialResult, cookies); - const initialMessages = partitionMessagesDetailsMapper(initialData as any); - + const initialMessages = partitionMessagesDetailsMapper(initialData); const totalMessages = initialMessages.currentOffset + 1; const offset = url.searchParams.get('offset') ?? (direction === 'desc' ? Math.max(0, totalMessages - MESSAGES_PER_PAGE).toString() : '0'); - const result = await fetchIggyApi({ + const data = await clientApi<any>({ method: 'GET', path: `/streams/${+params.streamId}/topics/${+params.topicId}/messages`, - cookies, queryParams: { kind: 'offset', value: offset.toString(), @@ -63,23 +59,22 @@ export const load = async ({ params, cookies, url }) => { } }); - const { data } = await handleFetchErrors(result, cookies); - return partitionMessagesDetailsMapper(data as any); + return partitionMessagesDetailsMapper(data); }; const getTopic = async () => { - const result = await fetchIggyApi({ + const data = await clientApi({ method: 'GET', - path: `/streams/${+params.streamId}/topics/${+params.topicId}`, - cookies + path: `/streams/${+params.streamId}/topics/${+params.topicId}` }); - const { data } = await handleFetchErrors(result, cookies); return topicDetailsMapper(data); }; const [partitionMessages, topic] = await Promise.all([getPartitionMessages(), getTopic()]); + const offset = url.searchParams.get('offset') || '0'; + return { partitionMessages, topic, diff --git a/web/svelte.config.js b/web/svelte.config.js index 34cdf47c8..0685bbe46 100644 --- a/web/svelte.config.js +++ b/web/svelte.config.js @@ -17,15 +17,28 @@ * under the License. */ -import adapter from '@sveltejs/adapter-node'; +import adapterNode from '@sveltejs/adapter-node'; +import adapterStatic from '@sveltejs/adapter-static'; import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; +// Use static adapter when STATIC_BUILD env is set (for embedding in Rust server) +const useStaticAdapter = process.env.STATIC_BUILD === 'true'; + /** @type {import('@sveltejs/kit').Config} */ const config = { kit: { - adapter: adapter({ - out: 'build' - }), + adapter: useStaticAdapter + ? adapterStatic({ + pages: 'build/static', + assets: 'build/static', + fallback: 'index.html' + }) + : adapterNode({ + out: 'build' + }), + paths: { + base: useStaticAdapter ? '/ui' : '' + }, csrf: { trustedOrigins: ['*'] }
