bjarneo 0c86efd56c
refactor: use fastify vite for the dev server (#284)
* refactor: use fastify vite for the server

by doing this we do not need to have local hacks for the routes. No local proxy needed. Everything just works.

* fix: update the dockerfile build path

* fix: update package.json

* fix: fonts path
2024-03-11 13:43:20 +01:00

272 lines
7.5 KiB
JavaScript

import validator from 'validator';
import getClientIp from '../helpers/client-ip.js';
import { compare, hash } from '../helpers/password.js';
import VALID_TTL from '../helpers/validate-ttl.js';
import prisma from '../services/prisma.js';
import { validUsername } from './authentication.js';
import { isValidSecretId } from '../helpers/regexp.js';
const DEFAULT_EXPIRATION = 60 * 60 * 24 * 1000;
const ipCheck = (ip) => {
if (ip === 'localhost') {
return true;
}
if (!validator.isIP(ip) && !validator.isIPRange(ip)) {
return false;
}
return validator.isIP(ip) || validator.isIPRange(ip);
};
async function getSecretRoute(request, reply) {
const { id } = request.params;
const { password = '' } = request.body ? request.body : {};
// If it does not match the valid characters set for nanoid, return 403
if (!isValidSecretId.test(id)) {
return reply.code(403).send({ error: 'Not a valid secret ID' });
}
const data = await prisma.secret.findFirst({
where: {
id,
expiresAt: { gte: new Date() },
},
include: { files: true },
});
if (!data) {
return reply.code(404).send({ error: 'Secret not found' });
}
if (data.password) {
const isPasswordValid = await compare(password, data.password);
if (!isPasswordValid) {
return reply.code(401).send({ error: 'Wrong password!' });
}
}
if (data.maxViews > 1) {
await prisma.secret.update({
where: {
id: data.id,
},
data: {
maxViews: {
decrement: 1,
},
},
});
}
if (!data.preventBurn && data.maxViews === 1) {
await prisma.file.deleteMany({ where: { secretId: id } });
await prisma.secret.delete({ where: { id } });
}
return {
title: data.title,
preventBurn: data.preventBurn,
secret: data.data,
files: data.files,
isPublic: data.isPublic,
};
}
async function secret(fastify) {
fastify.get(
'/:id',
{
preValidation: [fastify.allowedIp],
},
getSecretRoute
);
fastify.post(
'/:id',
{
preValidation: [fastify.allowedIp],
},
getSecretRoute
);
fastify.get(
'/',
{
preValidation: [fastify.authenticate],
},
async (req, reply) => {
const { user_id } = req.user;
const secrets = await prisma.secret.findMany({
where: {
user_id: validator.isUUID(user_id) ? user_id : '',
},
include: { files: true },
});
return reply.code(200).send(
secrets.map((secret) => ({
id: secret.id,
expiresAt: secret.expiresAt,
isPublic: secret.isPublic,
title: secret.title,
}))
);
}
);
fastify.post(
'/',
{
preValidation: [fastify.userFeatures, fastify.attachment],
schema: {
// Add a schema to define expected input
body: {
type: 'object',
required: ['text', 'ttl'],
properties: {
text: { type: 'string' },
title: { type: 'string', maxLength: 255 },
ttl: { type: 'integer', minimum: 1, enum: VALID_TTL },
password: { type: 'string' },
allowedIp: { type: 'string' },
preventBurn: { type: 'boolean' },
maxViews: { type: 'integer', minimum: 1, maximum: 999 },
isPublic: { type: 'boolean' },
},
},
},
},
async (req, reply) => {
const { text, title, ttl, password, allowedIp, preventBurn, maxViews, isPublic } =
req.body;
const { files } = req.secret;
if (allowedIp && !ipCheck(allowedIp)) {
return reply.code(400).send({ message: 'The IP address is not valid' });
}
const secret = await prisma.secret.create({
data: {
title,
maxViews,
data: text,
allowed_ip: allowedIp,
password: password ? await hash(password) : undefined,
preventBurn,
isPublic,
files: {
create: files,
},
user_id: req?.user?.user_id ?? null,
expiresAt: new Date(
Date.now() + (parseInt(ttl) ? parseInt(ttl) * 1000 : DEFAULT_EXPIRATION)
),
ipAddress: isPublic ? getClientIp(req.headers) : '',
},
});
await prisma.statistic.upsert({
where: {
id: 'secrets_created',
},
update: {
value: {
increment: 1,
},
},
create: { id: 'secrets_created' },
});
return reply.code(201).send({
id: secret.id,
});
}
);
// This will burn the secret 🔥
fastify.post('/:id/burn', async (request, reply) => {
const { id } = request.params;
if (!isValidSecretId.test(id)) {
return reply.code(403).send({ error: 'Not a valid secret id' });
}
const response = await prisma.secret.delete({ where: { id } });
if (!response) {
return { error: 'Secret can not be burned before the expiration date' };
} else {
return { success: 'Secret is burned' };
}
});
fastify.get('/:id/exist', async (request, reply) => {
const { id } = request.params;
if (!isValidSecretId.test(id)) {
return reply.code(403).send({ error: 'Not a valid secret id' });
}
const data = await prisma.secret.findFirst({
where: { id },
});
if (!data) {
return reply.code(404).send({ error: 'Secret not found' });
}
if (data.password) {
return reply.code(401).send({ error: 'Password required' });
}
return { id, maxViews: data.maxViews };
});
async function getPublicRoute(request, reply) {
const { username } = request.params;
const where = { isPublic: true };
if (username && !validUsername.test(username)) {
return reply.code(403).send([]);
}
if (username) {
where.user = {
username,
};
}
const data = await prisma.secret.findMany({
where,
orderBy: {
createdAt: 'desc',
},
take: 100,
select: {
id: true,
expiresAt: true,
title: true,
createdAt: true,
user: {
select: {
username: true,
},
},
},
});
return data;
}
fastify.get('/public/', getPublicRoute);
fastify.get('/public/:username', getPublicRoute);
}
export default secret;