From 6a414898acd7102e89f08bfc4e02dc630dcc0c43 Mon Sep 17 00:00:00 2001 From: Tchips46 Date: Wed, 18 Feb 2026 05:49:19 +0900 Subject: [PATCH 1/8] feat(api): saveFile --- src/routes/+page.server.ts | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/src/routes/+page.server.ts b/src/routes/+page.server.ts index e69de29..9cc4ad6 100644 --- a/src/routes/+page.server.ts +++ b/src/routes/+page.server.ts @@ -0,0 +1,33 @@ +import { fail } from '@sveltejs/kit'; +import * as fs from 'node:fs'; +import * as path from 'node:path'; + +import type { Actions } from './$types'; + +export const actions = { + saveFile: async (event) => { + const data = await event.request.json(); + + const token = event.cookies.get('token'); // TODO choose token cookie name + + // TODO check token + if (token === undefined || token.length === 0) { + return fail(403, { success: false, errorMsg: 'Unauthorized access!' }); + } + const projectId: number = data.project; // TODO choose how project id is passed + + // TODO check user is allow to modify project + // TODO resolve project path from projectId + + const projectPath = '/tmp/example/' + projectId; + + const filename = data.filename; + const filePath = path.resolve(projectPath, filename); + if (!filePath.startsWith(projectPath)) { + return fail(403, { success: false, errorMsg: 'Unauthorized access!' }); + } + + fs.writeFileSync(filePath, data.fileContent); + return { success: true }; + }, +} satisfies Actions; From 673474eb4923c100c24eff63fb5bade7ecad2b82 Mon Sep 17 00:00:00 2001 From: Tchips46 Date: Sun, 8 Mar 2026 03:30:09 +0900 Subject: [PATCH 2/8] feat(back): filesystem complete --- package.json | 2 +- pnpm-lock.yaml | 245 ++++++++--------- src/demo.spec.ts | 44 ++++ src/hooks.server.ts | 10 +- .../{server => utils-client/http}/client.ts | 0 src/lib/utils-client/http/index.ts | 6 + src/lib/utils-client/index.ts | 1 + src/lib/utils-client/server-api/clients.ts | 19 ++ .../server-api/guards/auth.guard.ts | 0 .../server-api/guards/error.guard.ts | 33 +++ .../server-api/guards/params.guard.ts | 32 +++ src/lib/utils-client/server-api/index.ts | 8 + src/lib/utils-client/server-api/repository.ts | 73 +++++ src/lib/utils-client/server-api/types.ts | 9 + src/lib/utils-client/server-api/utils.ts | 32 +++ .../http/api/repository/base.repository.ts | 29 ++ src/modules/http/client.ts | 9 + src/modules/http/index.ts | 2 + .../http/middlewares/auth.middleware.ts | 19 ++ .../http/middlewares/logger.middleware.ts | 18 ++ src/modules/project/save/save-handler.ts | 35 +++ src/routes/+page.server.ts | 33 --- src/routes/fs/+page.server.ts | 249 ++++++++++++------ src/routes/loadProject/+page.server.ts | 63 +++++ src/routes/loadProject/+page.svelte | 20 ++ svelte.config.js | 6 +- 26 files changed, 745 insertions(+), 252 deletions(-) create mode 100644 src/demo.spec.ts rename src/lib/{server => utils-client/http}/client.ts (100%) create mode 100644 src/lib/utils-client/http/index.ts create mode 100644 src/lib/utils-client/index.ts create mode 100644 src/lib/utils-client/server-api/clients.ts create mode 100644 src/lib/utils-client/server-api/guards/auth.guard.ts create mode 100644 src/lib/utils-client/server-api/guards/error.guard.ts create mode 100644 src/lib/utils-client/server-api/guards/params.guard.ts create mode 100644 src/lib/utils-client/server-api/index.ts create mode 100644 src/lib/utils-client/server-api/repository.ts create mode 100644 src/lib/utils-client/server-api/types.ts create mode 100644 src/lib/utils-client/server-api/utils.ts create mode 100644 src/modules/http/api/repository/base.repository.ts create mode 100644 src/modules/http/client.ts create mode 100644 src/modules/http/index.ts create mode 100644 src/modules/http/middlewares/auth.middleware.ts create mode 100644 src/modules/http/middlewares/logger.middleware.ts create mode 100644 src/modules/project/save/save-handler.ts create mode 100644 src/routes/loadProject/+page.server.ts create mode 100644 src/routes/loadProject/+page.svelte diff --git a/package.json b/package.json index b65d3bc..32a53cb 100644 --- a/package.json +++ b/package.json @@ -110,6 +110,6 @@ ] }, "dependencies": { - "svelte-kit-sessions": "catalog:core" + "svelte-kit-sessions": "^0.4.0" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b42ca1b..8bd02c1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -15,10 +15,10 @@ catalogs: ci: '@commitlint/cli': specifier: ^20.4.2 - version: 20.4.3 + version: 20.4.2 '@commitlint/config-conventional': specifier: ^20.4.2 - version: 20.4.3 + version: 20.4.2 '@favware/cliff-jumper': specifier: ^6.0.0 version: 6.0.0 @@ -60,9 +60,6 @@ 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 @@ -72,7 +69,7 @@ catalogs: version: 0.0.4 '@unocss/extractor-svelte': specifier: ^66.6.3 - version: 66.6.6 + version: 66.6.3 '@unocss/preset-icons': specifier: ^66.6.0 version: 66.6.0 @@ -95,7 +92,7 @@ catalogs: version: 1.2.4 '@iconify-json/material-icon-theme': specifier: ^1.2.51 - version: 1.2.55 + version: 1.2.51 '@iconify-json/solar': specifier: ^1.2.5 version: 1.2.5 @@ -123,7 +120,7 @@ catalogs: version: 3.8.1 prettier-plugin-svelte: specifier: ^3.5.0 - version: 3.5.1 + version: 3.5.0 typescript-eslint: specifier: ^8.53.1 version: 8.53.1 @@ -152,18 +149,18 @@ 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) + specifier: ^0.4.0 + version: 0.4.0(@sveltejs/kit@2.50.1(@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.3(@types/node@25.0.10)(typescript@5.9.3) + version: 20.4.2(@types/node@25.0.10)(typescript@5.9.3) '@commitlint/config-conventional': specifier: catalog:ci - version: 20.4.3 + version: 20.4.2 '@favware/cliff-jumper': specifier: catalog:ci version: 6.0.0 @@ -172,7 +169,7 @@ importers: version: 1.2.4 '@iconify-json/material-icon-theme': specifier: catalog:icons - version: 1.2.55 + version: 1.2.51 '@iconify-json/solar': specifier: catalog:icons version: 1.2.5 @@ -202,13 +199,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.1(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.0(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.6 + version: 66.6.3 '@unocss/preset-icons': specifier: catalog:css version: 66.6.0 @@ -256,7 +253,7 @@ importers: version: 3.8.1 prettier-plugin-svelte: specifier: catalog:lint - version: 3.5.1(prettier@3.8.1)(svelte@5.48.0) + version: 3.5.0(prettier@3.8.1)(svelte@5.48.0) svelte: specifier: catalog:core version: 5.48.0 @@ -362,73 +359,73 @@ packages: resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==} engines: {node: '>=18'} - '@commitlint/cli@20.4.3': - resolution: {integrity: sha512-Z37EMoDT7+Upg500vlr/vZrgRsb6Xc5JAA3Tv7BYbobnN/ZpqUeZnSLggBg2+1O+NptRDtyujr2DD1CPV2qwhA==} + '@commitlint/cli@20.4.2': + resolution: {integrity: sha512-YjYSX2yj/WsVoxh9mNiymfFS2ADbg2EK4+1WAsMuckwKMCqJ5PDG0CJU/8GvmHWcv4VRB2V02KqSiecRksWqZQ==} engines: {node: '>=v18'} hasBin: true - '@commitlint/config-conventional@20.4.3': - resolution: {integrity: sha512-9RtLySbYQAs8yEqWEqhSZo9nYhbm57jx7qHXtgRmv/nmeQIjjMcwf6Dl+y5UZcGWgWx435TAYBURONaJIuCjWg==} + '@commitlint/config-conventional@20.4.2': + resolution: {integrity: sha512-rwkTF55q7Q+6dpSKUmJoScV0f3EpDlWKw2UPzklkLS4o5krMN1tPWAVOgHRtyUTMneIapLeQwaCjn44Td6OzBQ==} engines: {node: '>=v18'} - '@commitlint/config-validator@20.4.3': - resolution: {integrity: sha512-jCZpZFkcSL3ZEdL5zgUzFRdytv3xPo8iukTe9VA+QGus/BGhpp1xXSVu2B006GLLb2gYUAEGEqv64kTlpZNgmA==} + '@commitlint/config-validator@20.4.0': + resolution: {integrity: sha512-zShmKTF+sqyNOfAE0vKcqnpvVpG0YX8F9G/ZIQHI2CoKyK+PSdladXMSns400aZ5/QZs+0fN75B//3Q5CHw++w==} engines: {node: '>=v18'} - '@commitlint/ensure@20.4.3': - resolution: {integrity: sha512-WcXGKBNn0wBKpX8VlXgxqedyrLxedIlLBCMvdamLnJFEbUGJ9JZmBVx4vhLV3ZyA8uONGOb+CzW0Y9HDbQ+ONQ==} + '@commitlint/ensure@20.4.1': + resolution: {integrity: sha512-WLQqaFx1pBooiVvBrA1YfJNFqZF8wS/YGOtr5RzApDbV9tQ52qT5VkTsY65hFTnXhW8PcDfZLaknfJTmPejmlw==} engines: {node: '>=v18'} '@commitlint/execute-rule@20.0.0': resolution: {integrity: sha512-xyCoOShoPuPL44gVa+5EdZsBVao/pNzpQhkzq3RdtlFdKZtjWcLlUFQHSWBuhk5utKYykeJPSz2i8ABHQA+ZZw==} engines: {node: '>=v18'} - '@commitlint/format@20.4.3': - resolution: {integrity: sha512-UDJVErjLbNghop6j111rsHJYGw6MjCKAi95K0GT2yf4eeiDHy3JDRLWYWEjIaFgO+r+dQSkuqgJ1CdMTtrvHsA==} + '@commitlint/format@20.4.0': + resolution: {integrity: sha512-i3ki3WR0rgolFVX6r64poBHXM1t8qlFel1G1eCBvVgntE3fCJitmzSvH5JD/KVJN/snz6TfaX2CLdON7+s4WVQ==} engines: {node: '>=v18'} - '@commitlint/is-ignored@20.4.3': - resolution: {integrity: sha512-W5VQKZ7fdJ1X3Tko+h87YZaqRMGN1KvQKXyCM8xFdxzMIf1KCZgN4uLz3osLB1zsFcVS4ZswHY64LI26/9ACag==} + '@commitlint/is-ignored@20.4.1': + resolution: {integrity: sha512-In5EO4JR1lNsAv1oOBBO24V9ND1IqdAJDKZiEpdfjDl2HMasAcT7oA+5BKONv1pRoLG380DGPE2W2RIcUwdgLA==} engines: {node: '>=v18'} - '@commitlint/lint@20.4.3': - resolution: {integrity: sha512-CYOXL23e+nRKij81+d0+dymtIi7Owl9QzvblJYbEfInON/4MaETNSLFDI74LDu+YJ0ML5HZyw9Vhp9QpckwQ0A==} + '@commitlint/lint@20.4.2': + resolution: {integrity: sha512-buquzNRtFng6xjXvBU1abY/WPEEjCgUipNQrNmIWe8QuJ6LWLtei/LDBAzEe5ASm45+Q9L2Xi3/GVvlj50GAug==} engines: {node: '>=v18'} - '@commitlint/load@20.4.3': - resolution: {integrity: sha512-3cdJOUVP+VcgHa7bhJoWS+Z8mBNXB5aLWMBu7Q7uX8PSeWDzdbrBlR33J1MGGf7r1PZDp+mPPiFktk031PgdRw==} + '@commitlint/load@20.4.0': + resolution: {integrity: sha512-Dauup/GfjwffBXRJUdlX/YRKfSVXsXZLnINXKz0VZkXdKDcaEILAi9oflHGbfydonJnJAbXEbF3nXPm9rm3G6A==} engines: {node: '>=v18'} - '@commitlint/message@20.4.3': - resolution: {integrity: sha512-6akwCYrzcrFcTYz9GyUaWlhisY4lmQ3KvrnabmhoeAV8nRH4dXJAh4+EUQ3uArtxxKQkvxJS78hNX2EU3USgxQ==} + '@commitlint/message@20.4.0': + resolution: {integrity: sha512-B5lGtvHgiLAIsK5nLINzVW0bN5hXv+EW35sKhYHE8F7V9Uz1fR4tx3wt7mobA5UNhZKUNgB/+ldVMQE6IHZRyA==} engines: {node: '>=v18'} - '@commitlint/parse@20.4.3': - resolution: {integrity: sha512-hzC3JCo3zs3VkQ833KnGVuWjWIzR72BWZWjQM7tY/7dfKreKAm7fEsy71tIFCRtxf2RtMP2d3RLF1U9yhFSccA==} + '@commitlint/parse@20.4.1': + resolution: {integrity: sha512-XNtZjeRcFuAfUnhYrCY02+mpxwY4OmnvD3ETbVPs25xJFFz1nRo/25nHj+5eM+zTeRFvWFwD4GXWU2JEtoK1/w==} engines: {node: '>=v18'} - '@commitlint/read@20.4.3': - resolution: {integrity: sha512-j42OWv3L31WfnP8WquVjHZRt03w50Y/gEE8FAyih7GQTrIv2+pZ6VZ6pWLD/ml/3PO+RV2SPtRtTp/MvlTb8rQ==} + '@commitlint/read@20.4.0': + resolution: {integrity: sha512-QfpFn6/I240ySEGv7YWqho4vxqtPpx40FS7kZZDjUJ+eHxu3azfhy7fFb5XzfTqVNp1hNoI3tEmiEPbDB44+cg==} engines: {node: '>=v18'} - '@commitlint/resolve-extends@20.4.3': - resolution: {integrity: sha512-QucxcOy+00FhS9s4Uy0OyS5HeUV+hbC6OLqkTSIm6fwMdKva+OEavaCDuLtgd9akZZlsUo//XzSmPP3sLKBPog==} + '@commitlint/resolve-extends@20.4.0': + resolution: {integrity: sha512-ay1KM8q0t+/OnlpqXJ+7gEFQNlUtSU5Gxr8GEwnVf2TPN3+ywc5DzL3JCxmpucqxfHBTFwfRMXxPRRnR5Ki20g==} engines: {node: '>=v18'} - '@commitlint/rules@20.4.3': - resolution: {integrity: sha512-Yuosd7Grn5qiT7FovngXLyRXTMUbj9PYiSkvUgWK1B5a7+ZvrbWDS7epeUapYNYatCy/KTpPFPbgLUdE+MUrBg==} + '@commitlint/rules@20.4.2': + resolution: {integrity: sha512-oz83pnp5Yq6uwwTAabuVQPNlPfeD2Y5ZjMb7Wx8FSUlu4sLYJjbBWt8031Z0osCFPfHzAwSYrjnfDFKtuSMdKg==} engines: {node: '>=v18'} '@commitlint/to-lines@20.0.0': resolution: {integrity: sha512-2l9gmwiCRqZNWgV+pX1X7z4yP0b3ex/86UmUFgoRt672Ez6cAM2lOQeHFRUTuE6sPpi8XBCGnd8Kh3bMoyHwJw==} engines: {node: '>=v18'} - '@commitlint/top-level@20.4.3': - resolution: {integrity: sha512-qD9xfP6dFg5jQ3NMrOhG0/w5y3bBUsVGyJvXxdWEwBm8hyx4WOk3kKXw28T5czBYvyeCVJgJJ6aoJZUWDpaacQ==} + '@commitlint/top-level@20.4.0': + resolution: {integrity: sha512-NDzq8Q6jmFaIIBC/GG6n1OQEaHdmaAAYdrZRlMgW6glYWGZ+IeuXmiymDvQNXPc82mVxq2KiE3RVpcs+1OeDeA==} engines: {node: '>=v18'} - '@commitlint/types@20.4.3': - resolution: {integrity: sha512-51OWa1Gi6ODOasPmfJPq6js4pZoomima4XLZZCrkldaH2V5Nb3bVhNXPeT6XV0gubbainSpTw4zi68NqAeCNCg==} + '@commitlint/types@20.4.0': + resolution: {integrity: sha512-aO5l99BQJ0X34ft8b0h7QFkQlqxC6e7ZPVmBKz13xM9O8obDaM1Cld4sQlJDXXU/VFuUzQ30mVtHjVz74TuStw==} engines: {node: '>=v18'} '@conventional-changelog/git-client@1.0.1': @@ -687,8 +684,8 @@ packages: '@iconify-json/ic@1.2.4': resolution: {integrity: sha512-pzPMmrZrBQuwT7nmtrYdkttun8KalRGgZPIL1Ny9KpF2zjRGIUPN+npTfuD3lrgO/OnSwAoJWuekQwBpt/Cqrw==} - '@iconify-json/material-icon-theme@1.2.55': - resolution: {integrity: sha512-V4FUXp2az00xpGYjj4MaOvp6aAIfOMTRRGrt66KH7DmqoIb4WV/YqH4TalgOswCJD/UPGdPuOoy+B6hxLuifTg==} + '@iconify-json/material-icon-theme@1.2.51': + resolution: {integrity: sha512-gZ/EEe2K+sP5f7lfd8TNiRSvHEbFBN4gzBC2fZonZuDjoB008s5Ni8Qvlz++xMBZsEg7gEKG8Ph5r6lFZpQ8AQ==} '@iconify-json/solar@1.2.5': resolution: {integrity: sha512-WMAiNwchU8zhfrySww6KQBRIBbsQ6SvgIu2yA+CHGyMima/0KQwT5MXogrZPJGoQF+1Ye3Qj6K+1CiyNn3YkoA==} @@ -1005,10 +1002,6 @@ 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==} @@ -1320,8 +1313,8 @@ packages: '@unocss/extractor-arbitrary-variants@66.6.0': resolution: {integrity: sha512-AsCmpbre4hQb+cKOf3gHUeYlF7guR/aCKZvw53VBk12qY5wNF7LdfIx4zWc5LFVCoRxIZlU2C7L4/Tt7AkiFMA==} - '@unocss/extractor-svelte@66.6.6': - resolution: {integrity: sha512-5+Et3jiSFlMqxkoyVLsoT2/Rd8x/Jd65i5KzIyXMtQccDmqN2wSXuyvB2h5sLauHn4bBe/qOWO3PfGjbXBGWOA==} + '@unocss/extractor-svelte@66.6.3': + resolution: {integrity: sha512-eFr6IVBH3xO7ztwmFrBFkVTUezfqX5PYiMSi+keflSs0PP/YOolaXeJx315b4eqkg3ot+lZUtvCv/6VV3k3zQg==} '@unocss/inspector@66.6.0': resolution: {integrity: sha512-BvdY8ah+OTmzFMb+z8RZkaF15+PWRFt9S2bOARkkRBubybX9EE1rxM07l74kO5Dj16++CS4nO15XFq39pPoBvg==} @@ -1619,12 +1612,12 @@ packages: resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} engines: {node: ^14.18.0 || >=16.10.0} - conventional-changelog-angular@8.3.0: - resolution: {integrity: sha512-DOuBwYSqWzfwuRByY9O4oOIvDlkUCTDzfbOgcSbkY+imXXj+4tmrEFao3K+FxemClYfYnZzsvudbwrhje9VHDA==} + conventional-changelog-angular@8.1.0: + resolution: {integrity: sha512-GGf2Nipn1RUCAktxuVauVr1e3r8QrLP/B0lEUsFktmGqc3ddbQkhoJZHJctVU829U1c6mTSWftrVOCHaL85Q3w==} engines: {node: '>=18'} - conventional-changelog-conventionalcommits@9.3.0: - resolution: {integrity: sha512-kYFx6gAyjSIMwNtASkI3ZE99U1fuVDJr0yTYgVy+I2QG46zNZfl2her+0+eoviG82c5WQvW1jMt1eOQTeJLodA==} + conventional-changelog-conventionalcommits@9.1.0: + resolution: {integrity: sha512-MnbEysR8wWa8dAEvbj5xcBgJKQlX/m0lhS8DsyAAWDHdfs2faDJxTgzRYlRYpXSe7UiKrIIlB4TrBKU9q9DgkA==} engines: {node: '>=18'} conventional-changelog-preset-loader@5.0.0: @@ -1640,11 +1633,6 @@ 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'} @@ -1665,8 +1653,8 @@ packages: cosmiconfig: '>=9' typescript: '>=5' - cosmiconfig@9.0.1: - resolution: {integrity: sha512-hr4ihw+DBqcvrsEDioRO31Z17x71pUYoNe/4h6Z0wB72p7MU7/9gH8Q3s12NFhHPfYBBOV3qyfUxmr/Yn3shnQ==} + cosmiconfig@9.0.0: + resolution: {integrity: sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==} engines: {node: '>=14'} peerDependencies: typescript: '>=4.9.5' @@ -2618,8 +2606,8 @@ packages: resolution: {integrity: sha512-SxToR7P8Y2lWmv/kTzVLC1t/GDI2WGjMwNhLLE9qtH8Q13C+aEmuRlzDst4Up4s0Wc8sF2M+J57iB3cMLqftfg==} engines: {node: '>=6.0.0'} - prettier-plugin-svelte@3.5.1: - resolution: {integrity: sha512-65+fr5+cgIKWKiqM1Doum4uX6bY8iFCdztvvp2RcF+AJoieaw9kJOFMNcJo/bkmKYsxFaM9OsVZK/gWauG/5mg==} + prettier-plugin-svelte@3.5.0: + resolution: {integrity: sha512-2lLO/7EupnjO/95t+XZesXs8Bf3nYLIDfCo270h5QWbj/vjLqmrQ1LiRk9LPggxSDsnVYfehamZNf+rgQYApZg==} peerDependencies: prettier: ^3.0.0 svelte: ^3.2.0 || ^4.0.0-next.0 || ^5.0.0-next.0 @@ -3234,32 +3222,32 @@ snapshots: '@bcoe/v8-coverage@1.0.2': {} - '@commitlint/cli@20.4.3(@types/node@25.0.10)(typescript@5.9.3)': + '@commitlint/cli@20.4.2(@types/node@25.0.10)(typescript@5.9.3)': dependencies: - '@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 + '@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 tinyexec: 1.0.2 yargs: 17.7.2 transitivePeerDependencies: - '@types/node' - typescript - '@commitlint/config-conventional@20.4.3': + '@commitlint/config-conventional@20.4.2': dependencies: - '@commitlint/types': 20.4.3 - conventional-changelog-conventionalcommits: 9.3.0 + '@commitlint/types': 20.4.0 + conventional-changelog-conventionalcommits: 9.1.0 - '@commitlint/config-validator@20.4.3': + '@commitlint/config-validator@20.4.0': dependencies: - '@commitlint/types': 20.4.3 + '@commitlint/types': 20.4.0 ajv: 8.17.1 - '@commitlint/ensure@20.4.3': + '@commitlint/ensure@20.4.1': dependencies: - '@commitlint/types': 20.4.3 + '@commitlint/types': 20.4.0 lodash.camelcase: 4.3.0 lodash.kebabcase: 4.1.1 lodash.snakecase: 4.1.1 @@ -3268,31 +3256,31 @@ snapshots: '@commitlint/execute-rule@20.0.0': {} - '@commitlint/format@20.4.3': + '@commitlint/format@20.4.0': dependencies: - '@commitlint/types': 20.4.3 + '@commitlint/types': 20.4.0 picocolors: 1.1.1 - '@commitlint/is-ignored@20.4.3': + '@commitlint/is-ignored@20.4.1': dependencies: - '@commitlint/types': 20.4.3 + '@commitlint/types': 20.4.0 semver: 7.7.3 - '@commitlint/lint@20.4.3': + '@commitlint/lint@20.4.2': dependencies: - '@commitlint/is-ignored': 20.4.3 - '@commitlint/parse': 20.4.3 - '@commitlint/rules': 20.4.3 - '@commitlint/types': 20.4.3 + '@commitlint/is-ignored': 20.4.1 + '@commitlint/parse': 20.4.1 + '@commitlint/rules': 20.4.2 + '@commitlint/types': 20.4.0 - '@commitlint/load@20.4.3(@types/node@25.0.10)(typescript@5.9.3)': + '@commitlint/load@20.4.0(@types/node@25.0.10)(typescript@5.9.3)': dependencies: - '@commitlint/config-validator': 20.4.3 + '@commitlint/config-validator': 20.4.0 '@commitlint/execute-rule': 20.0.0 - '@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) + '@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) is-plain-obj: 4.1.0 lodash.mergewith: 4.6.2 picocolors: 1.1.1 @@ -3300,47 +3288,47 @@ snapshots: - '@types/node' - typescript - '@commitlint/message@20.4.3': {} + '@commitlint/message@20.4.0': {} - '@commitlint/parse@20.4.3': + '@commitlint/parse@20.4.1': dependencies: - '@commitlint/types': 20.4.3 - conventional-changelog-angular: 8.3.0 - conventional-commits-parser: 6.3.0 + '@commitlint/types': 20.4.0 + conventional-changelog-angular: 8.1.0 + conventional-commits-parser: 6.2.1 - '@commitlint/read@20.4.3': + '@commitlint/read@20.4.0': dependencies: - '@commitlint/top-level': 20.4.3 - '@commitlint/types': 20.4.3 + '@commitlint/top-level': 20.4.0 + '@commitlint/types': 20.4.0 git-raw-commits: 4.0.0 minimist: 1.2.8 tinyexec: 1.0.2 - '@commitlint/resolve-extends@20.4.3': + '@commitlint/resolve-extends@20.4.0': dependencies: - '@commitlint/config-validator': 20.4.3 - '@commitlint/types': 20.4.3 + '@commitlint/config-validator': 20.4.0 + '@commitlint/types': 20.4.0 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.3': + '@commitlint/rules@20.4.2': dependencies: - '@commitlint/ensure': 20.4.3 - '@commitlint/message': 20.4.3 + '@commitlint/ensure': 20.4.1 + '@commitlint/message': 20.4.0 '@commitlint/to-lines': 20.0.0 - '@commitlint/types': 20.4.3 + '@commitlint/types': 20.4.0 '@commitlint/to-lines@20.0.0': {} - '@commitlint/top-level@20.4.3': + '@commitlint/top-level@20.4.0': dependencies: escalade: 3.2.0 - '@commitlint/types@20.4.3': + '@commitlint/types@20.4.0': dependencies: - conventional-commits-parser: 6.3.0 + conventional-commits-parser: 6.2.1 picocolors: 1.1.1 '@conventional-changelog/git-client@1.0.1(conventional-commits-filter@5.0.0)(conventional-commits-parser@6.2.1)': @@ -3530,7 +3518,7 @@ snapshots: dependencies: '@iconify/types': 2.0.0 - '@iconify-json/material-icon-theme@1.2.55': + '@iconify-json/material-icon-theme@1.2.51': dependencies: '@iconify/types': 2.0.0 @@ -3851,8 +3839,6 @@ 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': {} @@ -3998,7 +3984,7 @@ snapshots: dependencies: 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)': + '@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)': dependencies: '@babel/generator': 7.28.6 '@babel/parser': 7.28.6 @@ -4010,7 +3996,7 @@ snapshots: parse-imports-exports: 0.2.4 prettier: 3.8.1 optionalDependencies: - prettier-plugin-svelte: 3.5.1(prettier@3.8.1)(svelte@5.48.0) + prettier-plugin-svelte: 3.5.0(prettier@3.8.1)(svelte@5.48.0) svelte: 5.48.0 transitivePeerDependencies: - supports-color @@ -4176,7 +4162,7 @@ snapshots: dependencies: '@unocss/core': 66.6.0 - '@unocss/extractor-svelte@66.6.6': {} + '@unocss/extractor-svelte@66.6.3': {} '@unocss/inspector@66.6.0': dependencies: @@ -4545,11 +4531,11 @@ snapshots: consola@3.4.2: {} - conventional-changelog-angular@8.3.0: + conventional-changelog-angular@8.1.0: dependencies: compare-func: 2.0.0 - conventional-changelog-conventionalcommits@9.3.0: + conventional-changelog-conventionalcommits@9.1.0: dependencies: compare-func: 2.0.0 @@ -4561,11 +4547,6 @@ 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) @@ -4578,14 +4559,14 @@ snapshots: core-util-is@1.0.3: {} - cosmiconfig-typescript-loader@6.2.0(@types/node@25.0.10)(cosmiconfig@9.0.1(typescript@5.9.3))(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): dependencies: '@types/node': 25.0.10 - cosmiconfig: 9.0.1(typescript@5.9.3) + cosmiconfig: 9.0.0(typescript@5.9.3) jiti: 2.6.1 typescript: 5.9.3 - cosmiconfig@9.0.1(typescript@5.9.3): + cosmiconfig@9.0.0(typescript@5.9.3): dependencies: env-paths: 2.2.1 import-fresh: 3.3.1 @@ -5472,7 +5453,7 @@ snapshots: dependencies: fast-diff: 1.3.0 - prettier-plugin-svelte@3.5.1(prettier@3.8.1)(svelte@5.48.0): + prettier-plugin-svelte@3.5.0(prettier@3.8.1)(svelte@5.48.0): dependencies: prettier: 3.8.1 svelte: 5.48.0 @@ -5661,10 +5642,10 @@ 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): + svelte-kit-sessions@0.4.0(@sveltejs/kit@2.50.1(@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)) + '@sveltejs/kit': 2.50.1(@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): diff --git a/src/demo.spec.ts b/src/demo.spec.ts new file mode 100644 index 0000000..f80e516 --- /dev/null +++ b/src/demo.spec.ts @@ -0,0 +1,44 @@ +import { describe, expect, it, vi } from 'vitest'; + +import { load } from './routes/loadProject/+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/hooks.server.ts b/src/hooks.server.ts index 22b59c5..acc60a4 100644 --- a/src/hooks.server.ts +++ b/src/hooks.server.ts @@ -5,6 +5,12 @@ import { sequence } from '@sveltejs/kit/hooks'; import * as crypto from 'node:crypto'; import { sveltekitSessionHandle } from 'svelte-kit-sessions'; +declare module 'svelte-kit-sessions' { + interface SessionData { + path: string; + } +} + 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}`); @@ -15,8 +21,8 @@ const sessionHandle = sveltekitSessionHandle({ }); const checkAuthorizationHandle: Handle = async ({ event, resolve }) => { - if (!event.locals.session.data.path && event.url.pathname !== '/load-project') { - throw redirect(302, '/load-project'); + if (!event.locals.session.data.path && event.url.pathname !== '/loadProject') { + throw redirect(302, '/loadProject'); } return resolve(event); }; diff --git a/src/lib/server/client.ts b/src/lib/utils-client/http/client.ts similarity index 100% rename from src/lib/server/client.ts rename to src/lib/utils-client/http/client.ts diff --git a/src/lib/utils-client/http/index.ts b/src/lib/utils-client/http/index.ts new file mode 100644 index 0000000..2d6ee12 --- /dev/null +++ b/src/lib/utils-client/http/index.ts @@ -0,0 +1,6 @@ +export { + HttpClient, + type MiddlewareNext, + type MiddlewareParams, + type RequestOptions, +} from './client'; diff --git a/src/lib/utils-client/index.ts b/src/lib/utils-client/index.ts new file mode 100644 index 0000000..c202386 --- /dev/null +++ b/src/lib/utils-client/index.ts @@ -0,0 +1 @@ +export * from './http'; diff --git a/src/lib/utils-client/server-api/clients.ts b/src/lib/utils-client/server-api/clients.ts new file mode 100644 index 0000000..70e744a --- /dev/null +++ b/src/lib/utils-client/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/utils-client/server-api/guards/auth.guard.ts b/src/lib/utils-client/server-api/guards/auth.guard.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/lib/utils-client/server-api/guards/error.guard.ts b/src/lib/utils-client/server-api/guards/error.guard.ts new file mode 100644 index 0000000..95b6e29 --- /dev/null +++ b/src/lib/utils-client/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/utils-client/server-api/guards/params.guard.ts b/src/lib/utils-client/server-api/guards/params.guard.ts new file mode 100644 index 0000000..2073c67 --- /dev/null +++ b/src/lib/utils-client/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/utils-client/server-api/index.ts b/src/lib/utils-client/server-api/index.ts new file mode 100644 index 0000000..fcdf960 --- /dev/null +++ b/src/lib/utils-client/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/utils-client/server-api/repository.ts b/src/lib/utils-client/server-api/repository.ts new file mode 100644 index 0000000..88a4f27 --- /dev/null +++ b/src/lib/utils-client/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/utils-client/server-api/types.ts b/src/lib/utils-client/server-api/types.ts new file mode 100644 index 0000000..8bd14d9 --- /dev/null +++ b/src/lib/utils-client/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/utils-client/server-api/utils.ts b/src/lib/utils-client/server-api/utils.ts new file mode 100644 index 0000000..cad5ba9 --- /dev/null +++ b/src/lib/utils-client/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/modules/http/api/repository/base.repository.ts b/src/modules/http/api/repository/base.repository.ts new file mode 100644 index 0000000..0974d8e --- /dev/null +++ b/src/modules/http/api/repository/base.repository.ts @@ -0,0 +1,29 @@ +import { type HttpClient, type RequestOptions } from '@utils/http'; + +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/client.ts b/src/modules/http/client.ts new file mode 100644 index 0000000..af605ae --- /dev/null +++ b/src/modules/http/client.ts @@ -0,0 +1,9 @@ +import { HttpClient } from '@utils/http'; + +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 new file mode 100644 index 0000000..b0e8587 --- /dev/null +++ b/src/modules/http/index.ts @@ -0,0 +1,2 @@ +export { type RequestOptions } from '@utils/http'; +export * from './api'; diff --git a/src/modules/http/middlewares/auth.middleware.ts b/src/modules/http/middlewares/auth.middleware.ts new file mode 100644 index 0000000..7a3818a --- /dev/null +++ b/src/modules/http/middlewares/auth.middleware.ts @@ -0,0 +1,19 @@ +import type { MiddlewareNext, MiddlewareParams } from '@utils/http'; + +import { authStore } from '../../../stores/auth.store'; + +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 new file mode 100644 index 0000000..722ebdc --- /dev/null +++ b/src/modules/http/middlewares/logger.middleware.ts @@ -0,0 +1,18 @@ +import type { MiddlewareNext, MiddlewareParams } from '@utils/http'; +import { toast } from 'svelte-sonner'; + +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/save/save-handler.ts b/src/modules/project/save/save-handler.ts new file mode 100644 index 0000000..718626c --- /dev/null +++ b/src/modules/project/save/save-handler.ts @@ -0,0 +1,35 @@ +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 = window.setInterval(async () => { + await this.save(); + }, this._autoSaveInterval); + } + + disableAutoSave(): void { + if (this._autoSaveStatus === null) return; + clearInterval(this._autoSaveStatus); + this._autoSaveStatus = null; + } +} diff --git a/src/routes/+page.server.ts b/src/routes/+page.server.ts index 9cc4ad6..e69de29 100644 --- a/src/routes/+page.server.ts +++ b/src/routes/+page.server.ts @@ -1,33 +0,0 @@ -import { fail } from '@sveltejs/kit'; -import * as fs from 'node:fs'; -import * as path from 'node:path'; - -import type { Actions } from './$types'; - -export const actions = { - saveFile: async (event) => { - const data = await event.request.json(); - - const token = event.cookies.get('token'); // TODO choose token cookie name - - // TODO check token - if (token === undefined || token.length === 0) { - return fail(403, { success: false, errorMsg: 'Unauthorized access!' }); - } - const projectId: number = data.project; // TODO choose how project id is passed - - // TODO check user is allow to modify project - // TODO resolve project path from projectId - - const projectPath = '/tmp/example/' + projectId; - - const filename = data.filename; - const filePath = path.resolve(projectPath, filename); - if (!filePath.startsWith(projectPath)) { - return fail(403, { success: false, errorMsg: 'Unauthorized access!' }); - } - - fs.writeFileSync(filePath, data.fileContent); - return { success: true }; - }, -} satisfies Actions; diff --git a/src/routes/fs/+page.server.ts b/src/routes/fs/+page.server.ts index f425c50..0068972 100644 --- a/src/routes/fs/+page.server.ts +++ b/src/routes/fs/+page.server.ts @@ -1,10 +1,87 @@ 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 * as fs from 'node:fs'; +import * as path from 'node:path'; import type { Actions } from './$types'; +function checkPathIsInsideDir(filePath: string, projectPath: string) { + if (!filePath.startsWith(projectPath)) { + throw `Path ${filePath} outside of directory`; + } +} + +function checkPathExists(filePath: string) { + if (!fs.existsSync(filePath)) { + throw `Path ${filePath} should exist`; + } +} + +function checkPathNotExists(filePath: string) { + if (fs.existsSync(filePath)) { + throw `Path ${filePath} should not exist`; + } +} + +function checkPathIsFile(filePath: string) { + let stats: fs.Stats; + try { + stats = fs.lstatSync(filePath); + } catch { + throw `Path ${filePath} does not exist`; + } + if (!stats.isFile()) { + throw `Path ${filePath} is not a file`; + } +} + +function checkPathIsDir(filePath: string) { + let stats: fs.Stats; + try { + stats = fs.lstatSync(filePath); + } catch { + throw `Path ${filePath} does not exist`; + } + if (!stats.isDirectory()) { + throw `Path ${filePath} is not a directory`; + } +} + +function checkPathIsWritable(filePath: string) { + try { + fs.accessSync(filePath, fs.constants.W_OK); + } catch { + throw `Path ${filePath} writable`; + } +} + +function checkPathIsReadable(filePath: string) { + try { + fs.accessSync(filePath, fs.constants.R_OK); + } catch { + throw `Path ${filePath} writable`; + } +} + +function readDirContent( + absoluteDirPath: string, + recursive: boolean, +): { files: string[]; directories: {} } { + const dirContent: { files: string[]; directories: { [key: string]: any } } = { + files: [], + directories: {}, + }; + fs.readdirSync(absoluteDirPath, { withFileTypes: true, recursive: false }).forEach((item) => { + if (item.isFile()) { + dirContent.files.push(item.name); + } else if (item.isDirectory()) { + dirContent.directories[item.name] = recursive + ? readDirContent(absoluteDirPath + '/' + item.name, recursive) + : {}; + } + }); + return dirContent; +} + export const actions = { readFile: async ({ request, locals }) => { const data = await request.json(); @@ -14,53 +91,59 @@ export const actions = { } 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; + const absoluteFilePath = path.resolve(locals.session.data.path, './' + data.filePath); + checkPathIsInsideDir(absoluteFilePath, locals.session.data.path); + checkPathExists(absoluteFilePath); + checkPathIsFile(absoluteFilePath); + checkPathIsReadable(absoluteFilePath); + return { success: true, fileContent: fs.readFileSync(absoluteFilePath).toString() }; + } catch (e) { + return fail(403, { success: false, errorMsg: e }); } }, readDir: async ({ request, locals }) => { const data = await request.json(); + let dirPath = '/'; + if (data.dirPath) { + dirPath = data.dirPath; + } + try { + const absoluteDirPath = path.resolve(locals.session.data.path, './' + dirPath); + checkPathIsInsideDir(absoluteDirPath, locals.session.data.path); + checkPathExists(absoluteDirPath); + checkPathIsDir(absoluteDirPath); + const dirContent = readDirContent(absoluteDirPath, false); return { success: true, - dirContent: new ProjectDirectory( - data.dirPath ? data.dirPath : '/', - locals.session.data.path, - ).read(), + dirContent, }; - } catch (e: unknown) { - if (e instanceof FileSystemError) { - return fail(403, { success: false, errorMsg: e.message }); - } - throw e; + } catch (e) { + return fail(403, { success: false, errorMsg: e }); } }, + readDirRec: async ({ request, locals }) => { const data = await request.json(); + let dirPath = '/'; + if (data.dirPath) { + dirPath = data.dirPath; + } + 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; + const absoluteDirPath = path.resolve(locals.session.data.path, './' + dirPath); + checkPathIsInsideDir(absoluteDirPath, locals.session.data.path); + checkPathExists(absoluteDirPath); + checkPathIsDir(absoluteDirPath); + const dirContent = readDirContent(absoluteDirPath, true); + return { success: true, dirContent }; + } catch (e) { + return fail(403, { success: false, errorMsg: e }); } }, + writeFile: async ({ request, locals }) => { const data = await request.json(); @@ -72,12 +155,18 @@ export const actions = { } 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 }); + const absoluteFilePath = path.resolve(locals.session.data.path, './' + data.filePath); + checkPathIsInsideDir(absoluteFilePath, locals.session.data.path); + if (fs.existsSync(absoluteFilePath)) { + checkPathIsFile(absoluteFilePath); + checkPathIsWritable(absoluteFilePath); + } else { + const folderPath = path.dirname(absoluteFilePath); + checkPathIsWritable(folderPath); } - throw e; + fs.writeFileSync(absoluteFilePath, data.fileContent, { flush: true }); + } catch (e) { + return fail(403, { success: false, errorMsg: e }); } return { success: true }; }, @@ -90,12 +179,14 @@ export const actions = { } 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; + const absoluteFilePath = path.resolve(locals.session.data.path, './' + data.filePath); + checkPathIsInsideDir(absoluteFilePath, locals.session.data.path); + checkPathExists(absoluteFilePath); + checkPathIsFile(absoluteFilePath); + checkPathIsWritable(absoluteFilePath); + fs.rmSync(absoluteFilePath); + } catch (e) { + return fail(403, { success: false, errorMsg: e }); } return { success: true }; }, @@ -111,12 +202,20 @@ export const actions = { } 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; + const absoluteFilePath = path.resolve(locals.session.data.path, './' + data.filePath); + const absoluteNewFilePath = path.resolve(locals.session.data.path, './' + data.newFilePath); + checkPathIsInsideDir(absoluteFilePath, locals.session.data.path); + checkPathIsInsideDir(absoluteNewFilePath, locals.session.data.path); + checkPathExists(absoluteFilePath); + checkPathIsFile(absoluteFilePath); + checkPathIsWritable(absoluteFilePath); + const newFolderPath = path.dirname(absoluteNewFilePath); + checkPathExists(newFolderPath); + checkPathIsWritable(newFolderPath); + checkPathNotExists(absoluteNewFilePath); + fs.renameSync(absoluteFilePath, absoluteNewFilePath); + } catch (e) { + return fail(403, { success: false, errorMsg: e }); } return { success: true }; }, @@ -129,12 +228,13 @@ export const actions = { } 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; + const absoluteFilePath = path.resolve(locals.session.data.path, './' + data.dirPath); + checkPathIsInsideDir(absoluteFilePath, locals.session.data.path); + checkPathNotExists(absoluteFilePath); + + fs.mkdirSync(absoluteFilePath, { recursive: true }); + } catch (e) { + return fail(403, { success: false, errorMsg: e }); } return { success: true }; }, @@ -150,29 +250,20 @@ export const actions = { } 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; + const absoluteDirPath = path.resolve(locals.session.data.path, './' + data.dirPath); + const absoluteNewDirPath = path.resolve(locals.session.data.path, './' + data.newDirPath); + checkPathIsInsideDir(absoluteDirPath, locals.session.data.path); + checkPathIsInsideDir(absoluteNewDirPath, locals.session.data.path); + checkPathExists(absoluteDirPath); + checkPathIsDir(absoluteDirPath); + checkPathIsWritable(absoluteDirPath); + const newFolderPath = path.dirname(absoluteNewDirPath); + checkPathExists(newFolderPath); + checkPathIsWritable(newFolderPath); + checkPathNotExists(absoluteNewDirPath); + fs.renameSync(absoluteDirPath, absoluteNewDirPath); + } catch (e) { + return fail(403, { success: false, errorMsg: e }); } return { success: true }; }, diff --git a/src/routes/loadProject/+page.server.ts b/src/routes/loadProject/+page.server.ts new file mode 100644 index 0000000..4e109bb --- /dev/null +++ b/src/routes/loadProject/+page.server.ts @@ -0,0 +1,63 @@ +import { env } from '$env/dynamic/private'; +import { redirect } from '@sveltejs/kit'; +import { authGuard } from '@utils/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 accessToken = cookies.get('accessToken'); + + if (accessToken === undefined || accessToken.length === 0) { + return { success: false, errorMsg: 'Unauthorized access' }; + } + + const serverProjectPath = await authGuard(async (httpClient) => { + return await httpClient.post(`/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/loadProject/+page.svelte b/src/routes/loadProject/+page.svelte new file mode 100644 index 0000000..585933a --- /dev/null +++ b/src/routes/loadProject/+page.svelte @@ -0,0 +1,20 @@ + + +
+
+
+ + Logo + +
+ {data.success ? 'Project loading' : 'Error: ' + data.errorMsg} +
+
+
+
diff --git a/svelte.config.js b/svelte.config.js index 045e4bd..0c50cd8 100644 --- a/svelte.config.js +++ b/svelte.config.js @@ -10,10 +10,6 @@ export default { }, kit: { adapter: adapter(), - alias: { - '@utils/*': './src/lib/utils/*', - '@utils-client/*': './src/lib/utils-client/*', - '@utils-server/*': './src/lib/server/utils/*', - }, + alias: { '@utils/*': './src/utils/*' }, }, }; From e7969df38255a56a21a99f112876777fbbac85f8 Mon Sep 17 00:00:00 2001 From: Tchips46 Date: Sun, 8 Mar 2026 18:01:15 +0900 Subject: [PATCH 3/8] fix(back): reviews --- package.json | 2 +- pnpm-lock.yaml | 5 +- src/demo.spec.ts | 44 ------------- src/hooks.server.ts | 10 +-- .../{utils-client/http => server}/client.ts | 0 src/lib/utils-client/http/index.ts | 6 -- src/lib/utils-client/index.ts | 1 - .../http/api/repository/base.repository.ts | 29 --------- src/modules/http/client.ts | 9 --- src/modules/http/index.ts | 2 - .../http/middlewares/auth.middleware.ts | 19 ------ .../http/middlewares/logger.middleware.ts | 18 ------ src/modules/project/save/save-handler.ts | 35 ----------- src/routes/loadProject/+page.server.ts | 63 ------------------- src/routes/loadProject/+page.svelte | 20 ------ svelte.config.js | 6 +- 16 files changed, 12 insertions(+), 257 deletions(-) delete mode 100644 src/demo.spec.ts rename src/lib/{utils-client/http => server}/client.ts (100%) delete mode 100644 src/lib/utils-client/http/index.ts delete mode 100644 src/lib/utils-client/index.ts delete mode 100644 src/modules/http/api/repository/base.repository.ts delete mode 100644 src/modules/http/client.ts delete mode 100644 src/modules/http/index.ts delete mode 100644 src/modules/http/middlewares/auth.middleware.ts delete mode 100644 src/modules/http/middlewares/logger.middleware.ts delete mode 100644 src/modules/project/save/save-handler.ts delete mode 100644 src/routes/loadProject/+page.server.ts delete mode 100644 src/routes/loadProject/+page.svelte diff --git a/package.json b/package.json index 32a53cb..b65d3bc 100644 --- a/package.json +++ b/package.json @@ -110,6 +110,6 @@ ] }, "dependencies": { - "svelte-kit-sessions": "^0.4.0" + "svelte-kit-sessions": "catalog:core" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8bd02c1..ab55f10 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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 @@ -149,7 +152,7 @@ importers: .: dependencies: svelte-kit-sessions: - specifier: ^0.4.0 + specifier: catalog:core version: 0.4.0(@sveltejs/kit@2.50.1(@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': diff --git a/src/demo.spec.ts b/src/demo.spec.ts deleted file mode 100644 index f80e516..0000000 --- a/src/demo.spec.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { describe, expect, it, vi } from 'vitest'; - -import { load } from './routes/loadProject/+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/hooks.server.ts b/src/hooks.server.ts index acc60a4..22b59c5 100644 --- a/src/hooks.server.ts +++ b/src/hooks.server.ts @@ -5,12 +5,6 @@ import { sequence } from '@sveltejs/kit/hooks'; import * as crypto from 'node:crypto'; import { sveltekitSessionHandle } from 'svelte-kit-sessions'; -declare module 'svelte-kit-sessions' { - interface SessionData { - path: string; - } -} - 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}`); @@ -21,8 +15,8 @@ const sessionHandle = sveltekitSessionHandle({ }); const checkAuthorizationHandle: Handle = async ({ event, resolve }) => { - if (!event.locals.session.data.path && event.url.pathname !== '/loadProject') { - throw redirect(302, '/loadProject'); + if (!event.locals.session.data.path && event.url.pathname !== '/load-project') { + throw redirect(302, '/load-project'); } return resolve(event); }; diff --git a/src/lib/utils-client/http/client.ts b/src/lib/server/client.ts similarity index 100% rename from src/lib/utils-client/http/client.ts rename to src/lib/server/client.ts diff --git a/src/lib/utils-client/http/index.ts b/src/lib/utils-client/http/index.ts deleted file mode 100644 index 2d6ee12..0000000 --- a/src/lib/utils-client/http/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -export { - HttpClient, - type MiddlewareNext, - type MiddlewareParams, - type RequestOptions, -} from './client'; diff --git a/src/lib/utils-client/index.ts b/src/lib/utils-client/index.ts deleted file mode 100644 index c202386..0000000 --- a/src/lib/utils-client/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './http'; 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 0974d8e..0000000 --- a/src/modules/http/api/repository/base.repository.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { type HttpClient, type RequestOptions } from '@utils/http'; - -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/client.ts b/src/modules/http/client.ts deleted file mode 100644 index af605ae..0000000 --- a/src/modules/http/client.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { HttpClient } from '@utils/http'; - -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 b0e8587..0000000 --- a/src/modules/http/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { type RequestOptions } from '@utils/http'; -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 7a3818a..0000000 --- a/src/modules/http/middlewares/auth.middleware.ts +++ /dev/null @@ -1,19 +0,0 @@ -import type { MiddlewareNext, MiddlewareParams } from '@utils/http'; - -import { authStore } from '../../../stores/auth.store'; - -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 722ebdc..0000000 --- a/src/modules/http/middlewares/logger.middleware.ts +++ /dev/null @@ -1,18 +0,0 @@ -import type { MiddlewareNext, MiddlewareParams } from '@utils/http'; -import { toast } from 'svelte-sonner'; - -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/save/save-handler.ts b/src/modules/project/save/save-handler.ts deleted file mode 100644 index 718626c..0000000 --- a/src/modules/project/save/save-handler.ts +++ /dev/null @@ -1,35 +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 = window.setInterval(async () => { - await this.save(); - }, this._autoSaveInterval); - } - - disableAutoSave(): void { - if (this._autoSaveStatus === null) return; - clearInterval(this._autoSaveStatus); - this._autoSaveStatus = null; - } -} diff --git a/src/routes/loadProject/+page.server.ts b/src/routes/loadProject/+page.server.ts deleted file mode 100644 index 4e109bb..0000000 --- a/src/routes/loadProject/+page.server.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { env } from '$env/dynamic/private'; -import { redirect } from '@sveltejs/kit'; -import { authGuard } from '@utils/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 accessToken = cookies.get('accessToken'); - - if (accessToken === undefined || accessToken.length === 0) { - return { success: false, errorMsg: 'Unauthorized access' }; - } - - const serverProjectPath = await authGuard(async (httpClient) => { - return await httpClient.post(`/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/loadProject/+page.svelte b/src/routes/loadProject/+page.svelte deleted file mode 100644 index 585933a..0000000 --- a/src/routes/loadProject/+page.svelte +++ /dev/null @@ -1,20 +0,0 @@ - - -
-
-
- - Logo - -
- {data.success ? 'Project loading' : 'Error: ' + data.errorMsg} -
-
-
-
diff --git a/svelte.config.js b/svelte.config.js index 0c50cd8..045e4bd 100644 --- a/svelte.config.js +++ b/svelte.config.js @@ -10,6 +10,10 @@ export default { }, kit: { adapter: adapter(), - alias: { '@utils/*': './src/utils/*' }, + alias: { + '@utils/*': './src/lib/utils/*', + '@utils-client/*': './src/lib/utils-client/*', + '@utils-server/*': './src/lib/server/utils/*', + }, }, }; From c5fea80a519fa00cb6f5419ed936e73c19bf85a6 Mon Sep 17 00:00:00 2001 From: Tchips46 Date: Tue, 10 Mar 2026 15:38:55 +0900 Subject: [PATCH 4/8] refactor(back): generic fs --- .../utils/file-system/fileSystemError.ts | 7 + .../utils/file-system/projectDirectory.ts | 125 +++++++++ .../server/utils/file-system/projectFile.ts | 128 +++++++++ src/routes/fs/+page.server.ts | 249 ++++++------------ 4 files changed, 339 insertions(+), 170 deletions(-) create mode 100644 src/lib/server/utils/file-system/fileSystemError.ts create mode 100644 src/lib/server/utils/file-system/projectDirectory.ts create mode 100644 src/lib/server/utils/file-system/projectFile.ts diff --git a/src/lib/server/utils/file-system/fileSystemError.ts b/src/lib/server/utils/file-system/fileSystemError.ts new file mode 100644 index 0000000..00de3d5 --- /dev/null +++ b/src/lib/server/utils/file-system/fileSystemError.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/projectDirectory.ts b/src/lib/server/utils/file-system/projectDirectory.ts new file mode 100644 index 0000000..329ae57 --- /dev/null +++ b/src/lib/server/utils/file-system/projectDirectory.ts @@ -0,0 +1,125 @@ +import { FileSystemError } from '@utils-server/file-system/fileSystemError'; +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/projectFile.ts b/src/lib/server/utils/file-system/projectFile.ts new file mode 100644 index 0000000..2a3fbf4 --- /dev/null +++ b/src/lib/server/utils/file-system/projectFile.ts @@ -0,0 +1,128 @@ +import { FileSystemError } from '@utils-server/file-system/fileSystemError'; +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/routes/fs/+page.server.ts b/src/routes/fs/+page.server.ts index 0068972..f587ebe 100644 --- a/src/routes/fs/+page.server.ts +++ b/src/routes/fs/+page.server.ts @@ -1,87 +1,10 @@ import { fail } from '@sveltejs/kit'; -import * as fs from 'node:fs'; -import * as path from 'node:path'; +import { FileSystemError } from '@utils-server/file-system/fileSystemError'; +import { ProjectDirectory } from '@utils-server/file-system/projectDirectory'; +import { ProjectFile } from '@utils-server/file-system/projectFile'; import type { Actions } from './$types'; -function checkPathIsInsideDir(filePath: string, projectPath: string) { - if (!filePath.startsWith(projectPath)) { - throw `Path ${filePath} outside of directory`; - } -} - -function checkPathExists(filePath: string) { - if (!fs.existsSync(filePath)) { - throw `Path ${filePath} should exist`; - } -} - -function checkPathNotExists(filePath: string) { - if (fs.existsSync(filePath)) { - throw `Path ${filePath} should not exist`; - } -} - -function checkPathIsFile(filePath: string) { - let stats: fs.Stats; - try { - stats = fs.lstatSync(filePath); - } catch { - throw `Path ${filePath} does not exist`; - } - if (!stats.isFile()) { - throw `Path ${filePath} is not a file`; - } -} - -function checkPathIsDir(filePath: string) { - let stats: fs.Stats; - try { - stats = fs.lstatSync(filePath); - } catch { - throw `Path ${filePath} does not exist`; - } - if (!stats.isDirectory()) { - throw `Path ${filePath} is not a directory`; - } -} - -function checkPathIsWritable(filePath: string) { - try { - fs.accessSync(filePath, fs.constants.W_OK); - } catch { - throw `Path ${filePath} writable`; - } -} - -function checkPathIsReadable(filePath: string) { - try { - fs.accessSync(filePath, fs.constants.R_OK); - } catch { - throw `Path ${filePath} writable`; - } -} - -function readDirContent( - absoluteDirPath: string, - recursive: boolean, -): { files: string[]; directories: {} } { - const dirContent: { files: string[]; directories: { [key: string]: any } } = { - files: [], - directories: {}, - }; - fs.readdirSync(absoluteDirPath, { withFileTypes: true, recursive: false }).forEach((item) => { - if (item.isFile()) { - dirContent.files.push(item.name); - } else if (item.isDirectory()) { - dirContent.directories[item.name] = recursive - ? readDirContent(absoluteDirPath + '/' + item.name, recursive) - : {}; - } - }); - return dirContent; -} - export const actions = { readFile: async ({ request, locals }) => { const data = await request.json(); @@ -91,59 +14,53 @@ export const actions = { } try { - const absoluteFilePath = path.resolve(locals.session.data.path, './' + data.filePath); - checkPathIsInsideDir(absoluteFilePath, locals.session.data.path); - checkPathExists(absoluteFilePath); - checkPathIsFile(absoluteFilePath); - checkPathIsReadable(absoluteFilePath); - return { success: true, fileContent: fs.readFileSync(absoluteFilePath).toString() }; - } catch (e) { - return fail(403, { success: false, errorMsg: e }); + 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(); - let dirPath = '/'; - if (data.dirPath) { - dirPath = data.dirPath; - } - try { - const absoluteDirPath = path.resolve(locals.session.data.path, './' + dirPath); - checkPathIsInsideDir(absoluteDirPath, locals.session.data.path); - checkPathExists(absoluteDirPath); - checkPathIsDir(absoluteDirPath); - const dirContent = readDirContent(absoluteDirPath, false); return { success: true, - dirContent, + dirContent: new ProjectDirectory( + data.dirPath ? data.dirPath : '/', + locals.session.data.path, + ).read(), }; - } catch (e) { - return fail(403, { success: false, errorMsg: e }); + } 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(); - let dirPath = '/'; - if (data.dirPath) { - dirPath = data.dirPath; - } - try { - const absoluteDirPath = path.resolve(locals.session.data.path, './' + dirPath); - checkPathIsInsideDir(absoluteDirPath, locals.session.data.path); - checkPathExists(absoluteDirPath); - checkPathIsDir(absoluteDirPath); - const dirContent = readDirContent(absoluteDirPath, true); - return { success: true, dirContent }; - } catch (e) { - return fail(403, { success: false, errorMsg: e }); + 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(); @@ -155,18 +72,12 @@ export const actions = { } try { - const absoluteFilePath = path.resolve(locals.session.data.path, './' + data.filePath); - checkPathIsInsideDir(absoluteFilePath, locals.session.data.path); - if (fs.existsSync(absoluteFilePath)) { - checkPathIsFile(absoluteFilePath); - checkPathIsWritable(absoluteFilePath); - } else { - const folderPath = path.dirname(absoluteFilePath); - checkPathIsWritable(folderPath); + 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 }); } - fs.writeFileSync(absoluteFilePath, data.fileContent, { flush: true }); - } catch (e) { - return fail(403, { success: false, errorMsg: e }); + throw e; } return { success: true }; }, @@ -179,14 +90,12 @@ export const actions = { } try { - const absoluteFilePath = path.resolve(locals.session.data.path, './' + data.filePath); - checkPathIsInsideDir(absoluteFilePath, locals.session.data.path); - checkPathExists(absoluteFilePath); - checkPathIsFile(absoluteFilePath); - checkPathIsWritable(absoluteFilePath); - fs.rmSync(absoluteFilePath); - } catch (e) { - return fail(403, { success: false, errorMsg: e }); + 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 }; }, @@ -202,20 +111,12 @@ export const actions = { } try { - const absoluteFilePath = path.resolve(locals.session.data.path, './' + data.filePath); - const absoluteNewFilePath = path.resolve(locals.session.data.path, './' + data.newFilePath); - checkPathIsInsideDir(absoluteFilePath, locals.session.data.path); - checkPathIsInsideDir(absoluteNewFilePath, locals.session.data.path); - checkPathExists(absoluteFilePath); - checkPathIsFile(absoluteFilePath); - checkPathIsWritable(absoluteFilePath); - const newFolderPath = path.dirname(absoluteNewFilePath); - checkPathExists(newFolderPath); - checkPathIsWritable(newFolderPath); - checkPathNotExists(absoluteNewFilePath); - fs.renameSync(absoluteFilePath, absoluteNewFilePath); - } catch (e) { - return fail(403, { success: false, errorMsg: e }); + 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 }; }, @@ -228,13 +129,12 @@ export const actions = { } try { - const absoluteFilePath = path.resolve(locals.session.data.path, './' + data.dirPath); - checkPathIsInsideDir(absoluteFilePath, locals.session.data.path); - checkPathNotExists(absoluteFilePath); - - fs.mkdirSync(absoluteFilePath, { recursive: true }); - } catch (e) { - return fail(403, { success: false, errorMsg: e }); + 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 }; }, @@ -250,20 +150,29 @@ export const actions = { } try { - const absoluteDirPath = path.resolve(locals.session.data.path, './' + data.dirPath); - const absoluteNewDirPath = path.resolve(locals.session.data.path, './' + data.newDirPath); - checkPathIsInsideDir(absoluteDirPath, locals.session.data.path); - checkPathIsInsideDir(absoluteNewDirPath, locals.session.data.path); - checkPathExists(absoluteDirPath); - checkPathIsDir(absoluteDirPath); - checkPathIsWritable(absoluteDirPath); - const newFolderPath = path.dirname(absoluteNewDirPath); - checkPathExists(newFolderPath); - checkPathIsWritable(newFolderPath); - checkPathNotExists(absoluteNewDirPath); - fs.renameSync(absoluteDirPath, absoluteNewDirPath); - } catch (e) { - return fail(403, { success: false, errorMsg: e }); + 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 }; }, From fcb75eec7c0774b1562b60402b24f026427d2259 Mon Sep 17 00:00:00 2001 From: Tchips46 Date: Tue, 10 Mar 2026 16:43:26 +0900 Subject: [PATCH 5/8] fix(back): fs naming convention --- .../utils/file-system/fileSystemError.ts | 7 - .../utils/file-system/project-directory.ts | 4 + .../server/utils/file-system/project-file.ts | 4 + .../utils/file-system/projectDirectory.ts | 125 ----------------- .../server/utils/file-system/projectFile.ts | 128 ------------------ src/routes/fs/+page.server.ts | 6 +- 6 files changed, 11 insertions(+), 263 deletions(-) delete mode 100644 src/lib/server/utils/file-system/fileSystemError.ts delete mode 100644 src/lib/server/utils/file-system/projectDirectory.ts delete mode 100644 src/lib/server/utils/file-system/projectFile.ts diff --git a/src/lib/server/utils/file-system/fileSystemError.ts b/src/lib/server/utils/file-system/fileSystemError.ts deleted file mode 100644 index 00de3d5..0000000 --- a/src/lib/server/utils/file-system/fileSystemError.ts +++ /dev/null @@ -1,7 +0,0 @@ -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 index 92645d5..cb3aa5b 100644 --- a/src/lib/server/utils/file-system/project-directory.ts +++ b/src/lib/server/utils/file-system/project-directory.ts @@ -1,4 +1,8 @@ +<<<<<<< HEAD import { FileSystemError } from '@utils-server/file-system/file-system-error'; +======= +import { FileSystemError } from '@utils-server/file-system/fileSystemError'; +>>>>>>> 3ed1389 (fix(back): fs naming convention) import fs from 'node:fs'; import path from 'node:path'; diff --git a/src/lib/server/utils/file-system/project-file.ts b/src/lib/server/utils/file-system/project-file.ts index 1535f71..58a271b 100644 --- a/src/lib/server/utils/file-system/project-file.ts +++ b/src/lib/server/utils/file-system/project-file.ts @@ -1,4 +1,8 @@ +<<<<<<< HEAD import { FileSystemError } from '@utils-server/file-system/file-system-error'; +======= +import { FileSystemError } from '@utils-server/file-system/fileSystemError'; +>>>>>>> 3ed1389 (fix(back): fs naming convention) import fs from 'node:fs'; import path from 'node:path'; diff --git a/src/lib/server/utils/file-system/projectDirectory.ts b/src/lib/server/utils/file-system/projectDirectory.ts deleted file mode 100644 index 329ae57..0000000 --- a/src/lib/server/utils/file-system/projectDirectory.ts +++ /dev/null @@ -1,125 +0,0 @@ -import { FileSystemError } from '@utils-server/file-system/fileSystemError'; -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/projectFile.ts b/src/lib/server/utils/file-system/projectFile.ts deleted file mode 100644 index 2a3fbf4..0000000 --- a/src/lib/server/utils/file-system/projectFile.ts +++ /dev/null @@ -1,128 +0,0 @@ -import { FileSystemError } from '@utils-server/file-system/fileSystemError'; -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/routes/fs/+page.server.ts b/src/routes/fs/+page.server.ts index f587ebe..f425c50 100644 --- a/src/routes/fs/+page.server.ts +++ b/src/routes/fs/+page.server.ts @@ -1,7 +1,7 @@ import { fail } from '@sveltejs/kit'; -import { FileSystemError } from '@utils-server/file-system/fileSystemError'; -import { ProjectDirectory } from '@utils-server/file-system/projectDirectory'; -import { ProjectFile } from '@utils-server/file-system/projectFile'; +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'; From 0123ba038ec0ffad04c1834ee047dfdb9023e83e Mon Sep 17 00:00:00 2001 From: Tchips46 Date: Tue, 10 Mar 2026 16:46:41 +0900 Subject: [PATCH 6/8] fix(back): fs naming convention # Conflicts: # src/lib/server/utils/file-system/project-directory.ts # src/lib/server/utils/file-system/project-file.ts --- src/lib/server/utils/file-system/project-directory.ts | 4 ---- src/lib/server/utils/file-system/project-file.ts | 4 ---- 2 files changed, 8 deletions(-) diff --git a/src/lib/server/utils/file-system/project-directory.ts b/src/lib/server/utils/file-system/project-directory.ts index cb3aa5b..92645d5 100644 --- a/src/lib/server/utils/file-system/project-directory.ts +++ b/src/lib/server/utils/file-system/project-directory.ts @@ -1,8 +1,4 @@ -<<<<<<< HEAD import { FileSystemError } from '@utils-server/file-system/file-system-error'; -======= -import { FileSystemError } from '@utils-server/file-system/fileSystemError'; ->>>>>>> 3ed1389 (fix(back): fs naming convention) import fs from 'node:fs'; import path from 'node:path'; diff --git a/src/lib/server/utils/file-system/project-file.ts b/src/lib/server/utils/file-system/project-file.ts index 58a271b..1535f71 100644 --- a/src/lib/server/utils/file-system/project-file.ts +++ b/src/lib/server/utils/file-system/project-file.ts @@ -1,8 +1,4 @@ -<<<<<<< HEAD import { FileSystemError } from '@utils-server/file-system/file-system-error'; -======= -import { FileSystemError } from '@utils-server/file-system/fileSystemError'; ->>>>>>> 3ed1389 (fix(back): fs naming convention) import fs from 'node:fs'; import path from 'node:path'; From 628cc1c21219764c901295c1735f8f46c199105a Mon Sep 17 00:00:00 2001 From: Tchips46 Date: Thu, 12 Mar 2026 10:41:42 +0900 Subject: [PATCH 7/8] feat(back): create and start project --- .env.example | 3 + .idea/prettier.xml | 2 + pnpm-lock.yaml | 240 ++++++++++++---------- src/hooks.server.ts | 6 +- src/lib/server/utils/cli/cli-error.ts | 7 + src/lib/server/utils/cli/cli-interface.ts | 52 +++++ src/routes/cli/+page.server.ts | 61 ++++++ src/routes/cli/+page.svelte | 27 +++ src/routes/load-project/+page.server.ts | 10 +- src/routes/load-project/+page.svelte | 88 +++++++- 10 files changed, 379 insertions(+), 117 deletions(-) create mode 100644 src/lib/server/utils/cli/cli-error.ts create mode 100644 src/lib/server/utils/cli/cli-interface.ts create mode 100644 src/routes/cli/+page.server.ts create mode 100644 src/routes/cli/+page.svelte diff --git a/.env.example b/.env.example index 213e132..9194c4d 100644 --- a/.env.example +++ b/.env.example @@ -6,3 +6,6 @@ NODE_ENV=development # Leaving this empty will generate a new unique random session secret at start SESSION_SECRET= + +# Change if your nf cli executable isn't in the path +NF_CLI_PATH=nf diff --git a/.idea/prettier.xml b/.idea/prettier.xml index 0c83ac4..8c024db 100644 --- a/.idea/prettier.xml +++ b/.idea/prettier.xml @@ -3,5 +3,7 @@ \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ab55f10..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 @@ -72,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 @@ -95,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 @@ -123,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 @@ -153,17 +153,17 @@ importers: dependencies: svelte-kit-sessions: specifier: catalog:core - version: 0.4.0(@sveltejs/kit@2.50.1(@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) + 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 @@ -172,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 @@ -202,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 @@ -256,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 @@ -362,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': @@ -687,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==} @@ -1005,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==} @@ -1316,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==} @@ -1615,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: @@ -1636,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'} @@ -1656,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' @@ -2609,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 @@ -3225,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 @@ -3259,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 @@ -3291,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)': @@ -3521,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 @@ -3842,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': {} @@ -3987,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 @@ -3999,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 @@ -4165,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: @@ -4534,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 @@ -4550,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) @@ -4562,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 @@ -5456,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 @@ -5645,10 +5661,10 @@ snapshots: optionalDependencies: svelte: 5.48.0 - svelte-kit-sessions@0.4.0(@sveltejs/kit@2.50.1(@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-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.50.1(@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)) + '@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): diff --git a/src/hooks.server.ts b/src/hooks.server.ts index 22b59c5..0894943 100644 --- a/src/hooks.server.ts +++ b/src/hooks.server.ts @@ -15,7 +15,11 @@ const sessionHandle = sveltekitSessionHandle({ }); const checkAuthorizationHandle: Handle = async ({ event, resolve }) => { - if (!event.locals.session.data.path && event.url.pathname !== '/load-project') { + if ( + !event.locals.session.data.path && + event.url.pathname !== '/load-project' && + event.url.pathname + event.url.search !== '/cli?/createProject' + ) { throw redirect(302, '/load-project'); } return resolve(event); diff --git a/src/lib/server/utils/cli/cli-error.ts b/src/lib/server/utils/cli/cli-error.ts new file mode 100644 index 0000000..834113b --- /dev/null +++ b/src/lib/server/utils/cli/cli-error.ts @@ -0,0 +1,7 @@ +export class CliError extends Error { + message: string; + constructor(message: string) { + super(); + this.message = message; + } +} diff --git a/src/lib/server/utils/cli/cli-interface.ts b/src/lib/server/utils/cli/cli-interface.ts new file mode 100644 index 0000000..ce12da1 --- /dev/null +++ b/src/lib/server/utils/cli/cli-interface.ts @@ -0,0 +1,52 @@ +import { env } from '$env/dynamic/private'; +import { CliError } from '@utils-server/cli/cli-error'; +import child_process from 'node:child_process'; + +export class CliInterface { + private readonly projectPath: string; + + constructor(projectPath: string) { + this.projectPath = projectPath; + } + + createProject( + projectName: string, + packageManager: 'npm' | 'yarn' | 'pnpm' | 'bun', + language: 'js' | 'ts', + strictTypeChecking: boolean, + multiplayerServer: boolean, + skipDependencyInstallation: boolean, + dockerContainerization: boolean, + ) { + this.runCli([ + `new`, + `-d`, + this.projectPath, + `--name`, + projectName, + `--package-manager`, + packageManager, + `--language`, + language, + strictTypeChecking ? '--strict' : '--no-strict', + multiplayerServer ? '--server' : '--no-server', + skipDependencyInstallation ? '--skip-install' : '--no-skip-install', + dockerContainerization ? '--docker' : '--no-docker', + ]); + } + + startProject() { + this.runCli([`build`, `-d`, this.projectPath]); + this.runCli([`start`, `-d`, this.projectPath]); + } + + private runCli(params: string[]) { + const res = child_process.spawnSync(env.NF_CLI_PATH, params); + if (res.status === null) { + throw new CliError(`Executable ${env.NF_CLI_PATH} cannot be found or executed`); + } + if (res.status !== 0) { + throw new CliError(res.stderr.toString()); + } + } +} diff --git a/src/routes/cli/+page.server.ts b/src/routes/cli/+page.server.ts new file mode 100644 index 0000000..6eb50a8 --- /dev/null +++ b/src/routes/cli/+page.server.ts @@ -0,0 +1,61 @@ +import { fail } from '@sveltejs/kit'; +import { CliError } from '@utils-server/cli/cli-error'; +import { CliInterface } from '@utils-server/cli/cli-interface'; + +import type { Actions } from './$types'; + +export const actions = { + // Create project + // Run project + // Export project + createProject: async ({ request }) => { + const data = await request.json(); + + if (!data.projectPath) { + return fail(403, { success: false, errorMsg: "Missing arg: 'projectPath'" }); + } + if (!data.projectName) { + return fail(403, { success: false, errorMsg: "Missing arg: 'projectName'" }); + } + if (!data.packageManager) { + return fail(403, { success: false, errorMsg: "Missing arg: 'packageManager'" }); + } + if (!data.language) { + return fail(403, { success: false, errorMsg: "Missing arg: 'language'" }); + } + + try { + new CliInterface(data.projectPath).createProject( + data.projectName, + data.packageManager, + data.language, + data.strictTypeChecking, + data.multiplayerServer, + data.skipDependencyInstallation, + data.dockerContainerization, + ); + return { + success: true, + }; + } catch (e: unknown) { + if (e instanceof CliError) { + return fail(403, { success: false, errorMsg: e.message }); + } + throw e; + } + }, + + startProject: async ({ locals }) => { + try { + new CliInterface(locals.session.data.path).startProject(); + return { + success: true, + }; + } catch (e: unknown) { + if (e instanceof CliError) { + return fail(403, { success: false, errorMsg: e.message }); + } + throw e; + } + }, +} satisfies Actions; diff --git a/src/routes/cli/+page.svelte b/src/routes/cli/+page.svelte new file mode 100644 index 0000000..34cd7bd --- /dev/null +++ b/src/routes/cli/+page.svelte @@ -0,0 +1,27 @@ + + +
+
+
+ + Logo + +
+
{ + e.preventDefault(); + fetch('/cli?/startProject', { + method: 'POST', + body: JSON.stringify({}), + }); + }} + > + +
+
+
+
+
diff --git a/src/routes/load-project/+page.server.ts b/src/routes/load-project/+page.server.ts index f2607a1..32c01ee 100644 --- a/src/routes/load-project/+page.server.ts +++ b/src/routes/load-project/+page.server.ts @@ -28,7 +28,15 @@ export const load: PageServerLoad = async ({ url, cookies, locals }) => { } absoluteProjectPath = (await serverProjectPath.json())['projectPath']; } else { - return { success: false, errorMsg: 'No project provided' }; + return { + success: false, + creationPanel: env.API_URL ? 'api' : 'local', + errorMsg: `No project provided: ${ + env.API_URL + ? 'Go back to the NanoForge project manager to access a project' + : 'Select or create a local project' + }`, + }; } try { diff --git a/src/routes/load-project/+page.svelte b/src/routes/load-project/+page.svelte index 585933a..960e6f1 100644 --- a/src/routes/load-project/+page.svelte +++ b/src/routes/load-project/+page.svelte @@ -1,9 +1,10 @@
@@ -13,7 +14,88 @@ Logo
- {data.success ? 'Project loading' : 'Error: ' + data.errorMsg} + {#if !data.success} +
+ {'Error: ' + data.errorMsg} +
+ {/if} + {#if form?.errorMsg} +
+ {'Error: ' + form?.errorMsg} +
+ {/if} + {#if data.creationPanel === 'local'} +
{ + e.preventDefault(); + const formData = new FormData(e.currentTarget); + // eslint-disable-next-line svelte/no-navigation-without-resolve + goto(`/load-project?projectPath=${formData.get('projectPath')}`); + }} + > + + +
+
{ + e.preventDefault(); + const formData = new FormData(e.currentTarget); + fetch('/cli?/createProject', { + method: 'POST', + body: JSON.stringify({ + projectPath: formData.get('projectPath'), + projectName: formData.get('projectName'), + packageManager: formData.get('packageManager'), + language: formData.get('language'), + strictTypeChecking: formData.get('strictTypeChecking') ?? false, + multiplayerServer: formData.get('multiplayerServer') ?? false, + skipDependencyInstallation: formData.get('skipDependencyInstallation') ?? false, + dockerContainerization: formData.get('dockerContainerization') ?? false, + }), + }); + }} + > + + + + + + + + + + + + + + + + +
+ {/if}
From 17a008cc2c568f2cf187b20d02281e027e77a57c Mon Sep 17 00:00:00 2001 From: Tchips46 Date: Thu, 12 Mar 2026 11:00:27 +0900 Subject: [PATCH 8/8] fix: remove server-api from utils-client --- src/lib/utils-client/server-api/clients.ts | 19 ----- .../server-api/guards/auth.guard.ts | 0 .../server-api/guards/error.guard.ts | 33 --------- .../server-api/guards/params.guard.ts | 32 -------- src/lib/utils-client/server-api/index.ts | 8 -- src/lib/utils-client/server-api/repository.ts | 73 ------------------- src/lib/utils-client/server-api/types.ts | 9 --- src/lib/utils-client/server-api/utils.ts | 32 -------- 8 files changed, 206 deletions(-) delete mode 100644 src/lib/utils-client/server-api/clients.ts delete mode 100644 src/lib/utils-client/server-api/guards/auth.guard.ts delete mode 100644 src/lib/utils-client/server-api/guards/error.guard.ts delete mode 100644 src/lib/utils-client/server-api/guards/params.guard.ts delete mode 100644 src/lib/utils-client/server-api/index.ts delete mode 100644 src/lib/utils-client/server-api/repository.ts delete mode 100644 src/lib/utils-client/server-api/types.ts delete mode 100644 src/lib/utils-client/server-api/utils.ts diff --git a/src/lib/utils-client/server-api/clients.ts b/src/lib/utils-client/server-api/clients.ts deleted file mode 100644 index 70e744a..0000000 --- a/src/lib/utils-client/server-api/clients.ts +++ /dev/null @@ -1,19 +0,0 @@ -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/utils-client/server-api/guards/auth.guard.ts b/src/lib/utils-client/server-api/guards/auth.guard.ts deleted file mode 100644 index e69de29..0000000 diff --git a/src/lib/utils-client/server-api/guards/error.guard.ts b/src/lib/utils-client/server-api/guards/error.guard.ts deleted file mode 100644 index 95b6e29..0000000 --- a/src/lib/utils-client/server-api/guards/error.guard.ts +++ /dev/null @@ -1,33 +0,0 @@ -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/utils-client/server-api/guards/params.guard.ts b/src/lib/utils-client/server-api/guards/params.guard.ts deleted file mode 100644 index 2073c67..0000000 --- a/src/lib/utils-client/server-api/guards/params.guard.ts +++ /dev/null @@ -1,32 +0,0 @@ -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/utils-client/server-api/index.ts b/src/lib/utils-client/server-api/index.ts deleted file mode 100644 index fcdf960..0000000 --- a/src/lib/utils-client/server-api/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -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/utils-client/server-api/repository.ts b/src/lib/utils-client/server-api/repository.ts deleted file mode 100644 index 88a4f27..0000000 --- a/src/lib/utils-client/server-api/repository.ts +++ /dev/null @@ -1,73 +0,0 @@ -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/utils-client/server-api/types.ts b/src/lib/utils-client/server-api/types.ts deleted file mode 100644 index 8bd14d9..0000000 --- a/src/lib/utils-client/server-api/types.ts +++ /dev/null @@ -1,9 +0,0 @@ -export interface Token { - accessToken: string; - refreshToken: string; - tokenExpiresAt: string; -} - -export interface RefreshTokenInput { - refreshToken: string; -} diff --git a/src/lib/utils-client/server-api/utils.ts b/src/lib/utils-client/server-api/utils.ts deleted file mode 100644 index cad5ba9..0000000 --- a/src/lib/utils-client/server-api/utils.ts +++ /dev/null @@ -1,32 +0,0 @@ -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: '/' }); -};