Code-base structure¶
This project is based on svelte kit and follows its conventions and structure.
1-level folders¶
By default, the main application folder is located at /src
.
The structure of this folder is explained in depth
in svelte kit docs
/static
is a folder related to static files that will be served by the client server.
More on this could be found in
the svelte kit docs.
The /lib
folder contains files and sources that enable correct instrumentation and testing of this client application.
Currently, within this folder are present files to support the project's unit tests arrangements and other
library files to populate local instances of the fractal server in order to test the client.
As for testing, currently, there are two folders /__tests__
and /tests
.
The former contains files of unit tests executed by vitest
and the latest
is related to playwright
testing.
This structure could be improved and organized differently by updating project configuration files,
specifically vite.config.js
and playwrght.config.js
which are located at project root.
/examples
folder contains configuration guidance to set up local environments to test different
fractal server / fractal web interoperability architectures.
/docs
folder is the root of the project docs files.
[project root]
├ /src
├ /static
├ /lib
├ /__tests__
├ /tests
├ /examples
├ /docs
Application structure¶
With reference to /src/routes
folder, therein are defined svelte kit page components
that structure the fractal web client.
It is important to mention that, this client application is server-side rendered. By default, this is the default behaviour of svelte kit.
Before proceeding, it is important to understand the distinction of files terminating with the suffix
+[.]server.js
. Those files are explicitly processed by the client's server.
This client application is in essence a proxy that enables users to interact with a server application, the fractal server. Through the browser interface, the svelte application enable users to build HTTP requests that will be sent to the fractal server.
The svelte client and fractal server interact through a REST interface.
But how is this interaction implemented in this client?
Client server interoperability¶
As said, the svelte client communicates with the fractal server through a set of REST APIs.
In this client, every request to the fractal server is sent by the nodejs server that is serving the svelte application.
It is important to understand that these requests are made in the server context of the svelte client. No request to the fractal server is sent directly by the browser of the user.
The fact that every request on behalf of a user is sent through a common backend nodejs server, implies a proxy architecture. The way this works is the following: the svelte client in the browser sends a HTTP request to the nodejs server that is serving the application. This request, with the attached cookies, is then used to compose a new request to be sent to the fractal server.
Note that the authentication context is kept thanks to cookies that establish user sessions.
The following image provides an overview for the reader of the described architecture.
To avoid duplicating the logic of each fractal-server endpoint and simplify the error handling, a special Svelte route has been setup to act like a transparent proxy: src/routes/api/[...path]/+server.js
. This is one of the suggested way to handle a different backend according to Svelte Kit FAQ.
So, by default, the AJAX calls performed by the front-end have the same path and payload of the fractal-server API, but are sent to the Node.js Svelte back-end. Some Python API endpoints (like the /auth
and /admin
endpoints) that don't start with /api
are handled in a slightly different way. Indeed, the /api
prefix is needed by hooks.server.js
to detect if the received call is an AJAX call, in order to process the token expiration in a custom way. To preserve this behavior for all the API calls these paths are rewritten adding the /api
prefix.
Summarizing, the frontend code:
- uses exactly the same path of the fractal-server API for the
/api
endpoints - uses
/api/auth
for/auth
endpoints - uses
/api/admin
for/admin
endpoints
Other than the AJAX calls, there are also some calls to fractal-server API done by Svelte SSR, while generating the HTML page. These requests are defined in files under src/lib/server/api/v1
. Here requests are grouped by contexts as auth_api
, admin_api
, [...].
An example using actions¶
The login is still using the Svelte action approach, in which we have to extract the data from a formData object and then use it to build a JSON payload to be forwarded to fractal-server.
Consider the code at src/lib/server/api/auth_api.js:5
:
/**
* Request to authenticate user
* @param fetch
* @param data
* @returns {Promise<*>}
*/
export async function userAuthentication(fetch, data) {
const response = await fetch(FRACTAL_SERVER_HOST + '/auth/token/login', {
method: 'POST',
credentials: 'include',
mode: 'cors',
body: data
});
if (!response.ok) {
throw new Error('Authentication failed');
}
return await response.json();
}
This code is responsible to call the /auth/token/login
REST api endpoint of the fractal server.
The request is made by the svelte client application in a backend context within the nodejs server.
The client will, if the request succeeds, handle the fractal server response in a form action.
// src/routes/auth/login/+page.server.js
export const actions = {
// Default page action / Handles POST requests
default: async ({ request, cookies, fetch }) => {
// TODO: Handle login request
console.log('Login action');
// Get form data
const formData = await request.formData();
// Set auth data
let authData;
try {
authData = await userAuthentication(fetch, formData);
} catch (error) {
console.error(error);
return fail(400, { invalidMessage: 'Invalid credentials', invalid: true });
}
const authToken = authData.access_token;
// Decode JWT token claims
const tokenClaims = jose.decodeJwt(authToken);
// Set the authentication cookie
const cookieOptions = {
domain: `${AUTH_COOKIE_DOMAIN}`,
path: `${AUTH_COOKIE_PATH}`,
expires: new Date(tokenClaims.exp * 1000),
sameSite: `${AUTH_COOKIE_SAME_SITE}`,
secure: `${AUTH_COOKIE_SECURE}` === 'true',
httpOnly: `${AUTH_COOKIE_HTTP_ONLY}` === 'true'
};
console.log(cookieOptions);
cookies.set(AUTH_COOKIE_NAME, authData.access_token, cookieOptions);
redirect(302, '/');
}
};
The previous code is executed in the backend (as one could understand by the name of the file) and it basically provides a form action that allows a user to login.
This kind of pattern, form actions, is widely used within the client application, as it enables the browser-side client application to request different actions the server-side should take.
In this case, we briefly described the authentication flow of the svelte client application.
Notice that the authentication and session of a user is managed through cookies. The client application requests a authentication token to the fractal server, which data is used to create another cookie that the nodejs server of the client application sends to the browser.
The default action within the +page.server.js
is requested through an HTTP request that the browser-side client app
makes when a user sends an HTML from, for completion, the one defined in:
<!-- src/routes/auth/login/+page.svelte -->
<script>
export let form;
let loginError = false;
if (form?.invalid) {
loginError = true;
}
</script>
<div class='container'>
<div class='row'>
<h1>Login</h1>
</div>
<div class='row'>
<div class='col-md-4'>
<form method='POST'>
<div class='mb-3'>
<label for='userEmail' class='form-label'>Email address</label>
<input
name='username'
type='email'
class="form-control {loginError ? 'is-invalid' : ''}"
id='userEmail'
aria-describedby='emailHelp'
required
/>
<div id='emailHelp' class='form-text'>The email you provided to the IT manager</div>
<div class='invalid-feedback'>
{form?.invalidMessage}
</div>
</div>
<div class='mb-3'>
<label for='userPassword' class='form-label'>Password</label>
<input name='password' type='password' class='form-control' id='userPassword' required />
</div>
<button class='btn btn-primary'>Submit</button>
</form>
</div>
</div>
</div>
Application library¶
While the src/routes
is the public-facing side of the client application, src/lib
contains the client internals.
Within the lib are present four main sections: common
, components
, server
and stores
.
Common contains client modules that export shared functionalities for both browser and server side parts of the app.
Components contains all the svelte components definitions that are used within the client app. Components are organized by resources defined and managed by the fractal server.
Server this is a special section as it is never shared and bundled into the package that a user receives in the browser. More info about this could be found in the svelte kit doc about server-only modules
Stores are modules that export svelte store objects that are used by components to manage the state of the application.
Note that stores are currently not well-organized or used due to the youth of the client.