diff --git a/bun.lock b/bun.lock index 4879342..911687d 100644 --- a/bun.lock +++ b/bun.lock @@ -8,7 +8,6 @@ "@aws-sdk/client-s3": "^3.500.0", "@getgitops/gitdb": "^0.2.0", "@google-cloud/storage": "^7.21.0", - "better-sqlite3": "^12.11.1", "lucide-svelte": "^0.320.0", }, "devDependencies": { @@ -424,22 +423,14 @@ "baseline-browser-mapping": ["baseline-browser-mapping@2.10.43", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-AjYpR78kDWAY3Efj+cDTFH9t9SCoL7OoTp1BOb0mQV7S+6CiLwnWM3FyxhJtdPufDFKzmCSFoUncKjWgJEZTCQ=="], - "better-sqlite3": ["better-sqlite3@12.11.1", "", { "dependencies": { "bindings": "^1.5.0", "prebuild-install": "^7.1.1" } }, "sha512-dq9AtApgg5PGFtBzPFSBl3HZQjHok5gaQCM6zh2Yk0aSmDCs1CbnVI8/HgASQkNKsWFpseIO9beg5xxpYhbIfA=="], - "bignumber.js": ["bignumber.js@9.3.1", "", {}, "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ=="], - "bindings": ["bindings@1.5.0", "", { "dependencies": { "file-uri-to-path": "1.0.0" } }, "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ=="], - - "bl": ["bl@4.1.0", "", { "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", "readable-stream": "^3.4.0" } }, "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w=="], - "bowser": ["bowser@2.14.1", "", {}, "sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg=="], "brace-expansion": ["brace-expansion@1.1.16", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw=="], "browserslist": ["browserslist@4.28.6", "", { "dependencies": { "baseline-browser-mapping": "^2.10.42", "caniuse-lite": "^1.0.30001803", "electron-to-chromium": "^1.5.389", "node-releases": "^2.0.51", "update-browserslist-db": "^1.2.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-FQBYNK15VMslhLHpA7+n+n1GOlF1kId2xcCg7/j95f24AOF6VDYMNH4mFxF7KuaTdv627faazpOAjFzMrfJOUw=="], - "buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="], - "buffer-equal-constant-time": ["buffer-equal-constant-time@1.0.1", "", {}, "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="], "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], @@ -454,8 +445,6 @@ "chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="], - "chownr": ["chownr@1.1.4", "", {}, "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="], - "code-red": ["code-red@1.0.4", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15", "@types/estree": "^1.0.1", "acorn": "^8.10.0", "estree-walker": "^3.0.3", "periscopic": "^3.1.0" } }, "sha512-7qJWqItLA8/VPVlKJlFXU+NBlo/qyfs39aJcuMT/2ere32ZqvF5OSxgdM5xOfJJ7O429gg2HM47y8v9P+9wrNw=="], "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], @@ -478,10 +467,6 @@ "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], - "decompress-response": ["decompress-response@6.0.0", "", { "dependencies": { "mimic-response": "^3.1.0" } }, "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ=="], - - "deep-extend": ["deep-extend@0.6.0", "", {}, "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA=="], - "deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="], "deepmerge": ["deepmerge@4.3.1", "", {}, "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="], @@ -548,8 +533,6 @@ "event-target-shim": ["event-target-shim@5.0.1", "", {}, "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ=="], - "expand-template": ["expand-template@2.0.3", "", {}, "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg=="], - "expect-type": ["expect-type@1.4.0", "", {}, "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA=="], "extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="], @@ -568,8 +551,6 @@ "file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="], - "file-uri-to-path": ["file-uri-to-path@1.0.0", "", {}, "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw=="], - "find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="], "flat-cache": ["flat-cache@4.0.1", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" } }, "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw=="], @@ -580,8 +561,6 @@ "fraction.js": ["fraction.js@5.3.4", "", {}, "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ=="], - "fs-constants": ["fs-constants@1.0.0", "", {}, "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow=="], - "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], @@ -594,8 +573,6 @@ "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], - "github-from-package": ["github-from-package@0.0.0", "", {}, "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw=="], - "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], "globals": ["globals@15.15.0", "", {}, "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg=="], @@ -628,8 +605,6 @@ "husky": ["husky@9.1.7", "", { "bin": { "husky": "bin.js" } }, "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA=="], - "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], - "ignore": ["ignore@7.0.6", "", {}, "sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw=="], "import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="], @@ -640,8 +615,6 @@ "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], - "ini": ["ini@1.3.8", "", {}, "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="], - "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], @@ -736,14 +709,8 @@ "mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], - "mimic-response": ["mimic-response@3.1.0", "", {}, "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ=="], - "minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="], - "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], - - "mkdirp-classic": ["mkdirp-classic@0.5.3", "", {}, "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A=="], - "mri": ["mri@1.2.0", "", {}, "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA=="], "mrmime": ["mrmime@2.0.1", "", {}, "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ=="], @@ -752,12 +719,8 @@ "nanoid": ["nanoid@3.3.16", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q=="], - "napi-build-utils": ["napi-build-utils@2.0.0", "", {}, "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA=="], - "natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="], - "node-abi": ["node-abi@3.94.0", "", { "dependencies": { "semver": "^7.3.5" } }, "sha512-W5ZNO5KRPB5TkYmGVD9F6YqhsglXJzE6etpbmT+f6EQElhiX/UTG551cnsRGvLG3fyZEg9HwaDmNmj5nwJ4z9g=="], - "node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="], "node-releases": ["node-releases@2.0.51", "", {}, "sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ=="], @@ -800,20 +763,14 @@ "postcss-value-parser": ["postcss-value-parser@4.2.0", "", {}, "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="], - "prebuild-install": ["prebuild-install@7.1.3", "", { "dependencies": { "detect-libc": "^2.0.0", "expand-template": "^2.0.3", "github-from-package": "0.0.0", "minimist": "^1.2.3", "mkdirp-classic": "^0.5.3", "napi-build-utils": "^2.0.0", "node-abi": "^3.3.0", "pump": "^3.0.0", "rc": "^1.2.7", "simple-get": "^4.0.0", "tar-fs": "^2.0.0", "tunnel-agent": "^0.6.0" }, "bin": { "prebuild-install": "bin.js" } }, "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug=="], - "prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="], "prettier": ["prettier@3.9.5", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-/FVl766LpUfB5vXgCYOYa0MeV/441Ia99AeICQIQFTY/Nw0roZwULcXpku5i1/m5kt/baz+s4Zogspd839HSMg=="], "prettier-plugin-svelte": ["prettier-plugin-svelte@3.5.2", "", { "peerDependencies": { "prettier": "^3.0.0", "svelte": "^3.2.0 || ^4.0.0-next.0 || ^5.0.0-next.0" } }, "sha512-ItFouLvzSFE3ulNl4DKoWM3BGcbDCNVpIyy/Y3F2gC3aNiGLxtFUdffVqO5Z5hhYG+DFT5KULWaxmeFFpdbvaQ=="], - "pump": ["pump@3.0.4", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA=="], - "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], - "rc": ["rc@1.2.8", "", { "dependencies": { "deep-extend": "^0.6.0", "ini": "~1.3.0", "minimist": "^1.2.0", "strip-json-comments": "~2.0.1" }, "bin": { "rc": "./cli.js" } }, "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw=="], - "readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], "readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="], @@ -842,10 +799,6 @@ "siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="], - "simple-concat": ["simple-concat@1.0.1", "", {}, "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q=="], - - "simple-get": ["simple-get@4.0.1", "", { "dependencies": { "decompress-response": "^6.0.0", "once": "^1.3.1", "simple-concat": "^1.0.0" } }, "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA=="], - "sirv": ["sirv@3.0.2", "", { "dependencies": { "@polka/url": "^1.0.0-next.24", "mrmime": "^2.0.0", "totalist": "^3.0.0" } }, "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g=="], "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], @@ -880,10 +833,6 @@ "tapable": ["tapable@2.3.3", "", {}, "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A=="], - "tar-fs": ["tar-fs@2.1.5", "", { "dependencies": { "chownr": "^1.1.1", "mkdirp-classic": "^0.5.2", "pump": "^3.0.0", "tar-stream": "^2.1.4" } }, "sha512-OboTd8mmMhZDNPV+UjQcK9yKAatXu2aJ+r1w4im1Otd4M4fl2hwvdoXUxIYHFTHWK/3y3FarBP70v3vwmGlOxw=="], - - "tar-stream": ["tar-stream@2.2.0", "", { "dependencies": { "bl": "^4.0.3", "end-of-stream": "^1.4.1", "fs-constants": "^1.0.0", "inherits": "^2.0.3", "readable-stream": "^3.1.1" } }, "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ=="], - "teeny-request": ["teeny-request@9.0.0", "", { "dependencies": { "http-proxy-agent": "^5.0.0", "https-proxy-agent": "^5.0.0", "node-fetch": "^2.6.9", "stream-events": "^1.0.5", "uuid": "^9.0.0" } }, "sha512-resvxdc6Mgb7YEThw6G6bExlXKkv6+YbuzGg9xuXxSgxJF7Ozs+o8Y9+2R3sArdWdW8nOokoQb1yrpFB0pQK2g=="], "tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="], @@ -902,8 +851,6 @@ "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - "tunnel-agent": ["tunnel-agent@0.6.0", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w=="], - "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="], "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], @@ -970,8 +917,6 @@ "http-proxy-agent/agent-base": ["agent-base@6.0.2", "", { "dependencies": { "debug": "4" } }, "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ=="], - "rc/strip-json-comments": ["strip-json-comments@2.0.1", "", {}, "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ=="], - "svelte-eslint-parser/eslint-scope": ["eslint-scope@7.2.2", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg=="], "svelte-eslint-parser/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], diff --git a/package.json b/package.json index 9c89225..a7ca65c 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,6 @@ "@aws-sdk/client-s3": "^3.500.0", "@getgitops/gitdb": "^0.2.0", "@google-cloud/storage": "^7.21.0", - "better-sqlite3": "^12.11.1", "lucide-svelte": "^0.320.0" } } diff --git a/src/hooks.server.ts b/src/hooks.server.ts index 754e89c..02dcc9c 100644 --- a/src/hooks.server.ts +++ b/src/hooks.server.ts @@ -24,7 +24,7 @@ export const handle: Handle = async ({ event, resolve }) => { if (event.url.pathname.startsWith('/settings') || event.url.pathname.startsWith('/api/system')) { if (!canAccessAdminArea(currentUser)) { - return new Response(null, { status: 302, headers: { location: '/vault' } }); + return new Response(null, { status: 302, headers: { location: '/' } }); } } diff --git a/src/lib/components/AppNavbar.svelte b/src/lib/components/AppNavbar.svelte index 7a71341..0b08a16 100644 --- a/src/lib/components/AppNavbar.svelte +++ b/src/lib/components/AppNavbar.svelte @@ -1,9 +1,16 @@ + +
-
+
GitOps - + {#if user} + + {/if} + +
+ -
{#if user} -
+
{#if showUserDropdown} - - -
+

{user.username}

{user.role.name} Account

- - - Profile - -
+ + + Profile + +
-
\ No newline at end of file + diff --git a/src/lib/components/AppSidebar.svelte b/src/lib/components/AppSidebar.svelte index e67296d..fe63604 100644 --- a/src/lib/components/AppSidebar.svelte +++ b/src/lib/components/AppSidebar.svelte @@ -1,149 +1,181 @@ \ No newline at end of file + diff --git a/src/lib/database/schemas.ts b/src/lib/database/schemas.ts index 697122b..dd5c620 100644 --- a/src/lib/database/schemas.ts +++ b/src/lib/database/schemas.ts @@ -7,9 +7,15 @@ export const UserEntity = entity('users', { passwordHash: text().notNull(), roleId: uuid().notNull(), status: text().notNull().default('active'), - authProviders: json().notNull().$defaultFn(() => []), - createdAt: timestamp().notNull().$defaultFn(() => new Date().toISOString()), - updatedAt: timestamp().notNull().$defaultFn(() => new Date().toISOString()), + authProviders: json() + .notNull() + .$defaultFn(() => []), + createdAt: timestamp() + .notNull() + .$defaultFn(() => new Date().toISOString()), + updatedAt: timestamp() + .notNull() + .$defaultFn(() => new Date().toISOString()), lastLoginAt: timestamp(), disabled: bool().notNull().default(false), }); @@ -18,9 +24,15 @@ export const RoleEntity = entity('roles', { id: uuid().primaryKey(), slug: text().notNull(), name: text().notNull(), - permissions: json().notNull().$defaultFn(() => []), - createdAt: timestamp().notNull().$defaultFn(() => new Date().toISOString()), - updatedAt: timestamp().notNull().$defaultFn(() => new Date().toISOString()), + permissions: json() + .notNull() + .$defaultFn(() => []), + createdAt: timestamp() + .notNull() + .$defaultFn(() => new Date().toISOString()), + updatedAt: timestamp() + .notNull() + .$defaultFn(() => new Date().toISOString()), }); export const ApiKeyEntity = entity('api_keys', { @@ -32,7 +44,9 @@ export const ApiKeyEntity = entity('api_keys', { expiresAt: timestamp(), lastUsedAt: timestamp(), revokedAt: timestamp(), - createdAt: timestamp().notNull().$defaultFn(() => new Date().toISOString()), + createdAt: timestamp() + .notNull() + .$defaultFn(() => new Date().toISOString()), }); export const relations = defineRelations(); @@ -44,4 +58,45 @@ relations.for(UserEntity, ({ one, many }) => ({ relations.for(ApiKeyEntity, ({ one }) => ({ user: one(UserEntity, { fields: ['userId'], references: ['id'] }), -})); \ No newline at end of file +})); + +export const ProjectEntity = entity('projects', { + id: uuid().primaryKey(), + slug: text().notNull(), + name: text().notNull(), + description: text(), + status: text().notNull().default('active'), + modules: json() + .notNull() + .$defaultFn(() => ({ vault: true, openreport: true, stateiac: true })), + createdAt: timestamp() + .notNull() + .$defaultFn(() => new Date().toISOString()), + updatedAt: timestamp() + .notNull() + .$defaultFn(() => new Date().toISOString()), +}); + +export const ProjectRoleEntity = entity('project_roles', { + id: uuid().primaryKey(), + projectId: uuid().notNull(), + slug: text().notNull(), + name: text().notNull(), + permissions: json() + .notNull() + .$defaultFn(() => []), + createdAt: timestamp() + .notNull() + .$defaultFn(() => new Date().toISOString()), + updatedAt: timestamp() + .notNull() + .$defaultFn(() => new Date().toISOString()), +}); + +relations.for(ProjectEntity, ({ many }) => ({ + roles: many(ProjectRoleEntity, { fields: ['id'], references: ['projectId'] }), +})); + +relations.for(ProjectRoleEntity, ({ one }) => ({ + project: one(ProjectEntity, { fields: ['projectId'], references: ['id'] }), +})); diff --git a/src/lib/database/sqlite.client.ts b/src/lib/database/sqlite.client.ts deleted file mode 100644 index 1b7f717..0000000 --- a/src/lib/database/sqlite.client.ts +++ /dev/null @@ -1,40 +0,0 @@ -import type Database from 'better-sqlite3'; -import type { DatabaseClient, DatabaseParams } from './types'; - -export class SqliteDatabaseClient implements DatabaseClient { - constructor(private readonly sqlite: Database.Database) {} - - exec(sql: string): void { - this.sqlite.exec(sql); - } - - get(sql: string, params?: DatabaseParams): T | undefined { - if (params === undefined) { - return this.sqlite.prepare(sql).get() as T | undefined; - } - - return this.sqlite.prepare(sql).get(params as never) as T | undefined; - } - - all(sql: string, params?: DatabaseParams): T[] { - if (params === undefined) { - return this.sqlite.prepare(sql).all() as T[]; - } - - return this.sqlite.prepare(sql).all(params as never) as T[]; - } - - run(sql: string, params?: DatabaseParams): void { - if (params === undefined) { - this.sqlite.prepare(sql).run(); - return; - } - - this.sqlite.prepare(sql).run(params as never); - } - - transaction(callback: () => T): T { - const wrapped = this.sqlite.transaction(callback); - return wrapped(); - } -} diff --git a/src/lib/db.ts b/src/lib/db.ts deleted file mode 100644 index b37a6ae..0000000 --- a/src/lib/db.ts +++ /dev/null @@ -1,125 +0,0 @@ -import Database from 'better-sqlite3'; -import path from 'path'; -import fs from 'fs'; -import { SqliteDatabaseClient } from './database/sqlite.client'; - -const DB_DIR = path.resolve(process.cwd(), 'data', 'db'); -const DB_PATH = path.join(DB_DIR, 'states.sqlite'); - -if (!fs.existsSync(DB_DIR)) { - fs.mkdirSync(DB_DIR, { recursive: true }); -} - -export const db = new Database(DB_PATH); -export const databaseClient = new SqliteDatabaseClient(db); - -db.pragma('journal_mode = WAL'); - -db.exec(` - CREATE TABLE IF NOT EXISTS users ( - id TEXT PRIMARY KEY, - username TEXT UNIQUE NOT NULL, - email TEXT, - password_hash TEXT NOT NULL, - role TEXT NOT NULL DEFAULT 'developer', - created_at DATETIME DEFAULT CURRENT_TIMESTAMP - ); - - CREATE TABLE IF NOT EXISTS config ( - id INTEGER PRIMARY KEY CHECK (id = 1), - auth_method TEXT NOT NULL DEFAULT 'none', - encryption_key TEXT, - updated_at DATETIME DEFAULT CURRENT_TIMESTAMP - ); - - CREATE TABLE IF NOT EXISTS projects ( - id TEXT PRIMARY KEY, - name TEXT NOT NULL, - created_at DATETIME DEFAULT CURRENT_TIMESTAMP - ); - - CREATE TABLE IF NOT EXISTS storage_backends ( - id TEXT PRIMARY KEY, - name TEXT NOT NULL, - provider TEXT NOT NULL, - bucket TEXT NOT NULL, - region TEXT, - access_key_id TEXT, - secret_access_key TEXT, - endpoint TEXT, - gcp_project_id TEXT, - gcp_credentials TEXT, - created_at DATETIME DEFAULT CURRENT_TIMESTAMP - ); - - CREATE TABLE IF NOT EXISTS stacks ( - id TEXT PRIMARY KEY, - project_id TEXT NOT NULL, - name TEXT NOT NULL, - created_at DATETIME DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (project_id) REFERENCES projects (id) ON DELETE CASCADE - ); - - CREATE TABLE IF NOT EXISTS states ( - id TEXT PRIMARY KEY, - stack_id TEXT NOT NULL, - version INTEGER NOT NULL, - checkpoint JSON NOT NULL, - created_at DATETIME DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (stack_id) REFERENCES stacks (id) ON DELETE CASCADE - ); - - CREATE TABLE IF NOT EXISTS history ( - id TEXT PRIMARY KEY, - stack_id TEXT NOT NULL, - kind TEXT NOT NULL, - start_time INTEGER NOT NULL, - end_time INTEGER, - message TEXT, - environment JSON, - config JSON, - result TEXT, - resource_changes JSON, - created_at DATETIME DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (stack_id) REFERENCES stacks (id) ON DELETE CASCADE - ); - - CREATE TABLE IF NOT EXISTS api_keys ( - id TEXT PRIMARY KEY, - user_id TEXT NOT NULL, - name TEXT NOT NULL, - key_prefix TEXT NOT NULL, - key_hash TEXT NOT NULL, - last_used_at DATETIME, - revoked_at DATETIME, - created_at DATETIME DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE - ); -`); - -try { - db.exec('ALTER TABLE config ADD COLUMN encryption_key TEXT'); -} catch { - // Ignore if column already exists -} - -const migrations = [ - 'ALTER TABLE users ADD COLUMN email TEXT', - 'ALTER TABLE config ADD COLUMN public_access INTEGER DEFAULT 1', - 'ALTER TABLE config ADD COLUMN google_sso_enabled INTEGER DEFAULT 0', - 'ALTER TABLE config ADD COLUMN google_client_id TEXT', - 'ALTER TABLE config ADD COLUMN google_client_secret TEXT', - 'ALTER TABLE config ADD COLUMN saml_enabled INTEGER DEFAULT 0', - 'ALTER TABLE config ADD COLUMN saml_entry_point TEXT', - 'ALTER TABLE config ADD COLUMN saml_issuer TEXT', - 'ALTER TABLE config ADD COLUMN saml_cert TEXT', - "ALTER TABLE users ADD COLUMN role TEXT DEFAULT 'developer'", -]; - -for (const query of migrations) { - try { - db.exec(query); - } catch { - // Ignore if already applied - } -} diff --git a/src/lib/server/domain/domain.ts b/src/lib/server/domain/domain.ts new file mode 100644 index 0000000..f4c2fc6 --- /dev/null +++ b/src/lib/server/domain/domain.ts @@ -0,0 +1,15 @@ + +export class Domain { + public id: string = '' + public createdAt: Date = new Date() + public updatedAt: Date = new Date() + + constructor(data: any) { + this.id = data.id + this.createdAt = new Date(data.createdAt) + this.updatedAt = new Date(data.updatedAt) + } + toJson() { + return JSON.parse(JSON.stringify(this)); + } +} \ No newline at end of file diff --git a/src/lib/server/infra/repository.ts b/src/lib/server/infra/repository.ts new file mode 100644 index 0000000..6105e33 --- /dev/null +++ b/src/lib/server/infra/repository.ts @@ -0,0 +1,12 @@ +import { getGitDb } from '$lib/server/gitdb'; +export class Repository { + protected readonly db = getGitDb(); + + protected toDomain(row: any): any { + return row; + } + + protected toJSON(row: any): any { + return row; + } +} \ No newline at end of file diff --git a/src/modules/config/application/config.service.ts b/src/modules/config/application/config.service.ts deleted file mode 100644 index 39fdac7..0000000 --- a/src/modules/config/application/config.service.ts +++ /dev/null @@ -1,22 +0,0 @@ -import type { InstanceConfig } from '../domain/entities'; -import type { ConfigRepository } from '../domain/repositories'; - -const DEFAULT_CONFIG: InstanceConfig = { - publicAccess: false, - googleSsoEnabled: false, - samlEnabled: false, -}; - -export class ConfigService { - constructor(private readonly configRepository: ConfigRepository) {} - - getConfig(): InstanceConfig | null { - return this.configRepository.getConfig(); - } - - saveConfig(partial: Partial): void { - const existing = this.getConfig() ?? DEFAULT_CONFIG; - const merged: InstanceConfig = { ...existing, ...partial }; - this.configRepository.saveConfig(merged); - } -} diff --git a/src/modules/config/application/storage-backend.service.ts b/src/modules/config/application/storage-backend.service.ts deleted file mode 100644 index 70976f6..0000000 --- a/src/modules/config/application/storage-backend.service.ts +++ /dev/null @@ -1,50 +0,0 @@ -import crypto from 'crypto'; -import type { - StorageBackend, - StorageBackendPublic, - UpsertStorageBackendInput, -} from '../domain/entities'; -import type { StorageBackendRepository } from '../domain/repositories'; - -export class StorageBackendService { - constructor(private readonly repository: StorageBackendRepository) {} - - list(): StorageBackendPublic[] { - return this.repository.list().map((backend) => ({ - ...backend, - secretAccessKey: backend.secretAccessKey ? '***' : null, - gcpCredentials: backend.gcpCredentials ? '***' : null, - })); - } - - getById(id: string): StorageBackend | null { - return this.repository.findById(id); - } - - upsert(input: UpsertStorageBackendInput): string { - const id = input.id || crypto.randomUUID(); - const existing = input.id ? this.repository.findById(input.id) : null; - - const secretAccessKey = - input.secretAccessKey === '***' ? (existing?.secretAccessKey ?? null) : (input.secretAccessKey ?? null); - const gcpCredentials = - input.gcpCredentials === '***' ? (existing?.gcpCredentials ?? null) : (input.gcpCredentials ?? null); - - return this.repository.upsert({ - id, - name: input.name || 'Unnamed Backend', - provider: input.provider, - bucket: input.bucket, - region: input.region ?? null, - accessKeyId: input.accessKeyId ?? null, - secretAccessKey, - endpoint: input.endpoint ?? null, - gcpProjectId: input.gcpProjectId ?? null, - gcpCredentials, - }); - } - - deleteById(id: string): void { - this.repository.deleteById(id); - } -} diff --git a/src/modules/config/domain/entities.ts b/src/modules/config/domain/entities.ts deleted file mode 100644 index 142783b..0000000 --- a/src/modules/config/domain/entities.ts +++ /dev/null @@ -1,51 +0,0 @@ -export interface InstanceConfig { - publicAccess: boolean; - googleSsoEnabled: boolean; - googleClientId?: string | null; - googleClientSecret?: string | null; - samlEnabled: boolean; - samlEntryPoint?: string | null; - samlIssuer?: string | null; - samlCert?: string | null; -} - -export type StorageProvider = 's3' | 'gcs'; - -export interface StorageBackend { - id: string; - name: string; - provider: StorageProvider; - bucket: string; - region?: string | null; - accessKeyId?: string | null; - secretAccessKey?: string | null; - endpoint?: string | null; - gcpProjectId?: string | null; - gcpCredentials?: string | null; -} - -export interface StorageBackendPublic { - id: string; - name: string; - provider: StorageProvider; - bucket: string; - region?: string | null; - accessKeyId?: string | null; - secretAccessKey?: string | null; - endpoint?: string | null; - gcpProjectId?: string | null; - gcpCredentials?: string | null; -} - -export interface UpsertStorageBackendInput { - id?: string; - name?: string; - provider: StorageProvider; - bucket: string; - region?: string | null; - accessKeyId?: string | null; - secretAccessKey?: string | null; - endpoint?: string | null; - gcpProjectId?: string | null; - gcpCredentials?: string | null; -} diff --git a/src/modules/config/domain/repositories.ts b/src/modules/config/domain/repositories.ts deleted file mode 100644 index 3a849fc..0000000 --- a/src/modules/config/domain/repositories.ts +++ /dev/null @@ -1,13 +0,0 @@ -import type { InstanceConfig, StorageBackend } from './entities'; - -export interface ConfigRepository { - getConfig(): InstanceConfig | null; - saveConfig(config: InstanceConfig): void; -} - -export interface StorageBackendRepository { - list(): StorageBackend[]; - findById(id: string): StorageBackend | null; - upsert(backend: StorageBackend): string; - deleteById(id: string): void; -} diff --git a/src/modules/config/index.ts b/src/modules/config/index.ts deleted file mode 100644 index ff4f7fa..0000000 --- a/src/modules/config/index.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { databaseClient } from '$lib/db'; -import { ConfigService } from './application/config.service'; -import { StorageBackendService } from './application/storage-backend.service'; -import { SqliteConfigRepository } from './infrastructure/repositories/sqlite-config.repository'; -import { SqliteStorageBackendRepository } from './infrastructure/repositories/sqlite-storage-backend.repository'; - -const configRepository = new SqliteConfigRepository(databaseClient); -const storageBackendRepository = new SqliteStorageBackendRepository(databaseClient); - -export const configService = new ConfigService(configRepository); -export const storageBackendService = new StorageBackendService(storageBackendRepository); diff --git a/src/modules/config/infrastructure/repositories/sqlite-config.repository.ts b/src/modules/config/infrastructure/repositories/sqlite-config.repository.ts deleted file mode 100644 index dad8522..0000000 --- a/src/modules/config/infrastructure/repositories/sqlite-config.repository.ts +++ /dev/null @@ -1,75 +0,0 @@ -import type { DatabaseClient } from '$lib/database/types'; -import type { InstanceConfig } from '../../domain/entities'; -import type { ConfigRepository } from '../../domain/repositories'; - -type ConfigRow = { - public_access?: number | null; - google_sso_enabled?: number | null; - google_client_id?: string | null; - google_client_secret?: string | null; - saml_enabled?: number | null; - saml_entry_point?: string | null; - saml_issuer?: string | null; - saml_cert?: string | null; -}; - -export class SqliteConfigRepository implements ConfigRepository { - constructor(private readonly db: DatabaseClient) {} - - getConfig(): InstanceConfig | null { - const row = this.db.get('SELECT * FROM config WHERE id = 1'); - if (!row) { - return null; - } - - const publicAccess = row.public_access !== undefined ? row.public_access === 1 : true; - - return { - publicAccess, - googleSsoEnabled: row.google_sso_enabled === 1, - googleClientId: row.google_client_id, - googleClientSecret: row.google_client_secret, - samlEnabled: row.saml_enabled === 1, - samlEntryPoint: row.saml_entry_point, - samlIssuer: row.saml_issuer, - samlCert: row.saml_cert, - }; - } - - saveConfig(config: InstanceConfig): void { - this.db.run( - ` - INSERT INTO config ( - id, updated_at, public_access, google_sso_enabled, - google_client_id, google_client_secret, - saml_enabled, saml_entry_point, saml_issuer, saml_cert - ) - VALUES ( - 1, CURRENT_TIMESTAMP, @publicAccess, @googleSsoEnabled, - @googleClientId, @googleClientSecret, - @samlEnabled, @samlEntryPoint, @samlIssuer, @samlCert - ) - ON CONFLICT(id) DO UPDATE SET - public_access = excluded.public_access, - google_sso_enabled = excluded.google_sso_enabled, - google_client_id = excluded.google_client_id, - google_client_secret = excluded.google_client_secret, - saml_enabled = excluded.saml_enabled, - saml_entry_point = excluded.saml_entry_point, - saml_issuer = excluded.saml_issuer, - saml_cert = excluded.saml_cert, - updated_at = CURRENT_TIMESTAMP - `, - { - publicAccess: config.publicAccess ? 1 : 0, - googleSsoEnabled: config.googleSsoEnabled ? 1 : 0, - googleClientId: config.googleClientId || null, - googleClientSecret: config.googleClientSecret || null, - samlEnabled: config.samlEnabled ? 1 : 0, - samlEntryPoint: config.samlEntryPoint || null, - samlIssuer: config.samlIssuer || null, - samlCert: config.samlCert || null, - }, - ); - } -} diff --git a/src/modules/config/infrastructure/repositories/sqlite-storage-backend.repository.ts b/src/modules/config/infrastructure/repositories/sqlite-storage-backend.repository.ts deleted file mode 100644 index 6a73ae0..0000000 --- a/src/modules/config/infrastructure/repositories/sqlite-storage-backend.repository.ts +++ /dev/null @@ -1,89 +0,0 @@ -import type { DatabaseClient } from '$lib/database/types'; -import type { StorageBackend, StorageProvider } from '../../domain/entities'; -import type { StorageBackendRepository } from '../../domain/repositories'; - -type StorageBackendRow = { - id: string; - name: string; - provider: string; - bucket: string; - region?: string | null; - access_key_id?: string | null; - secret_access_key?: string | null; - endpoint?: string | null; - gcp_project_id?: string | null; - gcp_credentials?: string | null; -}; - -export class SqliteStorageBackendRepository implements StorageBackendRepository { - constructor(private readonly db: DatabaseClient) {} - - list(): StorageBackend[] { - const rows = this.db.all('SELECT * FROM storage_backends ORDER BY created_at DESC'); - return rows.map((row) => this.toEntity(row)); - } - - findById(id: string): StorageBackend | null { - const row = this.db.get('SELECT * FROM storage_backends WHERE id = ?', [id]); - return row ? this.toEntity(row) : null; - } - - upsert(backend: StorageBackend): string { - this.db.run( - ` - INSERT INTO storage_backends ( - id, name, provider, bucket, region, access_key_id, secret_access_key, endpoint, gcp_project_id, gcp_credentials - ) VALUES ( - @id, @name, @provider, @bucket, @region, @accessKeyId, @secretAccessKey, @endpoint, @gcpProjectId, @gcpCredentials - ) - ON CONFLICT(id) DO UPDATE SET - name = excluded.name, - provider = excluded.provider, - bucket = excluded.bucket, - region = excluded.region, - access_key_id = excluded.access_key_id, - secret_access_key = excluded.secret_access_key, - endpoint = excluded.endpoint, - gcp_project_id = excluded.gcp_project_id, - gcp_credentials = excluded.gcp_credentials - `, - { - id: backend.id, - name: backend.name, - provider: backend.provider, - bucket: backend.bucket, - region: backend.region || null, - accessKeyId: backend.accessKeyId || null, - secretAccessKey: backend.secretAccessKey || null, - endpoint: backend.endpoint || null, - gcpProjectId: backend.gcpProjectId || null, - gcpCredentials: backend.gcpCredentials || null, - }, - ); - - return backend.id; - } - - deleteById(id: string): void { - this.db.run('DELETE FROM storage_backends WHERE id = ?', [id]); - } - - private toEntity(row: StorageBackendRow): StorageBackend { - return { - id: row.id, - name: row.name, - provider: this.normalizeProvider(row.provider), - bucket: row.bucket, - region: row.region, - accessKeyId: row.access_key_id, - secretAccessKey: row.secret_access_key, - endpoint: row.endpoint, - gcpProjectId: row.gcp_project_id, - gcpCredentials: row.gcp_credentials, - }; - } - - private normalizeProvider(provider: string): StorageProvider { - return provider === 'gcs' ? 'gcs' : 's3'; - } -} diff --git a/src/modules/organization/application/organization.service.ts b/src/modules/organization/application/organization.service.ts new file mode 100644 index 0000000..71d885b --- /dev/null +++ b/src/modules/organization/application/organization.service.ts @@ -0,0 +1,8 @@ +export type Organization = { id: string; name: string; slug: string }; + +export class OrganizationService { + // stub: no persistence yet, always resolves to the single default organization + async findBySlug(_slug: string): Promise { + return { id: 'gitops', name: 'GitOps', slug: 'gitops' }; + } +} diff --git a/src/modules/organization/index.ts b/src/modules/organization/index.ts new file mode 100644 index 0000000..635db93 --- /dev/null +++ b/src/modules/organization/index.ts @@ -0,0 +1,4 @@ +import { OrganizationService } from './application/organization.service'; + +export const organizationService = new OrganizationService(); +export type { Organization } from './application/organization.service'; diff --git a/src/modules/projects/application/project.service.test.ts b/src/modules/projects/application/project.service.test.ts new file mode 100644 index 0000000..6616a25 --- /dev/null +++ b/src/modules/projects/application/project.service.test.ts @@ -0,0 +1,225 @@ +import { describe, expect, it, beforeEach } from 'vitest'; +import { ProjectService } from './project.service'; +import { ProjectDomain } from '../domain/project.domain'; + +class FakeProjectRepository { + rows: ProjectDomain[] = []; + + async findAll() { + return [...this.rows]; + } + + async findById(id: string) { + return this.rows.find((p) => p.id === id) ?? null; + } + + async findBySlug(slug: string) { + return this.rows.find((p) => p.slug === slug) ?? null; + } + + async create(input: { + id: string; + slug: string; + name: string; + description?: string; + status: string; + modules: { vault: boolean; openreport: boolean; stateiac: boolean }; + }) { + this.rows.push( + new ProjectDomain({ + id: input.id, + slug: input.slug, + name: input.name, + description: input.description, + status: input.status, + modules: input.modules, + createdAt: '2024-01-01T00:00:00.000Z', + updatedAt: '2024-01-01T00:00:00.000Z', + }), + ); + } + + async update( + id: string, + changes: { + name?: string; + slug?: string; + description?: string; + status?: string; + modules?: { vault: boolean; openreport: boolean; stateiac: boolean }; + }, + ) { + const project = this.rows.find((p) => p.id === id); + if (!project) return; + if (changes.name !== undefined) project.name = changes.name; + if (changes.slug !== undefined) project.slug = changes.slug; + if (changes.description !== undefined) project.description = changes.description; + if (changes.status !== undefined) project.status = changes.status as 'active' | 'inactive'; + if (changes.modules !== undefined) project.modules = changes.modules; + } + + async deleteById(id: string) { + this.rows = this.rows.filter((p) => p.id !== id); + } +} + +describe('ProjectService', () => { + let repository: FakeProjectRepository; + let service: ProjectService; + + beforeEach(() => { + repository = new FakeProjectRepository(); + service = new ProjectService(repository as any); + }); + + describe('listProjects', () => { + it('returns an empty list when there are no projects', async () => { + expect(await service.listProjects()).toEqual([]); + }); + + it('lists created projects', async () => { + await service.createProject({ name: 'Platform Core' }); + const projects = await service.listProjects(); + expect(projects).toHaveLength(1); + expect(projects[0].slug).toBe('platform-core'); + }); + }); + + describe('getProject / getProjectBySlug', () => { + it('throws when the project does not exist', async () => { + await expect(service.getProject('missing-id')).rejects.toThrow(/not found/); + await expect(service.getProjectBySlug('missing-slug')).rejects.toThrow(/not found/); + }); + + it('returns the project by id and by slug', async () => { + const created = await service.createProject({ name: 'Kettu Studio' }); + + const byId = await service.getProject(created.id); + expect(byId.slug).toBe('kettu-studio'); + + const bySlug = await service.getProjectBySlug('kettu-studio'); + expect(bySlug.id).toBe(created.id); + }); + }); + + describe('createProject', () => { + it('requires a non-empty name', async () => { + await expect(service.createProject({ name: ' ' })).rejects.toThrow(/name is required/); + }); + + it('auto-generates a normalized slug from the name when none is provided', async () => { + const project = await service.createProject({ name: 'Kettu Studio!!' }); + expect(project.slug).toBe('kettu-studio'); + }); + + it('uses the provided slug, normalized', async () => { + const project = await service.createProject({ name: 'Kettu Studio', slug: 'Custom Slug' }); + expect(project.slug).toBe('custom-slug'); + }); + + it('rejects creating a project with a duplicate slug', async () => { + await service.createProject({ name: 'Kettu Studio' }); + await expect(service.createProject({ name: 'Kettu Studio' })).rejects.toThrow( + /already exists/, + ); + }); + + it('defaults status to active for missing or invalid values', async () => { + const project = await service.createProject({ name: 'Project A' }); + expect(project.status).toBe('active'); + }); + + it('accepts an explicit inactive status', async () => { + const project = await service.createProject({ name: 'Project B', status: 'inactive' }); + expect(project.status).toBe('inactive'); + }); + + it('defaults all modules to enabled when none are provided', async () => { + const project = await service.createProject({ name: 'Project C' }); + expect(project.modules).toEqual({ vault: true, openreport: true, stateiac: true }); + }); + + it('accepts a partial modules override, defaulting the rest to enabled', async () => { + const project = await service.createProject({ + name: 'Project D', + modules: { vault: false }, + }); + expect(project.modules).toEqual({ vault: false, openreport: true, stateiac: true }); + }); + }); + + describe('updateProject', () => { + it('throws when the project does not exist', async () => { + await expect(service.updateProject('missing-id', { name: 'X' })).rejects.toThrow(/not found/); + }); + + it('updates only the fields provided', async () => { + const created = await service.createProject({ name: 'Original Name' }); + + const updated = await service.updateProject(created.id, { description: 'New description' }); + expect(updated.name).toBe('Original Name'); + expect(updated.description).toBe('New description'); + }); + + it('rejects clearing the name', async () => { + const created = await service.createProject({ name: 'Original Name' }); + await expect(service.updateProject(created.id, { name: ' ' })).rejects.toThrow( + /name is required/, + ); + }); + + it('normalizes the slug when updating it', async () => { + const created = await service.createProject({ name: 'Original Name' }); + const updated = await service.updateProject(created.id, { slug: 'New Slug!!' }); + expect(updated.slug).toBe('new-slug'); + }); + + it('rejects updating to a slug already used by another project', async () => { + await service.createProject({ name: 'Project A', slug: 'taken' }); + const created = await service.createProject({ name: 'Project B' }); + + await expect(service.updateProject(created.id, { slug: 'taken' })).rejects.toThrow( + /already exists/, + ); + }); + + it('allows keeping the same slug on the same project', async () => { + const created = await service.createProject({ name: 'Project A', slug: 'same-slug' }); + const updated = await service.updateProject(created.id, { slug: 'same-slug' }); + expect(updated.slug).toBe('same-slug'); + }); + + it('merges partial module updates with the existing modules', async () => { + const created = await service.createProject({ name: 'Project A' }); + + const updated = await service.updateProject(created.id, { modules: { vault: false } }); + expect(updated.modules).toEqual({ vault: false, openreport: true, stateiac: true }); + + const updatedAgain = await service.updateProject(created.id, { + modules: { openreport: false }, + }); + expect(updatedAgain.modules).toEqual({ vault: false, openreport: false, stateiac: true }); + }); + + it('updates the status', async () => { + const created = await service.createProject({ name: 'Project A' }); + const updated = await service.updateProject(created.id, { status: 'inactive' }); + expect(updated.status).toBe('inactive'); + }); + }); + + describe('deleteProject', () => { + it('throws when the project does not exist', async () => { + await expect(service.deleteProject('missing-id')).rejects.toThrow(/not found/); + }); + + it('deletes an existing project', async () => { + const created = await service.createProject({ name: 'Project A' }); + + await service.deleteProject(created.id); + + const projects = await service.listProjects(); + expect(projects.find((p) => p.id === created.id)).toBeUndefined(); + }); + }); +}); diff --git a/src/modules/projects/application/project.service.ts b/src/modules/projects/application/project.service.ts index c07c61e..89a92d7 100644 --- a/src/modules/projects/application/project.service.ts +++ b/src/modules/projects/application/project.service.ts @@ -1,48 +1,164 @@ -import type { Project } from '../domain/entities'; -import type { ProjectRepository } from '../domain/repositories'; +import crypto from 'crypto'; +import { ProjectRepository } from '../infrastructure/repositories/project.repostitory'; +import { DEFAULT_PROJECT_MODULES, type ProjectModules } from '../domain/project.domain'; + +export type ProjectStatusValue = 'active' | 'inactive'; export class ProjectService { constructor(private readonly repository: ProjectRepository) {} - listProjects(): Project[] { - return this.repository.list(); + async listProjects() { + const projects = await this.repository.findAll(); + return projects.map((project) => project.toJson()); } - createProject(name: string): Project { - const normalizedName = name.trim(); - if (!normalizedName) { + async getProject(id: string) { + const project = await this.repository.findById(id); + if (!project) { + throw new Error('Project not found'); + } + return project.toJson(); + } + + async getProjectBySlug(slug: string) { + const project = await this.repository.findBySlug(slug); + if (!project) { + throw new Error('Project not found'); + } + return project.toJson(); + } + + async createProject(input: { + name: string; + slug?: string; + description?: string; + status?: string; + modules?: Partial; + }) { + const name = input.name.trim(); + if (!name) { throw new Error('Project name is required'); } - const id = normalizedName.toLowerCase().replace(/[^a-z0-9-]/g, '-'); - this.repository.upsertIfMissing({ id, name: normalizedName }); + const slug = this.normalizeSlug(input.slug || name); + if (!slug) { + throw new Error('Project slug is required'); + } + + const existing = await this.repository.findBySlug(slug); + if (existing) { + throw new Error('A project with this slug already exists'); + } + + await this.repository.create({ + id: crypto.randomUUID(), + slug, + name, + description: input.description?.trim() || undefined, + status: this.sanitizeStatus(input.status), + modules: this.sanitizeModules(input.modules), + }); - return { id, name: normalizedName }; + const created = await this.repository.findBySlug(slug); + if (!created) { + throw new Error('Failed to create project'); + } + + return created.toJson(); } - syncFromPulumiStateKeys(keys: string[]): number { - const projects = new Set(); + async updateProject( + id: string, + changes: { + name?: string; + slug?: string; + description?: string; + status?: string; + modules?: Partial; + }, + ) { + const project = await this.repository.findById(id); + if (!project) { + throw new Error('Project not found'); + } + + const patch: { + name?: string; + slug?: string; + description?: string; + status?: string; + modules?: ProjectModules; + } = {}; + + if (changes.name !== undefined) { + const name = changes.name.trim(); + if (!name) { + throw new Error('Project name is required'); + } + patch.name = name; + } - for (const key of keys) { - let id = key; - if (id.startsWith('.pulumi/stacks/')) { - id = id.replace('.pulumi/stacks/', ''); + if (changes.slug !== undefined) { + const slug = this.normalizeSlug(changes.slug); + if (!slug) { + throw new Error('Project slug is required'); } - if (id.endsWith('.json')) { - id = id.slice(0, -5); + + const existing = await this.repository.findBySlug(slug); + if (existing && existing.id !== id) { + throw new Error('A project with this slug already exists'); } + patch.slug = slug; + } + + if (changes.description !== undefined) { + patch.description = changes.description.trim(); + } + + if (changes.status !== undefined) { + patch.status = this.sanitizeStatus(changes.status); + } + + if (changes.modules !== undefined) { + patch.modules = this.sanitizeModules({ ...project.modules, ...changes.modules }); + } + + await this.repository.update(id, patch); - const parts = id.split('/'); - projects.add(parts.length > 1 ? parts[0] : 'default'); + const updated = await this.repository.findById(id); + if (!updated) { + throw new Error('Failed to update project'); } - this.repository.upsertManyIfMissing( - Array.from(projects).map((project) => ({ - id: project, - name: project, - })), - ); + return updated.toJson(); + } + + async deleteProject(id: string) { + const project = await this.repository.findById(id); + if (!project) { + throw new Error('Project not found'); + } + + await this.repository.deleteById(id); + } + + private normalizeSlug(value: string): string { + return value + .trim() + .toLowerCase() + .replace(/[^a-z0-9]+/g, '-') + .replace(/^-+|-+$/g, ''); + } + + private sanitizeStatus(status?: string): ProjectStatusValue { + return status === 'inactive' ? 'inactive' : 'active'; + } - return projects.size; + private sanitizeModules(modules?: Partial): ProjectModules { + return { + vault: modules?.vault ?? DEFAULT_PROJECT_MODULES.vault, + openreport: modules?.openreport ?? DEFAULT_PROJECT_MODULES.openreport, + stateiac: modules?.stateiac ?? DEFAULT_PROJECT_MODULES.stateiac, + }; } } diff --git a/src/modules/projects/domain/entities.ts b/src/modules/projects/domain/entities.ts deleted file mode 100644 index 6514b74..0000000 --- a/src/modules/projects/domain/entities.ts +++ /dev/null @@ -1,5 +0,0 @@ -export interface Project { - id: string; - name: string; - createdAt?: string; -} diff --git a/src/modules/projects/domain/project.domain.ts b/src/modules/projects/domain/project.domain.ts new file mode 100644 index 0000000..f4d927d --- /dev/null +++ b/src/modules/projects/domain/project.domain.ts @@ -0,0 +1,47 @@ +import { Domain } from '$lib/server/domain/domain'; + +export interface ProjectStatus { + ACTIVE: 'active'; + INACTIVE: 'inactive'; +} + +export interface ProjectModules { + vault: boolean; + openreport: boolean; + stateiac: boolean; +} + +export const DEFAULT_PROJECT_MODULES: ProjectModules = { + vault: true, + openreport: true, + stateiac: true, +}; + +export class ProjectDomain extends Domain { + public name: string = ''; + public slug: string | null = null; + public description?: string | null = null; + public status: ProjectStatus[keyof ProjectStatus] = 'active'; + public modules: ProjectModules = { ...DEFAULT_PROJECT_MODULES }; + constructor(data: any) { + super(data); + this.name = data.name; + this.slug = data.slug; + this.description = data.description; + this.status = data.status; + this.modules = { ...DEFAULT_PROJECT_MODULES, ...(data.modules ?? {}) }; + } + + toJson() { + return { + id: this.id, + name: this.name, + slug: this.slug, + description: this.description, + createdAt: this.createdAt, + status: this.status, + modules: this.modules, + updatedAt: this.updatedAt, + }; + } +} diff --git a/src/modules/projects/domain/repositories.ts b/src/modules/projects/domain/repositories.ts deleted file mode 100644 index faa1e4b..0000000 --- a/src/modules/projects/domain/repositories.ts +++ /dev/null @@ -1,7 +0,0 @@ -import type { Project } from './entities'; - -export interface ProjectRepository { - list(): Project[]; - upsertIfMissing(project: { id: string; name: string }): void; - upsertManyIfMissing(projects: Array<{ id: string; name: string }>): void; -} diff --git a/src/modules/projects/index.ts b/src/modules/projects/index.ts index caa06a1..aab224f 100644 --- a/src/modules/projects/index.ts +++ b/src/modules/projects/index.ts @@ -1,7 +1,6 @@ -import { databaseClient } from '$lib/db'; import { ProjectService } from './application/project.service'; -import { SqliteProjectRepository } from './infrastructure/repositories/sqlite-project.repository'; +import { ProjectRepository } from './infrastructure/repositories/project.repostitory'; -const projectRepository = new SqliteProjectRepository(databaseClient); +const projectRepository = new ProjectRepository(); export const projectService = new ProjectService(projectRepository); diff --git a/src/modules/projects/infrastructure/repositories/project.repostitory.ts b/src/modules/projects/infrastructure/repositories/project.repostitory.ts new file mode 100644 index 0000000..c80539e --- /dev/null +++ b/src/modules/projects/infrastructure/repositories/project.repostitory.ts @@ -0,0 +1,68 @@ +import { Repository } from '$lib/server/infra/repository'; +import { ProjectDomain, type ProjectModules } from '../../domain/project.domain'; +import { ProjectEntity } from '$lib/database/schemas'; + +export class ProjectRepository extends Repository { + async findByName(name: string): Promise { + const role = await this.db.select().from(ProjectEntity).where({ name }).limit(1); + return role.rows[0] ? new ProjectDomain(role.rows[0]) : null; + } + + async findAll(): Promise { + const result = await this.db.select().from(ProjectEntity).orderBy('createdAt', 'asc'); + return result.rows.map((row: any) => new ProjectDomain(row)); + } + + async findById(id: string): Promise { + const result = await this.db.select().from(ProjectEntity).where({ id }).limit(1); + const row = result.rows[0]; + return row ? new ProjectDomain(row) : null; + } + + async findBySlug(slug: string): Promise { + const result = await this.db.select().from(ProjectEntity).where({ slug }).limit(1); + const row = result.rows[0]; + return row ? new ProjectDomain(row) : null; + } + + async create(input: { + id: string; + slug: string; + name: string; + description?: string; + status: string; + modules: ProjectModules; + }): Promise { + await this.db.insert(ProjectEntity).values({ + id: input.id, + slug: input.slug, + name: input.name, + description: input.description, + status: input.status, + modules: input.modules, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + }); + } + + async update( + id: string, + changes: { + name?: string; + slug?: string; + description?: string; + status?: string; + modules?: ProjectModules; + }, + ): Promise { + await this.db + .update(ProjectEntity) + .set({ ...changes, updatedAt: new Date().toISOString() }) + .where({ id }); + } + + async deleteById(id: string): Promise { + // TODO: Aqui deberemos borrar roles, usuarios y demás entidades relacionadas con el proyecto antes de borrarlo + await this.db.delete(ProjectEntity).where({ id }); + } +} diff --git a/src/modules/projects/infrastructure/repositories/sqlite-project.repository.ts b/src/modules/projects/infrastructure/repositories/sqlite-project.repository.ts deleted file mode 100644 index 6a6ff83..0000000 --- a/src/modules/projects/infrastructure/repositories/sqlite-project.repository.ts +++ /dev/null @@ -1,41 +0,0 @@ -import type { DatabaseClient } from '$lib/database/types'; -import type { Project } from '../../domain/entities'; -import type { ProjectRepository } from '../../domain/repositories'; - -type ProjectRow = { - id: string; - name: string; - created_at?: string; -}; - -export class SqliteProjectRepository implements ProjectRepository { - constructor(private readonly db: DatabaseClient) {} - - list(): Project[] { - const rows = this.db.all('SELECT * FROM projects ORDER BY created_at DESC'); - return rows.map((row) => ({ - id: row.id, - name: row.name, - createdAt: row.created_at, - })); - } - - upsertIfMissing(project: { id: string; name: string }): void { - this.db.run('INSERT INTO projects (id, name) VALUES (@id, @name) ON CONFLICT(id) DO NOTHING', { - id: project.id, - name: project.name, - }); - } - - upsertManyIfMissing(projects: Array<{ id: string; name: string }>): void { - if (!projects.length) { - return; - } - - this.db.transaction(() => { - for (const project of projects) { - this.upsertIfMissing(project); - } - }); - } -} diff --git a/src/routes/+layout.server.ts b/src/routes/+layout.server.ts index 3ce3a4f..5de5c53 100644 --- a/src/routes/+layout.server.ts +++ b/src/routes/+layout.server.ts @@ -1,21 +1,34 @@ -import { configService, storageBackendService } from '../modules/config'; +import { projectService } from '../modules/projects'; +import { organizationService } from '../modules/organization'; +import { can } from '../modules/auth'; +// import { configService, storageBackendService } from '../modules/config'; -export async function load({ cookies, locals }) { - const config = configService.getConfig(); - const backends = storageBackendService.list(); +export async function load({ locals }) { + // const config = configService.getConfig(); + // const backends = storageBackendService.list(); - let activeBackendId = cookies.get('active_backend'); - let activeBackend = backends.find((backend) => backend.id === activeBackendId); + // let activeBackendId = cookies.get('active_backend'); + // let activeBackend = backends.find((backend) => backend.id === activeBackendId); - if (!activeBackend && backends.length > 0) { - activeBackend = backends[0]; - activeBackendId = activeBackend.id; - } + // if (!activeBackend && backends.length > 0) { + // activeBackend = backends[0]; + // activeBackendId = activeBackend.id; + // } + + const organization = await organizationService.findBySlug('gitops'); + + const projects = + locals.user && can(locals.user, 'stateiac:read') + ? (await projectService.listProjects()).filter((project) => project.status === 'active') + : []; return { - isConfigured: !!config && backends.length > 0, - backends, - activeBackendId, + // isConfigured: !!config && backends.length > 0, + isConfigured: true, + // backends, + // activeBackendId, user: locals.user, + organization, + projects, }; } diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index a16430b..ede3a41 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -26,13 +26,24 @@ style={`--sidebar-width:${sidebarCollapsed ? '96px' : '340px'}`} > {#if $page.url.pathname !== '/login'} - - -
-