diff --git a/package-lock.json b/package-lock.json
index afe9015..705927e 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -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",
diff --git a/package.json b/package.json
index b260831..41a00f8 100644
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/src/content/config.ts b/src/content/config.ts
new file mode 100644
index 0000000..f385343
--- /dev/null
+++ b/src/content/config.ts
@@ -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,
+};
diff --git a/src/content/docs/errors/unauthorized-access.md b/src/content/docs/errors/unauthorized-access.md
new file mode 100644
index 0000000..9151889
--- /dev/null
+++ b/src/content/docs/errors/unauthorized-access.md
@@ -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
diff --git a/src/env.d.ts b/src/env.d.ts
index 092db3c..486617c 100644
--- a/src/env.d.ts
+++ b/src/env.d.ts
@@ -1,7 +1,8 @@
+///
///
+///
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
+ }
+}
diff --git a/src/libs/FilesUtils.ts b/src/libs/FilesUtils.ts
index 7312e57..6660a33 100644
--- a/src/libs/FilesUtils.ts
+++ b/src/libs/FilesUtils.ts
@@ -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 {
try {
await fs.stat(path)
@@ -9,4 +17,4 @@ export default class FilesUtils {
return false
}
}
-}
\ No newline at end of file
+}
diff --git a/src/libs/RFCs/RFC7807.ts b/src/libs/RFCs/RFC7807.ts
index 0fd0887..c7cfb10 100644
--- a/src/libs/RFCs/RFC7807.ts
+++ b/src/libs/RFCs/RFC7807.ts
@@ -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): Response {
- return new Response(JSON.stringify(error), {
- headers: {
- 'Content-Type': 'application/problem+json'
- },
- status: error.status ?? 500
- })
+export function buildRFC7807(error: RFC7807 & Record, response: ResponseBuilder = new ResponseBuilder()): Response {
+ response.addHeader('Content-Type', 'application/problem+json')
+ .body(JSON.stringify(error))
+ .status(error.status ?? 500)
+ return response.build()
}
diff --git a/src/libs/RateLimiter.ts b/src/libs/RateLimiter.ts
new file mode 100644
index 0000000..3053d2f
--- /dev/null
+++ b/src/libs/RateLimiter.ts
@@ -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 = {}
+
+ 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
+ }
+}
diff --git a/src/libs/ResponseBuilder.ts b/src/libs/ResponseBuilder.ts
new file mode 100644
index 0000000..25dfe5e
--- /dev/null
+++ b/src/libs/ResponseBuilder.ts
@@ -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) {
+ 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)
+ }
+}
diff --git a/src/libs/StatusCode.ts b/src/libs/StatusCode.ts
new file mode 100644
index 0000000..4bb9c46
--- /dev/null
+++ b/src/libs/StatusCode.ts
@@ -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
diff --git a/src/libs/gcodeUtils.ts b/src/libs/gcodeUtils.ts
index 65a49a2..2e476dc 100644
--- a/src/libs/gcodeUtils.ts
+++ b/src/libs/gcodeUtils.ts
@@ -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 = {}
+ // 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)
}
diff --git a/src/libs/validateAuth.ts b/src/libs/validateAuth.ts
index 160077e..6b854a2 100644
--- a/src/libs/validateAuth.ts
+++ b/src/libs/validateAuth.ts
@@ -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 {
+export async function validateAuth(request: Request, permission: Permission): Promise {
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`
})
}
diff --git a/src/middleware/apiAuth.ts b/src/middleware/apiAuth.ts
new file mode 100644
index 0000000..0d60795
--- /dev/null
+++ b/src/middleware/apiAuth.ts
@@ -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()
+})
diff --git a/src/middleware/apiRateLimit.ts b/src/middleware/apiRateLimit.ts
new file mode 100644
index 0000000..9620e0e
--- /dev/null
+++ b/src/middleware/apiRateLimit.ts
@@ -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()
+})
diff --git a/src/middleware/index.ts b/src/middleware/index.ts
new file mode 100644
index 0000000..bb0b03b
--- /dev/null
+++ b/src/middleware/index.ts
@@ -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)
diff --git a/src/middleware/responseBuilder.ts b/src/middleware/responseBuilder.ts
new file mode 100644
index 0000000..10bc260
--- /dev/null
+++ b/src/middleware/responseBuilder.ts
@@ -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()
+})
diff --git a/src/pages/api/process/[configId].ts b/src/pages/api/v1/process/[configId].ts
similarity index 58%
rename from src/pages/api/process/[configId].ts
rename to src/pages/api/v1/process/[configId].ts
index edce7ac..30c2df3 100644
--- a/src/pages/api/process/[configId].ts
+++ b/src/pages/api/v1/process/[configId].ts
@@ -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
+}
-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((res, rej) => {
- const slicer = spawn(slicerPath, slicerCommand.split(' '))
- slicer.stdout.on('data', (data) => {
- logger.log('[stdout]',data.toString('utf8'))
+ const logs: Array = []
+ 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()
}
diff --git a/src/pages/docs/[...page].astro b/src/pages/docs/[...page].astro
new file mode 100644
index 0000000..aed8784
--- /dev/null
+++ b/src/pages/docs/[...page].astro
@@ -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
+}
+---
+
+
+
+ {Result && }
+
+
+
diff --git a/tailwind.config.cjs b/tailwind.config.cjs
index 82d39ff..3cc3d69 100644
--- a/tailwind.config.cjs
+++ b/tailwind.config.cjs
@@ -4,5 +4,7 @@ module.exports = {
theme: {
extend: {},
},
- plugins: [],
+ plugins: [
+ require('@tailwindcss/typography'),
+ ],
}