Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
@@ -1 +1,8 @@
VITE_API_BASE_URL=http://localhost:3000
# Leave empty for local environment / no api authentication
API_URL=http://localhost:3000

# Set to production
NODE_ENV=development

# Leaving this empty will generate a new unique random session secret at start
SESSION_SECRET=
1 change: 0 additions & 1 deletion eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ export default [
},
{
rules: {
'no-console': 'error',
'svelte/no-unused-svelte-ignore': 'off',
},
},
Expand Down
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -108,5 +108,8 @@
"src/**/*.{ts,svelte}": [
"eslint --fix"
]
},
"dependencies": {
"svelte-kit-sessions": "catalog:core"
}
}
259 changes: 150 additions & 109 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions pnpm-workspace.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ catalogs:
svelte: ^5.48.0
svelte-check: ^4.3.5
vite: ^7.3.1
svelte-kit-sessions: "^0.4.0"
css:
'@alexanderniebuhr/prettier-plugin-unocss': ^0.0.4
'@unocss/extractor-svelte': ^66.6.3
Expand Down
6 changes: 6 additions & 0 deletions src/app.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,10 @@ declare global {
}
}

declare module 'svelte-kit-sessions' {
interface SessionData {
path: string;
}
}

export {};
5 changes: 0 additions & 5 deletions src/demo.spec.ts

This file was deleted.

24 changes: 22 additions & 2 deletions src/hooks.server.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,25 @@
import { env } from '$env/dynamic/private';
import { paraglideMiddleware } from '$lib/paraglide/server';
import type { Handle } from '@sveltejs/kit';
import { type Handle, redirect } from '@sveltejs/kit';
import { sequence } from '@sveltejs/kit/hooks';
import * as crypto from 'node:crypto';
import { sveltekitSessionHandle } from 'svelte-kit-sessions';

if (!env.SESSION_SECRET) {
env.SESSION_SECRET = crypto.randomBytes(20).toString('hex');
console.log(`SESSION_SECRET not found, generating a temporary one: ${env.SESSION_SECRET}`);
}

const sessionHandle = sveltekitSessionHandle({
secret: env.SESSION_SECRET,
});

const checkAuthorizationHandle: Handle = async ({ event, resolve }) => {
if (!event.locals.session.data.path && event.url.pathname !== '/load-project') {
throw redirect(302, '/load-project');
}
return resolve(event);
};

const handleParaglide: Handle = ({ event, resolve }) =>
paraglideMiddleware(event.request, ({ request, locale }) => {
Expand All @@ -10,4 +30,4 @@ const handleParaglide: Handle = ({ event, resolve }) =>
});
});

export const handle: Handle = handleParaglide;
export const handle: Handle = sequence(sessionHandle, handleParaglide, checkAuthorizationHandle);
54 changes: 33 additions & 21 deletions src/utils/http-client/http-client.ts → src/lib/server/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,71 +6,77 @@ export interface MiddlewareParams {
options: RequestOptions;
}

export type MiddlewareNext = (params?: MiddlewareParams) => Promise<Response>;
export type FullResponse = Response & { content: any };

export type MiddlewareNext = (params?: MiddlewareParams) => Promise<FullResponse>;

export type Middleware = (
params: MiddlewareParams,
next: MiddlewareNext,
) => Promise<Response | undefined> | undefined;
) => Promise<FullResponse | undefined> | undefined;

type BaseRequest = (path: string, options: RequestOptions) => Promise<Response>;
type BaseRequest = (path: string, options: RequestOptions) => Promise<FullResponse>;

export class HttpClient {
private readonly _baseUrl: string;
private readonly _baseOptions: RequestOptions;
private readonly _middlewares: Middleware[];

constructor(baseUrl: string) {
constructor(baseUrl: string, options?: RequestOptions) {
this._baseUrl = baseUrl;
this._baseOptions = {};
this._baseOptions = options ?? {
headers: {
'Content-Type': 'application/json',
},
};
this._middlewares = [];
}

get(path: string, options?: RequestOptions): Promise<Response> {
get(path: string, options?: RequestOptions): Promise<FullResponse> {
return this._applyMiddlewares(path, options, (newPath, newOptions) => {
return fetch(newPath, {
return this._request(newPath, {
...newOptions,
method: 'GET',
});
});
}

post(path: string, body?: any, options?: RequestOptions): Promise<Response> {
post(path: string, body?: string, options?: RequestOptions): Promise<FullResponse> {
return this._applyMiddlewares(path, options, (newPath, newOptions) => {
return fetch(newPath, {
return this._request(newPath, {
...newOptions,
method: 'POST',
body: body,
});
});
}

put(path: string, body?: any, options?: RequestOptions): Promise<Response> {
put(path: string, body?: string, options?: RequestOptions): Promise<FullResponse> {
return this._applyMiddlewares(path, options, (newPath, newOptions) => {
return fetch(newPath, {
return this._request(newPath, {
...newOptions,
method: 'PUT',
body: body,
});
});
}

patch(path: string, body?: any, options?: RequestOptions): Promise<Response> {
return this._applyMiddlewares(path, options, (newPath, newOptions) => {
return fetch(newPath, {
patch(path: string, body?: string, options?: RequestOptions): Promise<FullResponse> {
return this._applyMiddlewares(path, options, async (newPath, newOptions) => {
return this._request(newPath, {
...newOptions,
method: 'PATCH',
body: body,
});
});
}

delete(path: string, options?: RequestOptions): Promise<Response> {
delete(path: string, options?: RequestOptions): Promise<FullResponse> {
return this._applyMiddlewares(path, options, (newPath, newOptions) => {
return fetch(newPath, {
return this._request(newPath, {
...newOptions,
method: 'DELETE',
});
}) as Promise<FullResponse>;
});
}

Expand All @@ -79,11 +85,17 @@ export class HttpClient {
return this;
}

private async _request(path: string, request: RequestInit): Promise<FullResponse> {
const res = (await fetch(path, request)) as FullResponse;
res.content = null;
return res;
}

private _applyMiddlewares(
path: string,
options: RequestOptions | undefined,
callback: BaseRequest,
): Promise<Response> {
): Promise<FullResponse> {
const baseParams = {
path,
fullPath: this._getUrl(path),
Expand All @@ -93,14 +105,14 @@ export class HttpClient {
},
};
const middlewares = this._middlewares.slice();
let response: Response;
let response: FullResponse;

const execution = async (params?: MiddlewareParams): Promise<Response> => {
const execution = async (params?: MiddlewareParams): Promise<FullResponse> => {
if (!params) params = baseParams;

const middleware = middlewares.shift();

if (!middleware) response = (await callback(params.fullPath, params.options)) as Response;
if (!middleware) response = (await callback(params.fullPath, params.options)) as FullResponse;
else response = (await middleware(params, execution)) ?? response;

return response;
Expand Down
7 changes: 7 additions & 0 deletions src/lib/server/utils/file-system/file-system-error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export class FileSystemError extends Error {
message: string;
constructor(message: string) {
super();
this.message = message;
}
}
125 changes: 125 additions & 0 deletions src/lib/server/utils/file-system/project-directory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import { FileSystemError } from '@utils-server/file-system/file-system-error';
import fs from 'node:fs';
import path from 'node:path';

export class ProjectDirectory {
private path: string;
private readonly projectPath: string;

constructor(dirPath: string, projectPath: string) {
this.path = path.resolve(projectPath, './' + dirPath);
this.projectPath = projectPath;
}

read(recursive: boolean = false): { files: string[]; directories: {} } {
this._checkPathIsInsideProject();
this._checkPathExists();
this._checkPathIsDir();
this._checkPathIsReadable();
return this._readDirContent(this.path, recursive);
}

create(): void {
this._checkPathIsInsideProject();
this._checkPathNotExists();

fs.mkdirSync(this.path, { recursive: true });
}

delete(recursive: boolean = false): void {
this._checkPathIsInsideProject();
this._checkPathExists();
this._checkPathIsDir();
if (!recursive) {
this._checkDirIsEmpty();
}
fs.rmSync(this.path, { recursive: recursive });
}

rename(newPath: string): void {
const absoluteNewDirPath = path.resolve(this.projectPath, './' + newPath);
this._checkPathIsInsideProject();
this._checkPathIsInsideProject(absoluteNewDirPath);
this._checkPathExists();
this._checkPathIsDir();
this._checkPathIsWritable();
const newFolderPath = path.dirname(absoluteNewDirPath);
this._checkPathExists(newFolderPath);
this._checkPathIsWritable(newFolderPath);
this._checkPathNotExists(absoluteNewDirPath);
fs.renameSync(this.path, absoluteNewDirPath);
this.path = absoluteNewDirPath;
}

private _checkPathIsInsideProject(path: string = this.path) {
if (!path.startsWith(this.projectPath)) {
throw new FileSystemError(`Path ${path} is outside of the project directory`);
}
}

private _checkPathExists(path: string = this.path) {
if (!fs.existsSync(path)) {
throw new FileSystemError(`Path ${path} should exist`);
}
}

private _checkPathNotExists(path: string = this.path) {
if (fs.existsSync(path)) {
throw new FileSystemError(`Path ${path} should not exist`);
}
}

private _checkPathIsDir(path: string = this.path) {
let stats: fs.Stats;
try {
stats = fs.lstatSync(path);
} catch {
throw new FileSystemError(`Path ${path} does not exist`);
}
if (!stats.isDirectory()) {
throw new FileSystemError(`Path ${path} is not a directory`);
}
}

private _checkPathIsWritable(path: string = this.path) {
try {
fs.accessSync(path, fs.constants.W_OK);
} catch {
throw new FileSystemError(`Path ${path} writable`);
}
}

private _checkPathIsReadable(path: string = this.path) {
try {
fs.accessSync(path, fs.constants.R_OK);
} catch {
throw new FileSystemError(`Path ${path} writable`);
}
}

private _checkDirIsEmpty(path: string = this.path) {
if (fs.readdirSync(path).length > 0) {
throw new FileSystemError(`Directory ${path} is not empty`);
}
}

private _readDirContent(
path: string = this.path,
recursive: boolean = false,
): { files: string[]; directories: {} } {
const dirContent: { files: string[]; directories: { [key: string]: any } } = {
files: [],
directories: {},
};
fs.readdirSync(path, { withFileTypes: true, recursive: false }).forEach((item) => {
if (item.isFile()) {
dirContent.files.push(item.name);
} else if (item.isDirectory()) {
dirContent.directories[item.name] = recursive
? this._readDirContent(path + '/' + item.name, recursive)
: {};
}
});
return dirContent;
}
}
Loading
Loading