feat: typescript and fastify
This commit is contained in:
parent
77db6f34c6
commit
af0e46e552
|
@ -2,8 +2,8 @@
|
|||
"editor.detectIndentation": false,
|
||||
"editor.insertSpaces": false,
|
||||
"files.insertFinalNewline": true,
|
||||
"editor.formatOnPaste": false,
|
||||
"editor.formatOnSave": false,
|
||||
"editor.formatOnPaste": true,
|
||||
"editor.formatOnSave": true,
|
||||
"[javascript]": {
|
||||
"editor.defaultFormatter": "dbaeumer.vscode-eslint"
|
||||
},
|
||||
|
|
File diff suppressed because it is too large
Load Diff
43
package.json
43
package.json
|
@ -10,8 +10,8 @@
|
|||
},
|
||||
"scripts": {
|
||||
"setup": "node src/setup.js && npm run migrate && npm run seed",
|
||||
"start": "npm run migrate && nuxt build && cross-env NODE_ENV=production node src/api/structures/Server",
|
||||
"dev": "nodemon src/api/structures/Server",
|
||||
"start": "npm run migrate && nuxt build && cross-env NODE_ENV=production node src/api/main.ts",
|
||||
"dev": "nodemon src/api/main.ts",
|
||||
"migrate": "knex migrate:latest",
|
||||
"seed": "knex seed:run",
|
||||
"restart": "pm2 restart chibisafe",
|
||||
|
@ -45,29 +45,31 @@
|
|||
"busboy": "^0.3.1",
|
||||
"chrono-node": "^2.3.0",
|
||||
"cookie-universal-nuxt": "^2.0.14",
|
||||
"cors": "^2.8.5",
|
||||
"cron": "^1.8.2",
|
||||
"dotenv": "^8.2.0",
|
||||
"express": "^4.17.1",
|
||||
"express-rate-limit": "^5.2.6",
|
||||
"fastify": "^3.18.0",
|
||||
"fastify-cors": "^6.0.1",
|
||||
"fastify-formbody": "^5.0.0",
|
||||
"fastify-helmet": "^5.3.1",
|
||||
"fastify-nuxtjs": "^1.0.1",
|
||||
"fastify-rate-limit": "^5.5.0",
|
||||
"fastify-static": "^4.2.2",
|
||||
"ffmpeg-probe": "^1.0.6",
|
||||
"file-saver": "^2.0.1",
|
||||
"file-type": "^16.5.0",
|
||||
"fluent-ffmpeg": "^2.1.2",
|
||||
"fs-jetpack": "^4.1.0",
|
||||
"helmet": "^4.5.0",
|
||||
"imagesloaded": "^4.1.4",
|
||||
"joi": "^17.3.0",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"masonry-layout": "^4.2.2",
|
||||
"moment": "^2.29.1",
|
||||
"morgan": "^1.10.0",
|
||||
"nuxt": "^2.14.12",
|
||||
"nuxt-dropzone": "^0.2.8",
|
||||
"pino-pretty": "^5.0.2",
|
||||
"qoa": "^0.2.0",
|
||||
"randomstring": "^1.2.1",
|
||||
"rotating-file-stream": "^2.1.5",
|
||||
"search-query-parser": "^1.6.0",
|
||||
"sharp": "^0.28.3",
|
||||
"systeminformation": "^5.7.7",
|
||||
|
@ -104,10 +106,9 @@
|
|||
"cpy-cli": "^3.1.1",
|
||||
"cross-env": "^7.0.3",
|
||||
"eslint": "^7.24.0",
|
||||
"eslint-config-aqua": "^9.0.2",
|
||||
"eslint-config-marine": "^9.0.6",
|
||||
"eslint-import-resolver-nuxt": "^1.0.1",
|
||||
"eslint-plugin-vue": "^5.2.1",
|
||||
"eslint-plugin-vue": "^7.11.1",
|
||||
"jest": "^27.0.4",
|
||||
"jest-serializer-vue": "^2.0.2",
|
||||
"node-sass": "^6.0.0",
|
||||
|
@ -122,27 +123,17 @@
|
|||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"marine/node",
|
||||
"marine/vue"
|
||||
"marine/node"
|
||||
],
|
||||
"parserOptions": {
|
||||
"parser": "babel-eslint",
|
||||
"sourceType": "module"
|
||||
},
|
||||
"settings": {
|
||||
"import/resolver": {
|
||||
"nuxt": {
|
||||
"nuxtSrcDir": "./src/site",
|
||||
"extensions": [
|
||||
".js",
|
||||
".vue"
|
||||
]
|
||||
}
|
||||
}
|
||||
"rules": {
|
||||
"comma-dangle": [
|
||||
"error",
|
||||
"never"
|
||||
]
|
||||
}
|
||||
},
|
||||
"prisma": {
|
||||
"schema": "src/prisma/schema.prisma"
|
||||
"schema": "src/api/prisma/schema.prisma"
|
||||
},
|
||||
"nodemonConfig": {
|
||||
"watch": [
|
||||
|
|
103
src/api/main.ts
103
src/api/main.ts
|
@ -1,10 +1,11 @@
|
|||
import express from 'express';
|
||||
import helmet from 'helmet';
|
||||
import cors from 'cors';
|
||||
import morgan from 'morgan';
|
||||
// import express from 'express';
|
||||
import fastify from 'fastify';
|
||||
import helmet from 'fastify-helmet';
|
||||
import cors from 'fastify-cors';
|
||||
import formbody from 'fastify-formbody';
|
||||
import fstatic from 'fastify-static';
|
||||
import path from 'path';
|
||||
import rfs from 'rotating-file-stream';
|
||||
import rateLimit from 'express-rate-limit';
|
||||
import rateLimit from 'fastify-rate-limit';
|
||||
import jetpack from 'fs-jetpack';
|
||||
import cron from 'cron';
|
||||
// @ts-ignore - nuxt types can't be found - https://github.com/nuxt/nuxt.js/issues/7651
|
||||
|
@ -12,7 +13,42 @@ import { loadNuxt, build } from 'nuxt';
|
|||
|
||||
import Routes from './structures/routes';
|
||||
|
||||
const server = express();
|
||||
// const server = express();
|
||||
const server = fastify({
|
||||
trustProxy: true,
|
||||
logger: {
|
||||
serializers: {
|
||||
req(request) {
|
||||
return {
|
||||
ip: request.hostname,
|
||||
method: request.method,
|
||||
url: request.url
|
||||
};
|
||||
},
|
||||
res(reply) {
|
||||
return {
|
||||
statusCode: reply.statusCode
|
||||
};
|
||||
}
|
||||
},
|
||||
prettyPrint: {
|
||||
translateTime: 'SYS:yyyy-mm-dd HH:MM:ss.l',
|
||||
ignore: 'pid,hostname,reqId,req,res,responseTime',
|
||||
/*
|
||||
TODO:
|
||||
Find a way to merge incoming request and request completed into 1 line using
|
||||
messageFormat as a function instead of a string.
|
||||
Maybe even set a new flag under log like log.app instead of log.info
|
||||
to be able to print logs without req and res information
|
||||
|
||||
[2021-06-21 19:22:21.553] INFO: incoming request [localhost:5000 - GET /api/verify - ]
|
||||
[2021-06-21 19:22:21.556] INFO: request completed [ - - 401]
|
||||
*/
|
||||
messageFormat: '{msg} [{req.ip} - {req.method} {req.url} - {res.statusCode}]'
|
||||
}
|
||||
},
|
||||
connectionTimeout: 600000
|
||||
});
|
||||
|
||||
const start = async () => {
|
||||
// Create the folders needed for uploads
|
||||
|
@ -21,59 +57,50 @@ const start = async () => {
|
|||
jetpack.dir('uploads/thumbs/preview');
|
||||
|
||||
// Create the server and set it up
|
||||
server.use('trust proxy');
|
||||
server.use(helmet());
|
||||
server.use(cors());
|
||||
server.use(morgan('dev'));
|
||||
server.use(express.urlencoded({ extended: true }));
|
||||
server.use(express.json());
|
||||
server.use(cors({ allowedHeaders: ['Accept', 'Authorization', 'Cache-Control', 'X-Requested-With', 'Content-Type', 'albumId', 'finishedChunks'] }));
|
||||
server.use((req, res, next) => {
|
||||
void server.register(helmet);
|
||||
void server.register(cors, {
|
||||
allowedHeaders: ['Accept', 'Authorization', 'Cache-Control', 'X-Requested-With', 'Content-Type', 'albumId', 'finishedChunks']
|
||||
});
|
||||
void server.register(formbody);
|
||||
|
||||
server.addHook('onRequest', (req, reply, next) => {
|
||||
// This bypasses the headers.accept for album download, since it's accesed directly through the browser.
|
||||
if ((req.url.includes('/api/album/') || req.url.includes('/zip')) && req.method === 'GET') return next();
|
||||
// This bypasses the headers.accept if we are accessing the frontend
|
||||
if (!req.url.includes('/api/') && req.method === 'GET') return next();
|
||||
if (req.headers.accept?.includes('application/vnd.chibisafe.json')) return next();
|
||||
return res.status(405).json({ message: 'Incorrect `Accept` header provided' });
|
||||
return reply.status(405).send({ message: 'Incorrect `Accept` header provided' });
|
||||
});
|
||||
|
||||
// Set up logs for production and dev environments
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
const accessLogStream = rfs.createStream('access.log', {
|
||||
interval: '1d', // rotate daily
|
||||
path: path.join(__dirname, '../../../logs', 'log')
|
||||
});
|
||||
server.use(morgan('combined', { stream: accessLogStream }));
|
||||
} else {
|
||||
server.use(morgan('dev'));
|
||||
}
|
||||
|
||||
// Apply rate limiting to the api only
|
||||
server.use('/api/', rateLimit({
|
||||
windowMs: parseInt(process.env.RATE_LIMIT_WINDOW ?? '2000', 10),
|
||||
// TODO: Find a way to only apply this to /api routes
|
||||
void server.register(rateLimit, {
|
||||
global: false,
|
||||
max: parseInt(process.env.RATE_LIMIT_MAX ?? '5', 10),
|
||||
message: 'Too many requests from this IP. Slow down dude.'
|
||||
}));
|
||||
timeWindow: parseInt(process.env.RATE_LIMIT_WINDOW ?? '2000', 10)
|
||||
});
|
||||
|
||||
// Scan and load routes into express
|
||||
await Routes.load(server);
|
||||
|
||||
// Listen for incoming connections
|
||||
const listen = server.listen(process.env.port, () => {
|
||||
console.log(`> Chibisafe Server started on port ${process.env.port ?? 5000}.`);
|
||||
});
|
||||
listen.setTimeout(600000);
|
||||
void server.listen(process.env.port ?? 5000);
|
||||
|
||||
// Serve the uploads
|
||||
server.use(express.static(path.join(__dirname, '../../../uploads')));
|
||||
void server.register(fstatic, {
|
||||
root: path.join(__dirname, '../../uploads')
|
||||
});
|
||||
|
||||
// TODO: Enable this after Utils is ported to TypeScript
|
||||
/*
|
||||
const isProd = process.env.NODE_ENV === 'production';
|
||||
const nuxt = await loadNuxt(isProd ? 'start' : 'dev');
|
||||
server.use(nuxt.render);
|
||||
void server.register(nuxt.render);
|
||||
if (!isProd) build(nuxt);
|
||||
*/
|
||||
|
||||
// TODO: move into the database config. (we can just show the crontab line for start, later on we can add dropdowns and stuff)
|
||||
new cron.CronJob('0 0 * * * *', Util.saveStatsToDb, null, true);
|
||||
// new cron.CronJob('0 0 * * * *', Util.saveStatsToDb, null, true);
|
||||
};
|
||||
|
||||
void start();
|
||||
|
|
|
@ -1,32 +1,7 @@
|
|||
import { Request, Response, NextFunction } from 'express';
|
||||
import prisma from '../structures/database';
|
||||
import type { FastifyReply, HookHandlerDoneFunction } from 'fastify';
|
||||
import type { RequestWithUser } from './auth';
|
||||
|
||||
export default (req: RequestWithUser, res: Response, next: NextFunction) => {
|
||||
// if (this.options.adminOnly && !user.isAdmin) { return res.status(401).json({ message: 'Invalid authorization' }); }
|
||||
if (!req.headers.authorization) return res.status(401).json({ message: 'No authorization header provided' });
|
||||
|
||||
const token = req.headers.authorization.split(' ')[1];
|
||||
if (!token) return res.status(401).json({ message: 'No authorization header provided' });
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||
JWT.verify(token, process.env.secret ?? '', async (error, decoded) => {
|
||||
if (error) return res.status(401).json({ message: 'Invalid token' });
|
||||
const id = (decoded as Decoded | undefined)?.sub ?? null;
|
||||
if (!id) return res.status(401).json({ message: 'Invalid authorization' });
|
||||
|
||||
const user = await prisma.users.findFirst({
|
||||
where: {
|
||||
id
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
username: true
|
||||
}
|
||||
});
|
||||
|
||||
if (!user) return res.status(401).json({ message: 'Invalid authorization' });
|
||||
req.user = user;
|
||||
next();
|
||||
});
|
||||
export default (req: RequestWithUser, res: FastifyReply, next: HookHandlerDoneFunction) => {
|
||||
if (!req.user.isAdmin) return res.status(401).send({ message: 'Permission denied' });
|
||||
next();
|
||||
};
|
||||
|
|
|
@ -1,28 +1,29 @@
|
|||
import { Request, Response, NextFunction } from 'express';
|
||||
import type { FastifyRequest, FastifyReply, HookHandlerDoneFunction } from 'fastify';
|
||||
import JWT from 'jsonwebtoken';
|
||||
import prisma from '../structures/database';
|
||||
|
||||
interface Decoded {
|
||||
sub: number;
|
||||
}
|
||||
export interface RequestWithUser extends Request {
|
||||
export interface RequestWithUser extends FastifyRequest {
|
||||
user: {
|
||||
id: number;
|
||||
username: string | null;
|
||||
isAdmin: boolean | null;
|
||||
};
|
||||
}
|
||||
|
||||
export default (req: RequestWithUser, res: Response, next: NextFunction) => {
|
||||
if (!req.headers.authorization) return res.status(401).json({ message: 'No authorization header provided' });
|
||||
export default (req: RequestWithUser, res: FastifyReply, next: HookHandlerDoneFunction) => {
|
||||
if (!req.headers.authorization) return res.status(401).send({ message: 'No authorization header provided' });
|
||||
|
||||
const token = req.headers.authorization.split(' ')[1];
|
||||
if (!token) return res.status(401).json({ message: 'No authorization header provided' });
|
||||
if (!token) return res.status(401).send({ message: 'No authorization header provided' });
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||
JWT.verify(token, process.env.secret ?? '', async (error, decoded) => {
|
||||
if (error) return res.status(401).json({ message: 'Invalid token' });
|
||||
if (error) return res.status(401).send({ message: 'Invalid token' });
|
||||
const id = (decoded as Decoded | undefined)?.sub ?? null;
|
||||
if (!id) return res.status(401).json({ message: 'Invalid authorization' });
|
||||
if (!id) return res.status(401).send({ message: 'Invalid authorization' });
|
||||
|
||||
const user = await prisma.users.findFirst({
|
||||
where: {
|
||||
|
@ -30,11 +31,12 @@ export default (req: RequestWithUser, res: Response, next: NextFunction) => {
|
|||
},
|
||||
select: {
|
||||
id: true,
|
||||
username: true
|
||||
username: true,
|
||||
isAdmin: true
|
||||
}
|
||||
});
|
||||
|
||||
if (!user) return res.status(401).json({ message: 'Invalid authorization' });
|
||||
if (!user) return res.status(401).send({ message: 'User doesn\'t exist' });
|
||||
req.user = user;
|
||||
next();
|
||||
});
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
import type { FastifyRequest, FastifyReply, HookHandlerDoneFunction } from 'fastify';
|
||||
import prisma from '../structures/database';
|
||||
|
||||
export default async (req: FastifyRequest, res: FastifyReply, next: HookHandlerDoneFunction) => {
|
||||
const banned = await prisma.bans.findFirst({
|
||||
where: {
|
||||
ip: req.ip
|
||||
}
|
||||
});
|
||||
|
||||
if (banned) {
|
||||
return res.status(401).send({ message: 'This IP has been banned' });
|
||||
}
|
||||
next();
|
||||
};
|
|
@ -4,8 +4,8 @@ generator client {
|
|||
|
||||
datasource db {
|
||||
provider = "sqlite"
|
||||
url = "file:../../../../database/database.sqlite"
|
||||
shadowDatabaseUrl = "file:../../../../database/shadow.sqlite"
|
||||
url = "file:../../../database/database.sqlite"
|
||||
shadowDatabaseUrl = "file:../../../database/shadow.sqlite"
|
||||
}
|
||||
|
||||
model albums {
|
||||
|
|
|
@ -1,37 +0,0 @@
|
|||
import { Response } from 'express';
|
||||
import prisma from '../../../../structures/database';
|
||||
import type { RequestWithUser } from '../../../../middlewares/auth';
|
||||
|
||||
export const middlewares = ['auth'];
|
||||
|
||||
export const run = async (req: RequestWithUser, res: Response) => {
|
||||
if (!req.body) return res.status(400).json({ message: 'No body provided' });
|
||||
const { coinId, amount, fiatId, noCostTransaction, purchasePrice, label, feePrice, purchaseDate }: {
|
||||
coinId: number;
|
||||
amount: number;
|
||||
fiatId: number;
|
||||
noCostTransaction: boolean;
|
||||
purchasePrice: number;
|
||||
label: string;
|
||||
feePrice: number;
|
||||
purchaseDate: string;
|
||||
} = req.body;
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
if (!coinId || !amount || noCostTransaction === null || noCostTransaction === undefined) return res.sendStatus(400);
|
||||
|
||||
await prisma.wallet.create({
|
||||
data: {
|
||||
userId: req.user.id,
|
||||
coinId,
|
||||
amount,
|
||||
fiatId,
|
||||
purchasePrice,
|
||||
label,
|
||||
feePrice,
|
||||
paidPrice: amount * purchasePrice,
|
||||
purchaseDate
|
||||
}
|
||||
});
|
||||
|
||||
return res.sendStatus(200);
|
||||
};
|
|
@ -0,0 +1,44 @@
|
|||
import type { FastifyRequest, FastifyReply } from 'fastify';
|
||||
import prisma from '../../../structures/database';
|
||||
import bcrypt from 'bcryptjs';
|
||||
import JWT from 'jsonwebtoken';
|
||||
|
||||
interface body {
|
||||
username: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
export const run = async (req: FastifyRequest, res: FastifyReply) => {
|
||||
if (!req.body) return res.status(400).send({ message: 'No body provided' });
|
||||
// const { username, password }: { username: string; password: string } = req.body;
|
||||
const { username, password } = req.body as body;
|
||||
if (!username || !password) return res.status(400).send();
|
||||
|
||||
const user = await prisma.users.findFirst({
|
||||
where: {
|
||||
username
|
||||
}
|
||||
});
|
||||
|
||||
if (!user) return res.status(401).send({ message: 'User doesn\'t exist' });
|
||||
|
||||
const comparePassword = await bcrypt.compare(password, user.password ?? '');
|
||||
if (!comparePassword) return res.status(401).send({ message: 'Invalid authorization.' });
|
||||
|
||||
const jwt = JWT.sign({
|
||||
iss: 'chibisafe',
|
||||
sub: user.id,
|
||||
iat: new Date().getTime()
|
||||
}, process.env.secret ?? '', { expiresIn: '30d' });
|
||||
|
||||
return res.send({
|
||||
message: 'Successfully logged in.',
|
||||
user: {
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
apiKey: user.apiKey,
|
||||
isAdmin: user.isAdmin
|
||||
},
|
||||
token: jwt
|
||||
});
|
||||
};
|
|
@ -0,0 +1,45 @@
|
|||
import type { FastifyRequest, FastifyReply } from 'fastify';
|
||||
import prisma from '../../../structures/database';
|
||||
import bcrypt from 'bcryptjs';
|
||||
|
||||
interface body {
|
||||
username: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
export const run = async (req: FastifyRequest, res: FastifyReply) => {
|
||||
if (!req.body) return res.status(400).send({ message: 'No body provided' });
|
||||
const { username, password } = req.body as body;
|
||||
if (!username || !password) return res.status(400).send();
|
||||
|
||||
if (username.length < 4 || username.length > 32) {
|
||||
return res.status(400).send({ message: 'Username must have 4-32 characters' });
|
||||
}
|
||||
if (password.length < 6 || password.length > 64) {
|
||||
return res.status(400).send({ message: 'Password must have 6-64 characters' });
|
||||
}
|
||||
|
||||
const exists = await prisma.users.findFirst({
|
||||
where: {
|
||||
username
|
||||
}
|
||||
});
|
||||
|
||||
if (exists) return res.status(401).send({ message: 'Username already exists' });
|
||||
|
||||
let hash;
|
||||
try {
|
||||
hash = await bcrypt.hash(password, 10);
|
||||
} catch (err) {
|
||||
req.log.error(err);
|
||||
return res.status(401).send({ message: 'There was a problem processing your account' });
|
||||
}
|
||||
|
||||
await prisma.users.create({
|
||||
data: {
|
||||
username,
|
||||
password: hash
|
||||
}
|
||||
});
|
||||
return res.send({ message: 'The account was created successfully' });
|
||||
};
|
|
@ -0,0 +1,6 @@
|
|||
import type { FastifyReply } from 'fastify';
|
||||
import { RequestWithUser } from '../../middlewares/auth';
|
||||
|
||||
export const middlewares = ['auth'];
|
||||
|
||||
export const run = (req: RequestWithUser, res: FastifyReply) => res.status(200).send(req.user);
|
|
@ -1,54 +0,0 @@
|
|||
const nodePath = require('path');
|
||||
const Knex = require('knex');
|
||||
|
||||
// eslint-disable-next-line func-names
|
||||
Knex.QueryBuilder.extend('wasMutated', function() {
|
||||
this.client.config.userParams.lastMutationTime = Date.now();
|
||||
return this;
|
||||
});
|
||||
|
||||
const db = Knex({
|
||||
client: process.env.DB_CLIENT,
|
||||
connection: {
|
||||
host: process.env.DB_HOST,
|
||||
user: process.env.DB_USER,
|
||||
password: process.env.DB_PASSWORD,
|
||||
database: process.env.DB_DATABASE,
|
||||
filename: nodePath.join(__dirname, '../../../database/database.sqlite')
|
||||
},
|
||||
postProcessResponse: result => {
|
||||
/*
|
||||
Fun fact: Depending on the database used by the user and given that I don't want
|
||||
to force a specific database for everyone because of the nature of this project,
|
||||
some things like different data types for booleans need to be considered like in
|
||||
the implementation below where sqlite returns 1 and 0 instead of true and false.
|
||||
*/
|
||||
const booleanFields = ['enabled', 'enableDownload', 'isAdmin', 'nsfw', 'generateZips', 'publicMode', 'userAccounts'];
|
||||
|
||||
const processResponse = row => {
|
||||
Object.keys(row).forEach(key => {
|
||||
if (booleanFields.includes(key)) {
|
||||
if (row[key] === 0) row[key] = false;
|
||||
else if (row[key] === 1) row[key] = true;
|
||||
}
|
||||
});
|
||||
return row;
|
||||
};
|
||||
|
||||
if (Array.isArray(result)) return result.map(row => processResponse(row));
|
||||
if (typeof result === 'object') return processResponse(result);
|
||||
return result;
|
||||
},
|
||||
useNullAsDefault: process.env.DB_CLIENT === 'sqlite3',
|
||||
log: {
|
||||
warn: msg => {
|
||||
if (typeof msg === 'string' && msg.startsWith('.returning()')) return;
|
||||
console.warn(msg);
|
||||
}
|
||||
},
|
||||
userParams: {
|
||||
lastMutationTime: null
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = db;
|
|
@ -5,33 +5,3 @@ export interface User {
|
|||
password: string;
|
||||
stravaLink: string;
|
||||
}
|
||||
|
||||
export interface Segment {
|
||||
id: number;
|
||||
name: string;
|
||||
description: string;
|
||||
route: string;
|
||||
stravaLink: string;
|
||||
flagCount: number;
|
||||
removed: boolean;
|
||||
chunkyness: number;
|
||||
wayType: string;
|
||||
userId: number;
|
||||
}
|
||||
|
||||
export interface Rating {
|
||||
id: number;
|
||||
rating: number;
|
||||
comment: string;
|
||||
segmentId: number;
|
||||
}
|
||||
|
||||
export interface Picture {
|
||||
id: number;
|
||||
name: string;
|
||||
description?: string;
|
||||
path?: string;
|
||||
lat?: string;
|
||||
lng?: string;
|
||||
segmentId: number;
|
||||
}
|
||||
|
|
|
@ -1,14 +1,20 @@
|
|||
import { Application, Request, Response } from 'express';
|
||||
import type { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify';
|
||||
import jetpack from 'fs-jetpack';
|
||||
import path from 'path';
|
||||
|
||||
export default {
|
||||
load: async (server: Application) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||
for (const routeFile of await jetpack.findAsync(path.join(__dirname, '..', 'routes'), { matching: '*.{ts,js}' })) {
|
||||
load: async (server: FastifyInstance) => {
|
||||
/*
|
||||
While in development we only want to match routes written in TypeScript but for production
|
||||
we need to change it to javascript files since they will be compiled.
|
||||
TODO: Would running the TypeScript files directly with something like ts-node be a good move?
|
||||
*/
|
||||
|
||||
const matching = `*.${process.env.NODE_ENV === 'production' ? 'j' : 't'}s`;
|
||||
for (const routeFile of await jetpack.findAsync(path.join(__dirname, '..', 'routes'), { matching })) {
|
||||
try {
|
||||
const slash = process.platform === 'win32' ? '\\' : '/';
|
||||
const replace = process.env.NODE_ENV === 'production' ? `dist${slash}` : `src${slash}`;
|
||||
const replace = process.env.NODE_ENV === 'production' ? `dist${slash}` : `src${slash}api${slash}`;
|
||||
const route = await import(routeFile.replace(replace, `..${slash}`));
|
||||
const paths: Array<string> = routeFile.split(slash);
|
||||
const method = paths[paths.length - 1].split('.')[0];
|
||||
|
@ -16,40 +22,45 @@ export default {
|
|||
// Get rid of the filename
|
||||
paths.pop();
|
||||
|
||||
// Get rid of the src/routes part
|
||||
paths.splice(0, 2);
|
||||
// Get rid of the src/api/routes part
|
||||
paths.splice(0, 3);
|
||||
|
||||
let routePath: string = paths.join(slash);
|
||||
let url: string = paths.join(slash);
|
||||
|
||||
// Transform path variables to express variables
|
||||
routePath = routePath.replace('_', ':');
|
||||
url = url.replace('_', ':');
|
||||
|
||||
// Append the missing /
|
||||
routePath = `/${routePath}`;
|
||||
url = `/${url}`;
|
||||
|
||||
// Build final route
|
||||
const prefix = route.options?.ignoreRoutePrefix ? '' : process.env.routePrefix ?? '';
|
||||
routePath = `${prefix}${routePath}`;
|
||||
url = `${route.options?.ignoreRoutePrefix ? '' : '/api'}${url}`;
|
||||
|
||||
// Run middlewares if any
|
||||
// TODO: This is loading all middlewares if any, wrong.
|
||||
// TODO: Also the middlewares need to be run in the correct order, auth being the first one.
|
||||
// Run middlewares if any, and in order of execution
|
||||
const middlewares: any[] = [];
|
||||
if (route.middlewares?.length) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||
for (const middlewareFile of await jetpack.findAsync(path.join(__dirname, '..', 'middlewares'), { matching: '*.{ts,js}' })) {
|
||||
const middleware = await import(middlewareFile.replace(replace, `..${slash}`));
|
||||
middlewares.push(middleware.default);
|
||||
// Set default middlewares that need to be included
|
||||
route.middlewares.unshift('ban');
|
||||
// Load the middlewares defined in the route file
|
||||
for (const middleware of route.middlewares) {
|
||||
const importedMiddleware = await import(path.join(__dirname, '..', 'middlewares', middleware));
|
||||
middlewares.push(importedMiddleware.default);
|
||||
}
|
||||
}
|
||||
|
||||
// Register the route in Express
|
||||
(server as any)[method](routePath, ...middlewares, (req: Request, res: Response) => route.run(req, res));
|
||||
console.log(`Found route ${method.toUpperCase()} ${routePath}`);
|
||||
// Register the route in Fastify
|
||||
server.route({
|
||||
method: method.toUpperCase() as any,
|
||||
url,
|
||||
preHandler: middlewares,
|
||||
handler: (req: FastifyRequest, res: FastifyReply) => route.run(req, res)
|
||||
});
|
||||
|
||||
server.log.info(`Found route ${method.toUpperCase()} ${url}`);
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
||||
console.log(`${routeFile} :: ERROR`);
|
||||
console.log(error);
|
||||
server.log.error(`${routeFile} :: ERROR`);
|
||||
server.log.error(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,4 @@
|
|||
require('dotenv').config();
|
||||
|
||||
const Joi = require('joi');
|
||||
const { env } = process;
|
||||
import Joi from 'joi';
|
||||
|
||||
const StatsGenerator = require('../utils/StatsGenerator');
|
||||
|
||||
|
|
|
@ -1,20 +0,0 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"moduleResolution": "Node",
|
||||
"esModuleInterop": true,
|
||||
"sourceMap": true,
|
||||
"strict": true,
|
||||
"target": "esnext",
|
||||
"module": "commonjs",
|
||||
"jsx": "preserve",
|
||||
"lib": ["esnext", "dom"],
|
||||
"rootDir": "./",
|
||||
"outDir": "../../dist",
|
||||
"sourceRoot": "./",
|
||||
"types": [
|
||||
"@types/node",
|
||||
"@nuxt/types"
|
||||
]
|
||||
},
|
||||
"include": ["**/*.ts", "**/*.d.ts", "../main.ts"]
|
||||
}
|
|
@ -4,31 +4,17 @@
|
|||
"esModuleInterop": true,
|
||||
"sourceMap": true,
|
||||
"strict": true,
|
||||
"target": "ES2018",
|
||||
"module": "ESNext",
|
||||
"lib": [
|
||||
"ESNext",
|
||||
"ESNext.AsyncIterable",
|
||||
"DOM"
|
||||
],
|
||||
"allowJs": true,
|
||||
"noEmit": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"~/*": [
|
||||
"./*"
|
||||
],
|
||||
"@/*": [
|
||||
"./*"
|
||||
]
|
||||
},
|
||||
"target": "esnext",
|
||||
"module": "commonjs",
|
||||
"jsx": "preserve",
|
||||
"lib": ["esnext", "dom"],
|
||||
"rootDir": "./src/api",
|
||||
"outDir": "./dist",
|
||||
"sourceRoot": "./",
|
||||
"types": [
|
||||
"@nuxt/types",
|
||||
"@nuxtjs/axios",
|
||||
"@nuxtjs/color-mode"
|
||||
]
|
||||
"@types/node",
|
||||
"@nuxt/types"
|
||||
]
|
||||
},
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
"include": ["**/*.ts", "**/*.d.ts"]
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue