generated from avior/template-web-astro
Initial commit
This commit is contained in:
commit
ed8dcebcb7
22
.dockerignore
Normal file
22
.dockerignore
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
# build output
|
||||||
|
dist/
|
||||||
|
|
||||||
|
# generated types
|
||||||
|
.astro/
|
||||||
|
|
||||||
|
# dependencies
|
||||||
|
node_modules/
|
||||||
|
|
||||||
|
# logs
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
|
||||||
|
# macOS-specific files
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
slicers/*
|
||||||
|
|
||||||
|
# Coverage
|
||||||
|
coverage/
|
14
.editorconfig
Normal file
14
.editorconfig
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
indent_style = tab
|
||||||
|
indent_size = 4
|
||||||
|
charset = utf-8
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
insert_final_newline = true
|
||||||
|
max_line_length = 120
|
||||||
|
|
||||||
|
# Yaml Standard
|
||||||
|
[*.{yaml,yml}]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
35
.github/workflows/build_and_check.yml
vendored
Normal file
35
.github/workflows/build_and_check.yml
vendored
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
name: Build, check & Test
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ "master" ]
|
||||||
|
pull_request:
|
||||||
|
branches: [ "master" ]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
run:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Use Node.js 20
|
||||||
|
uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: 20
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: npm run build
|
||||||
|
|
||||||
|
- name: Check
|
||||||
|
run: npm run check
|
||||||
|
|
||||||
|
- name: Prepare Tests
|
||||||
|
run: npm run install:test
|
||||||
|
|
||||||
|
- name: Test
|
||||||
|
run : npm run test
|
29
.gitignore
vendored
Normal file
29
.gitignore
vendored
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
# build output
|
||||||
|
dist/
|
||||||
|
|
||||||
|
# generated types
|
||||||
|
.astro/
|
||||||
|
|
||||||
|
# dependencies
|
||||||
|
node_modules/
|
||||||
|
|
||||||
|
# logs
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
|
||||||
|
# environment variables
|
||||||
|
.env
|
||||||
|
.env.production
|
||||||
|
|
||||||
|
# macOS-specific files
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
slicers/*
|
||||||
|
|
||||||
|
# Coverage
|
||||||
|
coverage/
|
||||||
|
|
||||||
|
# Playwright
|
||||||
|
/playwright/
|
4
.vscode/extensions.json
vendored
Normal file
4
.vscode/extensions.json
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"recommendations": ["astro-build.astro-vscode"],
|
||||||
|
"unwantedRecommendations": []
|
||||||
|
}
|
11
.vscode/launch.json
vendored
Normal file
11
.vscode/launch.json
vendored
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"command": "./node_modules/.bin/astro dev",
|
||||||
|
"name": "Development server",
|
||||||
|
"request": "launch",
|
||||||
|
"type": "node-terminal"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
8
.vscode/settings.json
vendored
Normal file
8
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"editor.quickSuggestions": {
|
||||||
|
"strings": "on"
|
||||||
|
},
|
||||||
|
"tailwindCSS.includeLanguages": {
|
||||||
|
"astro": "html"
|
||||||
|
}
|
||||||
|
}
|
53
Dockerfile
Normal file
53
Dockerfile
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
# This Dockerfile allows you to run AstroJS in server mode
|
||||||
|
|
||||||
|
#########
|
||||||
|
# Build #
|
||||||
|
#########
|
||||||
|
FROM docker.io/node:20-alpine as BUILD_IMAGE
|
||||||
|
|
||||||
|
# run as non root user
|
||||||
|
USER node
|
||||||
|
|
||||||
|
# go to user repository
|
||||||
|
WORKDIR /home/node
|
||||||
|
|
||||||
|
# Add package json
|
||||||
|
ADD --chown=node:node package.json package-lock.json ./
|
||||||
|
|
||||||
|
# install dependencies from package lock
|
||||||
|
RUN npm ci
|
||||||
|
|
||||||
|
# Add project files
|
||||||
|
ADD --chown=node:node . .
|
||||||
|
|
||||||
|
# build
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
# remove dev deps
|
||||||
|
RUN npm prune --omit=dev
|
||||||
|
|
||||||
|
##############
|
||||||
|
# Production #
|
||||||
|
##############
|
||||||
|
FROM docker.io/node:20-alpine as PROD_IMAGE
|
||||||
|
|
||||||
|
# inform software to be in production
|
||||||
|
ENV NODE_ENV=production
|
||||||
|
ENV HOST=0.0.0.0
|
||||||
|
|
||||||
|
# run as non root user
|
||||||
|
USER node
|
||||||
|
|
||||||
|
# go to work folder
|
||||||
|
WORKDIR /home/node
|
||||||
|
|
||||||
|
# copy from build image
|
||||||
|
COPY --chown=node:node --from=BUILD_IMAGE /home/node/node_modules ./node_modules
|
||||||
|
COPY --chown=node:node --from=BUILD_IMAGE /home/node/dist ./dist
|
||||||
|
COPY --chown=node:node --from=BUILD_IMAGE /home/node/package.json /home/node/.env* ./
|
||||||
|
|
||||||
|
# Expose port
|
||||||
|
EXPOSE 3000
|
||||||
|
|
||||||
|
# run it !
|
||||||
|
CMD ["npm", "run", "start"]
|
38
Dockerfile.static
Normal file
38
Dockerfile.static
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
# This Dockerfile allows you to run Astro in a static container (no server side)
|
||||||
|
|
||||||
|
#########
|
||||||
|
# Build #
|
||||||
|
#########
|
||||||
|
FROM docker.io/node:20-alpine as BUILD_IMAGE
|
||||||
|
|
||||||
|
# run as non root user
|
||||||
|
USER node
|
||||||
|
|
||||||
|
# go to user repository
|
||||||
|
WORKDIR /home/node
|
||||||
|
|
||||||
|
# Add package json
|
||||||
|
ADD --chown=node:node package.json package-lock.json ./
|
||||||
|
|
||||||
|
# install dependencies from package lock
|
||||||
|
RUN npm ci
|
||||||
|
|
||||||
|
# Add project files
|
||||||
|
ADD --chown=node:node . .
|
||||||
|
|
||||||
|
# build
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
##############
|
||||||
|
# Production #
|
||||||
|
##############
|
||||||
|
FROM docker.io/nginx:1-alpine
|
||||||
|
|
||||||
|
# go to NGINX folder
|
||||||
|
WORKDIR /usr/share/nginx/html
|
||||||
|
|
||||||
|
# Copy the nginx config
|
||||||
|
ADD ./.docker/nginx.conf /etc/nginx/nginx.conf
|
||||||
|
|
||||||
|
# Copy dist fro mthe build image
|
||||||
|
COPY --from=BUILD_IMAGE /home/node/dist ./
|
36
README.md
Normal file
36
README.md
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
# Fi3D Slicer as a Service
|
||||||
|
|
||||||
|
## API key
|
||||||
|
|
||||||
|
add `Authorization: Bearer {token}`
|
||||||
|
|
||||||
|
## API Endpoints
|
||||||
|
|
||||||
|
| endpoint | method | permission required | cookie access | api access | Description |
|
||||||
|
| :----------------------------: | :----: | :-----------------: | :-----------: | :--------: | :---------------------------------------: |
|
||||||
|
| /api/v1/users | GET | user.list | no | no | List every user accounts |
|
||||||
|
| /api/v1/users | POST | user.create | no | no | Create a new account |
|
||||||
|
| /api/v1/users/{userId} | GET | user.get | yes | yes | Get a user's informations |
|
||||||
|
| /api/v1/users/{userId} | PUT | user.set | yes | yes | Set a user's informations |
|
||||||
|
| /api/v1/users/{userId}/configs | GET | configs.get | yes | yes | get the list of the user's configurations |
|
||||||
|
| /api/v1/users/{userId}/configs | POST | configs.create | yes | yes | Add a new configuration to the user |
|
||||||
|
| /api/v1/users/{userId}/keys | GET | keys.get | yes | no | get the list of API key for the user |
|
||||||
|
| /api/v1/users/{userId}/keys | POST | keys.create | yes | no | create a new API key for the user |
|
||||||
|
| /api/v1/slice/{configId} | POST | slice.slice | yes | yes | run the main website action |
|
||||||
|
|
||||||
|
endpoints not available through API can still be accessed by admins with the `admin.` prefix to the permission
|
||||||
|
|
||||||
|
## API Key Permissions
|
||||||
|
|
||||||
|
### `slicing:*` permissions related to the slicing
|
||||||
|
|
||||||
|
| name | Description |
|
||||||
|
| :-----------: | :---------------------: |
|
||||||
|
| slicing:slice | Slice the specified STL |
|
||||||
|
|
||||||
|
### `configs:*` permissions related to the configuration files
|
||||||
|
|
||||||
|
| name | description |
|
||||||
|
| :------------: | :--------------------------------: |
|
||||||
|
| configs:create | Create a new configuration file |
|
||||||
|
| configs:get | Get an existing configuration file |
|
44
astro.config.mjs
Normal file
44
astro.config.mjs
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
import { defineConfig } from 'astro/config'
|
||||||
|
import tailwind from "@astrojs/tailwind"
|
||||||
|
import node from "@astrojs/node"
|
||||||
|
|
||||||
|
// const faviconHook = {
|
||||||
|
// name: 'Favicon',
|
||||||
|
// hooks: {
|
||||||
|
// "astro:build:setup": async () => {
|
||||||
|
// await Manifest.create('./src/assets/favicon.png', {
|
||||||
|
// name: 'Template'
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// https://astro.build/config
|
||||||
|
export default defineConfig({
|
||||||
|
integrations: [tailwind()],
|
||||||
|
compressHTML: true,
|
||||||
|
build: {
|
||||||
|
assets: 'assets',
|
||||||
|
inlineStylesheets: 'auto'
|
||||||
|
},
|
||||||
|
server: {
|
||||||
|
host: true,
|
||||||
|
port: 3000
|
||||||
|
},
|
||||||
|
trailingSlash: 'never',
|
||||||
|
vite: {
|
||||||
|
server: {
|
||||||
|
watch: {
|
||||||
|
// support WSL strange things
|
||||||
|
usePolling: !!process.env.WSL_DISTRO_NAME
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Customizable depending on goal
|
||||||
|
output: 'server',
|
||||||
|
adapter: node({
|
||||||
|
mode: "standalone"
|
||||||
|
}),
|
||||||
|
site: 'https://example.com',
|
||||||
|
})
|
5
e2e/README.md
Normal file
5
e2e/README.md
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
# e2e
|
||||||
|
|
||||||
|
Hold End 2 End tests
|
||||||
|
|
||||||
|
currently WIP
|
8
e2e/example.spec.ts
Normal file
8
e2e/example.spec.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import { expect, test } from '@playwright/test'
|
||||||
|
|
||||||
|
test('has title', async ({ page }) => {
|
||||||
|
await page.goto('/');
|
||||||
|
|
||||||
|
// Expect a title "to contain" a substring.
|
||||||
|
await expect(page).toHaveTitle(/Astro/);
|
||||||
|
})
|
64
nginx.conf
Normal file
64
nginx.conf
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
worker_processes 1;
|
||||||
|
|
||||||
|
events {
|
||||||
|
worker_connections 1024;
|
||||||
|
}
|
||||||
|
|
||||||
|
http {
|
||||||
|
server {
|
||||||
|
listen 3000;
|
||||||
|
listen [::]:3000;
|
||||||
|
|
||||||
|
server_name _;
|
||||||
|
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
index index.html index.htm;
|
||||||
|
include /etc/nginx/mime.types;
|
||||||
|
|
||||||
|
gzip on;
|
||||||
|
gzip_min_length 1000;
|
||||||
|
gzip_proxied expired no-cache no-store private auth;
|
||||||
|
gzip_types text/plain text/css application/json application/javascript application/x-javascript text/xml application/xml application/xml+rss text/javascript;
|
||||||
|
|
||||||
|
error_page 404 /404.html;
|
||||||
|
error_page 500 502 503 504 /500.html;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri.html /$uri /index.html;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Plausible script
|
||||||
|
location /js/script.js {
|
||||||
|
# Change this if you use a different variant of the script
|
||||||
|
proxy_pass https://plausible.io/js/script.js;
|
||||||
|
proxy_set_header Host plausible.io;
|
||||||
|
|
||||||
|
# Tiny, negligible performance improvement. Very optional.
|
||||||
|
proxy_buffering on;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Plausible script
|
||||||
|
location /api/event {
|
||||||
|
proxy_pass https://plausible.io/api/event;
|
||||||
|
proxy_set_header Host plausible.io;
|
||||||
|
proxy_buffering on;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_set_header X-Forwarded-Host $host;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Media: images, icons, video, audio, HTC
|
||||||
|
location ~* \.(?:jpg|jpeg|gif|png|ico|cur|gz|svg|svgz|mp4|mp3|ogg|ogv|webp|webm|htc|woff2|woff)$ {
|
||||||
|
expires 1y;
|
||||||
|
access_log off;
|
||||||
|
}
|
||||||
|
|
||||||
|
# CSS and Javascript
|
||||||
|
location ~* \.(?:css|js)$ {
|
||||||
|
expires 1y;
|
||||||
|
access_log off;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
6845
package-lock.json
generated
Normal file
6845
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
34
package.json
Normal file
34
package.json
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
{
|
||||||
|
"name": "@dzeio/template",
|
||||||
|
"type": "module",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "astro dev",
|
||||||
|
"start": "node ./dist/server/entry.mjs",
|
||||||
|
"build": "astro build",
|
||||||
|
"check": "npm run check:astro && npm run check:typescript",
|
||||||
|
"check:astro": "astro check",
|
||||||
|
"check:typescript": "tsc --noEmit",
|
||||||
|
"test": "npm run test:unit && npm run test:e2e",
|
||||||
|
"test:unit": "vitest --coverage --run",
|
||||||
|
"test:e2e": "playwright test",
|
||||||
|
|
||||||
|
"install:test": "playwright install --with-deps"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@astrojs/node": "^6",
|
||||||
|
"@astrojs/tailwind": "^5",
|
||||||
|
"@dzeio/logger": "^3",
|
||||||
|
"@dzeio/object-util": "^1",
|
||||||
|
"@dzeio/url-manager": "^1",
|
||||||
|
"astro": "^3",
|
||||||
|
"lucide-astro": "^0",
|
||||||
|
"tailwindcss": "^3"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@playwright/test": "^1",
|
||||||
|
"@types/node": "^20",
|
||||||
|
"@vitest/coverage-v8": "^0",
|
||||||
|
"vitest": "^0"
|
||||||
|
}
|
||||||
|
}
|
62
playwright.config.ts
Normal file
62
playwright.config.ts
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
import { defineConfig, devices } from '@playwright/test'
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
webServer: {
|
||||||
|
command: 'npm run start',
|
||||||
|
url: 'http://localhost:3000',
|
||||||
|
timeout: 120 * 1000,
|
||||||
|
reuseExistingServer: !process.env.CI
|
||||||
|
},
|
||||||
|
outputDir: './playwright/results',
|
||||||
|
testDir: './e2e',
|
||||||
|
fullyParallel: true,
|
||||||
|
forbidOnly: !!process.env.CI,
|
||||||
|
retries: process.env.CI ? 2 : 0,
|
||||||
|
workers: process.env.CI ? 1 : undefined as any,
|
||||||
|
reporter: process.env.CI ? 'list' : [['html', {
|
||||||
|
outputFolder: './playwright/report',
|
||||||
|
open: 'never'
|
||||||
|
}]],
|
||||||
|
use: {
|
||||||
|
baseURL: 'http://localhost:3000',
|
||||||
|
trace: 'on-first-retry',
|
||||||
|
},
|
||||||
|
|
||||||
|
/* Configure projects for major browsers */
|
||||||
|
projects: [
|
||||||
|
{
|
||||||
|
name: 'chromium',
|
||||||
|
use: { ...devices['Desktop Chrome'] },
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: 'firefox',
|
||||||
|
use: { ...devices['Desktop Firefox'] },
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: 'webkit',
|
||||||
|
use: { ...devices['Desktop Safari'] },
|
||||||
|
},
|
||||||
|
|
||||||
|
/* Test against mobile viewports. */
|
||||||
|
// {
|
||||||
|
// name: 'Mobile Chrome',
|
||||||
|
// use: { ...devices['Pixel 5'] },
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// name: 'Mobile Safari',
|
||||||
|
// use: { ...devices['iPhone 12'] },
|
||||||
|
// },
|
||||||
|
|
||||||
|
/* Test against branded browsers. */
|
||||||
|
// {
|
||||||
|
// name: 'Microsoft Edge',
|
||||||
|
// use: { ...devices['Desktop Edge'], channel: 'msedge' },
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// name: 'Google Chrome',
|
||||||
|
// use: { ...devices['Desktop Chrome'], channel: 'chrome' },
|
||||||
|
// },
|
||||||
|
],
|
||||||
|
})
|
9
public/favicon.svg
Normal file
9
public/favicon.svg
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 128 128">
|
||||||
|
<path d="M50.4 78.5a75.1 75.1 0 0 0-28.5 6.9l24.2-65.7c.7-2 1.9-3.2 3.4-3.2h29c1.5 0 2.7 1.2 3.4 3.2l24.2 65.7s-11.6-7-28.5-7L67 45.5c-.4-1.7-1.6-2.8-2.9-2.8-1.3 0-2.5 1.1-2.9 2.7L50.4 78.5Zm-1.1 28.2Zm-4.2-20.2c-2 6.6-.6 15.8 4.2 20.2a17.5 17.5 0 0 1 .2-.7 5.5 5.5 0 0 1 5.7-4.5c2.8.1 4.3 1.5 4.7 4.7.2 1.1.2 2.3.2 3.5v.4c0 2.7.7 5.2 2.2 7.4a13 13 0 0 0 5.7 4.9v-.3l-.2-.3c-1.8-5.6-.5-9.5 4.4-12.8l1.5-1a73 73 0 0 0 3.2-2.2 16 16 0 0 0 6.8-11.4c.3-2 .1-4-.6-6l-.8.6-1.6 1a37 37 0 0 1-22.4 2.7c-5-.7-9.7-2-13.2-6.2Z" />
|
||||||
|
<style>
|
||||||
|
path { fill: #000; }
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
path { fill: #FFF; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 758 B |
7
src/assets/README.md
Normal file
7
src/assets/README.md
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
# Assets
|
||||||
|
|
||||||
|
Contains images that can be imported directly into the application
|
||||||
|
|
||||||
|
# Folder Architecture
|
||||||
|
|
||||||
|
- /assets/[path to element from src]/[folder named as the element]/[assets of the element].[ext]
|
BIN
src/assets/layouts/Base/favicon.png
Normal file
BIN
src/assets/layouts/Base/favicon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.9 KiB |
9
src/assets/layouts/Base/favicon.svg
Normal file
9
src/assets/layouts/Base/favicon.svg
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 128 128">
|
||||||
|
<path d="M50.4 78.5a75.1 75.1 0 0 0-28.5 6.9l24.2-65.7c.7-2 1.9-3.2 3.4-3.2h29c1.5 0 2.7 1.2 3.4 3.2l24.2 65.7s-11.6-7-28.5-7L67 45.5c-.4-1.7-1.6-2.8-2.9-2.8-1.3 0-2.5 1.1-2.9 2.7L50.4 78.5Zm-1.1 28.2Zm-4.2-20.2c-2 6.6-.6 15.8 4.2 20.2a17.5 17.5 0 0 1 .2-.7 5.5 5.5 0 0 1 5.7-4.5c2.8.1 4.3 1.5 4.7 4.7.2 1.1.2 2.3.2 3.5v.4c0 2.7.7 5.2 2.2 7.4a13 13 0 0 0 5.7 4.9v-.3l-.2-.3c-1.8-5.6-.5-9.5 4.4-12.8l1.5-1a73 73 0 0 0 3.2-2.2 16 16 0 0 0 6.8-11.4c.3-2 .1-4-.6-6l-.8.6-1.6 1a37 37 0 0 1-22.4 2.7c-5-.7-9.7-2-13.2-6.2Z" />
|
||||||
|
<style>
|
||||||
|
path { fill: #000; }
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
path { fill: #FFF; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 758 B |
24
src/components/Favicon/Favicon.astro
Normal file
24
src/components/Favicon/Favicon.astro
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
---
|
||||||
|
import { getImage } from 'astro:assets'
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
svg: ImageMetadata
|
||||||
|
png: ImageMetadata
|
||||||
|
icoPath?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Astro.props.icoPath !== '/favicon.ico') {
|
||||||
|
console.warn('It is recommanded that the ICO file should be located at /favicon.ico')
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
const appleTouch = await getImage({src: Astro.props.png, width: 180, height: 180})
|
||||||
|
---
|
||||||
|
|
||||||
|
<>
|
||||||
|
<link rel="icon" href={Astro.props.icoPath ?? "/favicon.ico"} sizes="any">
|
||||||
|
<link rel="icon" href={Astro.props.svg.src} type="image/svg+xml">
|
||||||
|
<link rel="apple-touch-icon" href={appleTouch.src} />
|
||||||
|
<!-- Currently not integrated until I find a way to. -->
|
||||||
|
<!-- <link rel="manifest" href="/site.webmanifest" /> -->
|
||||||
|
</>
|
36
src/components/Favicon/Manifest.ts
Normal file
36
src/components/Favicon/Manifest.ts
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import { getImage } from 'astro:assets'
|
||||||
|
|
||||||
|
export default class Manifest {
|
||||||
|
static async create(baseImage: ImageMetadata, options: {
|
||||||
|
name: string
|
||||||
|
color?: string
|
||||||
|
images?: Array<number>
|
||||||
|
}) {
|
||||||
|
const [
|
||||||
|
i192,
|
||||||
|
i512
|
||||||
|
] = await Promise.all([
|
||||||
|
getImage({src: baseImage, format: 'png', width: 192, height: 192}),
|
||||||
|
getImage({src: baseImage, format: 'png', width: 512, height: 512})
|
||||||
|
])
|
||||||
|
return JSON.stringify({
|
||||||
|
name: options.name,
|
||||||
|
short_name: options.name,
|
||||||
|
icons: [
|
||||||
|
{
|
||||||
|
src: i192.src,
|
||||||
|
sizes: "192x192",
|
||||||
|
type: "image/png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: i512.src,
|
||||||
|
sizes: "512x512",
|
||||||
|
type: "image/png"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
theme_color: options.color ?? "#fff",
|
||||||
|
background_color: options.color ?? "#fff",
|
||||||
|
display: "standalone"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
4
src/components/Passthrough/Passthrough.astro
Normal file
4
src/components/Passthrough/Passthrough.astro
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
const json = JSON.stringify(Astro.props)
|
||||||
|
---
|
||||||
|
<script id="ASTRO_DATA" is:inline type="application/json" set:html={json}></script>
|
10
src/components/Passthrough/utils.ts
Normal file
10
src/components/Passthrough/utils.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
/**
|
||||||
|
* note: you MUST only pass simple items that can go in JSON format natively
|
||||||
|
*/
|
||||||
|
export function load<T extends {} = {}>(): T {
|
||||||
|
const tag = document.querySelector<HTMLScriptElement>('#ASTRO_DATA')
|
||||||
|
if (!tag) {
|
||||||
|
throw new Error('could not load client variables, tag not found')
|
||||||
|
}
|
||||||
|
return JSON.parse(tag.innerText)
|
||||||
|
}
|
42
src/components/Picture.astro.tmp
Normal file
42
src/components/Picture.astro.tmp
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
---
|
||||||
|
import { LocalImageProps, RemoteImageProps, getImage } from 'astro:assets'
|
||||||
|
import AstroUtils from '../libs/AstroUtils'
|
||||||
|
type ImageProps = LocalImageProps | RemoteImageProps
|
||||||
|
export type Props = ImageProps
|
||||||
|
|
||||||
|
const res = await AstroUtils.wrap(async () => {
|
||||||
|
const image = Astro.props.src
|
||||||
|
const ext = typeof image === 'string' ? image.substring(image.lastIndexOf('.')) : image.format
|
||||||
|
if (ext === 'svg') {
|
||||||
|
return {
|
||||||
|
format: 'raw',
|
||||||
|
props: {
|
||||||
|
...Astro.props,
|
||||||
|
src: typeof image === 'string' ? image : image.src
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const avif = await getImage({src: Astro.props.src, format: 'avif'})
|
||||||
|
const webp = await getImage({src: Astro.props.src, format: 'webp'})
|
||||||
|
const orig = await getImage({src: Astro.props.src, format: ext})
|
||||||
|
|
||||||
|
return {
|
||||||
|
format: 'new',
|
||||||
|
avif,
|
||||||
|
webp,
|
||||||
|
orig
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
{res.format === 'new' && (
|
||||||
|
<picture class:list={[res.orig!.attributes.class, Astro.props.class]}>
|
||||||
|
<source srcset={res.avif!.src} type="image/avif" />
|
||||||
|
<source srcset={res.webp!.src} type="image/webp" />
|
||||||
|
<img src={res.orig!.src} class="" {...res.orig!.attributes} />
|
||||||
|
</picture>
|
||||||
|
) || (
|
||||||
|
<img {...res.props} />
|
||||||
|
)}
|
3
src/components/README.md
Normal file
3
src/components/README.md
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
# Components
|
||||||
|
|
||||||
|
Contains big elements that can be reused by themselve
|
5
src/content/README.md
Normal file
5
src/content/README.md
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
# Content
|
||||||
|
|
||||||
|
Contains raw content for pages.
|
||||||
|
|
||||||
|
Mostly some static pages or blog posts.
|
15
src/content/config.ts
Normal file
15
src/content/config.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
// 1. Import utilities from `astro:content`
|
||||||
|
// import { defineCollection, z } from 'astro:content'
|
||||||
|
|
||||||
|
// 2. Define your collection(s)
|
||||||
|
// const docsCollection = defineCollection({
|
||||||
|
// type: 'content',
|
||||||
|
// schema: z.object({
|
||||||
|
// title: z.string()
|
||||||
|
// })
|
||||||
|
// })
|
||||||
|
// 3. Export a single `collections` object to register your collection(s)
|
||||||
|
// This key should match your collection directory name in "src/content"
|
||||||
|
// export const collections = {
|
||||||
|
// 'docs': docsCollection,
|
||||||
|
// };
|
23
src/env.d.ts
vendored
Normal file
23
src/env.d.ts
vendored
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
/// <reference path="../.astro/types.d.ts" />
|
||||||
|
/// <reference types="astro/client" />
|
||||||
|
/// <reference path="./libs/ResponseBuilder" />
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Environment variables declaration
|
||||||
|
*/
|
||||||
|
interface ImportMetaEnv {
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ImportMeta {
|
||||||
|
readonly env: ImportMetaEnv;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
declare namespace App {
|
||||||
|
/**
|
||||||
|
* Middlewares variables
|
||||||
|
*/
|
||||||
|
interface Locals {
|
||||||
|
responseBuilder: ResponseBuilder
|
||||||
|
}
|
||||||
|
}
|
28
src/layouts/Base.astro
Normal file
28
src/layouts/Base.astro
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
---
|
||||||
|
export interface Props {
|
||||||
|
title: string
|
||||||
|
}
|
||||||
|
|
||||||
|
import Favicon from '../components/Favicon/Favicon.astro'
|
||||||
|
import IconSVG from '../assets/layouts/Base/favicon.svg'
|
||||||
|
import IconPNG from '../assets/layouts/Base/favicon.png'
|
||||||
|
|
||||||
|
const { title } = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="description" content="Astro description">
|
||||||
|
<meta name="viewport" content="width=device-width" />
|
||||||
|
<!-- Analytics -->
|
||||||
|
<script defer data-domain="example.com" src="/js/script.js"></script>
|
||||||
|
|
||||||
|
<Favicon svg={IconSVG} png={IconPNG} icoPath="/favicon.ico" />
|
||||||
|
<title>{title}</title>
|
||||||
|
</head>
|
||||||
|
<body class="bg-gray-50">
|
||||||
|
<slot />
|
||||||
|
</body>
|
||||||
|
</html>
|
11
src/layouts/Layout.astro
Normal file
11
src/layouts/Layout.astro
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
---
|
||||||
|
import Base, { type Props as BaseProps } from './Base.astro'
|
||||||
|
|
||||||
|
export interface Props extends BaseProps {}
|
||||||
|
---
|
||||||
|
|
||||||
|
<Base {...Astro.props}>
|
||||||
|
<main class="container">
|
||||||
|
<slot />
|
||||||
|
</main>
|
||||||
|
</Base>
|
7
src/layouts/README.md
Normal file
7
src/layouts/README.md
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
# Layouts
|
||||||
|
|
||||||
|
Application different layouts they should extends `Base.astro` if added and also pass the parameters of Base.astro to the page
|
||||||
|
|
||||||
|
## Base.astro
|
||||||
|
|
||||||
|
This is the base file for each path of the application, executed for each paths
|
5
src/libs/AstroUtils.ts
Normal file
5
src/libs/AstroUtils.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export default class AstroUtils {
|
||||||
|
public static async wrap<T = void>(fn: () => T | Promise<T>) {
|
||||||
|
return await fn()
|
||||||
|
}
|
||||||
|
}
|
289
src/libs/HTTP/StatusCode.ts
Normal file
289
src/libs/HTTP/StatusCode.ts
Normal file
@ -0,0 +1,289 @@
|
|||||||
|
/**
|
||||||
|
* HTTP Status code
|
||||||
|
*
|
||||||
|
* Following https://developer.mozilla.org/en-US/docs/Web/HTTP/Status an extension of the RFC9110
|
||||||
|
*/
|
||||||
|
enum StatusCode {
|
||||||
|
|
||||||
|
/****************
|
||||||
|
* 1xx Requests *
|
||||||
|
****************/
|
||||||
|
/**
|
||||||
|
* This interim response indicates that the client should continue the request or ignore the response if the request is already finished.
|
||||||
|
*/
|
||||||
|
CONTINUE = 100,
|
||||||
|
/**
|
||||||
|
* This code is sent in response to an Upgrade request header from the client and indicates the protocol the server is switching to.
|
||||||
|
*/
|
||||||
|
SWITCHING_PROTOCOLS,
|
||||||
|
/**
|
||||||
|
* This code indicates that the server has received and is processing the request, but no response is available yet.
|
||||||
|
*/
|
||||||
|
PROCESSING,
|
||||||
|
/**
|
||||||
|
* This status code is primarily intended to be used with the Link header, letting the user agent start preloading resources while the server prepares a response.
|
||||||
|
*/
|
||||||
|
EARLY_HINTS,
|
||||||
|
|
||||||
|
/****************
|
||||||
|
* 2xx Requests *
|
||||||
|
****************/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The request succeeded. The result meaning of "success" depends on the HTTP method:
|
||||||
|
* - `GET`: The resource has been fetched and transmitted in the message body.
|
||||||
|
* - `HEAD`: The representation headers are included in the response without any message body.
|
||||||
|
* - `PUT` or `POST`: The resource describing the result of the action is transmitted in the message body.
|
||||||
|
* - `TRACE`: The message body contains the request message as received by the server.
|
||||||
|
*/
|
||||||
|
OK = 200,
|
||||||
|
/**
|
||||||
|
* The request succeeded, and a new resource was created as a result. This is typically the response sent after `POST` requests, or some `PUT` requests.
|
||||||
|
*/
|
||||||
|
CREATED,
|
||||||
|
/**
|
||||||
|
* The request has been received but not yet acted upon. It is noncommittal, since there is no way in HTTP to later send an asynchronous response indicating the outcome of the request. It is intended for cases where another process or server handles the request, or for batch processing.
|
||||||
|
*/
|
||||||
|
ACCEPTED,
|
||||||
|
/**
|
||||||
|
* This response code means the returned metadata is not exactly the same as is available from the origin server, but is collected from a local or a third-party copy. This is mostly used for mirrors or backups of another resource. Except for that specific case, the `200 OK` response is preferred to this status.
|
||||||
|
*/
|
||||||
|
NON_AUTHORITATIVE_INFORMATION,
|
||||||
|
/**
|
||||||
|
* There is no content to send for this request, but the headers may be useful. The user agent may update its cached headers for this resource with the new ones.
|
||||||
|
*/
|
||||||
|
NO_CONTENT,
|
||||||
|
/**
|
||||||
|
* Tells the user agent to reset the document which sent this request.
|
||||||
|
*/
|
||||||
|
RESET_CONTENT,
|
||||||
|
/**
|
||||||
|
* This response code is used when the Range header is sent from the client to request only part of a resource.
|
||||||
|
*/
|
||||||
|
PARTIAL_CONTENT,
|
||||||
|
/**
|
||||||
|
* Conveys information about multiple resources, for situations where multiple status codes might be appropriate.
|
||||||
|
*/
|
||||||
|
MULTI_STATUS,
|
||||||
|
/**
|
||||||
|
* Used inside a `<dav:propstat>` response element to avoid repeatedly enumerating the internal members of multiple bindings to the same collection.
|
||||||
|
*/
|
||||||
|
ALREADY_REPORTED,
|
||||||
|
/**
|
||||||
|
* The server has fulfilled a `GET` request for the resource, and the response is a representation of the result of one or more instance-manipulations applied to the current instance.
|
||||||
|
*/
|
||||||
|
IM_USED = 226,
|
||||||
|
|
||||||
|
/****************
|
||||||
|
* 3xx Requests *
|
||||||
|
****************/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The request has more than one possible response. The user agent or user should choose one of them. (There is no standardized way of choosing one of the responses, but HTML links to the possibilities are recommended so the user can pick.)
|
||||||
|
*/
|
||||||
|
MULTIPLE_CHOICES = 300,
|
||||||
|
/**
|
||||||
|
* The URL of the requested resource has been changed permanently. The new URL is given in the response.
|
||||||
|
*/
|
||||||
|
MOVED_PERMANENTLY,
|
||||||
|
/**
|
||||||
|
* This response code means that the URI of requested resource has been changed temporarily. Further changes in the URI might be made in the future. Therefore, this same URI should be used by the client in future requests.
|
||||||
|
*/
|
||||||
|
FOUND,
|
||||||
|
/**
|
||||||
|
* The server sent this response to direct the client to get the requested resource at another URI with a GET request.
|
||||||
|
*/
|
||||||
|
SEE_OTHER,
|
||||||
|
/**
|
||||||
|
* This is used for caching purposes. It tells the client that the response has not been modified, so the client can continue to use the same cached version of the response.
|
||||||
|
*/
|
||||||
|
NOT_MODIFIED,
|
||||||
|
/**
|
||||||
|
* Defined in a previous version of the HTTP specification to indicate that a requested response must be accessed by a proxy. It has been deprecated due to security concerns regarding in-band configuration of a proxy.
|
||||||
|
*/
|
||||||
|
USE_PROXY,
|
||||||
|
/**
|
||||||
|
* This response code is no longer used; it is just reserved. It was used in a previous version of the HTTP/1.1 specification.
|
||||||
|
*/
|
||||||
|
// UNUSED
|
||||||
|
/**
|
||||||
|
* The server sends this response to direct the client to get the requested resource at another URI with the same method that was used in the prior request. This has the same semantics as the `302 Found` HTTP response code, with the exception that the user agent must not change the HTTP method used: if a `POST` was used in the first request, a `POST` must be used in the second request.
|
||||||
|
*/
|
||||||
|
TEMPORARY_REDIRECT = 307,
|
||||||
|
/**
|
||||||
|
* This means that the resource is now permanently located at another URI, specified by the `Location:` HTTP Response header. This has the same semantics as the `301 Moved Permanently` HTTP response code, with the exception that the user agent must not change the HTTP method used: if a `POST` was used in the first request, a `POST` must be used in the second request.
|
||||||
|
*/
|
||||||
|
PERMANENT_REDIRECT,
|
||||||
|
|
||||||
|
/****************
|
||||||
|
* 4xx Requests *
|
||||||
|
****************/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The server cannot or will not process the request due to something that is perceived to be a client error (e.g., malformed request syntax, invalid request message framing, or deceptive request routing).
|
||||||
|
*/
|
||||||
|
BAD_REQUEST = 400,
|
||||||
|
/**
|
||||||
|
* Although the HTTP standard specifies "unauthorized", semantically this response means "unauthenticated". That is, the client must authenticate itself to get the requested response.
|
||||||
|
*/
|
||||||
|
UNAUTHORIZED,
|
||||||
|
/**
|
||||||
|
* This response code is reserved for future use. The initial aim for creating this code was using it for digital payment systems, however this status code is used very rarely and no standard convention exists.
|
||||||
|
*/
|
||||||
|
PAYMENT_REQUIRED,
|
||||||
|
/**
|
||||||
|
* The client does not have access rights to the content; that is, it is unauthorized, so the server is refusing to give the requested resource. Unlike `401 Unauthorized`, the client's identity is known to the server.
|
||||||
|
*/
|
||||||
|
FORBIDDEN,
|
||||||
|
/**
|
||||||
|
* The server cannot find the requested resource. In the browser, this means the URL is not recognized. In an API, this can also mean that the endpoint is valid but the resource itself does not exist. Servers may also send this response instead of `403 Forbidden` to hide the existence of a resource from an unauthorized client. This response code is probably the most well known due to its frequent occurrence on the web.
|
||||||
|
*/
|
||||||
|
NOT_FOUND,
|
||||||
|
/**
|
||||||
|
* The request method is known by the server but is not supported by the target resource. For example, an API may not allow calling `DELETE` to remove a resource.
|
||||||
|
*/
|
||||||
|
METHOD_NOT_ALLOWED,
|
||||||
|
/**
|
||||||
|
* This response is sent when the web server, after performing server-driven content negotiation, doesn't find any content that conforms to the criteria given by the user agent.
|
||||||
|
*/
|
||||||
|
NOT_ACCEPTABLE,
|
||||||
|
/**
|
||||||
|
* This is similar to `401 Unauthorized` but authentication is needed to be done by a proxy.
|
||||||
|
*/
|
||||||
|
PROXY_AUTHENTIFICATION_REQUIRED,
|
||||||
|
/**
|
||||||
|
* This response is sent on an idle connection by some servers, even without any previous request by the client. It means that the server would like to shut down this unused connection. This response is used much more since some browsers, like Chrome, Firefox 27+, or IE9, use HTTP pre-connection mechanisms to speed up surfing. Also note that some servers merely shut down the connection without sending this message.
|
||||||
|
*/
|
||||||
|
REQUEST_TIMEOUT,
|
||||||
|
/**
|
||||||
|
* This response is sent when a request conflicts with the current state of the server.
|
||||||
|
*/
|
||||||
|
CONFLICT,
|
||||||
|
/**
|
||||||
|
* This response is sent when the requested content has been permanently deleted from server, with no forwarding address. Clients are expected to remove their caches and links to the resource. The HTTP specification intends this status code to be used for "limited-time, promotional services". APIs should not feel compelled to indicate resources that have been deleted with this status code.
|
||||||
|
*/
|
||||||
|
GONE,
|
||||||
|
/**
|
||||||
|
* Server rejected the request because the `Content-Length` header field is not defined and the server requires it.
|
||||||
|
*/
|
||||||
|
LENGTH_REQUIRED,
|
||||||
|
/**
|
||||||
|
* The client has indicated preconditions in its headers which the server does not meet.
|
||||||
|
*/
|
||||||
|
PRECONDITION_FAILED,
|
||||||
|
/**
|
||||||
|
* Request entity is larger than limits defined by server. The server might close the connection or return an `Retry-After` header field.
|
||||||
|
*/
|
||||||
|
PAYLOAD_TOO_LARGE,
|
||||||
|
/**
|
||||||
|
* The URI requested by the client is longer than the server is willing to interpret.
|
||||||
|
*/
|
||||||
|
URI_TOO_LONG,
|
||||||
|
/**
|
||||||
|
* The media format of the requested data is not supported by the server, so the server is rejecting the request.
|
||||||
|
*/
|
||||||
|
UNSUPPORTED_MEDIA_TYPE,
|
||||||
|
/**
|
||||||
|
* The range specified by the `Range` header field in the request cannot be fulfilled. It's possible that the range is outside the size of the target URI's data.
|
||||||
|
*/
|
||||||
|
RANGE_NOT_SATISFIABLE,
|
||||||
|
/**
|
||||||
|
* This response code means the expectation indicated by the `Expect` request header field cannot be met by the server.
|
||||||
|
*/
|
||||||
|
EXPECTATION_FAILED,
|
||||||
|
/**
|
||||||
|
* The server refuses the attempt to brew coffee with a teapot.
|
||||||
|
*/
|
||||||
|
IM_A_TEAPOT,
|
||||||
|
/**
|
||||||
|
* The request was directed at a server that is not able to produce a response. This can be sent by a server that is not configured to produce responses for the combination of scheme and authority that are included in the request URI.
|
||||||
|
*/
|
||||||
|
MIDIRECTED_REQUEST = 421,
|
||||||
|
/**
|
||||||
|
* The request was well-formed but was unable to be followed due to semantic errors.
|
||||||
|
*/
|
||||||
|
UNPROCESSABLE_CONTENT,
|
||||||
|
/**
|
||||||
|
* The resource that is being accessed is locked.
|
||||||
|
*/
|
||||||
|
LOCKED,
|
||||||
|
/**
|
||||||
|
* The request failed due to failure of a previous request.
|
||||||
|
*/
|
||||||
|
FAILED_DEPENDENCY,
|
||||||
|
/**
|
||||||
|
* Indicates that the server is unwilling to risk processing a request that might be replayed.
|
||||||
|
*/
|
||||||
|
TOO_EARLY,
|
||||||
|
/**
|
||||||
|
* The server refuses to perform the request using the current protocol but might be willing to do so after the client upgrades to a different protocol. The server sends an `Upgrade` header in a 426 response to indicate the required protocol(s).
|
||||||
|
*/
|
||||||
|
UPGRADE_REQUIRED,
|
||||||
|
/**
|
||||||
|
* The origin server requires the request to be conditional. This response is intended to prevent the 'lost update' problem, where a client `GET`s a resource's state, modifies it and `PUT`s it back to the server, when meanwhile a third party has modified the state on the server, leading to a conflict.
|
||||||
|
*/
|
||||||
|
PRECONDITION_REQUIRED = 428,
|
||||||
|
/**
|
||||||
|
* The user has sent too many requests in a given amount of time ("rate limiting").
|
||||||
|
*/
|
||||||
|
TOO_MANY_REQUESTS,
|
||||||
|
/**
|
||||||
|
* The server is unwilling to process the request because its header fields are too large. The request may be resubmitted after reducing the size of the request header fields.
|
||||||
|
*/
|
||||||
|
REQUEST_HEADER_FIELDS_TOO_LARGE = 431,
|
||||||
|
/**
|
||||||
|
* The user agent requested a resource that cannot legally be provided, such as a web page censored by a government.
|
||||||
|
*/
|
||||||
|
UNAVAILABLE_OR_LEGAL_REASONS = 451,
|
||||||
|
|
||||||
|
/****************
|
||||||
|
* 5xx Requests *
|
||||||
|
****************/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The server has encountered a situation it does not know how to handle.
|
||||||
|
*/
|
||||||
|
INTERNAL_SERVER_ERROR = 500,
|
||||||
|
/**
|
||||||
|
* The request method is not supported by the server and cannot be handled. The only methods that servers are required to support (and therefore that must not return this code) are `GET` and `HEAD`.
|
||||||
|
*/
|
||||||
|
NOT_IMPLEMENTED,
|
||||||
|
/**
|
||||||
|
* This error response means that the server, while working as a gateway to get a response needed to handle the request, got an invalid response.
|
||||||
|
*/
|
||||||
|
BAD_GATEWAY,
|
||||||
|
/**
|
||||||
|
* The server is not ready to handle the request. Common causes are a server that is down for maintenance or that is overloaded. Note that together with this response, a user-friendly page explaining the problem should be sent. This response should be used for temporary conditions and the `Retry-After` HTTP header should, if possible, contain the estimated time before the recovery of the service. The webmaster must also take care about the caching-related headers that are sent along with this response, as these temporary condition responses should usually not be cached.
|
||||||
|
*/
|
||||||
|
SERVICE_UNAVAILABLE,
|
||||||
|
/**
|
||||||
|
* This error response is given when the server is acting as a gateway and cannot get a response in time.
|
||||||
|
*/
|
||||||
|
GATEWAY_TIMEOUT,
|
||||||
|
/**
|
||||||
|
* The HTTP version used in the request is not supported by the server.
|
||||||
|
*/
|
||||||
|
HTTP_VERSION_NOT_SUPPORTED,
|
||||||
|
/**
|
||||||
|
* The server has an internal configuration error: the chosen variant resource is configured to engage in transparent content negotiation itself, and is therefore not a proper end point in the negotiation process.
|
||||||
|
*/
|
||||||
|
VARIANT_ALSO_NEGOTIATES,
|
||||||
|
/**
|
||||||
|
* The method could not be performed on the resource because the server is unable to store the representation needed to successfully complete the request.
|
||||||
|
*/
|
||||||
|
INSUFFICIENT_STORAGE,
|
||||||
|
/**
|
||||||
|
* The server detected an infinite loop while processing the request.
|
||||||
|
*/
|
||||||
|
LOOP_DETECTED,
|
||||||
|
/**
|
||||||
|
* Further extensions to the request are required for the server to fulfill it.
|
||||||
|
*/
|
||||||
|
NOT_EXTENDED = 510,
|
||||||
|
/**
|
||||||
|
* Indicates that the client needs to authenticate to gain network access.
|
||||||
|
*/
|
||||||
|
NETWORK_AUTHENTIFICATION_REQUIRED,
|
||||||
|
}
|
||||||
|
|
||||||
|
export default StatusCode
|
3
src/libs/README.md
Normal file
3
src/libs/README.md
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
# Libs
|
||||||
|
|
||||||
|
Globally independent objects/classes/functions that SHOULD be unit testable by themselve
|
66
src/libs/RFCs/RFC7807.ts
Normal file
66
src/libs/RFCs/RFC7807.ts
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
import ResponseBuilder from '../ResponseBuilder'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add headers:
|
||||||
|
* Content-Type: application/problem+json
|
||||||
|
*
|
||||||
|
* following https://www.rfc-editor.org/rfc/rfc7807.html
|
||||||
|
*/
|
||||||
|
export default interface RFC7807 {
|
||||||
|
/**
|
||||||
|
* A URI reference [RFC3986] that identifies the
|
||||||
|
* problem type.
|
||||||
|
*
|
||||||
|
* This specification encourages that, when
|
||||||
|
* dereferenced, it provide human-readable documentation for the
|
||||||
|
* problem type (e.g., using HTML [W3C.REC-html5-20141028]).
|
||||||
|
*
|
||||||
|
* When
|
||||||
|
* this member is not present, its value is assumed to be
|
||||||
|
* "about:blank"
|
||||||
|
*/
|
||||||
|
type?: string
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A short, human-readable summary of the problem
|
||||||
|
* type.
|
||||||
|
*
|
||||||
|
* It SHOULD NOT change from occurrence to occurrence of the
|
||||||
|
* problem, except for purposes of localization (e.g., using
|
||||||
|
* proactive content negotiation; see [RFC7231], Section 3.4).
|
||||||
|
*/
|
||||||
|
title?: string
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The HTTP status code ([RFC7231], Section 6)
|
||||||
|
* generated by the origin server for this occurrence of the problem.
|
||||||
|
*/
|
||||||
|
status?: number
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A human-readable explanation specific to this
|
||||||
|
* occurrence of the problem.
|
||||||
|
*/
|
||||||
|
details?: string
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A URI reference that identifies the specific
|
||||||
|
* occurrence of the problem.
|
||||||
|
*
|
||||||
|
* It may or may not yield further
|
||||||
|
* information if dereferenced.
|
||||||
|
*/
|
||||||
|
instance?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param error the error (base items are type, status, title details and instance)
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export function buildRFC7807(error: RFC7807 & Record<string, any>, response: ResponseBuilder = new ResponseBuilder()): Response {
|
||||||
|
response.addHeader('Content-Type', 'application/problem+json')
|
||||||
|
.body(JSON.stringify(error))
|
||||||
|
.status(error.status ?? 500)
|
||||||
|
return response.build()
|
||||||
|
}
|
59
src/libs/ResponseBuilder.ts
Normal file
59
src/libs/ResponseBuilder.ts
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
import { objectLoop } from '@dzeio/object-util'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simple builde to create a new Response object
|
||||||
|
*/
|
||||||
|
export default class ResponseBuilder {
|
||||||
|
|
||||||
|
private _body: BodyInit | null | undefined
|
||||||
|
public body(body: string | Buffer | object | null | undefined) {
|
||||||
|
if (typeof body === 'object' && !(body instanceof Buffer)) {
|
||||||
|
this._body = JSON.stringify(body)
|
||||||
|
this.addHeader('Content-Type', 'application/json')
|
||||||
|
} else if (body instanceof Buffer) {
|
||||||
|
this._body = body.toString()
|
||||||
|
} else {
|
||||||
|
this._body = body
|
||||||
|
}
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
private _headers: Record<string, string> = {}
|
||||||
|
public headers(headers: Record<string, string>) {
|
||||||
|
this._headers = headers
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
public addHeader(key: string, value: string) {
|
||||||
|
this._headers[key] = value
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
public addHeaders(headers: Record<string, string>) {
|
||||||
|
objectLoop(headers, (value, key) => {
|
||||||
|
this.addHeader(key, value)
|
||||||
|
})
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
public removeHeader(key: string) {
|
||||||
|
delete this._headers[key]
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
private _status?: number
|
||||||
|
public status(status: number) {
|
||||||
|
this._status = status
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
public build(): Response {
|
||||||
|
const init: ResponseInit = {
|
||||||
|
headers: this._headers
|
||||||
|
}
|
||||||
|
if (this._status) {
|
||||||
|
init.status = this._status
|
||||||
|
}
|
||||||
|
return new Response(this._body, init)
|
||||||
|
}
|
||||||
|
}
|
9
src/middleware/README.md
Normal file
9
src/middleware/README.md
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
# Middlewares
|
||||||
|
|
||||||
|
This folder contains middlewares for the SSR pages/endpoints
|
||||||
|
|
||||||
|
They are run for every paths independent of the middleware and in the specified order of the `index.ts`
|
||||||
|
|
||||||
|
## locals
|
||||||
|
|
||||||
|
You can pass variables to other middlewares and endpoints by adding a variable in `locals` and in `App.Locals` in `env.d.ts`
|
5
src/middleware/index.ts
Normal file
5
src/middleware/index.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import { sequence } from "astro/middleware"
|
||||||
|
|
||||||
|
import responseBuilder from './responseBuilder'
|
||||||
|
|
||||||
|
export const onRequest = sequence(responseBuilder)
|
21
src/middleware/responseBuilder.ts
Normal file
21
src/middleware/responseBuilder.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import { defineMiddleware } from "astro/middleware"
|
||||||
|
import { buildRFC7807 } from '../libs/RFCs/RFC7807'
|
||||||
|
import ResponseBuilder from '../libs/ResponseBuilder'
|
||||||
|
|
||||||
|
// `context` and `next` are automatically typed
|
||||||
|
export default defineMiddleware(async ({ request, locals }, next) => {
|
||||||
|
locals.responseBuilder = new ResponseBuilder()
|
||||||
|
console.log(`[${new Date().toISOString()}] ${request.headers.get('user-agent')?.slice(0, 32).padEnd(32)} ${request.method.padEnd(7)} ${request.url}`)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await next()
|
||||||
|
console.log(`[${new Date().toISOString()}] ${request.headers.get('user-agent')?.slice(0, 32).padEnd(32)} ${request.method.padEnd(7)} ${res.status} ${request.url}`)
|
||||||
|
return res
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
return buildRFC7807({
|
||||||
|
type: '/docs/errors/global-error',
|
||||||
|
status: 500
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
103
src/models/Dao.ts
Normal file
103
src/models/Dao.ts
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
/**
|
||||||
|
* the Dao is the object that connect the Database or source to the application layer
|
||||||
|
*
|
||||||
|
* you MUST call it through the `DaoFactory` file
|
||||||
|
*/
|
||||||
|
export default abstract class Dao<Object extends { id: any } = { id: any }> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* insert a new object into the source
|
||||||
|
*
|
||||||
|
* @param obj the object to create
|
||||||
|
* @returns the object with it's id filled if create or null otherwise
|
||||||
|
*/
|
||||||
|
abstract create(obj: Omit<Object, 'id'>): Promise<Object | null>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* insert a new object into the source
|
||||||
|
*
|
||||||
|
* @param obj the object to create
|
||||||
|
* @returns the object with it's id filled if create or null otherwise
|
||||||
|
*/
|
||||||
|
public insert: Dao<Object>['create'] = (obj: Parameters<Dao<Object>['create']>[0]) => this.create(obj)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* find the list of objects having elements from the query
|
||||||
|
*
|
||||||
|
* @param query a partial object which filter depending on the elements, if not set it will fetch everything
|
||||||
|
* @returns an array containing the list of elements that match with the query
|
||||||
|
*/
|
||||||
|
abstract findAll(query?: Partial<Object>): Promise<Array<Object>>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* find the list of objects having elements from the query
|
||||||
|
*
|
||||||
|
* @param query a partial object which filter depending on the elements, if not set it will fetch everything
|
||||||
|
* @returns an array containing the list of elements that match with the query
|
||||||
|
*/
|
||||||
|
public find: Dao<Object>['findAll'] = (query: Parameters<Dao<Object>['findAll']>[0]) => this.findAll(query)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* find an object by it's id
|
||||||
|
*
|
||||||
|
* (shortcut to findOne({id: id}))
|
||||||
|
*
|
||||||
|
* @param id the id of the object
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
public findById(id: Object['id']): Promise<Object | null> {
|
||||||
|
return this.findOne({id: id} as Partial<Object>)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* find an object by it's id
|
||||||
|
*
|
||||||
|
* (shortcut to findOne({id: id}))
|
||||||
|
*
|
||||||
|
* @param id the id of the object
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
public get(id: Object['id']) {
|
||||||
|
return this.findById(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* find the first element that match `query`
|
||||||
|
*
|
||||||
|
* @param query a partial object which filter depending on the elements, if not set it will fetch everything
|
||||||
|
* @returns the first element matching with the query or null otherwise
|
||||||
|
*/
|
||||||
|
public async findOne(query?: Partial<Object>): Promise<Object | null> {
|
||||||
|
return (await this.findAll(query))[0] ?? null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* update the remote reference of the object
|
||||||
|
*
|
||||||
|
* note: it will not try to insert an item (use `upsert` to handle this)
|
||||||
|
*
|
||||||
|
* @param obj the object to update
|
||||||
|
* @returns an object if it was able to update or null otherwise
|
||||||
|
*/
|
||||||
|
abstract update(obj: Object): Promise<Object | null>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* update the remote reference of the object or create it if not found
|
||||||
|
* @param obj the object to update/insert
|
||||||
|
* @returns the object is updated/inserted or null otherwise
|
||||||
|
*/
|
||||||
|
public async upsert(object: Object | Omit<Object, 'id'>): Promise<Object | null> {
|
||||||
|
if ('id' in object) {
|
||||||
|
return this.update(object)
|
||||||
|
}
|
||||||
|
return this.insert(object)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete the object
|
||||||
|
* @param obj the object to delete
|
||||||
|
*
|
||||||
|
* @returns if the object was deleted or not (if object is not in db it will return true)
|
||||||
|
*/
|
||||||
|
abstract delete(obj: Object): Promise<boolean>
|
||||||
|
}
|
54
src/models/DaoFactory.ts
Normal file
54
src/models/DaoFactory.ts
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
/**
|
||||||
|
* TODO:
|
||||||
|
* Add to `DaoItem` your model name
|
||||||
|
* Add to the function `initDao` the Dao
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* the different Daos that can be initialized
|
||||||
|
*
|
||||||
|
* Touch this interface to define which key is linked to which Dao
|
||||||
|
*/
|
||||||
|
interface DaoItem {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class to get any DAO
|
||||||
|
*/
|
||||||
|
export default class DaoFactory {
|
||||||
|
/**
|
||||||
|
* reference of the different Daos for a correct singleton implementation
|
||||||
|
*/
|
||||||
|
private static daos: Partial<DaoItem> = {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a a dao by its key
|
||||||
|
*
|
||||||
|
* it will throw an error if no Dao exists linked to the item key
|
||||||
|
*
|
||||||
|
* @param key the dao key to get
|
||||||
|
* @returns the Dao you want as a singleton
|
||||||
|
*/
|
||||||
|
public static get<Key extends keyof DaoItem>(key: Key): DaoItem[Key] {
|
||||||
|
if (!(key in this.daos)) {
|
||||||
|
const dao = this.initDao(key)
|
||||||
|
if (!dao) {
|
||||||
|
throw new Error(`${key} has no valid Dao`)
|
||||||
|
}
|
||||||
|
this.daos[key] = dao as DaoItem[Key]
|
||||||
|
}
|
||||||
|
return this.daos[key] as DaoItem[Key]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* init a dao by its key, it does not care if it exists or not
|
||||||
|
*
|
||||||
|
* @param item the element to init
|
||||||
|
* @returns a new initialized dao or undefined if no dao is linked
|
||||||
|
*/
|
||||||
|
private static initDao(item: keyof DaoItem): any | undefined {
|
||||||
|
switch (item) {
|
||||||
|
default: return undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
26
src/models/README.md
Normal file
26
src/models/README.md
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
# Models
|
||||||
|
|
||||||
|
this folder contains the Application data layer
|
||||||
|
|
||||||
|
## Workflow
|
||||||
|
|
||||||
|
1. Add a {model}/index.ts contianing your `interface`
|
||||||
|
2. Add a {model}/{model}Dao.ts containing your DAO that `extends` from `Dao.ts`
|
||||||
|
3. Add your Dao to the `DaoFactory.ts` file
|
||||||
|
|
||||||
|
|
||||||
|
## **/index.ts
|
||||||
|
|
||||||
|
file containing the definition of the model
|
||||||
|
|
||||||
|
## **/\*Dao.ts
|
||||||
|
|
||||||
|
File containing the implementation of the Dao
|
||||||
|
|
||||||
|
## Dao.ts
|
||||||
|
|
||||||
|
the Dao.ts is the file each `*Dao.ts` extends from allowing to have a simple, quick and easy to comprehend connectivity
|
||||||
|
|
||||||
|
## DaoFactory.ts
|
||||||
|
|
||||||
|
The DaoFactory file is the file in which you will have the only direct reference to each `*Dao` files and will be sent from there to the rest of the applicaiton layer
|
5
src/pages/README.md
Normal file
5
src/pages/README.md
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
# Content
|
||||||
|
|
||||||
|
Contains raw content for pages.
|
||||||
|
|
||||||
|
Mostly some static pages or blog posts.
|
13
src/pages/index.astro
Normal file
13
src/pages/index.astro
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
---
|
||||||
|
import Layout from '../layouts/Layout.astro'
|
||||||
|
---
|
||||||
|
|
||||||
|
<Layout title="Welcome to Astro.">
|
||||||
|
<main>
|
||||||
|
<h1>Welcome to <span class="text-gradient">Astro</span></h1>
|
||||||
|
<p class="instructions">
|
||||||
|
To get started, open the directory <code>src/pages</code> in your project.<br />
|
||||||
|
<strong>Code Challenge:</strong> Tweak the "Welcome to Astro" message above.
|
||||||
|
</p>
|
||||||
|
</main>
|
||||||
|
</Layout>
|
9
tailwind.config.cjs
Normal file
9
tailwind.config.cjs
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
// const defaultTheme = require('tailwindcss/defaultTheme')
|
||||||
|
|
||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
module.exports = {
|
||||||
|
content: ['./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}'],
|
||||||
|
theme: {
|
||||||
|
extend: {},
|
||||||
|
}
|
||||||
|
}
|
5
tests/README.md
Normal file
5
tests/README.md
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
# Tests
|
||||||
|
|
||||||
|
Old Unit tests for each elements
|
||||||
|
|
||||||
|
the paths should correspond to the base folder from `src`
|
9
tests/basic.test.ts
Normal file
9
tests/basic.test.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { expect, test } from 'vitest'
|
||||||
|
|
||||||
|
// Edit an assertion and save to see HMR in action
|
||||||
|
|
||||||
|
test('Math.sqrt()', () => {
|
||||||
|
expect(Math.sqrt(4)).toBe(2);
|
||||||
|
expect(Math.sqrt(144)).toBe(12);
|
||||||
|
expect(Math.sqrt(2)).toBe(Math.SQRT2);
|
||||||
|
});
|
4
tsconfig.json
Normal file
4
tsconfig.json
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"extends": "./node_modules/astro/tsconfigs/strictest.json",
|
||||||
|
"exclude": ["cypress"]
|
||||||
|
}
|
13
vitest.config.ts
Normal file
13
vitest.config.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
/// <reference types="vitest" />
|
||||||
|
import { getViteConfig } from 'astro/config'
|
||||||
|
// import { configDefaults } from 'vitest/config'
|
||||||
|
|
||||||
|
export default getViteConfig({
|
||||||
|
test: {
|
||||||
|
include: [
|
||||||
|
'./tests/**.ts'
|
||||||
|
]
|
||||||
|
/* for example, use global to avoid globals imports (describe, test, expect): */
|
||||||
|
// globals: true,
|
||||||
|
},
|
||||||
|
});
|
Loading…
x
Reference in New Issue
Block a user