diff --git a/.env.example b/.env.example index 99cbcd5..213e132 100644 --- a/.env.example +++ b/.env.example @@ -1 +1,8 @@ -VITE_API_BASE_URL=http://localhost:3000 +# Leave empty for local environment / no api authentication +API_URL=http://localhost:3000 + +# Set to production +NODE_ENV=development + +# Leaving this empty will generate a new unique random session secret at start +SESSION_SECRET= diff --git a/eslint.config.js b/eslint.config.js index d394cb0..5fa6710 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -23,7 +23,6 @@ export default [ }, { rules: { - 'no-console': 'error', 'svelte/no-unused-svelte-ignore': 'off', }, }, diff --git a/package.json b/package.json index e791142..b65d3bc 100644 --- a/package.json +++ b/package.json @@ -108,5 +108,8 @@ "src/**/*.{ts,svelte}": [ "eslint --fix" ] + }, + "dependencies": { + "svelte-kit-sessions": "catalog:core" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3a0302f..b42ca1b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -15,10 +15,10 @@ catalogs: ci: '@commitlint/cli': specifier: ^20.4.2 - version: 20.4.2 + version: 20.4.3 '@commitlint/config-conventional': specifier: ^20.4.2 - version: 20.4.2 + version: 20.4.3 '@favware/cliff-jumper': specifier: ^6.0.0 version: 6.0.0 @@ -60,6 +60,9 @@ catalogs: svelte-check: specifier: ^4.3.5 version: 4.3.5 + svelte-kit-sessions: + specifier: ^0.4.0 + version: 0.4.0 vite: specifier: ^7.3.1 version: 7.3.1 @@ -69,7 +72,7 @@ catalogs: version: 0.0.4 '@unocss/extractor-svelte': specifier: ^66.6.3 - version: 66.6.3 + version: 66.6.6 '@unocss/preset-icons': specifier: ^66.6.0 version: 66.6.0 @@ -92,7 +95,7 @@ catalogs: version: 1.2.4 '@iconify-json/material-icon-theme': specifier: ^1.2.51 - version: 1.2.51 + version: 1.2.55 '@iconify-json/solar': specifier: ^1.2.5 version: 1.2.5 @@ -120,7 +123,7 @@ catalogs: version: 3.8.1 prettier-plugin-svelte: specifier: ^3.5.0 - version: 3.5.0 + version: 3.5.1 typescript-eslint: specifier: ^8.53.1 version: 8.53.1 @@ -147,16 +150,20 @@ catalogs: importers: .: + dependencies: + svelte-kit-sessions: + specifier: catalog:core + version: 0.4.0(@sveltejs/kit@2.53.4(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.48.0)(vite@7.3.1(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.2)))(svelte@5.48.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.2)))(svelte@5.48.0) devDependencies: '@alexanderniebuhr/prettier-plugin-unocss': specifier: catalog:css version: 0.0.4 '@commitlint/cli': specifier: catalog:ci - version: 20.4.2(@types/node@25.0.10)(typescript@5.9.3) + version: 20.4.3(@types/node@25.0.10)(typescript@5.9.3) '@commitlint/config-conventional': specifier: catalog:ci - version: 20.4.2 + version: 20.4.3 '@favware/cliff-jumper': specifier: catalog:ci version: 6.0.0 @@ -165,7 +172,7 @@ importers: version: 1.2.4 '@iconify-json/material-icon-theme': specifier: catalog:icons - version: 1.2.51 + version: 1.2.55 '@iconify-json/solar': specifier: catalog:icons version: 1.2.5 @@ -195,13 +202,13 @@ importers: version: 6.2.4(svelte@5.48.0)(vite@7.3.1(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.2)) '@trivago/prettier-plugin-sort-imports': specifier: catalog:lint - version: 6.0.2(prettier-plugin-svelte@3.5.0(prettier@3.8.1)(svelte@5.48.0))(prettier@3.8.1)(svelte@5.48.0) + version: 6.0.2(prettier-plugin-svelte@3.5.1(prettier@3.8.1)(svelte@5.48.0))(prettier@3.8.1)(svelte@5.48.0) '@tsconfig/svelte': specifier: catalog:build version: 5.0.8 '@unocss/extractor-svelte': specifier: catalog:css - version: 66.6.3 + version: 66.6.6 '@unocss/preset-icons': specifier: catalog:css version: 66.6.0 @@ -249,7 +256,7 @@ importers: version: 3.8.1 prettier-plugin-svelte: specifier: catalog:lint - version: 3.5.0(prettier@3.8.1)(svelte@5.48.0) + version: 3.5.1(prettier@3.8.1)(svelte@5.48.0) svelte: specifier: catalog:core version: 5.48.0 @@ -355,73 +362,73 @@ packages: resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==} engines: {node: '>=18'} - '@commitlint/cli@20.4.2': - resolution: {integrity: sha512-YjYSX2yj/WsVoxh9mNiymfFS2ADbg2EK4+1WAsMuckwKMCqJ5PDG0CJU/8GvmHWcv4VRB2V02KqSiecRksWqZQ==} + '@commitlint/cli@20.4.3': + resolution: {integrity: sha512-Z37EMoDT7+Upg500vlr/vZrgRsb6Xc5JAA3Tv7BYbobnN/ZpqUeZnSLggBg2+1O+NptRDtyujr2DD1CPV2qwhA==} engines: {node: '>=v18'} hasBin: true - '@commitlint/config-conventional@20.4.2': - resolution: {integrity: sha512-rwkTF55q7Q+6dpSKUmJoScV0f3EpDlWKw2UPzklkLS4o5krMN1tPWAVOgHRtyUTMneIapLeQwaCjn44Td6OzBQ==} + '@commitlint/config-conventional@20.4.3': + resolution: {integrity: sha512-9RtLySbYQAs8yEqWEqhSZo9nYhbm57jx7qHXtgRmv/nmeQIjjMcwf6Dl+y5UZcGWgWx435TAYBURONaJIuCjWg==} engines: {node: '>=v18'} - '@commitlint/config-validator@20.4.0': - resolution: {integrity: sha512-zShmKTF+sqyNOfAE0vKcqnpvVpG0YX8F9G/ZIQHI2CoKyK+PSdladXMSns400aZ5/QZs+0fN75B//3Q5CHw++w==} + '@commitlint/config-validator@20.4.3': + resolution: {integrity: sha512-jCZpZFkcSL3ZEdL5zgUzFRdytv3xPo8iukTe9VA+QGus/BGhpp1xXSVu2B006GLLb2gYUAEGEqv64kTlpZNgmA==} engines: {node: '>=v18'} - '@commitlint/ensure@20.4.1': - resolution: {integrity: sha512-WLQqaFx1pBooiVvBrA1YfJNFqZF8wS/YGOtr5RzApDbV9tQ52qT5VkTsY65hFTnXhW8PcDfZLaknfJTmPejmlw==} + '@commitlint/ensure@20.4.3': + resolution: {integrity: sha512-WcXGKBNn0wBKpX8VlXgxqedyrLxedIlLBCMvdamLnJFEbUGJ9JZmBVx4vhLV3ZyA8uONGOb+CzW0Y9HDbQ+ONQ==} engines: {node: '>=v18'} '@commitlint/execute-rule@20.0.0': resolution: {integrity: sha512-xyCoOShoPuPL44gVa+5EdZsBVao/pNzpQhkzq3RdtlFdKZtjWcLlUFQHSWBuhk5utKYykeJPSz2i8ABHQA+ZZw==} engines: {node: '>=v18'} - '@commitlint/format@20.4.0': - resolution: {integrity: sha512-i3ki3WR0rgolFVX6r64poBHXM1t8qlFel1G1eCBvVgntE3fCJitmzSvH5JD/KVJN/snz6TfaX2CLdON7+s4WVQ==} + '@commitlint/format@20.4.3': + resolution: {integrity: sha512-UDJVErjLbNghop6j111rsHJYGw6MjCKAi95K0GT2yf4eeiDHy3JDRLWYWEjIaFgO+r+dQSkuqgJ1CdMTtrvHsA==} engines: {node: '>=v18'} - '@commitlint/is-ignored@20.4.1': - resolution: {integrity: sha512-In5EO4JR1lNsAv1oOBBO24V9ND1IqdAJDKZiEpdfjDl2HMasAcT7oA+5BKONv1pRoLG380DGPE2W2RIcUwdgLA==} + '@commitlint/is-ignored@20.4.3': + resolution: {integrity: sha512-W5VQKZ7fdJ1X3Tko+h87YZaqRMGN1KvQKXyCM8xFdxzMIf1KCZgN4uLz3osLB1zsFcVS4ZswHY64LI26/9ACag==} engines: {node: '>=v18'} - '@commitlint/lint@20.4.2': - resolution: {integrity: sha512-buquzNRtFng6xjXvBU1abY/WPEEjCgUipNQrNmIWe8QuJ6LWLtei/LDBAzEe5ASm45+Q9L2Xi3/GVvlj50GAug==} + '@commitlint/lint@20.4.3': + resolution: {integrity: sha512-CYOXL23e+nRKij81+d0+dymtIi7Owl9QzvblJYbEfInON/4MaETNSLFDI74LDu+YJ0ML5HZyw9Vhp9QpckwQ0A==} engines: {node: '>=v18'} - '@commitlint/load@20.4.0': - resolution: {integrity: sha512-Dauup/GfjwffBXRJUdlX/YRKfSVXsXZLnINXKz0VZkXdKDcaEILAi9oflHGbfydonJnJAbXEbF3nXPm9rm3G6A==} + '@commitlint/load@20.4.3': + resolution: {integrity: sha512-3cdJOUVP+VcgHa7bhJoWS+Z8mBNXB5aLWMBu7Q7uX8PSeWDzdbrBlR33J1MGGf7r1PZDp+mPPiFktk031PgdRw==} engines: {node: '>=v18'} - '@commitlint/message@20.4.0': - resolution: {integrity: sha512-B5lGtvHgiLAIsK5nLINzVW0bN5hXv+EW35sKhYHE8F7V9Uz1fR4tx3wt7mobA5UNhZKUNgB/+ldVMQE6IHZRyA==} + '@commitlint/message@20.4.3': + resolution: {integrity: sha512-6akwCYrzcrFcTYz9GyUaWlhisY4lmQ3KvrnabmhoeAV8nRH4dXJAh4+EUQ3uArtxxKQkvxJS78hNX2EU3USgxQ==} engines: {node: '>=v18'} - '@commitlint/parse@20.4.1': - resolution: {integrity: sha512-XNtZjeRcFuAfUnhYrCY02+mpxwY4OmnvD3ETbVPs25xJFFz1nRo/25nHj+5eM+zTeRFvWFwD4GXWU2JEtoK1/w==} + '@commitlint/parse@20.4.3': + resolution: {integrity: sha512-hzC3JCo3zs3VkQ833KnGVuWjWIzR72BWZWjQM7tY/7dfKreKAm7fEsy71tIFCRtxf2RtMP2d3RLF1U9yhFSccA==} engines: {node: '>=v18'} - '@commitlint/read@20.4.0': - resolution: {integrity: sha512-QfpFn6/I240ySEGv7YWqho4vxqtPpx40FS7kZZDjUJ+eHxu3azfhy7fFb5XzfTqVNp1hNoI3tEmiEPbDB44+cg==} + '@commitlint/read@20.4.3': + resolution: {integrity: sha512-j42OWv3L31WfnP8WquVjHZRt03w50Y/gEE8FAyih7GQTrIv2+pZ6VZ6pWLD/ml/3PO+RV2SPtRtTp/MvlTb8rQ==} engines: {node: '>=v18'} - '@commitlint/resolve-extends@20.4.0': - resolution: {integrity: sha512-ay1KM8q0t+/OnlpqXJ+7gEFQNlUtSU5Gxr8GEwnVf2TPN3+ywc5DzL3JCxmpucqxfHBTFwfRMXxPRRnR5Ki20g==} + '@commitlint/resolve-extends@20.4.3': + resolution: {integrity: sha512-QucxcOy+00FhS9s4Uy0OyS5HeUV+hbC6OLqkTSIm6fwMdKva+OEavaCDuLtgd9akZZlsUo//XzSmPP3sLKBPog==} engines: {node: '>=v18'} - '@commitlint/rules@20.4.2': - resolution: {integrity: sha512-oz83pnp5Yq6uwwTAabuVQPNlPfeD2Y5ZjMb7Wx8FSUlu4sLYJjbBWt8031Z0osCFPfHzAwSYrjnfDFKtuSMdKg==} + '@commitlint/rules@20.4.3': + resolution: {integrity: sha512-Yuosd7Grn5qiT7FovngXLyRXTMUbj9PYiSkvUgWK1B5a7+ZvrbWDS7epeUapYNYatCy/KTpPFPbgLUdE+MUrBg==} engines: {node: '>=v18'} '@commitlint/to-lines@20.0.0': resolution: {integrity: sha512-2l9gmwiCRqZNWgV+pX1X7z4yP0b3ex/86UmUFgoRt672Ez6cAM2lOQeHFRUTuE6sPpi8XBCGnd8Kh3bMoyHwJw==} engines: {node: '>=v18'} - '@commitlint/top-level@20.4.0': - resolution: {integrity: sha512-NDzq8Q6jmFaIIBC/GG6n1OQEaHdmaAAYdrZRlMgW6glYWGZ+IeuXmiymDvQNXPc82mVxq2KiE3RVpcs+1OeDeA==} + '@commitlint/top-level@20.4.3': + resolution: {integrity: sha512-qD9xfP6dFg5jQ3NMrOhG0/w5y3bBUsVGyJvXxdWEwBm8hyx4WOk3kKXw28T5czBYvyeCVJgJJ6aoJZUWDpaacQ==} engines: {node: '>=v18'} - '@commitlint/types@20.4.0': - resolution: {integrity: sha512-aO5l99BQJ0X34ft8b0h7QFkQlqxC6e7ZPVmBKz13xM9O8obDaM1Cld4sQlJDXXU/VFuUzQ30mVtHjVz74TuStw==} + '@commitlint/types@20.4.3': + resolution: {integrity: sha512-51OWa1Gi6ODOasPmfJPq6js4pZoomima4XLZZCrkldaH2V5Nb3bVhNXPeT6XV0gubbainSpTw4zi68NqAeCNCg==} engines: {node: '>=v18'} '@conventional-changelog/git-client@1.0.1': @@ -680,8 +687,8 @@ packages: '@iconify-json/ic@1.2.4': resolution: {integrity: sha512-pzPMmrZrBQuwT7nmtrYdkttun8KalRGgZPIL1Ny9KpF2zjRGIUPN+npTfuD3lrgO/OnSwAoJWuekQwBpt/Cqrw==} - '@iconify-json/material-icon-theme@1.2.51': - resolution: {integrity: sha512-gZ/EEe2K+sP5f7lfd8TNiRSvHEbFBN4gzBC2fZonZuDjoB008s5Ni8Qvlz++xMBZsEg7gEKG8Ph5r6lFZpQ8AQ==} + '@iconify-json/material-icon-theme@1.2.55': + resolution: {integrity: sha512-V4FUXp2az00xpGYjj4MaOvp6aAIfOMTRRGrt66KH7DmqoIb4WV/YqH4TalgOswCJD/UPGdPuOoy+B6hxLuifTg==} '@iconify-json/solar@1.2.5': resolution: {integrity: sha512-WMAiNwchU8zhfrySww6KQBRIBbsQ6SvgIu2yA+CHGyMima/0KQwT5MXogrZPJGoQF+1Ye3Qj6K+1CiyNn3YkoA==} @@ -703,6 +710,10 @@ packages: resolution: {integrity: sha512-eOgAX+eQpHvD/H4BMILc4tZ85XviTlwr/51RKkKUHozVVthj5avUPKP+4N4vcTUrqSscl2atTh9NbNTuvoBN0A==} engines: {node: '>=18.0.0'} + '@isaacs/ttlcache@1.4.1': + resolution: {integrity: sha512-RQgQ4uQ+pLbqXfOmieB91ejmLwvSgv9nLx6sT6sD83s7umBypgg+OIBOBbEUiJXrfpnp9j0mRhYYdzp9uqq3lA==} + engines: {node: '>=12'} + '@jridgewell/gen-mapping@0.3.13': resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} @@ -994,6 +1005,10 @@ packages: '@sec-ant/readable-stream@0.4.1': resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==} + '@simple-libs/stream-utils@1.2.0': + resolution: {integrity: sha512-KxXvfapcixpz6rVEB6HPjOUZT22yN6v0vI0urQSk1L8MlEWPDFCZkhw2xmkyoTGYeFw7tWTZd7e3lVzRZRN/EA==} + engines: {node: '>=18'} + '@sinclair/typebox@0.31.28': resolution: {integrity: sha512-/s55Jujywdw/Jpan+vsy6JZs1z2ZTGxTmbZTPiuSL2wz9mfzA2gN1zzaqmvfi4pq+uOt7Du85fkiwv5ymW84aQ==} @@ -1305,8 +1320,8 @@ packages: '@unocss/extractor-arbitrary-variants@66.6.0': resolution: {integrity: sha512-AsCmpbre4hQb+cKOf3gHUeYlF7guR/aCKZvw53VBk12qY5wNF7LdfIx4zWc5LFVCoRxIZlU2C7L4/Tt7AkiFMA==} - '@unocss/extractor-svelte@66.6.3': - resolution: {integrity: sha512-eFr6IVBH3xO7ztwmFrBFkVTUezfqX5PYiMSi+keflSs0PP/YOolaXeJx315b4eqkg3ot+lZUtvCv/6VV3k3zQg==} + '@unocss/extractor-svelte@66.6.6': + resolution: {integrity: sha512-5+Et3jiSFlMqxkoyVLsoT2/Rd8x/Jd65i5KzIyXMtQccDmqN2wSXuyvB2h5sLauHn4bBe/qOWO3PfGjbXBGWOA==} '@unocss/inspector@66.6.0': resolution: {integrity: sha512-BvdY8ah+OTmzFMb+z8RZkaF15+PWRFt9S2bOARkkRBubybX9EE1rxM07l74kO5Dj16++CS4nO15XFq39pPoBvg==} @@ -1604,12 +1619,12 @@ packages: resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} engines: {node: ^14.18.0 || >=16.10.0} - conventional-changelog-angular@8.1.0: - resolution: {integrity: sha512-GGf2Nipn1RUCAktxuVauVr1e3r8QrLP/B0lEUsFktmGqc3ddbQkhoJZHJctVU829U1c6mTSWftrVOCHaL85Q3w==} + conventional-changelog-angular@8.3.0: + resolution: {integrity: sha512-DOuBwYSqWzfwuRByY9O4oOIvDlkUCTDzfbOgcSbkY+imXXj+4tmrEFao3K+FxemClYfYnZzsvudbwrhje9VHDA==} engines: {node: '>=18'} - conventional-changelog-conventionalcommits@9.1.0: - resolution: {integrity: sha512-MnbEysR8wWa8dAEvbj5xcBgJKQlX/m0lhS8DsyAAWDHdfs2faDJxTgzRYlRYpXSe7UiKrIIlB4TrBKU9q9DgkA==} + conventional-changelog-conventionalcommits@9.3.0: + resolution: {integrity: sha512-kYFx6gAyjSIMwNtASkI3ZE99U1fuVDJr0yTYgVy+I2QG46zNZfl2her+0+eoviG82c5WQvW1jMt1eOQTeJLodA==} engines: {node: '>=18'} conventional-changelog-preset-loader@5.0.0: @@ -1625,6 +1640,11 @@ packages: engines: {node: '>=18'} hasBin: true + conventional-commits-parser@6.3.0: + resolution: {integrity: sha512-RfOq/Cqy9xV9bOA8N+ZH6DlrDR+5S3Mi0B5kACEjESpE+AviIpAptx9a9cFpWCCvgRtWT+0BbUw+e1BZfts9jg==} + engines: {node: '>=18'} + hasBin: true + conventional-recommended-bump@10.0.0: resolution: {integrity: sha512-RK/fUnc2btot0oEVtrj3p2doImDSs7iiz/bftFCDzels0Qs1mxLghp+DFHMaOC0qiCI6sWzlTDyBFSYuot6pRA==} engines: {node: '>=18'} @@ -1645,8 +1665,8 @@ packages: cosmiconfig: '>=9' typescript: '>=5' - cosmiconfig@9.0.0: - resolution: {integrity: sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==} + cosmiconfig@9.0.1: + resolution: {integrity: sha512-hr4ihw+DBqcvrsEDioRO31Z17x71pUYoNe/4h6Z0wB72p7MU7/9gH8Q3s12NFhHPfYBBOV3qyfUxmr/Yn3shnQ==} engines: {node: '>=14'} peerDependencies: typescript: '>=4.9.5' @@ -2598,8 +2618,8 @@ packages: resolution: {integrity: sha512-SxToR7P8Y2lWmv/kTzVLC1t/GDI2WGjMwNhLLE9qtH8Q13C+aEmuRlzDst4Up4s0Wc8sF2M+J57iB3cMLqftfg==} engines: {node: '>=6.0.0'} - prettier-plugin-svelte@3.5.0: - resolution: {integrity: sha512-2lLO/7EupnjO/95t+XZesXs8Bf3nYLIDfCo270h5QWbj/vjLqmrQ1LiRk9LPggxSDsnVYfehamZNf+rgQYApZg==} + prettier-plugin-svelte@3.5.1: + resolution: {integrity: sha512-65+fr5+cgIKWKiqM1Doum4uX6bY8iFCdztvvp2RcF+AJoieaw9kJOFMNcJo/bkmKYsxFaM9OsVZK/gWauG/5mg==} peerDependencies: prettier: ^3.0.0 svelte: ^3.2.0 || ^4.0.0-next.0 || ^5.0.0-next.0 @@ -2794,6 +2814,12 @@ packages: svelte: optional: true + svelte-kit-sessions@0.4.0: + resolution: {integrity: sha512-cWjHwd+EGIuZ0p8CxSqE5EMOT8EUsoYfAnbE8QB+r6FonroYiMvTLUgv8b9dVLC55Yw3UtTntjUaZ5fKJF3XOA==} + peerDependencies: + '@sveltejs/kit': ^1.0.0 || ^2.0.0 + svelte: ^5.1.13 + svelte-sonner@1.0.7: resolution: {integrity: sha512-1EUFYmd7q/xfs2qCHwJzGPh9n5VJ3X6QjBN10fof2vxgy8fYE7kVfZ7uGnd7i6fQaWIr5KvXcwYXE/cmTEjk5A==} peerDependencies: @@ -3208,32 +3234,32 @@ snapshots: '@bcoe/v8-coverage@1.0.2': {} - '@commitlint/cli@20.4.2(@types/node@25.0.10)(typescript@5.9.3)': + '@commitlint/cli@20.4.3(@types/node@25.0.10)(typescript@5.9.3)': dependencies: - '@commitlint/format': 20.4.0 - '@commitlint/lint': 20.4.2 - '@commitlint/load': 20.4.0(@types/node@25.0.10)(typescript@5.9.3) - '@commitlint/read': 20.4.0 - '@commitlint/types': 20.4.0 + '@commitlint/format': 20.4.3 + '@commitlint/lint': 20.4.3 + '@commitlint/load': 20.4.3(@types/node@25.0.10)(typescript@5.9.3) + '@commitlint/read': 20.4.3 + '@commitlint/types': 20.4.3 tinyexec: 1.0.2 yargs: 17.7.2 transitivePeerDependencies: - '@types/node' - typescript - '@commitlint/config-conventional@20.4.2': + '@commitlint/config-conventional@20.4.3': dependencies: - '@commitlint/types': 20.4.0 - conventional-changelog-conventionalcommits: 9.1.0 + '@commitlint/types': 20.4.3 + conventional-changelog-conventionalcommits: 9.3.0 - '@commitlint/config-validator@20.4.0': + '@commitlint/config-validator@20.4.3': dependencies: - '@commitlint/types': 20.4.0 + '@commitlint/types': 20.4.3 ajv: 8.17.1 - '@commitlint/ensure@20.4.1': + '@commitlint/ensure@20.4.3': dependencies: - '@commitlint/types': 20.4.0 + '@commitlint/types': 20.4.3 lodash.camelcase: 4.3.0 lodash.kebabcase: 4.1.1 lodash.snakecase: 4.1.1 @@ -3242,31 +3268,31 @@ snapshots: '@commitlint/execute-rule@20.0.0': {} - '@commitlint/format@20.4.0': + '@commitlint/format@20.4.3': dependencies: - '@commitlint/types': 20.4.0 + '@commitlint/types': 20.4.3 picocolors: 1.1.1 - '@commitlint/is-ignored@20.4.1': + '@commitlint/is-ignored@20.4.3': dependencies: - '@commitlint/types': 20.4.0 + '@commitlint/types': 20.4.3 semver: 7.7.3 - '@commitlint/lint@20.4.2': + '@commitlint/lint@20.4.3': dependencies: - '@commitlint/is-ignored': 20.4.1 - '@commitlint/parse': 20.4.1 - '@commitlint/rules': 20.4.2 - '@commitlint/types': 20.4.0 + '@commitlint/is-ignored': 20.4.3 + '@commitlint/parse': 20.4.3 + '@commitlint/rules': 20.4.3 + '@commitlint/types': 20.4.3 - '@commitlint/load@20.4.0(@types/node@25.0.10)(typescript@5.9.3)': + '@commitlint/load@20.4.3(@types/node@25.0.10)(typescript@5.9.3)': dependencies: - '@commitlint/config-validator': 20.4.0 + '@commitlint/config-validator': 20.4.3 '@commitlint/execute-rule': 20.0.0 - '@commitlint/resolve-extends': 20.4.0 - '@commitlint/types': 20.4.0 - cosmiconfig: 9.0.0(typescript@5.9.3) - cosmiconfig-typescript-loader: 6.2.0(@types/node@25.0.10)(cosmiconfig@9.0.0(typescript@5.9.3))(typescript@5.9.3) + '@commitlint/resolve-extends': 20.4.3 + '@commitlint/types': 20.4.3 + cosmiconfig: 9.0.1(typescript@5.9.3) + cosmiconfig-typescript-loader: 6.2.0(@types/node@25.0.10)(cosmiconfig@9.0.1(typescript@5.9.3))(typescript@5.9.3) is-plain-obj: 4.1.0 lodash.mergewith: 4.6.2 picocolors: 1.1.1 @@ -3274,47 +3300,47 @@ snapshots: - '@types/node' - typescript - '@commitlint/message@20.4.0': {} + '@commitlint/message@20.4.3': {} - '@commitlint/parse@20.4.1': + '@commitlint/parse@20.4.3': dependencies: - '@commitlint/types': 20.4.0 - conventional-changelog-angular: 8.1.0 - conventional-commits-parser: 6.2.1 + '@commitlint/types': 20.4.3 + conventional-changelog-angular: 8.3.0 + conventional-commits-parser: 6.3.0 - '@commitlint/read@20.4.0': + '@commitlint/read@20.4.3': dependencies: - '@commitlint/top-level': 20.4.0 - '@commitlint/types': 20.4.0 + '@commitlint/top-level': 20.4.3 + '@commitlint/types': 20.4.3 git-raw-commits: 4.0.0 minimist: 1.2.8 tinyexec: 1.0.2 - '@commitlint/resolve-extends@20.4.0': + '@commitlint/resolve-extends@20.4.3': dependencies: - '@commitlint/config-validator': 20.4.0 - '@commitlint/types': 20.4.0 + '@commitlint/config-validator': 20.4.3 + '@commitlint/types': 20.4.3 global-directory: 4.0.1 import-meta-resolve: 4.2.0 lodash.mergewith: 4.6.2 resolve-from: 5.0.0 - '@commitlint/rules@20.4.2': + '@commitlint/rules@20.4.3': dependencies: - '@commitlint/ensure': 20.4.1 - '@commitlint/message': 20.4.0 + '@commitlint/ensure': 20.4.3 + '@commitlint/message': 20.4.3 '@commitlint/to-lines': 20.0.0 - '@commitlint/types': 20.4.0 + '@commitlint/types': 20.4.3 '@commitlint/to-lines@20.0.0': {} - '@commitlint/top-level@20.4.0': + '@commitlint/top-level@20.4.3': dependencies: escalade: 3.2.0 - '@commitlint/types@20.4.0': + '@commitlint/types@20.4.3': dependencies: - conventional-commits-parser: 6.2.1 + conventional-commits-parser: 6.3.0 picocolors: 1.1.1 '@conventional-changelog/git-client@1.0.1(conventional-commits-filter@5.0.0)(conventional-commits-parser@6.2.1)': @@ -3504,7 +3530,7 @@ snapshots: dependencies: '@iconify/types': 2.0.0 - '@iconify-json/material-icon-theme@1.2.51': + '@iconify-json/material-icon-theme@1.2.55': dependencies: '@iconify/types': 2.0.0 @@ -3546,6 +3572,8 @@ snapshots: transitivePeerDependencies: - babel-plugin-macros + '@isaacs/ttlcache@1.4.1': {} + '@jridgewell/gen-mapping@0.3.13': dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -3823,6 +3851,8 @@ snapshots: '@sec-ant/readable-stream@0.4.1': {} + '@simple-libs/stream-utils@1.2.0': {} + '@sinclair/typebox@0.31.28': {} '@sindresorhus/merge-streams@4.0.0': {} @@ -3968,7 +3998,7 @@ snapshots: dependencies: svelte: 5.48.0 - '@trivago/prettier-plugin-sort-imports@6.0.2(prettier-plugin-svelte@3.5.0(prettier@3.8.1)(svelte@5.48.0))(prettier@3.8.1)(svelte@5.48.0)': + '@trivago/prettier-plugin-sort-imports@6.0.2(prettier-plugin-svelte@3.5.1(prettier@3.8.1)(svelte@5.48.0))(prettier@3.8.1)(svelte@5.48.0)': dependencies: '@babel/generator': 7.28.6 '@babel/parser': 7.28.6 @@ -3980,7 +4010,7 @@ snapshots: parse-imports-exports: 0.2.4 prettier: 3.8.1 optionalDependencies: - prettier-plugin-svelte: 3.5.0(prettier@3.8.1)(svelte@5.48.0) + prettier-plugin-svelte: 3.5.1(prettier@3.8.1)(svelte@5.48.0) svelte: 5.48.0 transitivePeerDependencies: - supports-color @@ -4146,7 +4176,7 @@ snapshots: dependencies: '@unocss/core': 66.6.0 - '@unocss/extractor-svelte@66.6.3': {} + '@unocss/extractor-svelte@66.6.6': {} '@unocss/inspector@66.6.0': dependencies: @@ -4515,11 +4545,11 @@ snapshots: consola@3.4.2: {} - conventional-changelog-angular@8.1.0: + conventional-changelog-angular@8.3.0: dependencies: compare-func: 2.0.0 - conventional-changelog-conventionalcommits@9.1.0: + conventional-changelog-conventionalcommits@9.3.0: dependencies: compare-func: 2.0.0 @@ -4531,6 +4561,11 @@ snapshots: dependencies: meow: 13.2.0 + conventional-commits-parser@6.3.0: + dependencies: + '@simple-libs/stream-utils': 1.2.0 + meow: 13.2.0 + conventional-recommended-bump@10.0.0: dependencies: '@conventional-changelog/git-client': 1.0.1(conventional-commits-filter@5.0.0)(conventional-commits-parser@6.2.1) @@ -4543,14 +4578,14 @@ snapshots: core-util-is@1.0.3: {} - cosmiconfig-typescript-loader@6.2.0(@types/node@25.0.10)(cosmiconfig@9.0.0(typescript@5.9.3))(typescript@5.9.3): + cosmiconfig-typescript-loader@6.2.0(@types/node@25.0.10)(cosmiconfig@9.0.1(typescript@5.9.3))(typescript@5.9.3): dependencies: '@types/node': 25.0.10 - cosmiconfig: 9.0.0(typescript@5.9.3) + cosmiconfig: 9.0.1(typescript@5.9.3) jiti: 2.6.1 typescript: 5.9.3 - cosmiconfig@9.0.0(typescript@5.9.3): + cosmiconfig@9.0.1(typescript@5.9.3): dependencies: env-paths: 2.2.1 import-fresh: 3.3.1 @@ -5437,7 +5472,7 @@ snapshots: dependencies: fast-diff: 1.3.0 - prettier-plugin-svelte@3.5.0(prettier@3.8.1)(svelte@5.48.0): + prettier-plugin-svelte@3.5.1(prettier@3.8.1)(svelte@5.48.0): dependencies: prettier: 3.8.1 svelte: 5.48.0 @@ -5626,6 +5661,12 @@ snapshots: optionalDependencies: svelte: 5.48.0 + svelte-kit-sessions@0.4.0(@sveltejs/kit@2.53.4(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.48.0)(vite@7.3.1(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.2)))(svelte@5.48.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.2)))(svelte@5.48.0): + dependencies: + '@isaacs/ttlcache': 1.4.1 + '@sveltejs/kit': 2.53.4(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.48.0)(vite@7.3.1(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.2)))(svelte@5.48.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.2)) + svelte: 5.48.0 + svelte-sonner@1.0.7(svelte@5.48.0): dependencies: runed: 0.28.0(svelte@5.48.0) diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index cb86f5d..beede95 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -21,6 +21,7 @@ catalogs: svelte: ^5.48.0 svelte-check: ^4.3.5 vite: ^7.3.1 + svelte-kit-sessions: "^0.4.0" css: '@alexanderniebuhr/prettier-plugin-unocss': ^0.0.4 '@unocss/extractor-svelte': ^66.6.3 diff --git a/src/app.d.ts b/src/app.d.ts index 520c421..a43c4ed 100644 --- a/src/app.d.ts +++ b/src/app.d.ts @@ -10,4 +10,10 @@ declare global { } } +declare module 'svelte-kit-sessions' { + interface SessionData { + path: string; + } +} + export {}; diff --git a/src/demo.spec.ts b/src/demo.spec.ts deleted file mode 100644 index e2efa47..0000000 --- a/src/demo.spec.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { expect, it } from 'vitest'; - -it('expect 1 + 2 = 3', async () => { - expect(1 + 2).equal(3); -}); diff --git a/src/hooks.server.ts b/src/hooks.server.ts index 917ce46..22b59c5 100644 --- a/src/hooks.server.ts +++ b/src/hooks.server.ts @@ -1,5 +1,25 @@ +import { env } from '$env/dynamic/private'; import { paraglideMiddleware } from '$lib/paraglide/server'; -import type { Handle } from '@sveltejs/kit'; +import { type Handle, redirect } from '@sveltejs/kit'; +import { sequence } from '@sveltejs/kit/hooks'; +import * as crypto from 'node:crypto'; +import { sveltekitSessionHandle } from 'svelte-kit-sessions'; + +if (!env.SESSION_SECRET) { + env.SESSION_SECRET = crypto.randomBytes(20).toString('hex'); + console.log(`SESSION_SECRET not found, generating a temporary one: ${env.SESSION_SECRET}`); +} + +const sessionHandle = sveltekitSessionHandle({ + secret: env.SESSION_SECRET, +}); + +const checkAuthorizationHandle: Handle = async ({ event, resolve }) => { + if (!event.locals.session.data.path && event.url.pathname !== '/load-project') { + throw redirect(302, '/load-project'); + } + return resolve(event); +}; const handleParaglide: Handle = ({ event, resolve }) => paraglideMiddleware(event.request, ({ request, locale }) => { @@ -10,4 +30,4 @@ const handleParaglide: Handle = ({ event, resolve }) => }); }); -export const handle: Handle = handleParaglide; +export const handle: Handle = sequence(sessionHandle, handleParaglide, checkAuthorizationHandle); diff --git a/src/utils/http-client/http-client.ts b/src/lib/server/client.ts similarity index 62% rename from src/utils/http-client/http-client.ts rename to src/lib/server/client.ts index 47b766e..b74bf1a 100644 --- a/src/utils/http-client/http-client.ts +++ b/src/lib/server/client.ts @@ -6,38 +6,44 @@ export interface MiddlewareParams { options: RequestOptions; } -export type MiddlewareNext = (params?: MiddlewareParams) => Promise; +export type FullResponse = Response & { content: any }; + +export type MiddlewareNext = (params?: MiddlewareParams) => Promise; export type Middleware = ( params: MiddlewareParams, next: MiddlewareNext, -) => Promise | undefined; +) => Promise | undefined; -type BaseRequest = (path: string, options: RequestOptions) => Promise; +type BaseRequest = (path: string, options: RequestOptions) => Promise; export class HttpClient { private readonly _baseUrl: string; private readonly _baseOptions: RequestOptions; private readonly _middlewares: Middleware[]; - constructor(baseUrl: string) { + constructor(baseUrl: string, options?: RequestOptions) { this._baseUrl = baseUrl; - this._baseOptions = {}; + this._baseOptions = options ?? { + headers: { + 'Content-Type': 'application/json', + }, + }; this._middlewares = []; } - get(path: string, options?: RequestOptions): Promise { + get(path: string, options?: RequestOptions): Promise { return this._applyMiddlewares(path, options, (newPath, newOptions) => { - return fetch(newPath, { + return this._request(newPath, { ...newOptions, method: 'GET', }); }); } - post(path: string, body?: any, options?: RequestOptions): Promise { + post(path: string, body?: string, options?: RequestOptions): Promise { return this._applyMiddlewares(path, options, (newPath, newOptions) => { - return fetch(newPath, { + return this._request(newPath, { ...newOptions, method: 'POST', body: body, @@ -45,9 +51,9 @@ export class HttpClient { }); } - put(path: string, body?: any, options?: RequestOptions): Promise { + put(path: string, body?: string, options?: RequestOptions): Promise { return this._applyMiddlewares(path, options, (newPath, newOptions) => { - return fetch(newPath, { + return this._request(newPath, { ...newOptions, method: 'PUT', body: body, @@ -55,9 +61,9 @@ export class HttpClient { }); } - patch(path: string, body?: any, options?: RequestOptions): Promise { - return this._applyMiddlewares(path, options, (newPath, newOptions) => { - return fetch(newPath, { + patch(path: string, body?: string, options?: RequestOptions): Promise { + return this._applyMiddlewares(path, options, async (newPath, newOptions) => { + return this._request(newPath, { ...newOptions, method: 'PATCH', body: body, @@ -65,12 +71,12 @@ export class HttpClient { }); } - delete(path: string, options?: RequestOptions): Promise { + delete(path: string, options?: RequestOptions): Promise { return this._applyMiddlewares(path, options, (newPath, newOptions) => { - return fetch(newPath, { + return this._request(newPath, { ...newOptions, method: 'DELETE', - }); + }) as Promise; }); } @@ -79,11 +85,17 @@ export class HttpClient { return this; } + private async _request(path: string, request: RequestInit): Promise { + const res = (await fetch(path, request)) as FullResponse; + res.content = null; + return res; + } + private _applyMiddlewares( path: string, options: RequestOptions | undefined, callback: BaseRequest, - ): Promise { + ): Promise { const baseParams = { path, fullPath: this._getUrl(path), @@ -93,14 +105,14 @@ export class HttpClient { }, }; const middlewares = this._middlewares.slice(); - let response: Response; + let response: FullResponse; - const execution = async (params?: MiddlewareParams): Promise => { + const execution = async (params?: MiddlewareParams): Promise => { if (!params) params = baseParams; const middleware = middlewares.shift(); - if (!middleware) response = (await callback(params.fullPath, params.options)) as Response; + if (!middleware) response = (await callback(params.fullPath, params.options)) as FullResponse; else response = (await middleware(params, execution)) ?? response; return response; diff --git a/src/lib/server/utils/file-system/file-system-error.ts b/src/lib/server/utils/file-system/file-system-error.ts new file mode 100644 index 0000000..00de3d5 --- /dev/null +++ b/src/lib/server/utils/file-system/file-system-error.ts @@ -0,0 +1,7 @@ +export class FileSystemError extends Error { + message: string; + constructor(message: string) { + super(); + this.message = message; + } +} diff --git a/src/lib/server/utils/file-system/project-directory.ts b/src/lib/server/utils/file-system/project-directory.ts new file mode 100644 index 0000000..92645d5 --- /dev/null +++ b/src/lib/server/utils/file-system/project-directory.ts @@ -0,0 +1,125 @@ +import { FileSystemError } from '@utils-server/file-system/file-system-error'; +import fs from 'node:fs'; +import path from 'node:path'; + +export class ProjectDirectory { + private path: string; + private readonly projectPath: string; + + constructor(dirPath: string, projectPath: string) { + this.path = path.resolve(projectPath, './' + dirPath); + this.projectPath = projectPath; + } + + read(recursive: boolean = false): { files: string[]; directories: {} } { + this._checkPathIsInsideProject(); + this._checkPathExists(); + this._checkPathIsDir(); + this._checkPathIsReadable(); + return this._readDirContent(this.path, recursive); + } + + create(): void { + this._checkPathIsInsideProject(); + this._checkPathNotExists(); + + fs.mkdirSync(this.path, { recursive: true }); + } + + delete(recursive: boolean = false): void { + this._checkPathIsInsideProject(); + this._checkPathExists(); + this._checkPathIsDir(); + if (!recursive) { + this._checkDirIsEmpty(); + } + fs.rmSync(this.path, { recursive: recursive }); + } + + rename(newPath: string): void { + const absoluteNewDirPath = path.resolve(this.projectPath, './' + newPath); + this._checkPathIsInsideProject(); + this._checkPathIsInsideProject(absoluteNewDirPath); + this._checkPathExists(); + this._checkPathIsDir(); + this._checkPathIsWritable(); + const newFolderPath = path.dirname(absoluteNewDirPath); + this._checkPathExists(newFolderPath); + this._checkPathIsWritable(newFolderPath); + this._checkPathNotExists(absoluteNewDirPath); + fs.renameSync(this.path, absoluteNewDirPath); + this.path = absoluteNewDirPath; + } + + private _checkPathIsInsideProject(path: string = this.path) { + if (!path.startsWith(this.projectPath)) { + throw new FileSystemError(`Path ${path} is outside of the project directory`); + } + } + + private _checkPathExists(path: string = this.path) { + if (!fs.existsSync(path)) { + throw new FileSystemError(`Path ${path} should exist`); + } + } + + private _checkPathNotExists(path: string = this.path) { + if (fs.existsSync(path)) { + throw new FileSystemError(`Path ${path} should not exist`); + } + } + + private _checkPathIsDir(path: string = this.path) { + let stats: fs.Stats; + try { + stats = fs.lstatSync(path); + } catch { + throw new FileSystemError(`Path ${path} does not exist`); + } + if (!stats.isDirectory()) { + throw new FileSystemError(`Path ${path} is not a directory`); + } + } + + private _checkPathIsWritable(path: string = this.path) { + try { + fs.accessSync(path, fs.constants.W_OK); + } catch { + throw new FileSystemError(`Path ${path} writable`); + } + } + + private _checkPathIsReadable(path: string = this.path) { + try { + fs.accessSync(path, fs.constants.R_OK); + } catch { + throw new FileSystemError(`Path ${path} writable`); + } + } + + private _checkDirIsEmpty(path: string = this.path) { + if (fs.readdirSync(path).length > 0) { + throw new FileSystemError(`Directory ${path} is not empty`); + } + } + + private _readDirContent( + path: string = this.path, + recursive: boolean = false, + ): { files: string[]; directories: {} } { + const dirContent: { files: string[]; directories: { [key: string]: any } } = { + files: [], + directories: {}, + }; + fs.readdirSync(path, { withFileTypes: true, recursive: false }).forEach((item) => { + if (item.isFile()) { + dirContent.files.push(item.name); + } else if (item.isDirectory()) { + dirContent.directories[item.name] = recursive + ? this._readDirContent(path + '/' + item.name, recursive) + : {}; + } + }); + return dirContent; + } +} diff --git a/src/lib/server/utils/file-system/project-file.ts b/src/lib/server/utils/file-system/project-file.ts new file mode 100644 index 0000000..1535f71 --- /dev/null +++ b/src/lib/server/utils/file-system/project-file.ts @@ -0,0 +1,128 @@ +import { FileSystemError } from '@utils-server/file-system/file-system-error'; +import fs from 'node:fs'; +import path from 'node:path'; + +export class ProjectFile { + private path: string; + private readonly projectPath: string; + + constructor(filePath: string, projectPath: string) { + this.path = path.resolve(projectPath, './' + filePath); + this.projectPath = projectPath; + } + + read(): string { + this._checkPathIsInsideProject(); + this._checkPathExists(); + this._checkPathIsFile(); + this._checkPathIsReadable(); + return fs.readFileSync(this.path).toString(); + } + + readJson(): T { + const raw = this.read(); + return JSON.parse(raw) as T; + } + + write(text: string): void { + this._checkPathIsInsideProject(); + try { + this._checkPathExists(); + } catch { + this._checkPathIsFile(); + this._checkPathIsWritable(); + fs.writeFileSync(this.path, text, { flush: true }); + return; + } + const folderPath = path.dirname(this.path); + this._checkPathExists(folderPath); + this._checkPathIsDir(folderPath); + this._checkPathIsWritable(folderPath); + fs.writeFileSync(this.path, text, { flush: true }); + } + + writeJson(content: any): void { + const raw = JSON.stringify(content); + this.write(raw); + } + + delete(): void { + this._checkPathIsInsideProject(); + this._checkPathExists(); + this._checkPathIsFile(); + this._checkPathIsWritable(); + fs.rmSync(this.path); + } + + rename(newPath: string): void { + const absoluteNewPath = path.resolve(this.projectPath, './' + newPath); + this._checkPathIsInsideProject(newPath); + this._checkPathExists(); + this._checkPathIsFile(); + this._checkPathIsWritable(); + const newFolderPath = path.dirname(absoluteNewPath); + this._checkPathExists(newFolderPath); + this._checkPathIsWritable(newFolderPath); + this._checkPathNotExists(absoluteNewPath); + fs.renameSync(this.path, absoluteNewPath); + this.path = absoluteNewPath; + } + + private _checkPathIsInsideProject(path: string = this.path) { + if (!path.startsWith(this.projectPath)) { + throw new FileSystemError(`Path ${path} is outside of the project directory`); + } + } + + private _checkPathExists(path: string = this.path) { + if (!fs.existsSync(path)) { + throw new FileSystemError(`Path ${path} should exist`); + } + } + + private _checkPathNotExists(path: string = this.path) { + if (fs.existsSync(path)) { + throw new FileSystemError(`Path ${path} should not exist`); + } + } + + private _checkPathIsFile(path: string = this.path) { + let stats: fs.Stats; + try { + stats = fs.lstatSync(path); + } catch { + throw new FileSystemError(`Path ${path} does not exist`); + } + if (!stats.isFile()) { + throw new FileSystemError(`Path ${path} is not a this.path`); + } + } + + private _checkPathIsDir(path: string = this.path) { + let stats: fs.Stats; + try { + stats = fs.lstatSync(path); + } catch { + throw new FileSystemError(`Path ${path} does not exist`); + } + if (!stats.isDirectory()) { + throw new FileSystemError(`Path ${path} is not a directory`); + } + } + + private _checkPathIsWritable(path: string = this.path) { + try { + fs.accessSync(path, fs.constants.W_OK); + } catch { + throw new FileSystemError(`Path ${path} writable`); + } + } + + private _checkPathIsReadable(path: string = this.path) { + try { + fs.accessSync(path, fs.constants.R_OK); + } catch { + throw new FileSystemError(`Path ${path} writable`); + } + } +} diff --git a/src/lib/server/utils/server-api/clients.ts b/src/lib/server/utils/server-api/clients.ts new file mode 100644 index 0000000..70e744a --- /dev/null +++ b/src/lib/server/utils/server-api/clients.ts @@ -0,0 +1,19 @@ +import { env } from '$env/dynamic/private'; +import { HttpClient } from '@utils/http'; + +import { Repository } from './repository'; + +const client = new HttpClient(env.API_URL ?? ''); + +export const serverApi = new Repository(client); + +export const withAuth = (token: string) => { + return new Repository( + new HttpClient(env.API_URL ?? '', { + headers: { + Authorization: `Bearer ${token}`, + 'Content-Type': 'application/json', + }, + }), + ); +}; diff --git a/src/lib/server/utils/server-api/guards/auth.guard.ts b/src/lib/server/utils/server-api/guards/auth.guard.ts new file mode 100644 index 0000000..ef0372c --- /dev/null +++ b/src/lib/server/utils/server-api/guards/auth.guard.ts @@ -0,0 +1,37 @@ +import { type Cookies, json } from '@sveltejs/kit'; + +import { serverApi, withAuth } from '../clients'; +import type { Repository } from '../index'; +import type { RefreshTokenInput, Token } from '../types'; +import { setTokensInCookies } from '../utils'; +import { errorGuard } from './error.guard'; + +export const authGuard = async ( + callback: (httpClient: Repository) => Promise, + cookies: Cookies, +): Promise => { + return errorGuard(async () => { + let accessToken = cookies.get('accessToken'); + + if (!accessToken) { + const refreshToken = cookies.get('refreshToken'); + + if (!refreshToken) { + return json({ error: 'Unauthorized', message: 'No token found' }, { status: 401 }); + } + + try { + const tokens = await serverApi.post('/auth/refresh-token', { + refreshToken, + }); + + setTokensInCookies(cookies, tokens); + accessToken = tokens.accessToken; + } catch { + return json({ error: 'Unauthorized', message: 'Invalid refresh token' }, { status: 401 }); + } + } + + return callback(withAuth(accessToken)); + }); +}; diff --git a/src/lib/server/utils/server-api/guards/error.guard.ts b/src/lib/server/utils/server-api/guards/error.guard.ts new file mode 100644 index 0000000..95b6e29 --- /dev/null +++ b/src/lib/server/utils/server-api/guards/error.guard.ts @@ -0,0 +1,33 @@ +import { json } from '@sveltejs/kit'; +import { STATUS_CODES } from 'node:http'; + +export const errorGuard = async (callback: () => Promise): Promise => { + try { + return await callback(); + } catch (error: any) { + const data: + | { + statusCode: number; + path: string; + error: { + message: string | string[]; + timestamp: string; + cause?: { + message: string; + }; + }; + } + | undefined = error?.cause; + + const statusCode = data?.statusCode ?? 500; + + return json( + { + error: STATUS_CODES[statusCode] || 'Unknown error', + message: data?.error?.message || 'Unknown error', + cause: data?.error?.cause?.message || undefined, + }, + { status: statusCode }, + ); + } +}; diff --git a/src/lib/server/utils/server-api/guards/params.guard.ts b/src/lib/server/utils/server-api/guards/params.guard.ts new file mode 100644 index 0000000..2073c67 --- /dev/null +++ b/src/lib/server/utils/server-api/guards/params.guard.ts @@ -0,0 +1,32 @@ +import { type RequestEvent, json } from '@sveltejs/kit'; + +export const parseParams = (event: RequestEvent, params: string[], regex: RegExp) => { + const searchParams = event.url.pathname.match(regex)?.slice(1); + + const res: Record = {}; + let i = 0; + + for (const param of params) { + res[param] = searchParams?.[i] ?? null; + i++; + } + + return res; +}; + +export const paramsGuard = async ( + event: RequestEvent, + rawParams: string[], + regex: RegExp, + callback: (params: Record) => Promise, +): Promise => { + const params = parseParams(event, rawParams, regex); + + for (const param in params) { + if (!params[param]) { + return json({ error: 'Missing required parameters' }, { status: 400 }); + } + } + + return callback(params as Record); +}; diff --git a/src/lib/server/utils/server-api/index.ts b/src/lib/server/utils/server-api/index.ts new file mode 100644 index 0000000..fcdf960 --- /dev/null +++ b/src/lib/server/utils/server-api/index.ts @@ -0,0 +1,8 @@ +export { serverApi } from './clients'; +export { Repository } from './repository'; +export type { Token } from './types'; +export { resetTokensInCookies, setTokensInCookies } from './utils'; + +export { authGuard } from './guards/auth.guard'; +export { errorGuard } from './guards/error.guard'; +export { paramsGuard } from './guards/params.guard'; diff --git a/src/lib/server/utils/server-api/repository.ts b/src/lib/server/utils/server-api/repository.ts new file mode 100644 index 0000000..88a4f27 --- /dev/null +++ b/src/lib/server/utils/server-api/repository.ts @@ -0,0 +1,73 @@ +import type { HttpClient, RequestOptions } from '@utils/http'; + +export class Repository { + private readonly _client: HttpClient; + + constructor(client: HttpClient) { + this._client = client; + } + + get(path: string, options?: RequestOptions): Promise { + return this.runRequest('get', path, options); + } + + post( + path: string, + body?: I, + options?: RequestOptions, + ): Promise { + return this.runRequestBody('post', path, body, options); + } + + put( + path: string, + body?: I, + options?: RequestOptions, + ): Promise { + return this.runRequestBody('put', path, body, options); + } + + patch( + path: string, + body?: I, + options?: RequestOptions, + ): Promise { + return this.runRequestBody('patch', path, body, options); + } + + delete(path: string, options?: RequestOptions): Promise { + return this.runRequest('delete', path, options); + } + + private async runRequest( + request: 'get' | 'delete', + path: string, + options?: RequestOptions, + ): Promise { + const res = await this._client[request](path, options); + if (!res.ok) + throw new Error(`Request failed with status code ${res.status}`, { + cause: res, + }); + return (await res.json()) as R; + } + + private async runRequestBody( + request: 'post' | 'put' | 'patch', + path: string, + body?: I, + options?: RequestOptions, + ): Promise { + const res = await this._client[request]( + path, + body === undefined ? undefined : JSON.stringify(body), + options, + ); + const data = (await res.json()) as R; + if (!res.ok) + throw new Error(`Request failed with status code ${res.status}`, { + cause: data, + }); + return data; + } +} diff --git a/src/lib/server/utils/server-api/types.ts b/src/lib/server/utils/server-api/types.ts new file mode 100644 index 0000000..8bd14d9 --- /dev/null +++ b/src/lib/server/utils/server-api/types.ts @@ -0,0 +1,9 @@ +export interface Token { + accessToken: string; + refreshToken: string; + tokenExpiresAt: string; +} + +export interface RefreshTokenInput { + refreshToken: string; +} diff --git a/src/lib/server/utils/server-api/utils.ts b/src/lib/server/utils/server-api/utils.ts new file mode 100644 index 0000000..cad5ba9 --- /dev/null +++ b/src/lib/server/utils/server-api/utils.ts @@ -0,0 +1,32 @@ +import { env } from '$env/dynamic/private'; +import type { Cookies, RequestEvent } from '@sveltejs/kit'; + +import type { Token } from './types'; + +export const setTokensInCookies = ( + cookies: Cookies, + { accessToken, refreshToken, tokenExpiresAt }: Token, +) => { + cookies.set('accessToken', accessToken, { + httpOnly: true, + secure: env.NODE_ENV === 'production', + sameSite: 'lax', + path: '/', + expires: new Date(tokenExpiresAt), + }); + + cookies.set('refreshToken', refreshToken, { + httpOnly: true, + secure: env.NODE_ENV === 'production', + sameSite: 'lax', + path: '/', + maxAge: 60 * 60 * 24 * 30, // 30 days + }); +}; + +export const resetTokensInCookies = (event: RequestEvent) => { + const { cookies } = event; + + cookies.delete('accessToken', { path: '/' }); + cookies.delete('refreshToken', { path: '/' }); +}; diff --git a/src/utils/file-system/file-system-directory.ts b/src/lib/utils-client/file-system/file-system-directory.ts similarity index 100% rename from src/utils/file-system/file-system-directory.ts rename to src/lib/utils-client/file-system/file-system-directory.ts diff --git a/src/utils/file-system/file-system-file.ts b/src/lib/utils-client/file-system/file-system-file.ts similarity index 100% rename from src/utils/file-system/file-system-file.ts rename to src/lib/utils-client/file-system/file-system-file.ts diff --git a/src/utils/file-system/file-system-handler.ts b/src/lib/utils-client/file-system/file-system-handler.ts similarity index 100% rename from src/utils/file-system/file-system-handler.ts rename to src/lib/utils-client/file-system/file-system-handler.ts diff --git a/src/utils/file-system/file-system-manager.ts b/src/lib/utils-client/file-system/file-system-manager.ts similarity index 100% rename from src/utils/file-system/file-system-manager.ts rename to src/lib/utils-client/file-system/file-system-manager.ts diff --git a/src/utils/file-system/index.ts b/src/lib/utils-client/file-system/index.ts similarity index 100% rename from src/utils/file-system/index.ts rename to src/lib/utils-client/file-system/index.ts diff --git a/src/utils/local-file-system/index.ts b/src/lib/utils-client/local-file-system/index.ts similarity index 100% rename from src/utils/local-file-system/index.ts rename to src/lib/utils-client/local-file-system/index.ts diff --git a/src/utils/local-file-system/local-file-system.ts b/src/lib/utils-client/local-file-system/local-file-system.ts similarity index 100% rename from src/utils/local-file-system/local-file-system.ts rename to src/lib/utils-client/local-file-system/local-file-system.ts diff --git a/src/lib/utils/http/index.ts b/src/lib/utils/http/index.ts new file mode 100644 index 0000000..866345e --- /dev/null +++ b/src/lib/utils/http/index.ts @@ -0,0 +1,6 @@ +export { + HttpClient, + type MiddlewareNext, + type MiddlewareParams, + type RequestOptions, +} from '../../server/client'; diff --git a/src/lib/utils/index.ts b/src/lib/utils/index.ts new file mode 100644 index 0000000..c202386 --- /dev/null +++ b/src/lib/utils/index.ts @@ -0,0 +1 @@ +export * from './http'; diff --git a/src/modules/http/api/client.ts b/src/modules/http/api/client.ts deleted file mode 100644 index 4d96e60..0000000 --- a/src/modules/http/api/client.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { client } from '../client'; -import { ComponentStorageRepository } from './repository/repositories/component-storage.repository'; - -export const api = { - componentStorage: new ComponentStorageRepository(client), -}; diff --git a/src/modules/http/api/index.ts b/src/modules/http/api/index.ts deleted file mode 100644 index 658b70d..0000000 --- a/src/modules/http/api/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { api } from './client'; diff --git a/src/modules/http/api/repository/base.repository.ts b/src/modules/http/api/repository/base.repository.ts deleted file mode 100644 index f3b01af..0000000 --- a/src/modules/http/api/repository/base.repository.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { type HttpClient, type RequestOptions } from '../../../../utils/http-client'; - -export class BaseRepository { - private readonly _client: HttpClient; - - constructor(client: HttpClient) { - this._client = client; - } - - protected get(path: string, options?: RequestOptions): Promise { - return this._client.get(path, options); - } - - protected post(path: string, body: object, options?: RequestOptions): Promise { - return this._client.post(path, JSON.stringify(body), options); - } - - protected put(path: string, body: object, options?: RequestOptions): Promise { - return this._client.put(path, JSON.stringify(body), options); - } - - protected patch(path: string, body: object, options?: RequestOptions): Promise { - return this._client.patch(path, JSON.stringify(body), options); - } - - protected delete(path: string, options?: RequestOptions): Promise { - return this._client.delete(path, options); - } -} diff --git a/src/modules/http/api/repository/repositories/component-storage.repository.ts b/src/modules/http/api/repository/repositories/component-storage.repository.ts deleted file mode 100644 index 1d54e28..0000000 --- a/src/modules/http/api/repository/repositories/component-storage.repository.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { BaseRepository } from '../base.repository'; - -export class ComponentStorageRepository extends BaseRepository { - getManifest(componentCode: string): Promise { - return this.get(`/components/storage/${componentCode}/manifest`); - } -} diff --git a/src/modules/http/client.ts b/src/modules/http/client.ts deleted file mode 100644 index 113bab2..0000000 --- a/src/modules/http/client.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { HttpClient } from '../../utils/http-client'; -import { AuthMiddleware } from './middlewares/auth.middleware'; -import { LoggerMiddleware } from './middlewares/logger.middleware'; - -export const client = new HttpClient(import.meta.env.VITE_API_BASE_URL).useMiddlewares( - AuthMiddleware, - LoggerMiddleware, -); diff --git a/src/modules/http/index.ts b/src/modules/http/index.ts deleted file mode 100644 index b9705d9..0000000 --- a/src/modules/http/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { type RequestOptions } from '../../utils/http-client'; -export * from './api'; diff --git a/src/modules/http/middlewares/auth.middleware.ts b/src/modules/http/middlewares/auth.middleware.ts deleted file mode 100644 index 998a3f6..0000000 --- a/src/modules/http/middlewares/auth.middleware.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { authStore } from '../../../stores/auth.store'; -import type { MiddlewareNext, MiddlewareParams } from '../../../utils/http-client'; - -let accessToken: string | null = null; - -authStore.subscribe((auth) => { - accessToken = auth.accessToken; -}); - -export const AuthMiddleware = async (params: MiddlewareParams, next: MiddlewareNext) => { - if (accessToken) { - params.options.headers = { - ...params.options.headers, - Authorization: `Bearer ${accessToken}`, - }; - } - return next(params); -}; diff --git a/src/modules/http/middlewares/logger.middleware.ts b/src/modules/http/middlewares/logger.middleware.ts deleted file mode 100644 index ce8af21..0000000 --- a/src/modules/http/middlewares/logger.middleware.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { toast } from 'svelte-sonner'; - -import type { MiddlewareNext, MiddlewareParams } from '../../../utils/http-client'; - -export const LoggerMiddleware = async (params: MiddlewareParams, next: MiddlewareNext) => { - const res = await next(params); - if (res.ok) return res; - const content = await res.json(); - const message = content?.error?.message - ? Array.isArray(content.error.message) - ? content.error.message.map((err: string) => `- ${err}`).join('\n') - : content.error.message - : 'Unknown error'; - toast.error(`An error occured :\n${message}`); - - throw new Error(message, { - cause: res, - }); -}; diff --git a/src/modules/project/project-manager.ts b/src/modules/project/project-manager.ts deleted file mode 100644 index 087349b..0000000 --- a/src/modules/project/project-manager.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { FileSystemManager } from '../../utils/file-system'; -import { LocalFileSystem } from '../../utils/local-file-system'; -import { Project } from './project'; -import { SaveHandler } from './save/save-handler'; - -export class ProjectManager { - private _currentId: string | null = null; - private _current: Project | null = null; - private _fs: FileSystemManager; - - constructor() { - this._fs = new FileSystemManager('projects'); - } - - getProject(): Project { - if (!this._current) throw new Error('No project loaded'); - return this._current; - } - - async create(name: string): Promise { - if (await this._fs.directoryExist(name)) - throw new Error(`Project named "${name}" already exists`); - - await this._loadProject(name); - await this._postLoad(); - } - - async loadFromSave(name: string): Promise { - if (!(await this._fs.directoryExist(name))) - throw new Error(`Project named "${name}" does not exists`); - - await this._loadProject(name); - await this._postLoad(); - } - - async loadFromLocal(name: string): Promise { - if (await this._fs.directoryExist(name)) - throw new Error(`Project named "${name}" already exists`); - - await this._loadProject(name); - const localFs = new LocalFileSystem('projects'); - await localFs.askFileAndCache(`${name}/save.nfps`); - await this._postLoad(); - } - - close(): void { - this._currentId = null; - this._current = null; - } - - private async _loadProject(name: string): Promise { - this._currentId = name; - const dir = await this._fs.getDirectory(name); - - const save = new SaveHandler(await dir.getFile('save.nfps')); - this._current = new Project(dir, save); - } - - private async _postLoad(): Promise { - await this._current?.save.fetchSave(); - this._current?.save.enableAutoSave(); - } -} diff --git a/src/modules/project/project.ts b/src/modules/project/project.ts deleted file mode 100644 index b5184fa..0000000 --- a/src/modules/project/project.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { type FileSystemDirectory } from '../../utils/file-system'; -import type { SaveHandler } from './save/save-handler'; - -export class Project { - private readonly _fs: FileSystemDirectory; - private readonly _saveHandler: SaveHandler; - - constructor(fs: FileSystemDirectory, saveHandler: SaveHandler) { - this._fs = fs; - this._saveHandler = saveHandler; - } - - get save(): SaveHandler { - return this._saveHandler; - } -} diff --git a/src/modules/project/save/save-handler.ts b/src/modules/project/save/save-handler.ts deleted file mode 100644 index 9352a5a..0000000 --- a/src/modules/project/save/save-handler.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { type FileSystemFile } from '../../../utils/file-system'; -import type { ISave } from './save.type'; - -export class SaveHandler { - public data!: ISave; - private _fs!: FileSystemFile; - private _autoSaveStatus: number | null = null; - private _autoSaveInterval: number = 10 * 1000; - - constructor(fs: FileSystemFile) { - this._fs = fs; - } - - async fetchSave(): Promise { - this.data = await this._fs.readJson(); - } - - async save(): Promise { - await this._fs.writeJson(this.data); - } - - enableAutoSave(): void { - if (this._autoSaveStatus !== null) return; - this._autoSaveStatus = setInterval(async () => { - await this.save(); - }, this._autoSaveInterval); - } - - disableAutoSave(): void { - if (this._autoSaveStatus === null) return; - clearInterval(this._autoSaveStatus); - this._autoSaveStatus = null; - } -} diff --git a/src/modules/project/save/save.type.ts b/src/modules/project/save/save.type.ts deleted file mode 100644 index ff1937d..0000000 --- a/src/modules/project/save/save.type.ts +++ /dev/null @@ -1 +0,0 @@ -export interface ISave {} diff --git a/src/routes/+page.server.ts b/src/routes/+page.server.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/routes/+page.ts b/src/routes/+page.ts index a72419a..e69de29 100644 --- a/src/routes/+page.ts +++ b/src/routes/+page.ts @@ -1,3 +0,0 @@ -// since there's no dynamic data here, we can prerender -// it so that it gets served as a static asset in production -export const prerender = true; diff --git a/src/routes/fs/+page.server.ts b/src/routes/fs/+page.server.ts new file mode 100644 index 0000000..f425c50 --- /dev/null +++ b/src/routes/fs/+page.server.ts @@ -0,0 +1,179 @@ +import { fail } from '@sveltejs/kit'; +import { FileSystemError } from '@utils-server/file-system/file-system-error'; +import { ProjectDirectory } from '@utils-server/file-system/project-directory'; +import { ProjectFile } from '@utils-server/file-system/project-file'; + +import type { Actions } from './$types'; + +export const actions = { + readFile: async ({ request, locals }) => { + const data = await request.json(); + + if (!data.filePath) { + return fail(403, { success: false, errorMsg: "Missing arg: 'filePath'" }); + } + + try { + return { + success: true, + fileContent: new ProjectFile(data.filePath, locals.session.data.path).read(), + }; + } catch (e: unknown) { + if (e instanceof FileSystemError) { + return fail(403, { success: false, errorMsg: e.message }); + } + throw e; + } + }, + readDir: async ({ request, locals }) => { + const data = await request.json(); + + try { + return { + success: true, + dirContent: new ProjectDirectory( + data.dirPath ? data.dirPath : '/', + locals.session.data.path, + ).read(), + }; + } catch (e: unknown) { + if (e instanceof FileSystemError) { + return fail(403, { success: false, errorMsg: e.message }); + } + throw e; + } + }, + readDirRec: async ({ request, locals }) => { + const data = await request.json(); + + try { + return { + success: true, + dirContent: new ProjectDirectory( + data.dirPath ? data.dirPath : '/', + locals.session.data.path, + ).read(true), + }; + } catch (e: unknown) { + if (e instanceof FileSystemError) { + return fail(403, { success: false, errorMsg: e.message }); + } + throw e; + } + }, + writeFile: async ({ request, locals }) => { + const data = await request.json(); + + if (!data.filePath) { + return fail(403, { success: false, errorMsg: "Missing arg: 'filePath'" }); + } + if (!data.fileContent) { + return fail(403, { success: false, errorMsg: "Missing arg: 'fileContent'" }); + } + + try { + new ProjectFile(data.filePath, locals.session.data.path).write(data.fileContent); + } catch (e: unknown) { + if (e instanceof FileSystemError) { + return fail(403, { success: false, errorMsg: e.message }); + } + throw e; + } + return { success: true }; + }, + + deleteFile: async ({ request, locals }) => { + const data = await request.json(); + + if (!data.filePath) { + return fail(403, { success: false, errorMsg: "Missing arg: 'filePath'" }); + } + + try { + new ProjectFile(data.filePath, locals.session.data.path).delete(); + } catch (e: unknown) { + if (e instanceof FileSystemError) { + return fail(403, { success: false, errorMsg: e.message }); + } + throw e; + } + return { success: true }; + }, + + renameFile: async ({ request, locals }) => { + const data = await request.json(); + + if (!data.filePath) { + return fail(403, { success: false, errorMsg: "Missing arg: 'filePath'" }); + } + if (!data.newFilePath) { + return fail(403, { success: false, errorMsg: "Missing arg: 'newFilePath'" }); + } + + try { + new ProjectFile(data.filePath, locals.session.data.path).rename(data.newFilePath); + } catch (e: unknown) { + if (e instanceof FileSystemError) { + return fail(403, { success: false, errorMsg: e.message }); + } + throw e; + } + return { success: true }; + }, + + createDir: async ({ request, locals }) => { + const data = await request.json(); + + if (!data.dirPath) { + return fail(403, { success: false, errorMsg: "Missing arg: 'dirPath'" }); + } + + try { + new ProjectDirectory(data.dirPath, locals.session.data.path).create(); + } catch (e: unknown) { + if (e instanceof FileSystemError) { + return fail(403, { success: false, errorMsg: e.message }); + } + throw e; + } + return { success: true }; + }, + + renameDir: async ({ request, locals }) => { + const data = await request.json(); + + if (!data.dirPath) { + return fail(403, { success: false, errorMsg: "Missing arg: 'dirPath'" }); + } + if (!data.newDirPath) { + return fail(403, { success: false, errorMsg: "Missing arg: 'newDirPath'" }); + } + + try { + new ProjectDirectory(data.dirPath, locals.session.data.path).rename(data.newDirPath); + } catch (e: unknown) { + if (e instanceof FileSystemError) { + return fail(403, { success: false, errorMsg: e.message }); + } + throw e; + } + return { success: true }; + }, + deleteDir: async ({ request, locals }) => { + const data = await request.json(); + + if (!data.dirPath) { + return fail(403, { success: false, errorMsg: "Missing arg: 'dirPath'" }); + } + + try { + new ProjectDirectory(data.dirPath, locals.session.data.path).delete(data.recursive === true); + } catch (e: unknown) { + if (e instanceof FileSystemError) { + return fail(403, { success: false, errorMsg: e.message }); + } + throw e; + } + return { success: true }; + }, +} satisfies Actions; diff --git a/src/routes/fs/+page.svelte b/src/routes/fs/+page.svelte new file mode 100644 index 0000000..304fe0c --- /dev/null +++ b/src/routes/fs/+page.svelte @@ -0,0 +1,178 @@ + + +
+
+
+ + Logo + +
+
{ + e.preventDefault(); + const formData = new FormData(e.currentTarget); + const response = await fetch('/fs?/readFile', { + method: 'POST', + body: JSON.stringify({ filePath: formData.get('filePath') }), + }); + const result = deserialize(await response.text()); + if (result.type === 'success' && result.data) { + readFileResult = result.data.fileContent; + } + }} + > + + +
+ {readFileResult} +
+
+
{ + e.preventDefault(); + const formData = new FormData(e.currentTarget); + const response = await fetch('/fs?/readDir', { + method: 'POST', + body: JSON.stringify({ dirPath: formData.get('dirPath') }), + }); + const result = deserialize(await response.text()); + readDirResult = ''; + if (result.type === 'success' && result.data) { + readDirResult += 'Files: ' + result.data.dirContent.files.join(' ') + '\n'; + readDirResult += 'Directories: '; + Object.keys(result.data.dirContent.directories).forEach((dir) => { + readDirResult += dir + ' '; + }); + } + }} + > + + +
+ {readDirResult} +
+
+
{ + function renderReadDirResult(dirContent, deep = 0) { + let res = ''; + res += ' '.repeat(deep) + '| Files: ' + dirContent.files.join(' ') + '\n'; + res += ' '.repeat(deep) + '| Directories:\n'; + Object.entries(dirContent.directories).forEach(([dirName, content]) => { + res += + ' '.repeat(deep) + + '|--- ' + + dirName + + '\n' + + renderReadDirResult(content, deep + 8); + }); + return res; + } + + e.preventDefault(); + const formData = new FormData(e.currentTarget); + const response = await fetch('/fs?/readDirRec', { + method: 'POST', + body: JSON.stringify({ dirPath: formData.get('dirPath') }), + }); + const result = deserialize(await response.text()); + readDirRecResult = ''; + if (result.type === 'success' && result.data) { + readDirRecResult = renderReadDirResult(result.data.dirContent); + } + }} + > + + +
+ {readDirRecResult} +
+
+
{ + e.preventDefault(); + const formData = new FormData(e.currentTarget); + fetch('/fs?/writeFile', { + method: 'POST', + body: JSON.stringify({ + filePath: formData.get('filePath'), + fileContent: formData.get('fileContent'), + }), + }); + }} + > + + + +
+
{ + e.preventDefault(); + const formData = new FormData(e.currentTarget); + fetch('/fs?/deleteFile', { + method: 'POST', + body: JSON.stringify({ filePath: formData.get('filePath') }), + }); + }} + > + + +
+
{ + e.preventDefault(); + const formData = new FormData(e.currentTarget); + fetch('/fs?/renameFile', { + method: 'POST', + body: JSON.stringify({ + filePath: formData.get('filePath'), + newFilePath: formData.get('newFilePath'), + }), + }); + }} + > + + + +
+
{ + e.preventDefault(); + const formData = new FormData(e.currentTarget); + fetch('/fs?/createDir', { + method: 'POST', + body: JSON.stringify({ dirPath: formData.get('dirPath') }), + }); + }} + > + + +
+
{ + e.preventDefault(); + const formData = new FormData(e.currentTarget); + fetch('/fs?/renameDir', { + method: 'POST', + body: JSON.stringify({ + dirPath: formData.get('dirPath'), + newDirPath: formData.get('newDirPath'), + }), + }); + }} + > + + + +
+
+
+
+
diff --git a/src/routes/load-project/+page.server.ts b/src/routes/load-project/+page.server.ts new file mode 100644 index 0000000..f2607a1 --- /dev/null +++ b/src/routes/load-project/+page.server.ts @@ -0,0 +1,58 @@ +import { env } from '$env/dynamic/private'; +import { redirect } from '@sveltejs/kit'; +import { authGuard } from '@utils-server/server-api'; +import * as fs from 'node:fs'; +import * as path from 'node:path'; + +import type { PageServerLoad } from './$types'; + +export const load: PageServerLoad = async ({ url, cookies, locals }) => { + const projectPath = url.searchParams.get('projectPath'); + const projectId = url.searchParams.get('projectId'); + let absoluteProjectPath: string = ''; + if (projectPath) { + if (env.API_URL) { + return { success: false, errorMsg: 'Cannot load local project if API_URL is present' }; + } + absoluteProjectPath = projectPath; + } else if (projectId) { + if (!env.API_URL) { + throw new Error('Missing API_URL'); + } + + const serverProjectPath = await authGuard(async (httpClient) => { + return await httpClient.post(`${env.API_URL}/editor/projects/${projectId}`); + }, cookies); + if (serverProjectPath.status !== 200) { + return { success: false, errorMsg: 'Cannot retrieve project from API' }; + } + absoluteProjectPath = (await serverProjectPath.json())['projectPath']; + } else { + return { success: false, errorMsg: 'No project provided' }; + } + + try { + const stats = fs.lstatSync(absoluteProjectPath); + if (!stats.isDirectory()) { + return { success: false, errorMsg: `Project folder ${projectPath} is not a folder` }; + } + } catch { + return { success: false, errorMsg: `Project folder ${projectPath} does not exist` }; + } + try { + fs.accessSync(absoluteProjectPath, fs.constants.W_OK); + } catch { + return { + success: false, + errorMsg: `Project folder ${projectPath} does not have the good rights`, + }; + } + absoluteProjectPath = path.resolve(absoluteProjectPath); + + const session = locals.session; + + await session.setData({ path: absoluteProjectPath }); + await session.save(); + + redirect(307, '/'); +}; diff --git a/src/routes/load-project/+page.svelte b/src/routes/load-project/+page.svelte new file mode 100644 index 0000000..585933a --- /dev/null +++ b/src/routes/load-project/+page.svelte @@ -0,0 +1,20 @@ + + +
+
+
+ + Logo + +
+ {data.success ? 'Project loading' : 'Error: ' + data.errorMsg} +
+
+
+
diff --git a/src/routes/load-project/load-project.spec.ts b/src/routes/load-project/load-project.spec.ts new file mode 100644 index 0000000..a5456ec --- /dev/null +++ b/src/routes/load-project/load-project.spec.ts @@ -0,0 +1,44 @@ +import { describe, expect, it, vi } from 'vitest'; + +import { load } from './+page.server'; + +describe('load', () => { + it('local project load and session cookie set', async () => { + const cookies = { + get: vi.fn(), + set: vi.fn(), + delete: vi.fn(), + }; + + const session = { + setData: vi.fn(), + save: vi.fn().mockImplementation(async () => { + cookies.set('session', 'abc123', { + path: '/', + httpOnly: true, + }); + }), + }; + + const event = { + cookies, + url: { + searchParams: { + get: (param: string) => (param === 'projectPath' ? '/tmp' : null), + }, + }, + locals: { session }, + } as any; + + await expect(load(event)).rejects.toMatchObject({ + status: 307, + location: '/', + }); + + expect(session.setData).toHaveBeenCalledWith({ path: '/tmp' }); + expect(cookies.set).toHaveBeenCalledWith('session', 'abc123', { + path: '/', + httpOnly: true, + }); + }); +}); diff --git a/src/utils/http-client/index.ts b/src/utils/http-client/index.ts deleted file mode 100644 index 29d4d49..0000000 --- a/src/utils/http-client/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './http-client'; diff --git a/svelte.config.js b/svelte.config.js index 0eab45a..045e4bd 100644 --- a/svelte.config.js +++ b/svelte.config.js @@ -10,5 +10,10 @@ export default { }, kit: { adapter: adapter(), + alias: { + '@utils/*': './src/lib/utils/*', + '@utils-client/*': './src/lib/utils-client/*', + '@utils-server/*': './src/lib/server/utils/*', + }, }, };