fet: Add changes lol

Signed-off-by: Florian BOUILLON <f.bouillon@aptatio.com>
This commit is contained in:
Florian Bouillon 2023-06-28 17:30:18 +02:00
parent 9530be5c43
commit ff07f8f4a5
24 changed files with 656 additions and 511 deletions

8
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,8 @@
{
"editor.quickSuggestions": {
"strings": "on"
},
"tailwindCSS.includeLanguages": {
"astro": "html"
}
}

View File

@ -2,4 +2,4 @@ it('titles are correct', () => {
const page = cy.visit('http://localhost:3000');
page.get('h1').should('have.text', 'Welcome to Astro');
});
})

757
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -12,25 +12,26 @@
"test": "vitest --coverage"
},
"dependencies": {
"@astrojs/node": "^5.2.0",
"@astrojs/tailwind": "^3.1.3",
"@dzeio/logger": "^3.0.0",
"@dzeio/object-util": "^1.5.0",
"@dzeio/url-manager": "^1.0.10",
"astro": "^2.6.4",
"bcryptjs": "^2.4.3",
"jsonwebtoken": "^9.0.0",
"mathjs": "^11.8.1",
"mongoose": "^7.3.0",
"tailwindcss": "^3.3.2"
"@astrojs/node": "^5",
"@astrojs/tailwind": "^3",
"@dzeio/logger": "^3",
"@dzeio/object-util": "^1",
"@dzeio/url-manager": "^1",
"astro": "^2",
"bcryptjs": "^2",
"jsonwebtoken": "^9",
"lucide-astro": "^0.256.1",
"mathjs": "^11",
"mongoose": "^7",
"tailwindcss": "^3"
},
"devDependencies": {
"@tailwindcss/typography": "^0.5.9",
"@types/bcryptjs": "^2.4.2",
"@types/jsonwebtoken": "^9.0.2",
"@types/node": "^20.3.1",
"@types/bcryptjs": "^2",
"@types/jsonwebtoken": "^9",
"@types/node": "^20",
"@vitest/coverage-v8": "^0.32.2",
"cypress": "^12.15.0",
"cypress": "^12",
"vitest": "^0.32.2"
}
}

5
public/logo.svg Normal file
View File

@ -0,0 +1,5 @@
<svg width="206" height="110" viewBox="0 0 206 110" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M63.7908 75.4986V37.0836H89.5535V44.3452H72.4524V52.383H87.6657V59.6447H72.4524V75.4986H63.7908ZM95.6732 37.0836H104.335V75.4986H95.6732V37.0836ZM121.459 76.3301C120.459 76.3301 119.404 76.2564 118.294 76.1083C117.183 75.9975 116.11 75.8312 115.073 75.6094C114.037 75.3877 113.093 75.1478 112.242 74.8888C111.39 74.6303 110.724 74.3899 110.243 74.1682L111.853 67.2945C112.815 67.7011 114.037 68.1446 115.518 68.6249C117.035 69.0683 118.904 69.2901 121.125 69.2901C123.679 69.2901 125.549 68.8098 126.733 67.8488C127.917 66.8883 128.51 65.5947 128.51 63.9685C128.51 62.9707 128.288 62.1392 127.844 61.474C127.436 60.772 126.863 60.2177 126.122 59.811C125.382 59.3675 124.494 59.0722 123.457 58.9241C122.458 58.7396 121.384 58.6469 120.237 58.6469H117.017V51.9949H120.681C121.495 51.9949 122.273 51.9213 123.013 51.7732C123.791 51.6255 124.475 51.3852 125.068 51.0526C125.66 50.6832 126.122 50.2029 126.456 49.6113C126.826 48.9834 127.011 48.2073 127.011 47.2831C127.011 46.5811 126.863 45.9714 126.567 45.4538C126.27 44.9368 125.882 44.5115 125.401 44.1789C124.956 43.8463 124.419 43.6064 123.791 43.4583C123.198 43.2736 122.587 43.1812 121.958 43.1812C120.366 43.1812 118.886 43.4214 117.516 43.9017C116.184 44.3824 114.962 44.9736 113.852 45.6756L110.909 39.6335C111.501 39.264 112.186 38.8759 112.963 38.4694C113.778 38.0629 114.666 37.6933 115.629 37.3608C116.591 37.0282 117.609 36.751 118.682 36.5293C119.793 36.3075 120.959 36.1967 122.18 36.1967C124.438 36.1967 126.381 36.4738 128.01 37.0282C129.676 37.5455 131.045 38.3031 132.119 39.3009C133.192 40.2618 133.988 41.4074 134.506 42.7378C135.024 44.0312 135.284 45.4538 135.284 47.006C135.284 48.5213 134.858 49.9994 134.007 51.4406C133.155 52.8451 132.008 53.9169 130.564 54.6557C132.563 55.469 134.099 56.6886 135.173 58.3143C136.283 59.9037 136.838 61.8252 136.838 64.0794C136.838 65.8532 136.542 67.498 135.95 69.0129C135.358 70.4914 134.432 71.7846 133.174 72.8932C131.915 73.9651 130.305 74.8152 128.343 75.4431C126.418 76.0347 124.124 76.3301 121.459 76.3301ZM153.46 68.4586C153.867 68.4958 154.33 68.5326 154.848 68.5694C155.403 68.5694 156.051 68.5694 156.791 68.5694C161.122 68.5694 164.324 67.4794 166.397 65.2989C168.507 63.1188 169.562 60.1068 169.562 56.2633C169.562 52.2353 168.562 49.1865 166.563 47.1168C164.564 45.0476 161.4 44.0126 157.069 44.0126C156.476 44.0126 155.866 44.0312 155.237 44.068C154.607 44.068 154.015 44.1053 153.46 44.1789V68.4586ZM178.501 56.2633C178.501 59.5893 177.982 62.4904 176.946 64.9663C175.909 67.4426 174.429 69.4936 172.504 71.1194C170.616 72.7455 168.303 73.9651 165.564 74.7779C162.825 75.5912 159.752 75.9975 156.347 75.9975C154.792 75.9975 152.978 75.9239 150.906 75.7757C148.833 75.6649 146.797 75.4063 144.798 74.9997V37.5825C146.797 37.2129 148.87 36.9727 151.017 36.8619C153.2 36.714 155.07 36.6401 156.625 36.6401C159.919 36.6401 162.899 37.0097 165.564 37.7488C168.266 38.4879 170.579 39.652 172.504 41.2411C174.429 42.8302 175.909 44.8627 176.946 47.3386C177.982 49.8149 178.501 52.7896 178.501 56.2633Z" fill="black"/>
<path d="M49.5 15C48.806 14.3071 48.0241 14 47 14H18C16.9758 14 16.1917 14.3071 15.5 15C14.806 15.6905 14.5 16.4775 14.5 17.5V75.5C14.5 76.5225 14.806 77.3071 15.5 78C16.1917 78.6908 16.9758 79 18 79H25.5V82.5C25.5 83.5225 25.8083 84.3071 26.5 85C27.194 85.6903 27.9758 86 29 86H36C37.0242 86 37.8084 85.6903 38.5 85C39.194 84.3071 39.5 83.5225 39.5 82.5V79H47C48.0241 79 48.806 78.6908 49.5 78C50.1914 77.3071 50.5 76.5225 50.5 75.5V17.5C50.5 16.4775 50.1914 15.6905 49.5 15Z" fill="black"/>
<path d="M189.53 96H33C30.5 96 29.5 95 29.5 92.5V86H35.5V88C35.5 90 36.5 91 38.5 91H189.53C190.5 91 191.5 91.5 191.5 93.5C191.5 95.5 190.5 96 189.53 96Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 3.7 KiB

View File

@ -1,11 +0,0 @@
{
"infiniteLoopProtection": true,
"hardReloadOnChange": false,
"view": "browser",
"template": "node",
"container": {
"port": 3000,
"startScript": "start",
"node": "14"
}
}

2
src/env.d.ts vendored
View File

@ -22,7 +22,7 @@ declare namespace App {
authKey?: string
responseBuilder: {
body(body: string | Buffer | object | null | undefined): this
headers(headers: HeadersInit ): this
headers(headers: Record<string, string>): this
addHeader(key: string, value: string): this
addHeaders(headers: Record<string, string>): this
removeHeader(key: string): this

View File

@ -1,4 +1,5 @@
---
import { WifiOff } from 'lucide-astro'
export interface Props {
title: string;
}
@ -16,21 +17,21 @@ const { title } = Astro.props;
<meta name="generator" content={Astro.generator} />
<title>{title}</title>
</head>
<body>
<body class="bg-gray-50">
<aside class="fixed top-0 left-0 z-40 w-64 p-4 h-screen bg-white border-r-2 border-gray-100">
<div class="mb-2 flex">
<img src="/logo.svg" class="w-1/2" alt="FI3D Logo">
<WifiOff class='pokemon' />
</div>
<ul class="space-y-2 font-medium">
<li class="p-4 w-full bg-red-100 hover:bg-red-200 cursor-pointer text-center">Item</li>
</ul>
</aside>
<div class="p-4 sm:ml-64">
<slot />
</div>
</body>
</html>
<style is:global>
:root {
--accent: 124, 58, 237;
--accent-gradient: linear-gradient(45deg, rgb(var(--accent)), #da62c4 30%, white 60%);
}
html {
font-family: system-ui, sans-serif;
background-color: #F6F6F6;
}
code {
font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono,
Bitstream Vera Sans Mono, Courier New, monospace;
}
</style>

3
src/libs/README.md Normal file
View File

@ -0,0 +1,3 @@
# Libs
Globally independent objects/classes/functions that MUST be unit testable by themselve

View File

@ -1,6 +1,6 @@
import { objectLoop } from '@dzeio/object-util'
import StatusCode from './HTTP/StatusCode'
import { buildRFC7807 } from './RFCs/RFC7807'
import StatusCode from './StatusCode'
import ResponseBuilder from './ResponseBuilder'
interface StorageItem {
pointsRemaining: number
@ -33,36 +33,40 @@ export default class RateLimiter {
private storage: Record<string, StorageItem> = {}
public constructor(
private points = RateLimiter.points,
private timeSpan = RateLimiter.timeSpan
) {}
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
pointsRemaining: this.points,
timeReset: now + this.timeSpan
}
}
if (item.timeReset <= now) {
item.timeReset = now + RateLimiter.timeSpan
item.pointsRemaining = RateLimiter.points
item.timeReset = now + this.timeSpan
item.pointsRemaining = this.points
}
item.pointsRemaining -= value
this.storage[key] = item
const headers: RateLimitHeaders = {
"X-RateLimit-Limit": RateLimiter.points.toFixed(0),
"X-RateLimit-Limit": this.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 res = new ResponseBuilder()
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)
})
}, res)
res.addHeader('Retry-After', (item.timeReset - now).toFixed(0))
res.addHeaders(headers as any)
return resp
}
return headers

View File

@ -18,18 +18,14 @@ export default class ResponseBuilder {
return this
}
private _headers: Headers = new Headers()
public headers(headers: HeadersInit ) {
if (headers instanceof Headers) {
private _headers: Record<string, string> = {}
public headers(headers: Record<string, string>) {
this._headers = headers
} else {
this._headers = new Headers(headers)
}
return this
}
public addHeader(key: string, value: string) {
this._headers.append(key, value)
this._headers[key] = value
return this
}
@ -41,7 +37,7 @@ export default class ResponseBuilder {
}
public removeHeader(key: string) {
this._headers.delete(key)
delete this._headers[key]
return this
}

View File

@ -4,7 +4,7 @@
* @returns a number if parsing happened correctly or undefined
*/
function parseNumber(str: string): number | undefined {
if (!/^(\d|\.)+$/g.test(str)) {
if (!/^-?(\d|\.)+$/g.test(str)) {
return undefined
}
const float = parseFloat(str)
@ -53,10 +53,10 @@ export function getParams(data: string) {
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
// loop through each config
for (const line of lines) {
// get its key and value
const [key, value] = line.split('=', 2).map((it) => it.slice(1).trim())
const [key, value] = line.slice(1).split(/ *= */, 2).map((it) => it.trim())
// sip if it has no key or value
if (!key || !value) {
continue
@ -78,10 +78,10 @@ export function getParams(data: string) {
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]}`)
}
// detect key collisions (it will never happens with the while above)
// if (obj[realKey] && obj[realKey] !== realValue) {
// throw new Error(`Key collision ${key}=${realValue} ${realKey}=${obj[realKey]}`)
// }
// set the value to the key
obj[realKey] = realValue
// transform the time to a number of seconds

View File

@ -1,5 +1,3 @@
import Client from './Client'
/**
* the Dao is the object that connect the Database or source to the application layer
*

View File

@ -3,7 +3,13 @@ import Passthrough from '../components/Passthrough.astro'
import Layout from '../layouts/Layout.astro'
import DaoFactory from '../models/DaoFactory'
const user = await DaoFactory.get('user').get('648f81f857503c7d29465318')
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
})

View File

@ -7,12 +7,9 @@ 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 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 {
@ -20,7 +17,6 @@ interface SliceError {
output: Array<string>
}
let tmpDir: string
/**

View File

@ -1,7 +1,7 @@
import { objectOmit } from '@dzeio/object-util'
import type { APIRoute } from 'astro'
import StatusCode from '../../../../../../libs/HTTP/StatusCode'
import { buildRFC7807 } from '../../../../../../libs/RFCs/RFC7807'
import StatusCode from '../../../../../../libs/StatusCode'
import DaoFactory from '../../../../../../models/DaoFactory'
export const post: APIRoute = async ({ params, request, locals }) => {

View File

@ -1,6 +1,6 @@
import type { APIRoute } from 'astro'
import crypto from 'node:crypto'
import StatusCode from '../../../../../../libs/StatusCode'
import StatusCode from '../../../../../../libs/HTTP/StatusCode'
import DaoFactory from '../../../../../../models/DaoFactory'
export const post: APIRoute = async ({ params, locals }) => {

View File

@ -1,6 +1,6 @@
import type { APIRoute } from 'astro'
import StatusCode from '../../../../libs/HTTP/StatusCode'
import { buildRFC7807 } from '../../../../libs/RFCs/RFC7807'
import StatusCode from '../../../../libs/StatusCode'
export const get: APIRoute = async ({ locals }) => {
return locals.responseBuilder

View File

@ -1,7 +1,7 @@
---
import Layout from '../../layouts/Layout.astro'
import { getEntry } from 'astro:content'
import StatusCode from '../../libs/StatusCode'
import StatusCode from '../../libs/HTTP/StatusCode'
const page = Astro.params.page

41
test.ts
View File

@ -1,41 +0,0 @@
import DaoFactory from './src/models/DaoFactory'
(async () => {
// await DaoFactory.get('config').create({user: {id: 'pouet'}})
// await DaoFactory.get('user').create({user: {id: 'pouet'}})
// await DaoFactory.get('config').create({user: {id: 'pouet'}})
// await DaoFactory.get('user').create({user: {id: 'pouet'}})
// await DaoFactory.get('config').create({user: {id: 'pouet'}})
// await DaoFactory.get('config').create({user: {id: 'pouet'}})
// await DaoFactory.get('config').create({user: {id: 'pouet'}})
// await DaoFactory.get('user').create({user: {id: 'pouet'}})
// await DaoFactory.get('config').create({user: {id: 'pouet'}})
// await DaoFactory.get('config').create({user: {id: 'pouet'}})
// await DaoFactory.get('user').create({user: {id: 'pouet'}})
// await DaoFactory.get('user').create({user: {id: 'pouet'}})
const dao = DaoFactory.get('user')
console.log(await dao.create({email: 'pokemon@go.com'}))
const obj = await dao.get('648f82be60a03b7398d36925')
console.log(obj)
if (!obj) {
console.log('no obj :(')
} else {
console.log('object :)', obj)
obj.email += 'jesuisundieu@pokemon.com'
console.log(await dao.update(obj))
}
const toDelete = await dao.findOne({email: 'pokemon@go.com'})
if (toDelete) {
console.log('todelete :)', toDelete)
await dao.delete(toDelete)
}
console.log()
console.log('done')
process.exit(0)
})()
// await mongoose.get('id')
// await fetch(`/api/users/${'id'}`).then((it) => it.json())

View File

@ -0,0 +1,22 @@
import { describe, expect, it } from 'vitest'
import { comparePassword, hashPassword } from '../../src/libs/AuthUtils'
describe('AuthUtils', () => {
it('should hash the password', async () => {
expect(await hashPassword('test')).toBeDefined()
})
it('should compared succeffully password', async () => {
const pass = 'test'
const hash = await hashPassword(pass)
expect(await comparePassword(pass, hash)).toBe(true)
})
it('should not generate twice the same hash', async () => {
const pass = 'test'
const hash1 = await hashPassword(pass)
const hash2 = await hashPassword(pass)
expect(hash1).not.toBe(hash2)
})
})

View File

@ -0,0 +1,50 @@
import { afterEach, assert, beforeEach, describe, expect, it, test, vi } from 'vitest'
import { getParams } from '../../src/libs/gcodeUtils'
describe('gcodeUtils', () => {
it('should get parameters', () => {
const gcode = `
balfs
dgfdf
sd
httphq
estimated_printing_time_normal_modewef
; test=a
fgd
;test =b
;test= c
;test = d
;number=1.12
;number2=-1
;invalid=
;invalid
;estimated_printing_time_normal_mode=1d 1h 1m 1s
sdffgaf
fgsdf
g
sfd
hh
ehf
`
expect(getParams(gcode)).toEqual({
test: 'a',
test_1: 'b',
test_2: 'c',
test_3: 'd',
number: 1.12,
number2: -1,
estimated_printing_time_normal_mode: '1d 1h 1m 1s',
estimated_printing_time_seconds: 90061
})
})
it('should get parameters', () => {
const gcode = `
;estimated_printing_time_normal_mode=1w 1d 1h 1m 1s
`
expect(() => getParams(gcode)).toThrow(/1w/g)
})
})

View File

@ -0,0 +1,122 @@
import { afterEach, assert, beforeEach, describe, expect, it, test, vi } from 'vitest'
import RateLimiter from '../../src/libs/RateLimiter'
// Mock Response
vi.stubGlobal('Response', class {
public constructor(
public body?: any,
public init?: any
) {}
})
describe('Rate Limit', () => {
beforeEach(() => {
vi.useFakeTimers()
})
it('should work with global variant', () => {
const now1 = new Date(1000)
vi.setSystemTime(now1)
const limit = RateLimiter.getInstance()
const result1 = limit.consume('key')
expect(result1).toEqual({
"X-RateLimit-Limit": '10',
"X-RateLimit-Remaining": '9',
"X-RateLimit-Reset": '61'
})
const limit2 = RateLimiter.getInstance()
const result2 = limit2.consume('key')
expect(result2).toEqual({
"X-RateLimit-Limit": '10',
"X-RateLimit-Remaining": '8',
"X-RateLimit-Reset": '61'
})
})
it('should consume points', () => {
const now1 = new Date(1000)
vi.setSystemTime(now1)
const limit = new RateLimiter(2, 10)
const result1 = limit.consume('key')
expect(result1).toEqual({
"X-RateLimit-Limit": '2',
"X-RateLimit-Remaining": '1',
"X-RateLimit-Reset": '11'
})
const result2 = limit.consume('key')
expect(result2).toEqual({
"X-RateLimit-Limit": '2',
"X-RateLimit-Remaining": '0',
"X-RateLimit-Reset": '11'
})
})
it('should rate limit', () => {
const now1 = new Date(1000)
vi.setSystemTime(now1)
const limit = new RateLimiter(1, 10)
const result1 = limit.consume('key')
expect(result1).toEqual({
"X-RateLimit-Limit": '1',
"X-RateLimit-Remaining": '0',
"X-RateLimit-Reset": '11'
})
const result2 = limit.consume('key')
expect(result2).instanceOf(Response)
const result3 = limit.consume('key')
expect(result3).instanceOf(Response)
})
it('should reset after some time', () => {
const now1 = new Date(1000)
vi.setSystemTime(now1)
const limit = new RateLimiter(2, 10)
const result1 = limit.consume('key')
expect(result1).toEqual({
"X-RateLimit-Limit": '2',
"X-RateLimit-Remaining": '1',
"X-RateLimit-Reset": '11'
})
const result2 = limit.consume('key')
expect(result2).toEqual({
"X-RateLimit-Limit": '2',
"X-RateLimit-Remaining": '0',
"X-RateLimit-Reset": '11'
})
const result3 = limit.consume('key')
expect(result3).instanceOf(Response)
const now2 = new Date(11000)
vi.setSystemTime(now2)
const result4 = limit.consume('key')
expect(result4).toEqual({
"X-RateLimit-Limit": '2',
"X-RateLimit-Remaining": '1',
"X-RateLimit-Reset": '21'
})
const result5 = limit.consume('key')
expect(result5).toEqual({
"X-RateLimit-Limit": '2',
"X-RateLimit-Remaining": '0',
"X-RateLimit-Reset": '21'
})
const result6 = limit.consume('key')
expect(result6).instanceOf(Response)
})
afterEach(() => {
vi.useRealTimers()
})
})