diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..7fc1766 --- /dev/null +++ b/.dockerignore @@ -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/ diff --git a/Dockerfile b/Dockerfile index 00242bc..c3724de 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,7 +3,7 @@ ######### # Build # ######### -FROM node:alpine as BUILD_IMAGE +FROM node:20-alpine as BUILD_IMAGE # External deps (for node-gyp add: "python3 make g++") # git is used to install the npm packages with git deps @@ -32,12 +32,11 @@ RUN npm prune --omit=dev ############## # Production # ############## -FROM node:latest as PROD_IMAGE +FROM node:20-slim as PROD_IMAGE # inform software to be in production ENV NODE_ENV=production ENV HOST=0.0.0.0 -ENV CONFIGS_PATH=./configs # Download & Install Slic3r # RUN apt-get update \ @@ -47,13 +46,13 @@ ENV CONFIGS_PATH=./configs # ENV SLICER_PATH slic3r # Download & install PrusaSlicer -ADD https://github.com/prusa3d/PrusaSlicer/releases/download/version_2.6.0/PrusaSlicer-2.6.0+linux-x64-GTK3-202306191220.tar.bz2 ./ -RUN tar -xvf PrusaSlicer-2.6.0+linux-x64-GTK3-202306191220.tar.bz2 -C /opt RUN apt-get update \ && apt-get install -y --no-install-recommends \ - prusa-slicer \ + prusa-slicer bzip2 \ && apt-get remove prusa-slicer -y \ && rm -rf /var/lib/apt/lists/* +ADD https://github.com/prusa3d/PrusaSlicer/releases/download/version_2.6.0/PrusaSlicer-2.6.0+linux-x64-GTK3-202306191220.tar.bz2 ./ +RUN tar -xvf PrusaSlicer-2.6.0+linux-x64-GTK3-202306191220.tar.bz2 -C /opt ENV PATH /opt/PrusaSlicer-2.6.0+linux-x64-GTK3-202306191220/bin:$PATH ENV SLICER_PATH prusa-slicer @@ -71,7 +70,6 @@ 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/configs ./configs 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* ./ diff --git a/package-lock.json b/package-lock.json index 705927e..8e9c85b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,7 @@ "@astrojs/tailwind": "^3.1.3", "@dzeio/logger": "^3.0.0", "@dzeio/object-util": "^1.5.0", - "@dzeio/url-manager": "^1.0.9", + "@dzeio/url-manager": "^1.0.10", "astro": "^2.6.4", "bcryptjs": "^2.4.3", "jsonwebtoken": "^9.0.0", @@ -604,11 +604,11 @@ "integrity": "sha512-NlKJulgd95Gvca3Wx9BQi0rABzwtvuCe9JDPBe9lOVmDDs2ufEWiTsLq59rtE1BEuuGzIm9wj/+8Imgk1Svfrw==" }, "node_modules/@dzeio/url-manager": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/@dzeio/url-manager/-/url-manager-1.0.9.tgz", - "integrity": "sha512-aBERzF4xznhQhHlzv35fp2cl99N+VjM37CL+R5DJiyIJmgmiz56zru1SKq8Xnv2rulF+6QrrnlT6ZulQDdp9EA==", + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/@dzeio/url-manager/-/url-manager-1.0.10.tgz", + "integrity": "sha512-gy8/0yp60crudcqIwsyFsfbHOnG32TAYg14m+at9wzw/bau5URsarlLkO5A3tigk91m05tZQfHLa4xwrXBSY/w==", "dependencies": { - "@dzeio/object-util": "^1.4.0" + "@dzeio/object-util": "^1.5.0" } }, "node_modules/@emmetio/abbreviation": { diff --git a/package.json b/package.json index 41a00f8..911ca97 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "@astrojs/tailwind": "^3.1.3", "@dzeio/logger": "^3.0.0", "@dzeio/object-util": "^1.5.0", - "@dzeio/url-manager": "^1.0.9", + "@dzeio/url-manager": "^1.0.10", "astro": "^2.6.4", "bcryptjs": "^2.4.3", "jsonwebtoken": "^9.0.0", diff --git a/src/env.d.ts b/src/env.d.ts index 486617c..f03fc50 100644 --- a/src/env.d.ts +++ b/src/env.d.ts @@ -1,6 +1,5 @@ /// /// -/// interface ImportMetaEnv { PRUSASLICER_PATH?: string @@ -21,6 +20,14 @@ declare namespace App { * authentification key is the api key or the session token */ authKey?: string - responseBuilder?: ResponseBuilder + responseBuilder: { + body(body: string | Buffer | object | null | undefined): this + headers(headers: HeadersInit ): this + addHeader(key: string, value: string): this + addHeaders(headers: Record): this + removeHeader(key: string): this + status(status: number): this + build(): Response + } // ./libs/ResponseBuilder object } } diff --git a/src/libs/RateLimiter.ts b/src/libs/RateLimiter.ts index 3053d2f..3522e11 100644 --- a/src/libs/RateLimiter.ts +++ b/src/libs/RateLimiter.ts @@ -24,7 +24,7 @@ export default class RateLimiter { /** * timeSpan in seconds */ - public static timeSpan = 600 + public static timeSpan = 60 private static instance: RateLimiter = new RateLimiter() public static getInstance(): RateLimiter { diff --git a/src/libs/ResponseBuilder.ts b/src/libs/ResponseBuilder.ts index 25dfe5e..af4a701 100644 --- a/src/libs/ResponseBuilder.ts +++ b/src/libs/ResponseBuilder.ts @@ -6,10 +6,12 @@ import { objectLoop } from '@dzeio/object-util' export default class ResponseBuilder { private _body: BodyInit | null | undefined - public body(body: string | object | null | undefined) { - if (typeof body === 'object') { + 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 } diff --git a/src/libs/validateAuth.ts b/src/libs/validateAuth.ts index 6b854a2..12e7795 100644 --- a/src/libs/validateAuth.ts +++ b/src/libs/validateAuth.ts @@ -2,7 +2,7 @@ import DaoFactory from '../models/DaoFactory' import CookieManager from './CookieManager' import { buildRFC7807 } from './RFCs/RFC7807' -interface Permission { +export interface Permission { name: string /** * if set it will be usable by users diff --git a/src/middleware/apiAuth.ts b/src/middleware/apiAuth.ts index 0d60795..5581c1c 100644 --- a/src/middleware/apiAuth.ts +++ b/src/middleware/apiAuth.ts @@ -1,16 +1,43 @@ +import { objectLoop } from '@dzeio/object-util' +import URLManager from '@dzeio/url-manager' import { defineMiddleware } from "astro/middleware" -import { validateAuth } from '../libs/validateAuth' +import { buildRFC7807 } from '../libs/RFCs/RFC7807' +import { Permission, validateAuth } from '../libs/validateAuth' + +const endpointsPermissions: Record = { + '/api/v1/users/[userId]/configs/[configId]/files/[fileName]': { + api: true, + cookie: true, + name: 'configs.get' + } +} + +function objectFind(obj: object, fn: (value: any, key: any) => boolean): {key: string, value: any} | null { + let res: {key: string, value: any} | null = null + objectLoop(obj, (value, key) => { + const tmp = fn(value, key) + if (tmp) { + res = { + key, value + } + } + return tmp + }) + return res +} // `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 - }) + const permission = objectFind(endpointsPermissions, (_, key) => new URLManager(key).toString(context.params as any) === context.url.pathname) + if (!permission) { + return buildRFC7807({ + type: 'idk' + }) + } + const auth = await validateAuth(context.request, permission.value) if (typeof auth === 'object') { return auth } diff --git a/src/middleware/responseBuilder.ts b/src/middleware/responseBuilder.ts index 10bc260..51b97c4 100644 --- a/src/middleware/responseBuilder.ts +++ b/src/middleware/responseBuilder.ts @@ -1,9 +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 (context, next) => { - context.locals.responseBuilder = new ResponseBuilder() +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}`) - return next() + 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 + }) + } }) diff --git a/src/pages/api/users/[userId]/keys/index.ts b/src/pages/api/users/[userId]/keys/index.ts deleted file mode 100644 index ff6344c..0000000 --- a/src/pages/api/users/[userId]/keys/index.ts +++ /dev/null @@ -1,25 +0,0 @@ -import type { APIRoute } from 'astro' -import crypto from 'node:crypto' -import { validateAuth } from '../../../../../libs/validateAuth' -import DaoFactory from '../../../../../models/DaoFactory' - -export const post: APIRoute = async ({ params, request }) => { - validateAuth(request, { - name: 'keys.create', - cookie: true, - api: false - }) - const userId = params.userId as string - - const dao = await DaoFactory.get('apiKey').create({ - user: userId, - key: crypto.randomUUID(), - permissions: [ - 'admin.user.list' - ] - }) - return { - status: 201, - body: JSON.stringify(dao) - } -} diff --git a/src/pages/api/users/index.ts b/src/pages/api/users/index.ts deleted file mode 100644 index 6c20cad..0000000 --- a/src/pages/api/users/index.ts +++ /dev/null @@ -1,17 +0,0 @@ -import type { APIRoute } from 'astro' -import { validateAuth } from '../../../libs/validateAuth' - -export const get: APIRoute = async ({ params, request }) => { - const requestInvalid = await validateAuth(request, { - name: 'user.list', - api: false, - cookie: true - }) - if (requestInvalid) { - return requestInvalid - } - return { - status: 200, - body: JSON.stringify({iam: true}) - } -} diff --git a/src/pages/api/users/[userId]/configs/[configId]/files/[fileName].ts b/src/pages/api/v1/users/[userId]/configs/[configId]/files/[fileName].ts similarity index 50% rename from src/pages/api/users/[userId]/configs/[configId]/files/[fileName].ts rename to src/pages/api/v1/users/[userId]/configs/[configId]/files/[fileName].ts index 14639c4..b4472af 100644 --- a/src/pages/api/users/[userId]/configs/[configId]/files/[fileName].ts +++ b/src/pages/api/v1/users/[userId]/configs/[configId]/files/[fileName].ts @@ -1,10 +1,8 @@ -import { objectOmit } from '@dzeio/object-util' import type { APIRoute } from 'astro' -import { buildRFC7807 } from '../../../../../../../libs/RFCs/RFC7807' -import DaoFactory from '../../../../../../../models/DaoFactory' +import { buildRFC7807 } from '../../../../../../../../libs/RFCs/RFC7807' +import DaoFactory from '../../../../../../../../models/DaoFactory' -export const get: APIRoute = async ({ params, request }) => { - const userId = params.userId as string +export const get: APIRoute = async ({ params, locals }) => { const configId = params.configId as string const fileName = params.fileName as string @@ -19,8 +17,8 @@ export const get: APIRoute = async ({ params, request }) => { const file = dao.files.find((it) => it.name === fileName) - return { - status: 200, - body: file?.data - } + return locals.responseBuilder + .status(200) + .body(file?.data) + .build() } diff --git a/src/pages/api/users/[userId]/configs/index.ts b/src/pages/api/v1/users/[userId]/configs/index.ts similarity index 63% rename from src/pages/api/users/[userId]/configs/index.ts rename to src/pages/api/v1/users/[userId]/configs/index.ts index 9c3ea5c..76a0f40 100644 --- a/src/pages/api/users/[userId]/configs/index.ts +++ b/src/pages/api/v1/users/[userId]/configs/index.ts @@ -1,9 +1,10 @@ import { objectOmit } from '@dzeio/object-util' import type { APIRoute } from 'astro' -import { buildRFC7807 } from '../../../../../libs/RFCs/RFC7807' -import DaoFactory from '../../../../../models/DaoFactory' +import { buildRFC7807 } from '../../../../../../libs/RFCs/RFC7807' +import StatusCode from '../../../../../../libs/StatusCode' +import DaoFactory from '../../../../../../models/DaoFactory' -export const post: APIRoute = async ({ params, request }) => { +export const post: APIRoute = async ({ params, request, locals }) => { const userId = params.userId as string const body = request.body @@ -37,8 +38,8 @@ export const post: APIRoute = async ({ params, request }) => { data: buffer }] }) - return { - status: 201, - body: JSON.stringify(objectOmit(dao ?? {}, 'files')) - } + return locals.responseBuilder + .status(StatusCode.CREATED) + .body(objectOmit(dao ?? {}, 'files')) + .build() } diff --git a/src/pages/api/v1/users/[userId]/keys/index.ts b/src/pages/api/v1/users/[userId]/keys/index.ts new file mode 100644 index 0000000..acabab2 --- /dev/null +++ b/src/pages/api/v1/users/[userId]/keys/index.ts @@ -0,0 +1,20 @@ +import type { APIRoute } from 'astro' +import crypto from 'node:crypto' +import StatusCode from '../../../../../../libs/StatusCode' +import DaoFactory from '../../../../../../models/DaoFactory' + +export const post: APIRoute = async ({ params, locals }) => { + const userId = params.userId as string + + const dao = await DaoFactory.get('apiKey').create({ + user: userId, + key: crypto.randomUUID(), + permissions: [ + 'admin.user.list' + ] + }) + return locals.responseBuilder + .status(StatusCode.CREATED) + .body(dao) + .build() +} diff --git a/src/pages/api/v1/users/index.ts b/src/pages/api/v1/users/index.ts new file mode 100644 index 0000000..543fd6f --- /dev/null +++ b/src/pages/api/v1/users/index.ts @@ -0,0 +1,17 @@ +import type { APIRoute } from 'astro' +import { buildRFC7807 } from '../../../../libs/RFCs/RFC7807' +import StatusCode from '../../../../libs/StatusCode' + +export const get: APIRoute = async ({ locals }) => { + return locals.responseBuilder + .status(200) + .body({iam: true}) + .build() +} + +export const options: APIRoute = async () => { + return buildRFC7807({ + status: StatusCode.METHOD_NOT_ALLOWED, + details: 'Allowed methods: "GET"' + }) +}