feat: Add moer element
Signed-off-by: Florian BOUILLON <f.bouillon@aptatio.com>
This commit is contained in:
parent
4cd2a365ae
commit
d76f412b82
47
package-lock.json
generated
47
package-lock.json
generated
@ -21,6 +21,7 @@
|
||||
"tailwindcss": "^3.3.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/typography": "^0.5.9",
|
||||
"@types/bcryptjs": "^2.4.2",
|
||||
"@types/jsonwebtoken": "^9.0.2",
|
||||
"@types/node": "^20.3.1",
|
||||
@ -1083,6 +1084,34 @@
|
||||
"escalade": "^3.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/typography": {
|
||||
"version": "0.5.9",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.9.tgz",
|
||||
"integrity": "sha512-t8Sg3DyynFysV9f4JDOVISGsjazNb48AeIYQwcL+Bsq5uf4RYL75C1giZ43KISjeDGBaTN3Kxh7Xj/vRSMJUUg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"lodash.castarray": "^4.4.0",
|
||||
"lodash.isplainobject": "^4.0.6",
|
||||
"lodash.merge": "^4.6.2",
|
||||
"postcss-selector-parser": "6.0.10"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"tailwindcss": ">=3.0.0 || insiders"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/typography/node_modules/postcss-selector-parser": {
|
||||
"version": "6.0.10",
|
||||
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz",
|
||||
"integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"cssesc": "^3.0.0",
|
||||
"util-deprecate": "^1.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/babel__core": {
|
||||
"version": "7.20.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.1.tgz",
|
||||
@ -4786,6 +4815,24 @@
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
|
||||
},
|
||||
"node_modules/lodash.castarray": {
|
||||
"version": "4.4.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.castarray/-/lodash.castarray-4.4.0.tgz",
|
||||
"integrity": "sha512-aVx8ztPv7/2ULbArGJ2Y42bG1mEQ5mGjpdvrbJcJFU3TbYybe+QlLS4pst9zV52ymy2in1KpFPiZnAOATxD4+Q==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/lodash.isplainobject": {
|
||||
"version": "4.0.6",
|
||||
"resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
|
||||
"integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/lodash.merge": {
|
||||
"version": "4.6.2",
|
||||
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
|
||||
"integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/lodash.once": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
|
||||
|
@ -25,6 +25,7 @@
|
||||
"tailwindcss": "^3.3.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/typography": "^0.5.9",
|
||||
"@types/bcryptjs": "^2.4.2",
|
||||
"@types/jsonwebtoken": "^9.0.2",
|
||||
"@types/node": "^20.3.1",
|
||||
|
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/content/docs/errors/unauthorized-access.md
Normal file
23
src/content/docs/errors/unauthorized-access.md
Normal file
@ -0,0 +1,23 @@
|
||||
---
|
||||
title: 'Unauthorized Access Error'
|
||||
---
|
||||
|
||||
# Unauthorized Access Error
|
||||
|
||||
## Possible Errors
|
||||
|
||||
### Permission Error
|
||||
|
||||
You need to have an API key with the correct permission to use the specified resource
|
||||
|
||||
### Missing API Key
|
||||
|
||||
To have access to most of the API Urls you need to have n API key in your headers like the following
|
||||
|
||||
```http
|
||||
Authorization: Bearer 935b6640-25fe-402e-8b18-e60de88bc0da
|
||||
```
|
||||
|
||||
### Cookies not found
|
||||
|
||||
An alternative to the API Key is to have your cookies set, this is mostly for in browser and will only work while being connected on the website
|
14
src/env.d.ts
vendored
14
src/env.d.ts
vendored
@ -1,7 +1,8 @@
|
||||
/// <reference path="../.astro/types.d.ts" />
|
||||
/// <reference types="astro/client" />
|
||||
/// <reference types="./libs/ResponseBuilder" />
|
||||
|
||||
interface ImportMetaEnv {
|
||||
CONFIGS_PATH?: string
|
||||
PRUSASLICER_PATH?: string
|
||||
BAMBUSTUDIO_PATH?: string
|
||||
MONGODB?: string
|
||||
@ -12,3 +13,14 @@ interface ImportMetaEnv {
|
||||
interface ImportMeta {
|
||||
readonly env: ImportMetaEnv;
|
||||
}
|
||||
|
||||
|
||||
declare namespace App {
|
||||
interface Locals {
|
||||
/**
|
||||
* authentification key is the api key or the session token
|
||||
*/
|
||||
authKey?: string
|
||||
responseBuilder?: ResponseBuilder
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,14 @@
|
||||
import { promises as fs } from 'node:fs'
|
||||
|
||||
/**
|
||||
* File manipulation utility class
|
||||
*/
|
||||
export default class FilesUtils {
|
||||
/**
|
||||
* heck if a file/folder exists at the specified location
|
||||
* @param path the path to check
|
||||
* @returns if the file exists or not
|
||||
*/
|
||||
public static async exists(path: string): Promise<boolean> {
|
||||
try {
|
||||
await fs.stat(path)
|
||||
|
@ -1,3 +1,4 @@
|
||||
import ResponseBuilder from '../ResponseBuilder'
|
||||
|
||||
/**
|
||||
* Add headers:
|
||||
@ -57,11 +58,9 @@ export default interface RFC7807 {
|
||||
* @param error the error (base items are type, status, title details and instance)
|
||||
* @returns
|
||||
*/
|
||||
export function buildRFC7807(error: RFC7807 & Record<string, any>): Response {
|
||||
return new Response(JSON.stringify(error), {
|
||||
headers: {
|
||||
'Content-Type': 'application/problem+json'
|
||||
},
|
||||
status: error.status ?? 500
|
||||
})
|
||||
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()
|
||||
}
|
||||
|
70
src/libs/RateLimiter.ts
Normal file
70
src/libs/RateLimiter.ts
Normal file
@ -0,0 +1,70 @@
|
||||
import { objectLoop } from '@dzeio/object-util'
|
||||
import { buildRFC7807 } from './RFCs/RFC7807'
|
||||
import StatusCode from './StatusCode'
|
||||
|
||||
interface StorageItem {
|
||||
pointsRemaining: number
|
||||
timeReset: number
|
||||
}
|
||||
|
||||
export interface RateLimitHeaders {
|
||||
"Retry-After"?: string,
|
||||
"X-RateLimit-Limit": string,
|
||||
"X-RateLimit-Remaining": string,
|
||||
"X-RateLimit-Reset": string
|
||||
}
|
||||
|
||||
export default class RateLimiter {
|
||||
|
||||
/**
|
||||
* number of points that can be used per {timeSpan}
|
||||
*/
|
||||
public static points = 10
|
||||
|
||||
/**
|
||||
* timeSpan in seconds
|
||||
*/
|
||||
public static timeSpan = 600
|
||||
|
||||
private static instance: RateLimiter = new RateLimiter()
|
||||
public static getInstance(): RateLimiter {
|
||||
return this.instance
|
||||
}
|
||||
|
||||
private storage: Record<string, StorageItem> = {}
|
||||
|
||||
public consume(key: string, value: number = 1): Response | RateLimitHeaders {
|
||||
let item = this.storage[key]
|
||||
const now = (new Date().getTime() / 1000)
|
||||
if (!item) {
|
||||
item = {
|
||||
pointsRemaining: RateLimiter.points,
|
||||
timeReset: now + RateLimiter.timeSpan
|
||||
}
|
||||
}
|
||||
if (item.timeReset <= now) {
|
||||
item.timeReset = now + RateLimiter.timeSpan
|
||||
item.pointsRemaining = RateLimiter.points
|
||||
}
|
||||
item.pointsRemaining -= value
|
||||
this.storage[key] = item
|
||||
const headers: RateLimitHeaders = {
|
||||
"X-RateLimit-Limit": RateLimiter.points.toFixed(0),
|
||||
"X-RateLimit-Remaining": Math.max(item.pointsRemaining, 0).toFixed(0),
|
||||
"X-RateLimit-Reset": item.timeReset.toFixed(0)
|
||||
}
|
||||
if (item.pointsRemaining < 0) {
|
||||
const resp = buildRFC7807({
|
||||
type: '/docs/error/rate-limited',
|
||||
status: StatusCode.TOO_MANY_REQUESTS,
|
||||
title: 'You are being rate limited as you have done too many requests to the server'
|
||||
})
|
||||
resp.headers.append('Retry-After', (item.timeReset - now).toFixed(0))
|
||||
objectLoop(headers, (value, key) => {
|
||||
resp.headers.append(key, value)
|
||||
})
|
||||
return resp
|
||||
}
|
||||
return headers
|
||||
}
|
||||
}
|
61
src/libs/ResponseBuilder.ts
Normal file
61
src/libs/ResponseBuilder.ts
Normal file
@ -0,0 +1,61 @@
|
||||
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 | object | null | undefined) {
|
||||
if (typeof body === 'object') {
|
||||
this._body = JSON.stringify(body)
|
||||
this.addHeader('Content-Type', 'application/json')
|
||||
} else {
|
||||
this._body = body
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
private _headers: Headers = new Headers()
|
||||
public headers(headers: HeadersInit ) {
|
||||
if (headers instanceof Headers) {
|
||||
this._headers = headers
|
||||
} else {
|
||||
this._headers = new Headers(headers)
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
public addHeader(key: string, value: string) {
|
||||
this._headers.append(key, value)
|
||||
return this
|
||||
}
|
||||
|
||||
public addHeaders(headers: Record<string, string>) {
|
||||
objectLoop(headers, (value, key) => {
|
||||
this.addHeader(key, value)
|
||||
})
|
||||
return this
|
||||
}
|
||||
|
||||
public removeHeader(key: string) {
|
||||
this._headers.delete(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)
|
||||
}
|
||||
}
|
71
src/libs/StatusCode.ts
Normal file
71
src/libs/StatusCode.ts
Normal file
@ -0,0 +1,71 @@
|
||||
enum StatusCode {
|
||||
CONTINUE = 100,
|
||||
SWITCHING_PROTOCOLS,
|
||||
PROCESSING,
|
||||
EARLY_HINTS,
|
||||
|
||||
OK = 200,
|
||||
CREATED,
|
||||
ACCEPTED,
|
||||
NON_AUTHORITATIVE_INFORMATION,
|
||||
NO_CONTENT,
|
||||
RESET_CONTENT,
|
||||
PARTIAL_CONTENT,
|
||||
MULTI_STATUS,
|
||||
ALREADY_REPORTED,
|
||||
IM_USED = 226,
|
||||
|
||||
MULTIPLE_CHOICES = 300,
|
||||
MOVED_PERMANENTLY,
|
||||
FOUND,
|
||||
SEE_OTHER,
|
||||
NOT_MODIFIED,
|
||||
USE_PROXY,
|
||||
// UNUSED
|
||||
TEMPORARY_REDIRECT = 307,
|
||||
PERMANENT_REDIRECT,
|
||||
|
||||
BAD_REQUEST = 400,
|
||||
UNAUTHORIZED,
|
||||
PAYMENT_REQUIRED,
|
||||
FORBIDDEN,
|
||||
NOT_FOUND,
|
||||
METHOD_NOT_ALLOWED,
|
||||
NOT_ACCEPTABLE,
|
||||
PROXY_AUTHENTIFICATION_REQUIRED,
|
||||
REQUEST_TIMEOUT,
|
||||
CONFLICT,
|
||||
GONE,
|
||||
LENGTH_REQUIRED,
|
||||
PRECONDITION_FAILED,
|
||||
PAYLOAD_TOO_LARGE,
|
||||
URI_TOO_LONG,
|
||||
UNSUPPORTED_MEDIA_TYPE,
|
||||
RANGE_NOT_SATISFIABLE,
|
||||
EXPECTATION_FAILED,
|
||||
IM_A_TEAPOT,
|
||||
MIDIRECTED_REQUEST = 421,
|
||||
UNPROCESSABLE_CONTENT,
|
||||
LOCKED,
|
||||
FAILED_DEPENDENCY,
|
||||
TOO_EARLY,
|
||||
UPGRADE_REQUIRED,
|
||||
PRECONDITION_REQUIRED = 428,
|
||||
TOO_MANY_REQUESTS,
|
||||
REQUEST_HEADER_FIELDS_TOO_LARGE = 431,
|
||||
UNAVAILABLE_OR_LEGAL_REASONS = 451,
|
||||
|
||||
INTERNAL_SERVER_ERROR = 500,
|
||||
NOT_IMPLEMENTED,
|
||||
BAD_GATEWAY,
|
||||
SERVICE_UNAVAILABLE,
|
||||
GATEWAY_TIMEOUT,
|
||||
HTTP_VERSION_NOT_SUPPORTED,
|
||||
VARIANT_ALSO_NEGOTIATES,
|
||||
INSUFFICIENT_STORAGE,
|
||||
LOOP_DETECTED,
|
||||
NOT_EXTENDED = 510,
|
||||
NETWORK_AUTHENTIFICATION_REQUIRED,
|
||||
}
|
||||
|
||||
export default StatusCode
|
@ -1,3 +1,8 @@
|
||||
/**
|
||||
* try to parse a GCode config string into a number
|
||||
* @param str the string to try parsing
|
||||
* @returns a number if parsing happened correctly or undefined
|
||||
*/
|
||||
function parseNumber(str: string): number | undefined {
|
||||
if (!/^(\d|\.)+$/g.test(str)) {
|
||||
return undefined
|
||||
@ -13,6 +18,11 @@ function parseNumber(str: string): number | undefined {
|
||||
return int
|
||||
}
|
||||
|
||||
/**
|
||||
* decode a print time to a number of seconds
|
||||
* @param text the text to decode
|
||||
* @returns the number of seconds in the text
|
||||
*/
|
||||
function decodeTime(text: string): number {
|
||||
let timeInSec = 0
|
||||
for (const it of text.split(' ')) {
|
||||
@ -39,23 +49,42 @@ function decodeTime(text: string): number {
|
||||
}
|
||||
|
||||
export function getParams(data: string) {
|
||||
// get the configuration lines
|
||||
const lines = data.split('\n').filter((it) => it.startsWith(';') && it.includes('='))
|
||||
// create the config object
|
||||
const obj: Record<string, string | number> = {}
|
||||
// loop through eacj config
|
||||
for (const line of lines) {
|
||||
// get its key and value
|
||||
const [key, value] = line.split('=', 2).map((it) => it.slice(1).trim())
|
||||
let realKey = key.replace(/ /g, '_').replace(/\[|\]|\(|\)/g, '')
|
||||
// sip if it has no key or value
|
||||
if (!key || !value) {
|
||||
continue
|
||||
}
|
||||
// process the key
|
||||
let realKey = key
|
||||
// replace spaces by _
|
||||
.replace(/ /g, '_')
|
||||
// remove unparseable characters
|
||||
.replace(/\[|\]|\(|\)/g, '')
|
||||
// process the value
|
||||
const realValue = parseNumber(value) ?? value
|
||||
// add an offset if the key is already cited
|
||||
let offset = 0
|
||||
while (obj[`${realKey}${offset > 0 ? `_${offset}` : ''}`] && obj[`${realKey}${offset > 0 ? `_${offset}` : ''}`] !== realValue) {
|
||||
offset++
|
||||
}
|
||||
// chnge the key to add the offset
|
||||
if (offset > 0) {
|
||||
realKey = `${realKey}_${offset}`
|
||||
}
|
||||
// detect key collisions
|
||||
if (obj[realKey] && obj[realKey] !== realValue) {
|
||||
throw new Error(`Key collision ${key}=${realValue} ${realKey}=${obj[realKey]}`)
|
||||
}
|
||||
obj[realKey] = parseNumber(value) ?? value
|
||||
// set the value to the key
|
||||
obj[realKey] = realValue
|
||||
// transform the time to a number of seconds
|
||||
if (realKey === 'estimated_printing_time_normal_mode') {
|
||||
obj['estimated_printing_time_seconds'] = decodeTime(value)
|
||||
}
|
||||
|
@ -25,13 +25,13 @@ interface Permission {
|
||||
*
|
||||
* TODO: implement rate limiting
|
||||
* http/2.0 429 TOO MANY REQUESTS
|
||||
* Content-Type: application/json
|
||||
* X-RateLimit-Limit: 1000
|
||||
* X-RateLimit-Remaining: 0
|
||||
* X-RateLimit-Reset: 123456789
|
||||
* X-RateLimit-Reset-After: 9 // Number of seconds before the remaining Rate go back to 0
|
||||
* Content-Type: application/json+problem
|
||||
* X-RateLimit-Limit: 1000 // number of request you cn make until hitting the rate limit
|
||||
* X-RateLimit-Remaining: 0 // number of request remaining until the rate limit is atteined
|
||||
* X-RateLimit-Reset: 123456789 // EPOCH time when the rate limit reset
|
||||
* X-RateLimit-Reset-After: 9 // Number of seconds before the remaining Rate reset
|
||||
*/
|
||||
export async function validateAuth(request: Request, permission: Permission): Promise<Response | null> {
|
||||
export async function validateAuth(request: Request, permission: Permission): Promise<Response | string> {
|
||||
const apiKeyHeader = request.headers.get('Authorization')
|
||||
const cookieHeader = request.headers.get('Cookie')
|
||||
if (apiKeyHeader) {
|
||||
@ -58,10 +58,10 @@ export async function validateAuth(request: Request, permission: Permission): Pr
|
||||
// return it.endsWith(permission.name)
|
||||
})
|
||||
if (match && (permission.api || match.startsWith('admin.'))) {
|
||||
return null
|
||||
return apiKey
|
||||
} else if (permission.api) {
|
||||
return buildRFC7807({
|
||||
type: '/unauthorized-access',
|
||||
type: '/docs/errors/unauthorized-access',
|
||||
status: 401,
|
||||
title: 'Unauthorized access',
|
||||
details: `you are missing the permission "${permission.name}" or is not an admin`
|
||||
@ -69,7 +69,7 @@ export async function validateAuth(request: Request, permission: Permission): Pr
|
||||
}
|
||||
} else if (permission.api && !permission.cookie) {
|
||||
return buildRFC7807({
|
||||
type: '/unauthorized-access',
|
||||
type: '/docs/errors/unauthorized-access',
|
||||
status: 401,
|
||||
title: 'Unauthorized access',
|
||||
details: `You MUST define an API key fo use this endpoint`
|
||||
@ -82,7 +82,7 @@ export async function validateAuth(request: Request, permission: Permission): Pr
|
||||
const userCookie = cookies.get('userId')
|
||||
if (!userCookie) {
|
||||
return buildRFC7807({
|
||||
type: '/unauthorized-access',
|
||||
type: '/docs/errors/unauthorized-access',
|
||||
status: 401,
|
||||
title: 'Unauthorized access',
|
||||
details: `you must be connected to use this endpoint (missing the userId cookie)`
|
||||
@ -91,33 +91,33 @@ export async function validateAuth(request: Request, permission: Permission): Pr
|
||||
const dao = await DaoFactory.get('user').get(userCookie)
|
||||
if (!dao) {
|
||||
return buildRFC7807({
|
||||
type: '/unauthorized-access',
|
||||
type: '/docs/errors/unauthorized-access',
|
||||
status: 401,
|
||||
title: 'Unauthorized access',
|
||||
details: `the user does not exists`
|
||||
})
|
||||
}
|
||||
return null
|
||||
return userCookie
|
||||
} else if (!permission.api && permission.cookie) {
|
||||
return buildRFC7807({
|
||||
type: '/unauthorized-access',
|
||||
type: '/docs/errors/unauthorized-access',
|
||||
status: 401,
|
||||
title: 'Unauthorized access',
|
||||
details: `You MUST be connected to your account to use this endpoint`
|
||||
})
|
||||
} else if (permission.api && permission.cookie) {
|
||||
return buildRFC7807({
|
||||
type: '/unauthorized-access',
|
||||
type: '/docs/errors/unauthorized-access',
|
||||
status: 401,
|
||||
title: 'Unauthorized access',
|
||||
details: `You must be connected or use an API key to access this endpoint`
|
||||
})
|
||||
}
|
||||
return buildRFC7807({
|
||||
type: '/unauthorized-access',
|
||||
status: 401,
|
||||
title: 'Unauthorized access',
|
||||
details: `the following endpoint is not currently accessible by any means`
|
||||
type: '/docs/errors/page-not-found',
|
||||
status: 404,
|
||||
title: 'Page not found',
|
||||
details: `the following endpoint does not exists`
|
||||
})
|
||||
|
||||
}
|
||||
|
20
src/middleware/apiAuth.ts
Normal file
20
src/middleware/apiAuth.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import { defineMiddleware } from "astro/middleware"
|
||||
import { validateAuth } from '../libs/validateAuth'
|
||||
|
||||
// `context` and `next` are automatically typed
|
||||
export default defineMiddleware(async (context, next) => {
|
||||
if (!context.request.url.includes('api')) {
|
||||
return next()
|
||||
}
|
||||
const auth = await validateAuth(context.request, {
|
||||
name: 'slicing.slice',
|
||||
api: true,
|
||||
cookie: true
|
||||
})
|
||||
if (typeof auth === 'object') {
|
||||
return auth
|
||||
}
|
||||
context.locals.authKey = auth
|
||||
|
||||
return next()
|
||||
})
|
18
src/middleware/apiRateLimit.ts
Normal file
18
src/middleware/apiRateLimit.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import { defineMiddleware } from "astro/middleware"
|
||||
import RateLimiter from '../libs/RateLimiter'
|
||||
|
||||
// `context` and `next` are automatically typed
|
||||
export default defineMiddleware(async ({ request, locals }, next) => {
|
||||
if (!request.url.includes('api')) {
|
||||
return next()
|
||||
}
|
||||
|
||||
const limit = RateLimiter.getInstance().consume(locals.authKey as string)
|
||||
|
||||
if ('status' in limit) {
|
||||
return limit
|
||||
}
|
||||
locals.responseBuilder.addHeaders(limit)
|
||||
|
||||
return next()
|
||||
})
|
7
src/middleware/index.ts
Normal file
7
src/middleware/index.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import { sequence } from "astro/middleware"
|
||||
|
||||
import apiAuth from './apiAuth'
|
||||
import apiRateLimit from './apiRateLimit'
|
||||
import responseBuilder from './responseBuilder'
|
||||
|
||||
export const onRequest = sequence(responseBuilder, apiAuth, apiRateLimit)
|
9
src/middleware/responseBuilder.ts
Normal file
9
src/middleware/responseBuilder.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import { defineMiddleware } from "astro/middleware"
|
||||
import ResponseBuilder from '../libs/ResponseBuilder'
|
||||
|
||||
// `context` and `next` are automatically typed
|
||||
export default defineMiddleware(async (context, next) => {
|
||||
context.locals.responseBuilder = new ResponseBuilder()
|
||||
|
||||
return next()
|
||||
})
|
@ -3,17 +3,23 @@ import { objectMap, objectOmit } from '@dzeio/object-util'
|
||||
import URLManager from '@dzeio/url-manager'
|
||||
import type { APIRoute } from 'astro'
|
||||
import { evaluate } from 'mathjs'
|
||||
import { exec as execSync, spawn } from 'node:child_process'
|
||||
import { spawn } from 'node:child_process'
|
||||
import fs from 'node:fs/promises'
|
||||
import os from 'node:os'
|
||||
import path from 'node:path'
|
||||
import { promisify } from 'node:util'
|
||||
import { buildRFC7807 } from '../../../libs/RFCs/RFC7807'
|
||||
import { getParams } from '../../../libs/gcodeUtils'
|
||||
import { validateAuth } from '../../../libs/validateAuth'
|
||||
import DaoFactory from '../../../models/DaoFactory'
|
||||
import { buildRFC7807 } from '../../../../libs/RFCs/RFC7807'
|
||||
import RateLimiter from '../../../../libs/RateLimiter'
|
||||
import ResponseBuilder from '../../../../libs/ResponseBuilder'
|
||||
import StatusCode from '../../../../libs/StatusCode'
|
||||
import { getParams } from '../../../../libs/gcodeUtils'
|
||||
import { validateAuth } from '../../../../libs/validateAuth'
|
||||
import DaoFactory from '../../../../models/DaoFactory'
|
||||
|
||||
interface SliceError {
|
||||
code: number
|
||||
output: Array<string>
|
||||
}
|
||||
|
||||
const exec = promisify(execSync)
|
||||
|
||||
let tmpDir: string
|
||||
|
||||
@ -23,15 +29,7 @@ let tmpDir: string
|
||||
* price: algorithm from settings
|
||||
* adionnal settings from https://manual.slic3r.org/advanced/command-line
|
||||
*/
|
||||
export const post: APIRoute = async ({ params, request }) => {
|
||||
const res = await validateAuth(request, {
|
||||
name: 'slicing.slice',
|
||||
api: true,
|
||||
cookie: true
|
||||
})
|
||||
if (res) {
|
||||
return res
|
||||
}
|
||||
export const post: APIRoute = async ({ params, request, locals }) => {
|
||||
if (!tmpDir) {
|
||||
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'saas-'))
|
||||
}
|
||||
@ -41,13 +39,24 @@ export const post: APIRoute = async ({ params, request }) => {
|
||||
|
||||
if (!config) {
|
||||
return buildRFC7807({
|
||||
type: '/missing-config',
|
||||
status: 404,
|
||||
type: '/docs/errors/missing-config',
|
||||
status: StatusCode.NOT_FOUND,
|
||||
title: 'The configuration does not exists',
|
||||
details: `The configuration ${configId} does not exists`
|
||||
})
|
||||
}
|
||||
|
||||
const input = new Uint8Array(Buffer.from(await request.arrayBuffer()))
|
||||
|
||||
if (input.length <= 0) {
|
||||
return buildRFC7807({
|
||||
type: '/docs/errors/missing-input-file',
|
||||
status: StatusCode.BAD_REQUEST,
|
||||
title: 'You are missing the STL file',
|
||||
details: `To process a file you need to input the file binary datas as the only body in your request`
|
||||
})
|
||||
}
|
||||
|
||||
const query = new URLManager(request.url).query()
|
||||
|
||||
const processId = (Math.random() * 1000000).toFixed(0)
|
||||
@ -72,7 +81,7 @@ export const post: APIRoute = async ({ params, request }) => {
|
||||
|
||||
logger.log('writing STL to filesystem')
|
||||
// write input
|
||||
await fs.writeFile(stlPath, new Uint8Array(Buffer.from(await request.arrayBuffer())), {
|
||||
await fs.writeFile(stlPath, input, {
|
||||
encoding: null
|
||||
})
|
||||
|
||||
@ -85,87 +94,112 @@ export const post: APIRoute = async ({ params, request }) => {
|
||||
|
||||
if (config.type === 'prusa' || true) {
|
||||
slicerPath = import.meta.env.PRUSASLICER_PATH ?? 'prusa-slicer'
|
||||
additionnalParams += ' --export-gcode'
|
||||
additionnalParams += ' --export-gcode --loglevel 4'
|
||||
slicerCommand = `${path.normalize(stlPath)} --load ${path.normalize(`${processFolder}/config.ini`)} --output ${path.normalize(gcodePath)} ${additionnalParams}`
|
||||
}
|
||||
|
||||
|
||||
// TODO: check if it does work on a linux environment
|
||||
// TODO: Externalise IO for the different slicers
|
||||
try {
|
||||
logger.log('Running', slicerPath, slicerCommand)
|
||||
await new Promise<void>((res, rej) => {
|
||||
const slicer = spawn(slicerPath, slicerCommand.split(' '))
|
||||
slicer.stdout.on('data', (data) => {
|
||||
logger.log('[stdout]',data.toString('utf8'))
|
||||
const logs: Array<string> = []
|
||||
const slicer = spawn(`"${slicerPath}"`, slicerCommand.split(' '), {
|
||||
shell: true
|
||||
})
|
||||
slicer.stderr.on('data', (data: Buffer) => {
|
||||
logger.log('[stderr]', data.toString('utf8'))
|
||||
const log = (data: Buffer) => {
|
||||
const line = `${data.toString('utf-8')}`
|
||||
logger.log(line)
|
||||
logs.push(line)
|
||||
}
|
||||
slicer.stdout.on('data', log)
|
||||
slicer.stderr.on('data', log)
|
||||
|
||||
slicer.on('spawn', () => {
|
||||
logs.push('Process spawned')
|
||||
logger.log('Process spawned')
|
||||
})
|
||||
|
||||
slicer.on('error', (err) => {
|
||||
logs.push('Process error')
|
||||
logger.log('Process error')
|
||||
logger.log('error', err)
|
||||
logs.push(err.toString())
|
||||
rej(err)
|
||||
})
|
||||
slicer.on('close', (code, signal) => {
|
||||
logger.log('code', code)
|
||||
logger.log('signal', signal)
|
||||
if (typeof code === 'number' && code > 0) {
|
||||
|
||||
rej(code)
|
||||
slicer.on('close', (code, signal) => {
|
||||
logs.push('Process closed')
|
||||
logger.log('Process closed')
|
||||
logs.push(`with code ${code}`)
|
||||
logger.log(`with code ${code}`)
|
||||
logs.push(`and signal ${signal}`)
|
||||
logger.log(`and signal ${signal}`)
|
||||
if (typeof code === 'number' && code !== 0) {
|
||||
rej({
|
||||
code: code,
|
||||
output: logs
|
||||
} as SliceError)
|
||||
return
|
||||
}
|
||||
res()
|
||||
})
|
||||
})
|
||||
} catch (e: any) {
|
||||
const err = e as SliceError
|
||||
logger.log('request finished in error :(', processId)
|
||||
const line = e.toString()
|
||||
logger.error(e)
|
||||
if (line.includes('Objects could not fit on the bed')) {
|
||||
const line = err.toString()
|
||||
logger.error('error', err, typeof err)
|
||||
if (err.code === 3221226505 || line.includes('Objects could not fit on the bed')) {
|
||||
await fs.rm(stlPath)
|
||||
return buildRFC7807({
|
||||
type: '/object-too-large',
|
||||
status: 413,
|
||||
title: 'Object is too large'
|
||||
})
|
||||
type: '/docs/errors/object-too-large',
|
||||
status: StatusCode.PAYLOAD_TOO_LARGE,
|
||||
title: 'Object is too large',
|
||||
details: 'The STL you are trying to compile is too large for the configuration you chose'
|
||||
}, locals.responseBuilder)
|
||||
} else if (line.includes('No such file')) {
|
||||
await fs.rm(stlPath)
|
||||
return buildRFC7807({
|
||||
type: '/missing-config-file',
|
||||
status: 404,
|
||||
type: '/docs/errors/missing-config-file',
|
||||
status: StatusCode.NOT_FOUND,
|
||||
title: 'Configuration file is missing',
|
||||
details: `the configuration file "${configId}" is not available on the remote server`
|
||||
})
|
||||
}, locals.responseBuilder)
|
||||
} else if (line.includes('Unknown option')) {
|
||||
await fs.rm(stlPath)
|
||||
return buildRFC7807({
|
||||
type: '/slicer-option-unknown',
|
||||
type: '/docs/errors/slicer-option-unknown',
|
||||
status: 400,
|
||||
title: ' config override doew not exists',
|
||||
details: 'an override does not exists, please contact an administrator or refer to the documentation'
|
||||
})
|
||||
}, locals.responseBuilder)
|
||||
} else if (
|
||||
line.includes('is not recognized as an internal or external command') ||
|
||||
line.includes('.dll was not loaded')
|
||||
) {
|
||||
await fs.rm(stlPath)
|
||||
return buildRFC7807({
|
||||
type: '/slicer-not-found',
|
||||
status: 408,
|
||||
type: '/docs/errors/slicer-not-found',
|
||||
status: StatusCode.SERVICE_UNAVAILABLE,
|
||||
title: 'the slicer used to process this file has not been found',
|
||||
details: 'the server has a misconfiguration meaning that we can\'t process the request, please contact an administrator',
|
||||
additionnalInfo: line.includes('dll') ? 'Missing DLL' : 'Slicer not found '
|
||||
})
|
||||
}, locals.responseBuilder)
|
||||
} else if (line.includes('ETIMEDOUT')) {
|
||||
await fs.rm(stlPath)
|
||||
return buildRFC7807({
|
||||
type: '/timed-out-slicing',
|
||||
status: 408,
|
||||
type: '/docs/errors/timed-out-slicing',
|
||||
status: StatusCode.PAYLOAD_TOO_LARGE,
|
||||
title: 'Timed out slicing file',
|
||||
detail: `The file you are trying to process takes too long to be processed`,
|
||||
processingTimeoutMillis: 60000
|
||||
})
|
||||
}, locals.responseBuilder)
|
||||
}
|
||||
return buildRFC7807({
|
||||
type: '/general-input-output-error',
|
||||
status: 500,
|
||||
type: '/docs/errors/general-input-output-error',
|
||||
status: StatusCode.INTERNAL_SERVER_ERROR,
|
||||
title: 'General I/O error',
|
||||
details: 'A server error make it impossible to slice the file, please contact an administrator with the json error',
|
||||
fileId: processId,
|
||||
@ -173,8 +207,8 @@ export const post: APIRoute = async ({ params, request }) => {
|
||||
// fileSize: req.body.length,
|
||||
overrides: overrides,
|
||||
serverMessage:
|
||||
e.toString().replace(new RegExp(stlPath), `***FILE***`).replace(new RegExp(processFolder), '')
|
||||
})
|
||||
err.output.map((line) => line.replace(new RegExp(stlPath), `***FILE***`).replace(new RegExp(processFolder), ''))
|
||||
}, locals.responseBuilder)
|
||||
}
|
||||
const gcode = await fs.readFile(gcodePath, 'utf-8')
|
||||
await fs.rm(processFolder, { recursive: true, force: true })
|
||||
@ -198,35 +232,35 @@ export const post: APIRoute = async ({ params, request }) => {
|
||||
price = tmp.toFixed(2)
|
||||
} else {
|
||||
return buildRFC7807({
|
||||
type: '/algorithm-error',
|
||||
type: '/docs/errors/algorithm-error',
|
||||
status: 500,
|
||||
title: 'Algorithm compilation error',
|
||||
details: 'It seems the algorithm resolution failed',
|
||||
algorithm: algo,
|
||||
algorithmError: 'Algorithm return a Unit',
|
||||
parameters: gcodeParams
|
||||
})
|
||||
}, locals.responseBuilder)
|
||||
}
|
||||
} catch (e) {
|
||||
logger.dir(e)
|
||||
return buildRFC7807({
|
||||
type: '/algorithm-error',
|
||||
type: '/docs/errors/algorithm-error',
|
||||
status: 500,
|
||||
title: 'Algorithm compilation error',
|
||||
details: 'It seems the algorithm resolution failed',
|
||||
algorithm: algo,
|
||||
algorithmError: e,
|
||||
parameters: gcodeParams
|
||||
})
|
||||
}, locals.responseBuilder)
|
||||
}
|
||||
}
|
||||
logger.log('request successfull :)')
|
||||
return {
|
||||
status: 200,
|
||||
body: JSON.stringify({
|
||||
return locals.responseBuilder
|
||||
.body({
|
||||
price: price ? parseFloat(price) : undefined,
|
||||
...getParams(gcode),
|
||||
gcode
|
||||
})
|
||||
}
|
||||
.status(200)
|
||||
.build()
|
||||
}
|
24
src/pages/docs/[...page].astro
Normal file
24
src/pages/docs/[...page].astro
Normal file
@ -0,0 +1,24 @@
|
||||
---
|
||||
import Layout from '../../layouts/Layout.astro'
|
||||
import { getEntry } from 'astro:content'
|
||||
import StatusCode from '../../libs/StatusCode'
|
||||
|
||||
const page = Astro.params.page
|
||||
|
||||
let Result: any
|
||||
|
||||
const entry = await getEntry('docs', page as any)
|
||||
if (!entry) {
|
||||
Astro.response.status = StatusCode.NOT_FOUND
|
||||
} else {
|
||||
const { Content } = await entry.render()
|
||||
Result = Content
|
||||
}
|
||||
---
|
||||
|
||||
<Layout title={entry?.data.title ?? ''}>
|
||||
<main class="prose">
|
||||
{Result && <Result />}
|
||||
|
||||
</main>
|
||||
</Layout>
|
@ -4,5 +4,7 @@ module.exports = {
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
plugins: [
|
||||
require('@tailwindcss/typography'),
|
||||
],
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user