feat: first version
Some checks failed
Build, check & Test / run (push) Failing after 39s

Signed-off-by: Florian BOUILLON <f.bouillon@aptatio.com>
This commit is contained in:
2023-07-20 17:41:16 +02:00
parent 2bd59f902f
commit 09ed4c487d
80 changed files with 1171 additions and 2755 deletions

5
src/pages/README.md Normal file
View File

@ -0,0 +1,5 @@
# Content
Contains raw content for pages.
Mostly some static pages or blog posts.

View File

@ -1,66 +0,0 @@
---
import URLManager from '@dzeio/url-manager'
import Layout from '../../layouts/Layout.astro'
import DaoFactory from '../../models/DaoFactory'
import { comparePassword } from '../../libs/AuthUtils'
import Passthrough from '../../components/Passthrough.astro'
const logout = typeof new URLManager(Astro.url).query('logout') === 'string'
if (logout) {
DaoFactory.get('session').removeSession(Astro.response)
}
// DaoFactory.get('session').removeSession(Astro.response)
let connected = false
const sessionDao = DaoFactory.get('session')
if (sessionDao.getSession(Astro.request) && !logout) {
connected = true
}
if (!connected && Astro.request.method === 'POST') {
const form = await Astro.request.formData()
const email = form.get('email') as string
const password = form.get('password') as string
const account = await DaoFactory.get('user').findOne({
email
})
if (account) {
const valid = await comparePassword(password, account.password)
if (valid) {
DaoFactory.get('session').setSession({
userId: account.id
}, Astro.response)
connected = true
}
}
}
---
<Layout title="Welcome to Astro.">
<main>
<form method="post">
<input type="email" name="email" />
<input type="password" name="password" />
<button>Connect</button>
<button></button>
</form>
</main>
<Passthrough connected={connected} />
</Layout>
<script>import { load } from '../../components/Passthrough.astro'
const {
connected
} = load<{connected: boolean}>()
if (connected) {
window.location.pathname = '/'
}
</script>

View File

@ -1,45 +0,0 @@
---
import Layout from '../../layouts/Layout.astro'
import { hashPassword } from '../../libs/AuthUtils'
import DaoFactory from '../../models/DaoFactory'
let errorMessage: string | undefined
if (Astro.request.method === 'POST') {
const form = await Astro.request.formData()
const email = form.get('email') as string
const password = form.get('password') as string
const user = await DaoFactory.get('user').create({
email: email,
password: await hashPassword(password)
})
if (!user) {
errorMessage = 'User already exists'
return
}
DaoFactory.get('session').setSession({
userId: user.id
}, Astro.response)
}
---
<Layout title="Welcome to Astro.">
<main>
{errorMessage && (
<div>
{errorMessage}
</div>
)}
<form method="post">
<input type="email" name="email" id="email"/>
<input type="password" name="password" id="password" />
<input type="password" name="repeat-password" id="repeat-password">
<button>Register</button>
</form>
</main>
</Layout>

View File

@ -1,59 +0,0 @@
---
import Passthrough from '../components/Passthrough.astro'
import Layout from '../layouts/Layout.astro'
import DaoFactory from '../models/DaoFactory'
const session = DaoFactory.get('session').getSession(Astro.request)
if (!session) {
return Astro.redirect('/')
}
const user = await DaoFactory.get('user').get(session.userId)
const list = await DaoFactory.get('apiKey').findAll({
user: user!.id
})
const configs = await DaoFactory.get('config').findAll({
user: user!.id
})
const userId = user?.id ?? 'unknown'
---
<Layout title="Welcome to Astro.">
<main>
<h1>{user?.id}</h1>
<ul>
<li>API Keys</li>
{list.map((it) => (
<li>
<p>access key: {it.key}</p>
<p>permissions: {it.permissions}</p>
</li>
))}
<li>Configurations</li>
{configs.map((it) => (
<li>
<p>{it.id}: {it.type}</p>
<p>{it.files.map((it) => it.name)}</p>
</li>
))}
<button>Create a new API Key</button>
</ul>
</main>
<Passthrough userId={userId} />
</Layout>
<script>
import { load } from '../components/Passthrough.astro'
const vars = load<{userId: string}>()
console.log(vars)
document.querySelector('button')?.addEventListener('click', async () => {
await fetch(`/api/users/${vars.userId}/keys`, {
method: 'POST'
})
window.location.reload()
})
</script>

View File

@ -1,262 +0,0 @@
import Logger from '@dzeio/logger'
import { objectMap, objectOmit } from '@dzeio/object-util'
import URLManager from '@dzeio/url-manager'
import type { APIRoute } from 'astro'
import { evaluate } from 'mathjs'
import { spawn } from 'node:child_process'
import fs from 'node:fs/promises'
import os from 'node:os'
import path from 'node:path'
import StatusCode from '../../../../libs/HTTP/StatusCode'
import { buildRFC7807 } from '../../../../libs/RFCs/RFC7807'
import { getParams } from '../../../../libs/gcodeUtils'
import DaoFactory from '../../../../models/DaoFactory'
interface SliceError {
code: number
output: Array<string>
}
let tmpDir: string
/**
* body is the stl
* query
* price: algorithm from settings
* adionnal settings from https://manual.slic3r.org/advanced/command-line
*/
export const post: APIRoute = async ({ params, request, locals }) => {
if (!tmpDir) {
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'saas-'))
}
const configId = params.configId ?? 'undefined'
const config = await DaoFactory.get('config').get(configId)
if (!config) {
return buildRFC7807({
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)
const logger = new Logger(`process-${processId}`)
const processFolder = `${tmpDir}/${processId}`
const pouet = await fs.mkdir(processFolder, { recursive: true })
logger.log('poeut', pouet)
logger.log('started processing request')
logger.log('writing configs to dir')
for (const file of config.files) {
await fs.writeFile(`${processFolder}/${file.name}`, file.data)
}
const overrides = objectOmit(query, 'algo')
const stlPath = `${processFolder}/input.stl`
const gcodePath = `${processFolder}/output.gcode`
logger.log('writing STL to filesystem')
// write input
await fs.writeFile(stlPath, input, {
encoding: null
})
// additionnal parameters
let additionnalParams = objectMap(overrides, (value, key) => `--${(key as string).replace(/_/g, '-')} ${value}`).join(' ')
let slicerPath: string
let slicerCommand: string
if (config.type === 'prusa' || true) {
slicerPath = import.meta.env.PRUSASLICER_PATH ?? 'prusa-slicer'
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 logs: Array<string> = []
const slicer = spawn(`"${slicerPath}"`, slicerCommand.split(' '), {
shell: true
})
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) => {
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 = 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: '/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: '/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: '/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: '/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: '/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: '/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,
config: configId,
// fileSize: req.body.length,
overrides: overrides,
serverMessage:
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 })
logger.log('Getting parameters')
const gcodeParams = getParams(gcode)
let price: string | undefined
if (query?.algo) {
let algo = decodeURI(query.algo as string)
// objectLoop(params, (value, key) => {
// if (typeof value !== 'number') {
// return
// }
// while (algo.includes(key)) {
// algo = algo.replace(key, value.toString())
// }
// })
try {
logger.log('Evaluating Alogrithm')
const tmp = evaluate(algo, gcodeParams)
if (typeof tmp === 'number') {
price = tmp.toFixed(2)
} else {
return buildRFC7807({
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: '/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 locals.responseBuilder
.body({
price: price ? parseFloat(price) : undefined,
...getParams(gcode),
gcode
})
.status(200)
.build()
}

View File

@ -1,24 +0,0 @@
import type { APIRoute } from 'astro'
import { buildRFC7807 } from '../../../../../../../../libs/RFCs/RFC7807'
import DaoFactory from '../../../../../../../../models/DaoFactory'
export const get: APIRoute = async ({ params, locals }) => {
const configId = params.configId as string
const fileName = params.fileName as string
const dao = await DaoFactory.get('config').get(configId)
if (!dao) {
return buildRFC7807({
title: 'Config does not exists :('
})
}
const file = dao.files.find((it) => it.name === fileName)
return locals.responseBuilder
.status(200)
.body(file?.data)
.build()
}

View File

@ -1,45 +0,0 @@
import { objectOmit } from '@dzeio/object-util'
import type { APIRoute } from 'astro'
import StatusCode from '../../../../../../libs/HTTP/StatusCode'
import { buildRFC7807 } from '../../../../../../libs/RFCs/RFC7807'
import DaoFactory from '../../../../../../models/DaoFactory'
export const post: APIRoute = async ({ params, request, locals }) => {
const userId = params.userId as string
const body = request.body
if (!body) {
return buildRFC7807({
title: 'Missing config file'
})
}
const reader = body.getReader()
const chunks: Array<Uint8Array> = []
let finished= false
do {
const { done, value } = await reader.read()
finished = done
if (value) {
chunks.push(value)
}
} while (!finished)
const buffer = Buffer.concat(chunks)
const dao = await DaoFactory.get('config').create({
user: userId,
type: 'prusa',
files: [{
name: 'config.ini',
data: buffer
}]
})
return locals.responseBuilder
.status(StatusCode.CREATED)
.body(objectOmit(dao ?? {}, 'files'))
.build()
}

View File

@ -1,20 +0,0 @@
import type { APIRoute } from 'astro'
import crypto from 'node:crypto'
import StatusCode from '../../../../../../libs/HTTP/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()
}

View File

@ -1,17 +0,0 @@
import type { APIRoute } from 'astro'
import StatusCode from '../../../../libs/HTTP/StatusCode'
import { buildRFC7807 } from '../../../../libs/RFCs/RFC7807'
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"'
})
}

View File

@ -1,24 +0,0 @@
---
import Layout from '../../layouts/Layout.astro'
import { getEntry } from 'astro:content'
import StatusCode from '../../libs/HTTP/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>

View File

@ -1,5 +1,5 @@
---
import Layout from '../layouts/Layout.astro';
import Layout from '../layouts/Layout.astro'
---
<Layout title="Welcome to Astro.">
@ -9,9 +9,5 @@ import Layout from '../layouts/Layout.astro';
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>
<ul role="list" class="link-card-grid">
<li><a href="/account/login">Login</a></li>
<li><a href="/account/register">Register</a></li>
</ul>
</main>
</Layout>