init: repo
This commit is contained in:
commit
5a415586b7
|
@ -0,0 +1,2 @@
|
||||||
|
node_modules
|
||||||
|
dist
|
|
@ -0,0 +1,2 @@
|
||||||
|
node_modules
|
||||||
|
dist
|
|
@ -0,0 +1,4 @@
|
||||||
|
{
|
||||||
|
"editor.formatOnSave": true,
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
{
|
||||||
|
"clients": [
|
||||||
|
{
|
||||||
|
"name": "izz",
|
||||||
|
"keySha256Sum": "eeb36e726e3ffec16da7798415bb4e531bf8a57fbe276fcc3fc6ea986cb02e9a",
|
||||||
|
"quota": 512
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"storePath": "store"
|
||||||
|
}
|
|
@ -0,0 +1,24 @@
|
||||||
|
// @ts-check
|
||||||
|
|
||||||
|
import tse from 'typescript-eslint';
|
||||||
|
|
||||||
|
export default tse.config(...tse.configs.recommended, {
|
||||||
|
rules: {
|
||||||
|
'no-unused-vars': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
argsIgnorePattern: '_',
|
||||||
|
args: 'after-used',
|
||||||
|
caughtErrors: 'none',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'no-invalid-regexp': 'error',
|
||||||
|
'no-var': 'error',
|
||||||
|
eqeqeq: 'error',
|
||||||
|
'no-useless-concat': 'error',
|
||||||
|
'no-useless-rename': 'error',
|
||||||
|
'no-useless-assignment': 'error',
|
||||||
|
'default-case': 'error',
|
||||||
|
'prefer-const': 'error',
|
||||||
|
},
|
||||||
|
});
|
|
@ -0,0 +1,26 @@
|
||||||
|
{
|
||||||
|
"name": "fastify-ts-template",
|
||||||
|
"license": "GPL-3.0-only",
|
||||||
|
"author": "blek! <me@blek.codes>",
|
||||||
|
"type": "module",
|
||||||
|
"main": "dist/index.js",
|
||||||
|
"dependencies": {
|
||||||
|
"ajv": "^8.13.0",
|
||||||
|
"fastify": "^4.27.0",
|
||||||
|
"typescript-eslint": "^7.8.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/eslint": "^8.56.10",
|
||||||
|
"@types/node": "^20.12.11",
|
||||||
|
"esbuild": "^0.21.1",
|
||||||
|
"eslint": "^9.2.0",
|
||||||
|
"prettier": "3.2.5",
|
||||||
|
"typescript": "^5.4.5"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"start": "yarn build && node dist/index.js",
|
||||||
|
"build": "yarn lint && node scripts/build.js",
|
||||||
|
"lint": "eslint",
|
||||||
|
"format": "prettier"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type { import("prettier").Config }
|
||||||
|
*/
|
||||||
|
export default {
|
||||||
|
trailingComma: 'all',
|
||||||
|
tabWidth: 4,
|
||||||
|
semi: true,
|
||||||
|
singleQuote: true,
|
||||||
|
endOfLine: 'lf'
|
||||||
|
}
|
|
@ -0,0 +1,18 @@
|
||||||
|
import esbuild from 'esbuild';
|
||||||
|
import fs from 'node:fs';
|
||||||
|
|
||||||
|
const entryPoints = fs
|
||||||
|
.readdirSync('src', { recursive: true })
|
||||||
|
.filter((x) => x.endsWith('.ts'))
|
||||||
|
.map((x) => 'src/' + x);
|
||||||
|
|
||||||
|
if (fs.existsSync('dist')) fs.rmSync('dist', { recursive: true, force: true });
|
||||||
|
|
||||||
|
try {
|
||||||
|
fs.mkdirSync('dist');
|
||||||
|
} catch (_) {}
|
||||||
|
|
||||||
|
await esbuild.build({
|
||||||
|
entryPoints,
|
||||||
|
outdir: 'dist',
|
||||||
|
});
|
|
@ -0,0 +1,61 @@
|
||||||
|
import fsp from 'node:fs/promises';
|
||||||
|
import Ajv from 'ajv';
|
||||||
|
const ajv = new Ajv();
|
||||||
|
|
||||||
|
const config_validate = ajv.compile({
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
clients: {
|
||||||
|
type: 'array',
|
||||||
|
items: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
name: { type: 'string' },
|
||||||
|
keySha256Sum: { type: 'string' },
|
||||||
|
quota: { type: 'number', nullable: true },
|
||||||
|
},
|
||||||
|
required: ['name', 'keySha256Sum'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
storePath: { type: 'string' },
|
||||||
|
},
|
||||||
|
required: ['clients', 'storePath'],
|
||||||
|
});
|
||||||
|
|
||||||
|
export class Config {
|
||||||
|
clients: {
|
||||||
|
name: string;
|
||||||
|
keySha256Sum: string;
|
||||||
|
/**
|
||||||
|
* In megabytes
|
||||||
|
*/
|
||||||
|
quota?: number;
|
||||||
|
}[];
|
||||||
|
storePath: string;
|
||||||
|
|
||||||
|
static async load(): Promise<Config> {
|
||||||
|
const path = process.env.CONFIG_PATH ?? 'config.json';
|
||||||
|
const raw = JSON.parse(
|
||||||
|
await fsp.readFile(path, { encoding: 'utf8' }),
|
||||||
|
) as Config;
|
||||||
|
if (!config_validate(raw)) {
|
||||||
|
throw (
|
||||||
|
'Config invalid: ' +
|
||||||
|
(config_validate.errors
|
||||||
|
?.map((x) => x.keyword + ': ' + x.message)
|
||||||
|
?.join('; ') ?? 'No validation errors provided')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await fsp.mkdir(raw.storePath, { recursive: true });
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Could not create storePath directory:');
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
|
||||||
|
return raw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const config = await Config.load();
|
|
@ -0,0 +1,28 @@
|
||||||
|
import { FastifyError } from 'fastify';
|
||||||
|
|
||||||
|
export const InvalidPayload = () =>
|
||||||
|
({
|
||||||
|
code: '400',
|
||||||
|
statusCode: 400,
|
||||||
|
name: 'InvalidPayload',
|
||||||
|
message:
|
||||||
|
"Payload type is invalid. Did you forget to set a proper 'Content-Type'?",
|
||||||
|
}) as FastifyError;
|
||||||
|
|
||||||
|
export const ValidationError = (err?: string) =>
|
||||||
|
({
|
||||||
|
code: '400',
|
||||||
|
statusCode: 400,
|
||||||
|
name: 'ValidationError',
|
||||||
|
message: err
|
||||||
|
? 'The schema does not match'
|
||||||
|
: 'The schema does not match: ' + err,
|
||||||
|
}) as FastifyError;
|
||||||
|
|
||||||
|
export const InvalidAuthorization = () =>
|
||||||
|
({
|
||||||
|
code: '401',
|
||||||
|
statusCode: 401,
|
||||||
|
name: 'InvalidAuthorization',
|
||||||
|
message: 'Provided authorization credentials are invalid',
|
||||||
|
}) as FastifyError;
|
|
@ -0,0 +1,19 @@
|
||||||
|
import Fastify from 'fastify';
|
||||||
|
import routes from './routes/index.js';
|
||||||
|
|
||||||
|
const fastify = Fastify();
|
||||||
|
|
||||||
|
// register any plugins here, i.e.
|
||||||
|
// fastify.register(middie);
|
||||||
|
|
||||||
|
fastify.register(routes);
|
||||||
|
|
||||||
|
const port = parseInt(process.env.PORT ?? '80');
|
||||||
|
const host = process.env.HOST ?? 'localhost';
|
||||||
|
|
||||||
|
console.log(`Listening on ${host}:${port}`);
|
||||||
|
|
||||||
|
fastify.listen({
|
||||||
|
port,
|
||||||
|
host,
|
||||||
|
});
|
|
@ -0,0 +1,6 @@
|
||||||
|
import { FastifyPluginAsync } from 'fastify';
|
||||||
|
import upload from './upload.js';
|
||||||
|
|
||||||
|
export default (async function (fastify) {
|
||||||
|
await fastify.register(upload);
|
||||||
|
} as FastifyPluginAsync);
|
|
@ -0,0 +1,64 @@
|
||||||
|
import { FastifyPluginAsync } from 'fastify';
|
||||||
|
import Ajv, { JSONSchemaType, type Schema } from 'ajv';
|
||||||
|
import crypto from 'node:crypto';
|
||||||
|
|
||||||
|
import {
|
||||||
|
InvalidAuthorization,
|
||||||
|
InvalidPayload,
|
||||||
|
ValidationError,
|
||||||
|
} from '../errors.js';
|
||||||
|
import { config } from '../config.js';
|
||||||
|
import { save } from '../store.js';
|
||||||
|
|
||||||
|
type UploadPayload = {
|
||||||
|
data: string; // base64-encoded
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
const ajv = new Ajv();
|
||||||
|
const upload_schema = {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
data: { type: 'string' },
|
||||||
|
name: { type: 'string' },
|
||||||
|
},
|
||||||
|
required: ['data', 'name'],
|
||||||
|
} as Schema | JSONSchemaType<UploadPayload>;
|
||||||
|
|
||||||
|
export default (async function (fastify) {
|
||||||
|
fastify.post('/upload', async (req) => {
|
||||||
|
const sha = crypto.createHash('sha256');
|
||||||
|
sha.update(req.headers.authorization?.replace('Bearer ', '') ?? 'none');
|
||||||
|
const key = sha.digest().toString('hex');
|
||||||
|
|
||||||
|
const client = config.clients.find((x) => x.keySha256Sum === key);
|
||||||
|
|
||||||
|
if (!client) {
|
||||||
|
throw InvalidAuthorization();
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = req.body as UploadPayload;
|
||||||
|
|
||||||
|
if (typeof body !== 'object') {
|
||||||
|
throw InvalidPayload();
|
||||||
|
}
|
||||||
|
|
||||||
|
const compiled = ajv.compile(upload_schema);
|
||||||
|
if (!compiled(body)) {
|
||||||
|
throw ValidationError(
|
||||||
|
compiled.errors
|
||||||
|
?.map((x) => x.keyword + ': ' + x.message)
|
||||||
|
.join('; '),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = Buffer.from(body.data, 'base64');
|
||||||
|
const name = body.name;
|
||||||
|
|
||||||
|
await save(data, name, client.name);
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: 'ok',
|
||||||
|
name,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
} as FastifyPluginAsync);
|
|
@ -0,0 +1,16 @@
|
||||||
|
import { config } from './config.js';
|
||||||
|
|
||||||
|
import path from 'path';
|
||||||
|
import fsp from 'node:fs/promises';
|
||||||
|
|
||||||
|
export function getPathFor(name: string, clientName: string): string {
|
||||||
|
return path.join(config.storePath, clientName + '-' + name);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function save(
|
||||||
|
data: string | Buffer | NodeJS.ArrayBufferView,
|
||||||
|
name: string,
|
||||||
|
clientName: string,
|
||||||
|
): Promise<undefined> {
|
||||||
|
await fsp.writeFile(getPathFor(name, clientName), data);
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
uwu
|
Loading…
Reference in New Issue