Compare commits
15 Commits
master
...
feature/ty
Author | SHA1 | Date |
---|---|---|
Pitu | beb529e483 | |
Pitu | b0ab951a98 | |
Pitu | 858f0ae4b0 | |
Pitu | 9321750461 | |
Pitu | 7114830582 | |
Pitu | b924fbd314 | |
Pitu | af0e46e552 | |
Pitu | 77db6f34c6 | |
Pitu | 8dd9b20d20 | |
Pitu | a634cefb0c | |
Pitu | bc5ca732b0 | |
Pitu | ff80ea8431 | |
Pitu | 15674358f1 | |
Pitu | 01e85b5e2f | |
Pitu | c8f19898ae |
|
@ -1 +1 @@
|
|||
* text=auto eol=lf
|
||||
* text=auto eol=lf
|
||||
|
|
Binary file not shown.
23
knexfile.js
23
knexfile.js
|
@ -1,23 +0,0 @@
|
|||
require('dotenv').config();
|
||||
|
||||
module.exports = {
|
||||
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: 'database/database.sqlite'
|
||||
},
|
||||
pool: {
|
||||
min: process.env.DATABASE_POOL_MIN || 2,
|
||||
max: process.env.DATABASE_POOL_MAX || 10
|
||||
},
|
||||
migrations: {
|
||||
directory: 'src/api/database/migrations'
|
||||
},
|
||||
seeds: {
|
||||
directory: 'src/api/database/seeds'
|
||||
},
|
||||
useNullAsDefault: process.env.DB_CLIENT === 'sqlite3'
|
||||
};
|
File diff suppressed because it is too large
Load Diff
138
package.json
138
package.json
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "chibisafe",
|
||||
"version": "4.0.2",
|
||||
"version": "5.0.0",
|
||||
"description": "Blazing fast file uploader and bunker written in node! 🚀",
|
||||
"license": "MIT",
|
||||
"author": {
|
||||
|
@ -9,9 +9,10 @@
|
|||
"url": "https://github.com/Pitu"
|
||||
},
|
||||
"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",
|
||||
"setup": "node src/api/scripts/setup.ts && npm run migrate && npm run seed",
|
||||
"build": "tsc --noEmit",
|
||||
"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",
|
||||
|
@ -30,57 +31,53 @@
|
|||
"url": "https://github.com/WeebDev/chibisafe/issues"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
"node": ">=16.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@mdi/font": "^5.8.55",
|
||||
"@nuxtjs/axios": "^5.12.5",
|
||||
"adm-zip": "^0.4.13",
|
||||
"bcrypt": "^5.0.1",
|
||||
"@prisma/client": "^2.29.1",
|
||||
"adm-zip": "^0.5.5",
|
||||
"axios": "^0.21.1",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"better-sqlite3": "^7.1.5",
|
||||
"blake3": "^2.1.4",
|
||||
"body-parser": "^1.18.3",
|
||||
"buefy": "^0.9.4",
|
||||
"busboy": "^0.2.14",
|
||||
"chalk": "^2.4.1",
|
||||
"chrono-node": "^2.1.4",
|
||||
"compression": "^1.7.2",
|
||||
"busboy": "^0.3.1",
|
||||
"chrono-node": "^2.3.0",
|
||||
"cookie-universal-nuxt": "^2.0.14",
|
||||
"cors": "^2.8.5",
|
||||
"cron": "^1.8.2",
|
||||
"dotenv": "^6.2.0",
|
||||
"dumper.js": "^1.3.1",
|
||||
"dotenv": "^8.2.0",
|
||||
"express": "^4.17.1",
|
||||
"express-rate-limit": "^3.4.0",
|
||||
"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.1.0",
|
||||
"file-type": "^16.5.0",
|
||||
"fluent-ffmpeg": "^2.1.2",
|
||||
"fs-jetpack": "^2.2.2",
|
||||
"helmet": "^3.15.1",
|
||||
"fs-jetpack": "^4.1.0",
|
||||
"imagesloaded": "^4.1.4",
|
||||
"joi": "^17.3.0",
|
||||
"jsonwebtoken": "^8.5.0",
|
||||
"knex": "^0.21.15",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"masonry-layout": "^4.2.2",
|
||||
"moment": "^2.24.0",
|
||||
"morgan": "^1.10.0",
|
||||
"multer": "^1.4.1",
|
||||
"mysql": "^2.16.0",
|
||||
"moment": "^2.29.1",
|
||||
"nuxt": "^2.14.12",
|
||||
"nuxt-dropzone": "^0.2.8",
|
||||
"pg": "^7.8.1",
|
||||
"pino-pretty": "^5.0.2",
|
||||
"qoa": "^0.2.0",
|
||||
"randomstring": "^1.1.5",
|
||||
"rotating-file-stream": "^2.1.3",
|
||||
"search-query-parser": "^1.5.5",
|
||||
"serve-static": "^1.13.2",
|
||||
"sharp": "^0.27.0",
|
||||
"sqlite3": "^5.0.0",
|
||||
"systeminformation": "^4.34.5",
|
||||
"uuid": "^3.3.2",
|
||||
"randomstring": "^1.2.1",
|
||||
"search-query-parser": "^1.6.0",
|
||||
"sharp": "^0.28.3",
|
||||
"systeminformation": "^5.7.7",
|
||||
"uuid": "^8.3.2",
|
||||
"v-clipboard": "^2.2.1",
|
||||
"vue-axios": "^2.1.4",
|
||||
"vue-isyourpasswordsafe": "^1.0.2",
|
||||
"vue-isyourpasswordsafe": "^2.0.0",
|
||||
"vue-plyr": "^5.1.0",
|
||||
"vue-timeago": "^3.4.4",
|
||||
"vue2-transitions": "^0.2.3",
|
||||
|
@ -89,48 +86,61 @@
|
|||
"devDependencies": {
|
||||
"@babel/core": "^7.12.10",
|
||||
"@babel/preset-env": "^7.12.11",
|
||||
"@vue/test-utils": "^1.1.2",
|
||||
"@nuxt/types": "2.15.7",
|
||||
"@types/adm-zip": "^0.4.34",
|
||||
"@types/bcryptjs": "^2.4.2",
|
||||
"@types/cors": "^2.8.10",
|
||||
"@types/cron": "^1.7.2",
|
||||
"@types/express": "^4.17.11",
|
||||
"@types/express-rate-limit": "^5.1.2",
|
||||
"@types/fluent-ffmpeg": "^2.1.17",
|
||||
"@types/jsonwebtoken": "^8.5.1",
|
||||
"@types/morgan": "^1.9.2",
|
||||
"@types/node": "^14.14.41",
|
||||
"@types/randomstring": "^1.1.6",
|
||||
"@types/sharp": "^0.27.1",
|
||||
"@types/systeminformation": "^3.54.1",
|
||||
"@types/uuid": "^8.3.0",
|
||||
"@typescript-eslint/eslint-plugin": "^4.22.0",
|
||||
"@typescript-eslint/parser": "^4.22.0",
|
||||
"@vue/test-utils": "^1.2.1",
|
||||
"autoprefixer": "^9.4.7",
|
||||
"axios": "^0.21.1",
|
||||
"babel-core": "^7.0.0-bridge.0",
|
||||
"babel-eslint": "^10.0.1",
|
||||
"babel-jest": "^26.6.3",
|
||||
"cross-env": "^5.2.0",
|
||||
"eslint": "^7.17.0",
|
||||
"eslint-config-aqua": "^7.3.0",
|
||||
"babel-jest": "^27.0.2",
|
||||
"cpy-cli": "^3.1.1",
|
||||
"cross-env": "^7.0.3",
|
||||
"eslint": "^7.24.0",
|
||||
"eslint-config-marine": "^9.0.6",
|
||||
"eslint-import-resolver-nuxt": "^1.0.1",
|
||||
"eslint-plugin-vue": "^5.2.1",
|
||||
"jest": "^26.6.3",
|
||||
"eslint-plugin-vue": "^7.11.1",
|
||||
"jest": "^27.0.4",
|
||||
"jest-serializer-vue": "^2.0.2",
|
||||
"node-sass": "^5.0.0",
|
||||
"nodemon": "^1.19.4",
|
||||
"node-sass": "^6.0.0",
|
||||
"nodemon": "^2.0.7",
|
||||
"postcss-css-variables": "^0.11.0",
|
||||
"postcss-nested": "^3.0.0",
|
||||
"puppeteer": "^5.5.0",
|
||||
"sass-loader": "^10.1.0",
|
||||
"postcss-nested": "^5.0.5",
|
||||
"prisma": "^2.29.1",
|
||||
"sass-loader": "^12.1.0",
|
||||
"ts-node": "^9.1.1",
|
||||
"typescript": "^4.2.2",
|
||||
"vue-jest": "^3.0.7"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"aqua/node",
|
||||
"aqua/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/api/prisma/schema.prisma"
|
||||
},
|
||||
"nodemonConfig": {
|
||||
"watch": [
|
||||
"src/api/*"
|
||||
|
@ -149,6 +159,6 @@
|
|||
"images"
|
||||
],
|
||||
"volta": {
|
||||
"node": "14.17.0"
|
||||
"node": "16.3.0"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,120 @@
|
|||
// 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 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
|
||||
import { Nuxt, Builder } from 'nuxt';
|
||||
import nuxtDefaults from './structures/nuxt';
|
||||
|
||||
import Routes from './structures/routes';
|
||||
|
||||
// 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
|
||||
jetpack.dir('uploads/chunks');
|
||||
jetpack.dir('uploads/thumbs/square');
|
||||
jetpack.dir('uploads/thumbs/preview');
|
||||
|
||||
// Create the server and set it up
|
||||
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 reply.status(405).send({ message: 'Incorrect `Accept` header provided' });
|
||||
});
|
||||
|
||||
// Apply rate limiting to the api only
|
||||
// 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),
|
||||
timeWindow: parseInt(process.env.RATE_LIMIT_WINDOW ?? '2000', 10)
|
||||
});
|
||||
|
||||
// Scan and load routes into express
|
||||
await Routes.load(server);
|
||||
|
||||
// Listen for incoming connections
|
||||
void server.listen(process.env.port ?? 5000);
|
||||
|
||||
// Serve the uploads
|
||||
void server.register(fstatic, {
|
||||
root: path.join(__dirname, '../../uploads')
|
||||
});
|
||||
|
||||
const nuxtConfig = await nuxtDefaults();
|
||||
nuxtConfig.dev = !(process.env.NODE_ENV === 'production');
|
||||
const nuxt = new Nuxt(nuxtConfig);
|
||||
if (nuxtConfig.dev) {
|
||||
const builder = new Builder(nuxt);
|
||||
await builder.build();
|
||||
} else {
|
||||
await nuxt.ready();
|
||||
}
|
||||
|
||||
void server.register(nuxt.render);
|
||||
|
||||
|
||||
/*
|
||||
const isProd = process.env.NODE_ENV === 'production';
|
||||
const nuxt = await loadNuxt(isProd ? 'start' : 'dev');
|
||||
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);
|
||||
};
|
||||
|
||||
export const log = server.log;
|
||||
void start();
|
|
@ -0,0 +1,7 @@
|
|||
import type { FastifyReply, HookHandlerDoneFunction } from 'fastify';
|
||||
import type { RequestWithUser } from '../structures/interfaces';
|
||||
|
||||
export default (req: RequestWithUser, res: FastifyReply, next: HookHandlerDoneFunction) => {
|
||||
if (!req.user.isAdmin) return res.status(401).send({ message: 'Permission denied' });
|
||||
next();
|
||||
};
|
|
@ -0,0 +1,21 @@
|
|||
import type { FastifyReply, HookHandlerDoneFunction } from 'fastify';
|
||||
import type { RequestWithUser } from '../structures/interfaces';
|
||||
import prisma from '../structures/database';
|
||||
|
||||
|
||||
export default async (req: RequestWithUser, res: FastifyReply, next: HookHandlerDoneFunction) => {
|
||||
// TODO: Search for canApiKey in the codebase and add this file as middleware on those, before auth
|
||||
const token = req.headers.token as string;
|
||||
if (!token) return next();
|
||||
const user = await prisma.users.findFirst({
|
||||
where: {
|
||||
apiKey: token
|
||||
}
|
||||
});
|
||||
|
||||
if (!user) return res.status(401).send({ message: 'Invalid authorization' });
|
||||
if (!user.enabled) return res.status(401).send({ message: 'This account has been disabled' });
|
||||
|
||||
req.user = user;
|
||||
next();
|
||||
};
|
|
@ -0,0 +1,37 @@
|
|||
import type { FastifyReply, HookHandlerDoneFunction } from 'fastify';
|
||||
import type { RequestWithUser } from '../structures/interfaces';
|
||||
import JWT from 'jsonwebtoken';
|
||||
import prisma from '../structures/database';
|
||||
|
||||
interface Decoded {
|
||||
sub: number;
|
||||
}
|
||||
|
||||
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).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).send({ message: 'Invalid token' });
|
||||
const id = (decoded as Decoded | undefined)?.sub ?? null;
|
||||
if (!id) return res.status(401).send({ message: 'Invalid authorization' });
|
||||
|
||||
const user = await prisma.users.findFirst({
|
||||
where: {
|
||||
id
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
username: true,
|
||||
isAdmin: true
|
||||
}
|
||||
});
|
||||
|
||||
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();
|
||||
};
|
|
@ -0,0 +1,119 @@
|
|||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
previewFeatures = ["selectRelationCount"]
|
||||
}
|
||||
|
||||
datasource db {
|
||||
provider = "sqlite"
|
||||
url = "file:../../../database/database.sqlite"
|
||||
shadowDatabaseUrl = "file:../../../database/shadow.sqlite"
|
||||
}
|
||||
|
||||
model albums {
|
||||
id Int @id @default(autoincrement())
|
||||
userId Int
|
||||
name String
|
||||
zippedAt DateTime?
|
||||
createdAt DateTime @default(now())
|
||||
editedAt DateTime?
|
||||
nsfw Boolean @default(false)
|
||||
|
||||
@@unique([userId, name], name: "albums_userid_name_unique")
|
||||
}
|
||||
|
||||
model albumsFiles {
|
||||
id Int @id @default(autoincrement())
|
||||
albumId Int
|
||||
fileId Int
|
||||
|
||||
@@unique([albumId, fileId], name: "albumsfiles_albumid_fileid_unique")
|
||||
}
|
||||
|
||||
model albumsLinks {
|
||||
id Int @id @default(autoincrement())
|
||||
albumId Int
|
||||
linkId Int @unique
|
||||
}
|
||||
|
||||
model bans {
|
||||
id Int @id @default(autoincrement())
|
||||
ip String
|
||||
createdAt DateTime @default(now())
|
||||
}
|
||||
|
||||
model fileTags {
|
||||
id Int @id @default(autoincrement())
|
||||
fileId Int
|
||||
tagId Int
|
||||
|
||||
@@unique([fileId, tagId], name: "filetags_fileid_tagid_unique")
|
||||
}
|
||||
|
||||
model files {
|
||||
id Int @id @default(autoincrement())
|
||||
userId Int?
|
||||
name String
|
||||
original String
|
||||
type String
|
||||
size Int
|
||||
hash String
|
||||
ip String
|
||||
createdAt DateTime @default(now())
|
||||
editedAt DateTime?
|
||||
}
|
||||
|
||||
model links {
|
||||
id Int @id @default(autoincrement())
|
||||
userId Int
|
||||
albumId Int
|
||||
identifier String @unique
|
||||
views Int @default(0)
|
||||
enabled Boolean @default(true)
|
||||
enableDownload Boolean @default(false)
|
||||
expiresAt DateTime?
|
||||
createdAt DateTime @default(now())
|
||||
editedAt DateTime?
|
||||
|
||||
@@unique([userId, identifier], name: "links_userid_identifier_unique")
|
||||
}
|
||||
|
||||
model settings {
|
||||
id Int @id @default(autoincrement())
|
||||
key String
|
||||
value String
|
||||
}
|
||||
|
||||
model statistics {
|
||||
id Int @id @default(autoincrement())
|
||||
batchId Int?
|
||||
type String?
|
||||
// TODO: This was JSON before so make sure to stringify and parse when saving stats
|
||||
data String?
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
@@unique([batchId, type], name: "statistics_batchid_type_unique")
|
||||
}
|
||||
|
||||
model tags {
|
||||
id Int @id @default(autoincrement())
|
||||
uuid String
|
||||
userId Int
|
||||
name String
|
||||
createdAt DateTime @default(now())
|
||||
editedAt DateTime?
|
||||
|
||||
@@unique([userId, name], name: "tags_userid_name_unique")
|
||||
}
|
||||
|
||||
model users {
|
||||
id Int @id @default(autoincrement())
|
||||
username String @unique
|
||||
password String
|
||||
enabled Boolean @default(true)
|
||||
isAdmin Boolean @default(false)
|
||||
apiKey String? @unique
|
||||
passwordEditedAt DateTime?
|
||||
apiKeyEditedAt DateTime?
|
||||
createdAt DateTime @default(now())
|
||||
editedAt DateTime?
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
import type { FastifyRequest, FastifyReply } from 'fastify';
|
||||
import prisma from '../../../../structures/database';
|
||||
|
||||
interface body {
|
||||
ip: string;
|
||||
}
|
||||
export const middlewares = ['auth', 'admin'];
|
||||
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 { ip } = req.body as body;
|
||||
if (!ip) return res.status(400).send({ message: 'No ip provided' });
|
||||
|
||||
await prisma.bans.create({
|
||||
data: {
|
||||
ip
|
||||
}
|
||||
});
|
||||
|
||||
return res.send({
|
||||
message: 'Successfully banned the ip'
|
||||
});
|
||||
};
|
|
@ -1,25 +0,0 @@
|
|||
const Route = require('../../structures/Route');
|
||||
|
||||
class banIP extends Route {
|
||||
constructor() {
|
||||
super('/admin/ban/ip', 'post', { adminOnly: true });
|
||||
}
|
||||
|
||||
async run(req, res, db) {
|
||||
if (!req.body) return res.status(400).json({ message: 'No body provided' });
|
||||
const { ip } = req.body;
|
||||
if (!ip) return res.status(400).json({ message: 'No ip provided' });
|
||||
|
||||
try {
|
||||
await db.table('bans').insert({ ip });
|
||||
} catch (error) {
|
||||
return super.error(res, error);
|
||||
}
|
||||
|
||||
return res.json({
|
||||
message: 'Successfully banned the ip'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = banIP;
|
|
@ -0,0 +1,58 @@
|
|||
import type { FastifyRequest, FastifyReply } from 'fastify';
|
||||
import prisma from '../../../../structures/database';
|
||||
import { constructFilePublicLink } from '../../../../utils/Util';
|
||||
import type { User } from '../../../../structures/interfaces';
|
||||
interface params {
|
||||
id: number;
|
||||
}
|
||||
interface UserWithFileCount extends User {
|
||||
fileCount?: number;
|
||||
}
|
||||
|
||||
export const middlewares = ['auth', 'admin'];
|
||||
|
||||
export const run = async (req: FastifyRequest, res: FastifyReply) => {
|
||||
const { id } = req.params as params;
|
||||
if (!id) return res.status(400).send({ message: 'Invalid file ID supplied' });
|
||||
|
||||
const file = await prisma.files.findUnique({
|
||||
where: {
|
||||
id
|
||||
}
|
||||
});
|
||||
|
||||
if (!file) return res.status(404).send({ message: 'File doesn\'t exist' });
|
||||
|
||||
let user;
|
||||
if (file.userId) {
|
||||
user = await prisma.users.findUnique({
|
||||
where: {
|
||||
id: file.userId
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
username: true,
|
||||
enabled: true,
|
||||
createdAt: true,
|
||||
editedAt: true,
|
||||
isAdmin: true
|
||||
}
|
||||
});
|
||||
|
||||
if (user) {
|
||||
// TODO: ???
|
||||
(user as unknown as UserWithFileCount).fileCount = await prisma.files.count({
|
||||
where: {
|
||||
userId: user.id
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
const extendedFile = constructFilePublicLink(req, file);
|
||||
|
||||
return res.send({
|
||||
message: 'Successfully retrieved file',
|
||||
file: extendedFile,
|
||||
user
|
||||
});
|
||||
};
|
|
@ -1,32 +0,0 @@
|
|||
const Route = require('../../structures/Route');
|
||||
const Util = require('../../utils/Util');
|
||||
|
||||
class filesGET extends Route {
|
||||
constructor() {
|
||||
super('/admin/file/:id', 'get', { adminOnly: true });
|
||||
}
|
||||
|
||||
async run(req, res, db) {
|
||||
const { id } = req.params;
|
||||
if (!id) return res.status(400).json({ message: 'Invalid file ID supplied' });
|
||||
|
||||
let file = await db.table('files').where({ id }).first();
|
||||
const user = await db.table('users')
|
||||
.select('id', 'username', 'enabled', 'createdAt', 'editedAt', 'apiKeyEditedAt', 'isAdmin')
|
||||
.where({ id: file.userId })
|
||||
.first();
|
||||
file = Util.constructFilePublicLink(req, file);
|
||||
|
||||
// Additional relevant data
|
||||
const filesFromUser = await db.table('files').where({ userId: user.id }).select('id');
|
||||
user.fileCount = filesFromUser.length;
|
||||
|
||||
return res.json({
|
||||
message: 'Successfully retrieved file',
|
||||
file,
|
||||
user
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = filesGET;
|
|
@ -1,27 +0,0 @@
|
|||
const Route = require('../../structures/Route');
|
||||
|
||||
class unBanIP extends Route {
|
||||
constructor() {
|
||||
super('/admin/unban/ip', 'post', { adminOnly: true });
|
||||
}
|
||||
|
||||
async run(req, res, db) {
|
||||
if (!req.body) return res.status(400).json({ message: 'No body provided' });
|
||||
const { ip } = req.body;
|
||||
if (!ip) return res.status(400).json({ message: 'No ip provided' });
|
||||
|
||||
try {
|
||||
await db.table('bans')
|
||||
.where({ ip })
|
||||
.delete();
|
||||
} catch (error) {
|
||||
return super.error(res, error);
|
||||
}
|
||||
|
||||
return res.json({
|
||||
message: 'Successfully unbanned the ip'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = unBanIP;
|
|
@ -0,0 +1,33 @@
|
|||
import type { FastifyRequest, FastifyReply } from 'fastify';
|
||||
import prisma from '../../../../structures/database';
|
||||
|
||||
interface body {
|
||||
ip: string;
|
||||
}
|
||||
|
||||
export const middlewares = ['auth', 'admin'];
|
||||
|
||||
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 { ip } = req.body as body;
|
||||
if (!ip) return res.status(400).send({ message: 'No ip provided' });
|
||||
|
||||
const record = await prisma.bans.findFirst({
|
||||
where: {
|
||||
ip
|
||||
}
|
||||
});
|
||||
|
||||
if (record) {
|
||||
await prisma.bans.delete({
|
||||
where: {
|
||||
id: record.id
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return res.send({
|
||||
message: 'Successfully unbanned the ip'
|
||||
});
|
||||
};
|
|
@ -0,0 +1,58 @@
|
|||
import type { FastifyRequest, FastifyReply } from 'fastify';
|
||||
import prisma from '../../../../structures/database';
|
||||
import { constructFilePublicLink } from '../../../../utils/Util';
|
||||
|
||||
export const middlewares = ['auth', 'admin'];
|
||||
|
||||
interface params {
|
||||
id: number;
|
||||
}
|
||||
export const run = async (req: FastifyRequest, res: FastifyReply) => {
|
||||
const { id } = req.params as params;
|
||||
if (!id) return res.status(400).send({ message: 'Invalid user ID supplied' });
|
||||
|
||||
const { page = 1, limit = 100 } = req.query as { page: number; limit: number };
|
||||
|
||||
const user = await prisma.users.findUnique({
|
||||
where: {
|
||||
id
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
username: true,
|
||||
enabled: true,
|
||||
isAdmin: true,
|
||||
createdAt: true,
|
||||
editedAt: true,
|
||||
apiKeyEditedAt: true
|
||||
}
|
||||
});
|
||||
|
||||
if (!user) return res.status(404).send({ message: 'User not found' });
|
||||
|
||||
const count = await prisma.files.count({
|
||||
where: {
|
||||
userId: user.id
|
||||
}
|
||||
});
|
||||
|
||||
const files = await prisma.files.findMany({
|
||||
take: limit,
|
||||
skip: (page - 1) * limit,
|
||||
where: {
|
||||
userId: user.id
|
||||
}
|
||||
});
|
||||
|
||||
const readyFiles = [];
|
||||
for (const file of files) {
|
||||
readyFiles.push(constructFilePublicLink(req, file));
|
||||
}
|
||||
|
||||
return res.send({
|
||||
message: 'Successfully retrieved users',
|
||||
user,
|
||||
files: readyFiles,
|
||||
count
|
||||
});
|
||||
};
|
|
@ -0,0 +1,29 @@
|
|||
import type { FastifyReply } from 'fastify';
|
||||
import type { RequestWithUser } from '../../../../structures/interfaces';
|
||||
import prisma from '../../../../structures/database';
|
||||
|
||||
interface body {
|
||||
id: number;
|
||||
}
|
||||
|
||||
export const middlewares = ['auth', 'admin'];
|
||||
|
||||
export const run = async (req: RequestWithUser, res: FastifyReply) => {
|
||||
if (!req.body) return res.status(400).send({ message: 'No body provided' });
|
||||
const { id } = req.body as body;
|
||||
if (!id) return res.status(400).send({ message: 'No id provided' });
|
||||
if (id === req.user.id) return res.status(400).send({ message: 'You can\'t apply this action to yourself' });
|
||||
|
||||
await prisma.users.update({
|
||||
where: {
|
||||
id
|
||||
},
|
||||
data: {
|
||||
isAdmin: false
|
||||
}
|
||||
});
|
||||
|
||||
return res.send({
|
||||
message: 'Successfully demoted user'
|
||||
});
|
||||
};
|
|
@ -0,0 +1,29 @@
|
|||
import type { FastifyReply } from 'fastify';
|
||||
import type { RequestWithUser } from '../../../../structures/interfaces';
|
||||
import prisma from '../../../../structures/database';
|
||||
|
||||
interface body {
|
||||
id: number;
|
||||
}
|
||||
|
||||
export const middlewares = ['auth', 'admin'];
|
||||
|
||||
export const run = async (req: RequestWithUser, res: FastifyReply) => {
|
||||
if (!req.body) return res.status(400).send({ message: 'No body provided' });
|
||||
const { id } = req.body as body;
|
||||
if (!id) return res.status(400).send({ message: 'No id provided' });
|
||||
if (id === req.user.id) return res.status(400).send({ message: 'You can\'t apply this action to yourself' });
|
||||
|
||||
await prisma.users.update({
|
||||
where: {
|
||||
id
|
||||
},
|
||||
data: {
|
||||
enabled: false
|
||||
}
|
||||
});
|
||||
|
||||
return res.send({
|
||||
message: 'Successfully disabled user'
|
||||
});
|
||||
};
|
|
@ -0,0 +1,29 @@
|
|||
import type { FastifyReply } from 'fastify';
|
||||
import type { RequestWithUser } from '../../../../structures/interfaces';
|
||||
import prisma from '../../../../structures/database';
|
||||
|
||||
interface body {
|
||||
id: number;
|
||||
}
|
||||
|
||||
export const middlewares = ['auth', 'admin'];
|
||||
|
||||
export const run = async (req: RequestWithUser, res: FastifyReply) => {
|
||||
if (!req.body) return res.status(400).send({ message: 'No body provided' });
|
||||
const { id } = req.body as body;
|
||||
if (!id) return res.status(400).send({ message: 'No id provided' });
|
||||
if (id === req.user.id) return res.status(400).send({ message: 'You can\'t apply this action to yourself' });
|
||||
|
||||
await prisma.users.update({
|
||||
where: {
|
||||
id
|
||||
},
|
||||
data: {
|
||||
enabled: true
|
||||
}
|
||||
});
|
||||
|
||||
return res.send({
|
||||
message: 'Successfully enabled user'
|
||||
});
|
||||
};
|
|
@ -0,0 +1,29 @@
|
|||
import type { FastifyReply } from 'fastify';
|
||||
import type { RequestWithUser } from '../../../../structures/interfaces';
|
||||
import prisma from '../../../../structures/database';
|
||||
|
||||
interface body {
|
||||
id: number;
|
||||
}
|
||||
|
||||
export const middlewares = ['auth', 'admin'];
|
||||
|
||||
export const run = async (req: RequestWithUser, res: FastifyReply) => {
|
||||
if (!req.body) return res.status(400).send({ message: 'No body provided' });
|
||||
const { id } = req.body as body;
|
||||
if (!id) return res.status(400).send({ message: 'No id provided' });
|
||||
if (id === req.user.id) return res.status(400).send({ message: 'You can\'t apply this action to yourself' });
|
||||
|
||||
await prisma.users.update({
|
||||
where: {
|
||||
id
|
||||
},
|
||||
data: {
|
||||
isAdmin: true
|
||||
}
|
||||
});
|
||||
|
||||
return res.send({
|
||||
message: 'Successfully promoted user'
|
||||
});
|
||||
};
|
|
@ -0,0 +1,20 @@
|
|||
import type { FastifyRequest, FastifyReply } from 'fastify';
|
||||
import { deleteAllFilesFromUser } from '../../../../utils/Util';
|
||||
|
||||
interface body {
|
||||
id: number;
|
||||
}
|
||||
|
||||
export const middlewares = ['auth', 'admin'];
|
||||
|
||||
export const run = async (req: FastifyRequest, res: FastifyReply) => {
|
||||
if (!req.body) return res.status(400).send({ message: 'No body provided' });
|
||||
const { id } = req.body as body;
|
||||
if (!id) return res.status(400).send({ message: 'No id provided' });
|
||||
|
||||
await deleteAllFilesFromUser(id);
|
||||
|
||||
return res.send({
|
||||
message: 'Successfully demoted user'
|
||||
});
|
||||
};
|
|
@ -1,29 +0,0 @@
|
|||
const Route = require('../../structures/Route');
|
||||
|
||||
class userDemote extends Route {
|
||||
constructor() {
|
||||
super('/admin/users/demote', 'post', { adminOnly: true });
|
||||
}
|
||||
|
||||
async run(req, res, db, user) {
|
||||
if (!req.body) return res.status(400).json({ message: 'No body provided' });
|
||||
const { id } = req.body;
|
||||
if (!id) return res.status(400).json({ message: 'No id provided' });
|
||||
if (id === user.id) return res.status(400).json({ message: 'You can\'t apply this action to yourself' });
|
||||
|
||||
try {
|
||||
await db.table('users')
|
||||
.where({ id })
|
||||
.update({ isAdmin: false })
|
||||
.wasMutated();
|
||||
} catch (error) {
|
||||
return super.error(res, error);
|
||||
}
|
||||
|
||||
return res.json({
|
||||
message: 'Successfully demoted user'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = userDemote;
|
|
@ -1,29 +0,0 @@
|
|||
const Route = require('../../structures/Route');
|
||||
|
||||
class userDisable extends Route {
|
||||
constructor() {
|
||||
super('/admin/users/disable', 'post', { adminOnly: true });
|
||||
}
|
||||
|
||||
async run(req, res, db, user) {
|
||||
if (!req.body) return res.status(400).json({ message: 'No body provided' });
|
||||
const { id } = req.body;
|
||||
if (!id) return res.status(400).json({ message: 'No id provided' });
|
||||
if (id === user.id) return res.status(400).json({ message: 'You can\'t apply this action to yourself' });
|
||||
|
||||
try {
|
||||
await db.table('users')
|
||||
.where({ id })
|
||||
.update({ enabled: false })
|
||||
.wasMutated();
|
||||
} catch (error) {
|
||||
return super.error(res, error);
|
||||
}
|
||||
|
||||
return res.json({
|
||||
message: 'Successfully disabled user'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = userDisable;
|
|
@ -1,29 +0,0 @@
|
|||
const Route = require('../../structures/Route');
|
||||
|
||||
class userEnable extends Route {
|
||||
constructor() {
|
||||
super('/admin/users/enable', 'post', { adminOnly: true });
|
||||
}
|
||||
|
||||
async run(req, res, db, user) {
|
||||
if (!req.body) return res.status(400).json({ message: 'No body provided' });
|
||||
const { id } = req.body;
|
||||
if (!id) return res.status(400).json({ message: 'No id provided' });
|
||||
if (id === user.id) return res.status(400).json({ message: 'You can\'t apply this action to yourself' });
|
||||
|
||||
try {
|
||||
await db.table('users')
|
||||
.where({ id })
|
||||
.update({ enabled: true })
|
||||
.wasMutated();
|
||||
} catch (error) {
|
||||
return super.error(res, error);
|
||||
}
|
||||
|
||||
return res.json({
|
||||
message: 'Successfully enabled user'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = userEnable;
|
|
@ -1,55 +0,0 @@
|
|||
const Route = require('../../structures/Route');
|
||||
const Util = require('../../utils/Util');
|
||||
|
||||
class usersGET extends Route {
|
||||
constructor() {
|
||||
super('/admin/users/:id', 'get', { adminOnly: true });
|
||||
}
|
||||
|
||||
async run(req, res, db) {
|
||||
const { id } = req.params;
|
||||
if (!id) return res.status(400).json({ message: 'Invalid user ID supplied' });
|
||||
|
||||
try {
|
||||
const user = await db.table('users')
|
||||
.select('id', 'username', 'enabled', 'createdAt', 'editedAt', 'apiKeyEditedAt', 'isAdmin')
|
||||
.where({ id })
|
||||
.first();
|
||||
|
||||
let count = 0;
|
||||
let files = db.table('files')
|
||||
.where({ userId: user.id })
|
||||
.orderBy('id', 'desc');
|
||||
|
||||
const { page, limit = 100 } = req.query;
|
||||
if (page && page >= 0) {
|
||||
files = await files.offset((page - 1) * limit).limit(limit);
|
||||
|
||||
const dbRes = await db.table('files')
|
||||
.count('* as count')
|
||||
.where({ userId: user.id })
|
||||
.first();
|
||||
|
||||
count = dbRes.count;
|
||||
} else {
|
||||
files = await files; // execute the query
|
||||
count = files.length;
|
||||
}
|
||||
|
||||
for (let file of files) {
|
||||
file = Util.constructFilePublicLink(req, file);
|
||||
}
|
||||
|
||||
return res.json({
|
||||
message: 'Successfully retrieved user',
|
||||
user,
|
||||
files,
|
||||
count
|
||||
});
|
||||
} catch (error) {
|
||||
return super.error(res, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = usersGET;
|
|
@ -1,28 +0,0 @@
|
|||
const Route = require('../../structures/Route');
|
||||
|
||||
class userPromote extends Route {
|
||||
constructor() {
|
||||
super('/admin/users/promote', 'post', { adminOnly: true });
|
||||
}
|
||||
|
||||
async run(req, res, db, user) {
|
||||
if (!req.body) return res.status(400).json({ message: 'No body provided' });
|
||||
const { id } = req.body;
|
||||
if (!id) return res.status(400).json({ message: 'No id provided' });
|
||||
if (id === user.id) return res.status(400).json({ message: 'You can\'t apply this action to yourself' });
|
||||
|
||||
try {
|
||||
await db.table('users')
|
||||
.where({ id })
|
||||
.update({ isAdmin: true });
|
||||
} catch (error) {
|
||||
return super.error(res, error);
|
||||
}
|
||||
|
||||
return res.json({
|
||||
message: 'Successfully promoted user'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = userPromote;
|
|
@ -1,26 +0,0 @@
|
|||
const Route = require('../../structures/Route');
|
||||
const Util = require('../../utils/Util');
|
||||
|
||||
class userDemote extends Route {
|
||||
constructor() {
|
||||
super('/admin/users/purge', 'post', { adminOnly: true });
|
||||
}
|
||||
|
||||
async run(req, res) {
|
||||
if (!req.body) return res.status(400).json({ message: 'No body provided' });
|
||||
const { id } = req.body;
|
||||
if (!id) return res.status(400).json({ message: 'No id provided' });
|
||||
|
||||
try {
|
||||
await Util.deleteAllFilesFromUser(id);
|
||||
} catch (error) {
|
||||
return super.error(res, error);
|
||||
}
|
||||
|
||||
return res.json({
|
||||
message: 'Successfully deleted the user\'s files'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = userDemote;
|
|
@ -0,0 +1,21 @@
|
|||
import type { FastifyRequest, FastifyReply } from 'fastify';
|
||||
import prisma from '../../../structures/database';
|
||||
|
||||
export const middlewares = ['auth', 'admin'];
|
||||
|
||||
export const run = async (req: FastifyRequest, res: FastifyReply) => {
|
||||
const users = await prisma.users.findMany({
|
||||
select: {
|
||||
id: true,
|
||||
username: true,
|
||||
enabled: true,
|
||||
isAdmin: true,
|
||||
createdAt: true
|
||||
}
|
||||
});
|
||||
|
||||
return res.send({
|
||||
message: 'Successfully retrieved users',
|
||||
users
|
||||
});
|
||||
};
|
|
@ -1,23 +0,0 @@
|
|||
const Route = require('../../structures/Route');
|
||||
|
||||
class usersGET extends Route {
|
||||
constructor() {
|
||||
super('/admin/users', 'get', { adminOnly: true });
|
||||
}
|
||||
|
||||
async run(req, res, db) {
|
||||
try {
|
||||
const users = await db.table('users')
|
||||
.select('id', 'username', 'enabled', 'isAdmin', 'createdAt');
|
||||
|
||||
return res.json({
|
||||
message: 'Successfully retrieved users',
|
||||
users
|
||||
});
|
||||
} catch (error) {
|
||||
return super.error(res, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = usersGET;
|
|
@ -0,0 +1,26 @@
|
|||
import type { FastifyReply } from 'fastify';
|
||||
import type { RequestWithUser } from '../../../../structures/interfaces';
|
||||
import prisma from '../../../../structures/database';
|
||||
|
||||
interface params {
|
||||
id: number;
|
||||
}
|
||||
|
||||
export const middlewares = ['auth'];
|
||||
|
||||
export const run = async (req: RequestWithUser, res: FastifyReply) => {
|
||||
const { id } = req.params as params;
|
||||
if (!id) return res.status(400).send({ message: 'Invalid id supplied' });
|
||||
|
||||
const links = await prisma.links.findMany({
|
||||
where: {
|
||||
albumId: id,
|
||||
userId: req.user.id
|
||||
}
|
||||
});
|
||||
|
||||
return res.send({
|
||||
message: 'Successfully retrieved links',
|
||||
links
|
||||
});
|
||||
};
|
|
@ -0,0 +1,43 @@
|
|||
import type { FastifyReply } from 'fastify';
|
||||
import prisma from '../../../../../structures/database';
|
||||
import { RequestWithUser } from '../../../../../structures/interfaces';
|
||||
|
||||
interface body {
|
||||
identifier: string;
|
||||
}
|
||||
|
||||
export const middlewares = ['auth'];
|
||||
|
||||
export const run = async (req: RequestWithUser, res: FastifyReply) => {
|
||||
if (!req.params) return res.status(400).send({ message: 'No body provided' });
|
||||
const { identifier } = req.body as body;
|
||||
if (!identifier) return res.status(400).send({ message: 'No identifier provided' });
|
||||
|
||||
const link = await prisma.links.findFirst({
|
||||
where: {
|
||||
userId: req.user.id,
|
||||
identifier
|
||||
}
|
||||
});
|
||||
|
||||
if (!link) return res.status(400).send({ message: 'No link found' });
|
||||
|
||||
await prisma.links.delete({
|
||||
where: {
|
||||
links_userid_identifier_unique: {
|
||||
userId: req.user.id,
|
||||
identifier
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
await prisma.albumsLinks.delete({
|
||||
where: {
|
||||
linkId: link.id
|
||||
}
|
||||
});
|
||||
|
||||
return res.send({
|
||||
message: 'Successfully deleted the link'
|
||||
});
|
||||
};
|
|
@ -0,0 +1,44 @@
|
|||
import type { FastifyReply } from 'fastify';
|
||||
import type { RequestWithUser } from '../../../../structures/interfaces';
|
||||
import prisma from '../../../../structures/database';
|
||||
|
||||
interface body {
|
||||
identifier: string;
|
||||
enableDownload: boolean;
|
||||
expiresAt: Date;
|
||||
}
|
||||
|
||||
export const middlewares = ['auth'];
|
||||
|
||||
export const run = async (req: RequestWithUser, res: FastifyReply) => {
|
||||
if (!req.params) return res.status(400).send({ message: 'No body provided' });
|
||||
const { identifier, enableDownload, expiresAt } = req.body as body;
|
||||
if (!identifier) return res.status(400).send({ message: 'No identifier provided' });
|
||||
|
||||
const link = await prisma.links.findFirst({
|
||||
where: {
|
||||
userId: req.user.id,
|
||||
identifier
|
||||
}
|
||||
});
|
||||
|
||||
if (!link) return res.status(400).send({ message: 'No link found' });
|
||||
|
||||
const updateObj = {
|
||||
enableDownload: enableDownload || false,
|
||||
expiresAt // This one should be null if not supplied
|
||||
};
|
||||
|
||||
await prisma.links.update({
|
||||
where: {
|
||||
identifier
|
||||
},
|
||||
data: {
|
||||
...updateObj
|
||||
}
|
||||
});
|
||||
|
||||
return res.send({
|
||||
message: 'Successfully edited link'
|
||||
});
|
||||
};
|
|
@ -0,0 +1,61 @@
|
|||
import type { FastifyReply } from 'fastify';
|
||||
import prisma from '../../../../structures/database';
|
||||
import { RequestWithUser } from '../../../../structures/interfaces';
|
||||
import { getUniqueAlbumIdentifier } from '../../../../utils/Util';
|
||||
|
||||
interface body {
|
||||
albumId: number;
|
||||
identifier?: string;
|
||||
}
|
||||
|
||||
export const middlewares = ['auth'];
|
||||
|
||||
export const run = async (req: RequestWithUser, res: FastifyReply) => {
|
||||
if (!req.params) return res.status(400).send({ message: 'No body provided' });
|
||||
const { albumId } = req.body as body;
|
||||
if (!albumId) return res.status(400).send({ message: 'No albumId provided' });
|
||||
|
||||
const exists = await prisma.albums.findFirst({
|
||||
where: {
|
||||
id: albumId,
|
||||
userId: req.user.id
|
||||
}
|
||||
});
|
||||
|
||||
if (!exists) return res.status(400).send({ message: 'Album doesn\t exist' });
|
||||
|
||||
let { identifier } = req.body as body;
|
||||
if (identifier) {
|
||||
if (!req.user.isAdmin) return res.status(401).send({ message: 'Only administrators can create custom links' });
|
||||
if (!(/^[a-zA-Z0-9-_]+$/.test(identifier))) return res.status(400).send({ message: 'Only alphanumeric, dashes, and underscore characters are allowed' });
|
||||
|
||||
const identifierExists = await prisma.links.findFirst({
|
||||
where: {
|
||||
identifier
|
||||
}
|
||||
});
|
||||
if (identifierExists) return res.status(400).send({ message: 'Album with this identifier already exists' });
|
||||
} else {
|
||||
identifier = await getUniqueAlbumIdentifier();
|
||||
if (!identifier) return res.status(500).send({ message: 'There was a problem allocating a link for your album' });
|
||||
}
|
||||
|
||||
const insertObj = {
|
||||
identifier,
|
||||
userId: req.user.id,
|
||||
albumId,
|
||||
enabled: true,
|
||||
enableDownload: true,
|
||||
expiresAt: null,
|
||||
views: 0
|
||||
};
|
||||
|
||||
await prisma.links.create({
|
||||
data: insertObj
|
||||
});
|
||||
|
||||
return res.send({
|
||||
message: 'Successfully created link',
|
||||
data: insertObj
|
||||
});
|
||||
};
|
|
@ -1,35 +0,0 @@
|
|||
const Route = require('../../../structures/Route');
|
||||
|
||||
class linkDELETE extends Route {
|
||||
constructor() {
|
||||
super('/album/link/delete/:identifier', 'delete');
|
||||
}
|
||||
|
||||
async run(req, res, db, user) {
|
||||
const { identifier } = req.params;
|
||||
if (!identifier) return res.status(400).json({ message: 'Invalid identifier supplied' });
|
||||
|
||||
try {
|
||||
const link = await db.table('links')
|
||||
.where({ identifier, userId: user.id })
|
||||
.first();
|
||||
|
||||
if (!link) return res.status(400).json({ message: 'Identifier doesn\'t exist or doesnt\'t belong to the user' });
|
||||
|
||||
await db.table('links')
|
||||
.where({ id: link.id })
|
||||
.delete();
|
||||
await db.table('albumsLinks')
|
||||
.where({ linkId: link.id })
|
||||
.delete();
|
||||
} catch (error) {
|
||||
return super.error(res, error);
|
||||
}
|
||||
|
||||
return res.json({
|
||||
message: 'Successfully deleted link'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = linkDELETE;
|
|
@ -1,38 +0,0 @@
|
|||
const Route = require('../../../structures/Route');
|
||||
|
||||
class linkEditPOST extends Route {
|
||||
constructor() {
|
||||
super('/album/link/edit', 'post');
|
||||
}
|
||||
|
||||
async run(req, res, db, user) {
|
||||
if (!req.body) return res.status(400).json({ message: 'No body provided' });
|
||||
const { identifier, enableDownload, expiresAt } = req.body;
|
||||
if (!identifier) return res.status(400).json({ message: 'Invalid album identifier supplied' });
|
||||
|
||||
/*
|
||||
Make sure the link exists
|
||||
*/
|
||||
const link = await db
|
||||
.table('links')
|
||||
.where({ identifier, userId: user.id })
|
||||
.first();
|
||||
if (!link) return res.status(400).json({ message: "The link doesn't exist or doesn't belong to the user" });
|
||||
|
||||
try {
|
||||
const updateObj = {
|
||||
enableDownload: enableDownload || false,
|
||||
expiresAt // This one should be null if not supplied
|
||||
};
|
||||
await db
|
||||
.table('links')
|
||||
.where({ identifier })
|
||||
.update(updateObj);
|
||||
return res.json({ message: 'Editing the link was successful', data: updateObj });
|
||||
} catch (error) {
|
||||
return super.error(res, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = linkEditPOST;
|
|
@ -1,68 +0,0 @@
|
|||
const Route = require('../../../structures/Route');
|
||||
const Util = require('../../../utils/Util');
|
||||
|
||||
class linkPOST extends Route {
|
||||
constructor() {
|
||||
super('/album/link/new', 'post');
|
||||
}
|
||||
|
||||
async run(req, res, db, user) {
|
||||
if (!req.body) return res.status(400).json({ message: 'No body provided' });
|
||||
const { albumId } = req.body;
|
||||
if (!albumId) return res.status(400).json({ message: 'No album provided' });
|
||||
|
||||
/*
|
||||
Make sure the album exists
|
||||
*/
|
||||
const exists = await db
|
||||
.table('albums')
|
||||
.where({ id: albumId, userId: user.id })
|
||||
.first();
|
||||
if (!exists) return res.status(400).json({ message: 'Album doesn\t exist' });
|
||||
|
||||
let { identifier } = req.body;
|
||||
if (identifier) {
|
||||
if (!user.isAdmin) return res.status(401).json({ message: 'Only administrators can create custom links' });
|
||||
|
||||
if (!(/^[a-zA-Z0-9-_]+$/.test(identifier))) return res.status(400).json({ message: 'Only alphanumeric, dashes, and underscore characters are allowed' });
|
||||
|
||||
/*
|
||||
Make sure that the id doesn't already exists in the database
|
||||
*/
|
||||
const idExists = await db
|
||||
.table('links')
|
||||
.where({ identifier })
|
||||
.first();
|
||||
|
||||
if (idExists) return res.status(400).json({ message: 'Album with this identifier already exists' });
|
||||
} else {
|
||||
/*
|
||||
Try to allocate a new identifier in the database
|
||||
*/
|
||||
identifier = await Util.getUniqueAlbumIdentifier();
|
||||
if (!identifier) return res.status(500).json({ message: 'There was a problem allocating a link for your album' });
|
||||
}
|
||||
|
||||
try {
|
||||
const insertObj = {
|
||||
identifier,
|
||||
userId: user.id,
|
||||
albumId,
|
||||
enabled: true,
|
||||
enableDownload: true,
|
||||
expiresAt: null,
|
||||
views: 0
|
||||
};
|
||||
await db.table('links').insert(insertObj).wasMutated();
|
||||
|
||||
return res.json({
|
||||
message: 'The link was created successfully',
|
||||
data: insertObj
|
||||
});
|
||||
} catch (error) {
|
||||
return super.error(res, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = linkPOST;
|
|
@ -1,22 +0,0 @@
|
|||
const Route = require('../../../structures/Route');
|
||||
|
||||
class linkPOST extends Route {
|
||||
constructor() {
|
||||
super('/album/:id/links', 'get');
|
||||
}
|
||||
|
||||
async run(req, res, db, user) {
|
||||
const { id } = req.params;
|
||||
if (!id) return res.status(400).json({ message: 'Invalid id supplied' });
|
||||
|
||||
const links = await db.table('links')
|
||||
.where({ albumId: id, userId: user.id });
|
||||
|
||||
return res.json({
|
||||
message: 'Successfully retrieved links',
|
||||
links
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = linkPOST;
|
|
@ -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
|
||||
});
|
||||
};
|
|
@ -1,57 +0,0 @@
|
|||
const bcrypt = require('bcrypt');
|
||||
const moment = require('moment');
|
||||
const JWT = require('jsonwebtoken');
|
||||
const Route = require('../../structures/Route');
|
||||
const Util = require('../../utils/Util');
|
||||
|
||||
class loginPOST extends Route {
|
||||
constructor() {
|
||||
super('/auth/login', 'post', { bypassAuth: true });
|
||||
}
|
||||
|
||||
async run(req, res, db) {
|
||||
if (!req.body) return res.status(400).json({ message: 'No body provided' });
|
||||
const { username, password } = req.body;
|
||||
if (!username || !password) return res.status(401).json({ message: 'Invalid body provided' });
|
||||
|
||||
/*
|
||||
Checks if the user exists
|
||||
*/
|
||||
const user = await db.table('users').where('username', username).first();
|
||||
if (!user) return res.status(401).json({ message: 'Invalid authorization' });
|
||||
|
||||
/*
|
||||
Checks if the user is disabled
|
||||
*/
|
||||
if (!user.enabled) return res.status(401).json({ message: 'This account has been disabled' });
|
||||
|
||||
/*
|
||||
Checks if the password is right
|
||||
*/
|
||||
const comparePassword = await bcrypt.compare(password, user.password);
|
||||
if (!comparePassword) return res.status(401).json({ message: 'Invalid authorization.' });
|
||||
|
||||
/*
|
||||
Create the jwt with some data
|
||||
*/
|
||||
const jwt = JWT.sign({
|
||||
iss: 'chibisafe',
|
||||
sub: user.id,
|
||||
iat: moment.utc().valueOf()
|
||||
}, Util.config.secret, { expiresIn: '30d' });
|
||||
|
||||
return res.json({
|
||||
message: 'Successfully logged in.',
|
||||
user: {
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
apiKey: user.apiKey,
|
||||
isAdmin: user.isAdmin
|
||||
},
|
||||
token: jwt,
|
||||
apiKey: user.apiKey
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = loginPOST;
|
|
@ -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' });
|
||||
};
|
|
@ -1,63 +0,0 @@
|
|||
const bcrypt = require('bcrypt');
|
||||
const moment = require('moment');
|
||||
const Route = require('../../structures/Route');
|
||||
const log = require('../../utils/Log');
|
||||
const Util = require('../../utils/Util');
|
||||
|
||||
class registerPOST extends Route {
|
||||
constructor() {
|
||||
super('/auth/register', 'post', { bypassAuth: true });
|
||||
}
|
||||
|
||||
async run(req, res, db) {
|
||||
// Only allow admins to create new accounts if the sign up is deactivated
|
||||
const user = await Util.isAuthorized(req);
|
||||
if ((!user || !user.isAdmin) && !Util.config.userAccounts) return res.status(401).json({ message: 'Creation of new accounts is currently disabled' });
|
||||
|
||||
if (!req.body) return res.status(400).json({ message: 'No body provided' });
|
||||
const { username, password } = req.body;
|
||||
if (!username || !password) return res.status(401).json({ message: 'Invalid body provided' });
|
||||
|
||||
if (username.length < 4 || username.length > 32) {
|
||||
return res.status(400).json({ message: 'Username must have 4-32 characters' });
|
||||
}
|
||||
if (password.length < 6 || password.length > 64) {
|
||||
return res.status(400).json({ message: 'Password must have 6-64 characters' });
|
||||
}
|
||||
|
||||
/*
|
||||
Make sure the username doesn't exist yet
|
||||
*/
|
||||
const exists = await db.table('users').where('username', username).first();
|
||||
if (exists) return res.status(401).json({ message: 'Username already exists' });
|
||||
|
||||
/*
|
||||
Hash the supplied password
|
||||
*/
|
||||
let hash;
|
||||
try {
|
||||
hash = await bcrypt.hash(password, 10);
|
||||
} catch (error) {
|
||||
log.error('Error generating password hash');
|
||||
log.error(error);
|
||||
return res.status(401).json({ message: 'There was a problem processing your account' });
|
||||
}
|
||||
|
||||
/*
|
||||
Create the user
|
||||
*/
|
||||
const now = moment.utc().toDate();
|
||||
await db.table('users').insert({
|
||||
username,
|
||||
password: hash,
|
||||
passwordEditedAt: now,
|
||||
createdAt: now,
|
||||
editedAt: now,
|
||||
enabled: true,
|
||||
isAdmin: false
|
||||
}).wasMutated();
|
||||
return res.json({ message: 'The account was created successfully' });
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = registerPOST;
|
|
@ -0,0 +1,10 @@
|
|||
import type { FastifyReply } from 'fastify';
|
||||
import type { RequestWithUser } from '../../../../structures/interfaces';
|
||||
import { getConfig } from '../../../../utils/Util';
|
||||
|
||||
export const middlewares = ['auth', 'admin'];
|
||||
|
||||
export const run = async (req: RequestWithUser, res: FastifyReply) => res.send({
|
||||
message: 'Successfully retrieved config',
|
||||
config: (await getConfig())
|
||||
});
|
|
@ -0,0 +1,27 @@
|
|||
import type { FastifyReply } from 'fastify';
|
||||
import type { RequestWithUser } from '../../../structures/interfaces';
|
||||
import { getConfig } from '../../../utils/Util';
|
||||
|
||||
export const middlewares = ['auth', 'admin'];
|
||||
|
||||
export const run = async (req: RequestWithUser, res: FastifyReply) => {
|
||||
const config = await getConfig();
|
||||
|
||||
return res.send({
|
||||
message: 'Successfully retrieved config',
|
||||
config: {
|
||||
version: process.env.npm_package_version,
|
||||
serviceName: config.serviceName,
|
||||
maxUploadSize: config.maxSize,
|
||||
filenameLength: config.generatedFilenameLength,
|
||||
albumLinkLength: config.generatedAlbumLength,
|
||||
chunkSize: config.chunkSize,
|
||||
publicMode: config.publicMode,
|
||||
userAccounts: config.userAccounts,
|
||||
metaThemeColor: config.metaThemeColor,
|
||||
metaDescription: config.metaDescription,
|
||||
metaKeywords: config.metaKeywords,
|
||||
metaTwitterHandle: config.metaTwitterHandle
|
||||
}
|
||||
});
|
||||
};
|
|
@ -1,17 +0,0 @@
|
|||
const Route = require('../../structures/Route');
|
||||
const Util = require('../../utils/Util');
|
||||
|
||||
class configGET extends Route {
|
||||
constructor() {
|
||||
super('/service/config/all', 'get', { adminOnly: true });
|
||||
}
|
||||
|
||||
run(req, res) {
|
||||
return res.json({
|
||||
message: 'Successfully retrieved config',
|
||||
config: Util.config
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = configGET;
|
|
@ -1,30 +0,0 @@
|
|||
const Route = require('../../structures/Route');
|
||||
const Util = require('../../utils/Util');
|
||||
|
||||
class configGET extends Route {
|
||||
constructor() {
|
||||
super('/service/config', 'get', { bypassAuth: true });
|
||||
}
|
||||
|
||||
run(req, res) {
|
||||
return res.json({
|
||||
message: 'Successfully retrieved config',
|
||||
config: {
|
||||
version: process.env.npm_package_version,
|
||||
serviceName: Util.config.serviceName,
|
||||
maxUploadSize: Util.config.maxSize,
|
||||
filenameLength: Util.config.generatedFilenameLength,
|
||||
albumLinkLength: Util.config.generatedAlbumLength,
|
||||
chunkSize: Util.config.chunkSize,
|
||||
publicMode: Util.config.publicMode,
|
||||
userAccounts: Util.config.userAccounts,
|
||||
metaThemeColor: Util.config.metaThemeColor,
|
||||
metaDescription: Util.config.metaDescription,
|
||||
metaKeywords: Util.config.metaKeywords,
|
||||
metaTwitterHandle: Util.metaTwitterHandle
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = configGET;
|
|
@ -0,0 +1,11 @@
|
|||
import type { FastifyReply } from 'fastify';
|
||||
import type { RequestWithUser } from '../../../structures/interfaces';
|
||||
|
||||
export const middlewares = ['auth', 'admin'];
|
||||
|
||||
export const run = async (req: RequestWithUser, res: FastifyReply) => {
|
||||
void res.send({
|
||||
message: 'Successfully triggered restart'
|
||||
});
|
||||
process.exit(0);
|
||||
};
|
|
@ -1,14 +0,0 @@
|
|||
const Route = require('../../structures/Route');
|
||||
|
||||
class restartPOST extends Route {
|
||||
constructor() {
|
||||
super('/service/restart', 'post', { adminOnly: true });
|
||||
}
|
||||
|
||||
run(req, res) {
|
||||
res.json({ message: 'Restarting...' });
|
||||
process.exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = restartPOST;
|
|
@ -1,15 +0,0 @@
|
|||
const Route = require('../../structures/Route');
|
||||
|
||||
class versionGET extends Route {
|
||||
constructor() {
|
||||
super('/version', 'get', { bypassAuth: true });
|
||||
}
|
||||
|
||||
run(req, res) {
|
||||
return res.json({
|
||||
version: process.env.npm_package_version
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = versionGET;
|
|
@ -1,34 +0,0 @@
|
|||
const randomstring = require('randomstring');
|
||||
const moment = require('moment');
|
||||
const { dump } = require('dumper.js');
|
||||
const Route = require('../../structures/Route');
|
||||
|
||||
class apiKeyPOST extends Route {
|
||||
constructor() {
|
||||
super('/user/apikey/change', 'post');
|
||||
}
|
||||
|
||||
async run(req, res, db, user) {
|
||||
const now = moment.utc().toDate();
|
||||
const apiKey = randomstring.generate(64);
|
||||
|
||||
try {
|
||||
await db.table('users')
|
||||
.where({ id: user.id })
|
||||
.update({
|
||||
apiKey,
|
||||
apiKeyEditedAt: now
|
||||
});
|
||||
} catch (error) {
|
||||
dump(error);
|
||||
return res.status(401).json({ message: 'There was a problem processing your account' });
|
||||
}
|
||||
|
||||
return res.json({
|
||||
message: 'Successfully created new api key',
|
||||
apiKey
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = apiKeyPOST;
|
|
@ -0,0 +1,26 @@
|
|||
import type { FastifyReply } from 'fastify';
|
||||
import type { RequestWithUser } from '../../../../structures/interfaces';
|
||||
import prisma from '../../../../structures/database';
|
||||
import { utc } from 'moment';
|
||||
import randomstring from 'randomstring';
|
||||
export const middlewares = ['auth'];
|
||||
|
||||
export const run = async (req: RequestWithUser, res: FastifyReply) => {
|
||||
const now = utc().toDate();
|
||||
const apiKey = randomstring.generate(64);
|
||||
|
||||
await prisma.users.update({
|
||||
where: {
|
||||
id: req.user.id
|
||||
},
|
||||
data: {
|
||||
apiKey,
|
||||
apiKeyEditedAt: now
|
||||
}
|
||||
});
|
||||
|
||||
return res.send({
|
||||
message: 'Successfully created new api key',
|
||||
apiKey
|
||||
});
|
||||
};
|
|
@ -1,46 +0,0 @@
|
|||
const bcrypt = require('bcrypt');
|
||||
const moment = require('moment');
|
||||
const Route = require('../../structures/Route');
|
||||
const log = require('../../utils/Log');
|
||||
|
||||
class changePasswordPOST extends Route {
|
||||
constructor() {
|
||||
super('/user/password/change', 'post');
|
||||
}
|
||||
|
||||
async run(req, res, db, user) {
|
||||
if (!req.body) return res.status(400).json({ message: 'No body provided' });
|
||||
const { password, newPassword } = req.body;
|
||||
if (!password || !newPassword) return res.status(401).json({ message: 'Invalid body provided' });
|
||||
if (password === newPassword) return res.status(400).json({ message: 'Passwords have to be different' });
|
||||
|
||||
/*
|
||||
Checks if the password is right
|
||||
*/
|
||||
const comparePassword = await bcrypt.compare(password, user.password);
|
||||
if (!comparePassword) return res.status(401).json({ message: 'Current password is incorrect' });
|
||||
|
||||
if (newPassword.length < 6 || newPassword.length > 64) {
|
||||
return res.status(400).json({ message: 'Password must have 6-64 characters' });
|
||||
}
|
||||
|
||||
let hash;
|
||||
try {
|
||||
hash = await bcrypt.hash(newPassword, 10);
|
||||
} catch (error) {
|
||||
log.error('Error generating password hash');
|
||||
log.error(error);
|
||||
return res.status(401).json({ message: 'There was a problem processing your account' });
|
||||
}
|
||||
|
||||
const now = moment.utc().toDate();
|
||||
await db.table('users').where('id', user.id).update({
|
||||
password: hash,
|
||||
passwordEditedAt: now
|
||||
});
|
||||
|
||||
return res.json({ message: 'The password was changed successfully' });
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = changePasswordPOST;
|
|
@ -0,0 +1,11 @@
|
|||
import type { FastifyReply } from 'fastify';
|
||||
import type { RequestWithUser } from '../../../structures/interfaces';
|
||||
|
||||
export const middlewares = ['auth'];
|
||||
|
||||
export const run = async (req: RequestWithUser, res: FastifyReply) => res.send({
|
||||
message: 'Successfully retrieved user',
|
||||
user: {
|
||||
...req.user
|
||||
}
|
||||
});
|
|
@ -0,0 +1,54 @@
|
|||
import type { FastifyReply } from 'fastify';
|
||||
import type { RequestWithUser } from '../../../../structures/interfaces';
|
||||
import prisma from '../../../../structures/database';
|
||||
import bcrypt from 'bcryptjs';
|
||||
import { utc } from 'moment';
|
||||
|
||||
interface body {
|
||||
password: string;
|
||||
newPassword: string;
|
||||
}
|
||||
|
||||
export const run = async (req: RequestWithUser, res: FastifyReply) => {
|
||||
if (!req.body) return res.status(400).send({ message: 'No body provided' });
|
||||
const { password, newPassword } = req.body as body;
|
||||
if (!password || !newPassword) return res.status(400).send({ message: 'Invalid body provided' });
|
||||
if (password === newPassword) return res.status(400).send({ message: 'Passwords have to be different' });
|
||||
|
||||
const user = await prisma.users.findUnique({
|
||||
where: {
|
||||
id: req.user.id
|
||||
},
|
||||
select: {
|
||||
password: true
|
||||
}
|
||||
});
|
||||
|
||||
const comparePassword = await bcrypt.compare(password, user?.password ?? '');
|
||||
if (!comparePassword) return res.status(401).send({ message: 'Current password is incorrect' });
|
||||
|
||||
if (newPassword.length < 6 || newPassword.length > 64) {
|
||||
return res.status(400).send({ message: 'Password must have 6-64 characters' });
|
||||
}
|
||||
|
||||
let hash;
|
||||
try {
|
||||
hash = await bcrypt.hash(newPassword, 10);
|
||||
} catch (err) {
|
||||
req.log.error(err);
|
||||
return res.status(401).send({ message: 'There was a problem processing your account' });
|
||||
}
|
||||
|
||||
const now = utc().toDate();
|
||||
await prisma.users.update({
|
||||
where: {
|
||||
id: req.user.id
|
||||
},
|
||||
data: {
|
||||
password: hash,
|
||||
passwordEditedAt: now
|
||||
}
|
||||
});
|
||||
|
||||
return res.send({ message: 'TThe password was changed successfully' });
|
||||
};
|
|
@ -1,21 +0,0 @@
|
|||
const Route = require('../../structures/Route');
|
||||
|
||||
class usersGET extends Route {
|
||||
constructor() {
|
||||
super('/users/me', 'get');
|
||||
}
|
||||
|
||||
run(req, res, db, user) {
|
||||
return res.json({
|
||||
message: 'Successfully retrieved user',
|
||||
user: {
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
isAdmin: user.isAdmin,
|
||||
apiKey: user.apiKey
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = usersGET;
|
|
@ -0,0 +1,6 @@
|
|||
import type { FastifyReply } from 'fastify';
|
||||
import { RequestWithUser } from '../../structures/interfaces';
|
||||
|
||||
export const middlewares = ['auth'];
|
||||
|
||||
export const run = (req: RequestWithUser, res: FastifyReply) => res.status(200).send(req.user);
|
|
@ -1,21 +0,0 @@
|
|||
const Route = require('../structures/Route');
|
||||
|
||||
class verifyGET extends Route {
|
||||
constructor() {
|
||||
super('/verify', 'get');
|
||||
}
|
||||
|
||||
run(req, res, db, user) {
|
||||
return res.json({
|
||||
message: 'Successfully verified token',
|
||||
user: {
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
isAdmin: user.isAdmin,
|
||||
apiKey: user.apiKey
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = verifyGET;
|
|
@ -0,0 +1,5 @@
|
|||
import type { FastifyRequest, FastifyReply } from 'fastify';
|
||||
|
||||
export const run = async (req: FastifyRequest, res: FastifyReply) => res.send({
|
||||
version: process.env.npm_package_version
|
||||
});
|
|
@ -1,15 +0,0 @@
|
|||
require('dotenv').config();
|
||||
|
||||
const Util = require('../utils/Util');
|
||||
|
||||
const start = async () => {
|
||||
try {
|
||||
await Util.writeConfigToDb(Util.getEnvironmentDefaults());
|
||||
console.log('Configuration overwriten, you can now start chibisafe');
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
start();
|
|
@ -0,0 +1,23 @@
|
|||
import dotenv from 'dotenv';
|
||||
dotenv.config();
|
||||
|
||||
import { writeConfigToDb, getEnvironmentDefaults } from '../utils/Util';
|
||||
|
||||
const start = async () => {
|
||||
try {
|
||||
const defaults = getEnvironmentDefaults();
|
||||
const keys = Object.keys(defaults);
|
||||
for (const item of keys) {
|
||||
await writeConfigToDb({
|
||||
key: item,
|
||||
value: defaults[item]
|
||||
});
|
||||
}
|
||||
console.log('Configuration overwriten, you can now start chibisafe');
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
void start();
|
|
@ -1,6 +1,7 @@
|
|||
/* eslint-disable no-console */
|
||||
const jetpack = require('fs-jetpack');
|
||||
const qoa = require('qoa');
|
||||
import jetpack from 'fs-jetpack';
|
||||
// @ts-ignore no typings for qoa
|
||||
import qoa from 'qoa';
|
||||
|
||||
qoa.config({
|
||||
prefix: '>',
|
||||
|
@ -72,6 +73,7 @@ async function start() {
|
|||
const keys = Object.keys(defaultSettings);
|
||||
|
||||
for (const item of keys) {
|
||||
// @ts-ignore no idea
|
||||
envfile += `${item}=${defaultSettings[item]}\n`;
|
||||
}
|
||||
jetpack.write('.env', envfile);
|
||||
|
@ -85,7 +87,8 @@ async function start() {
|
|||
console.log('== MAKE SURE TO CHANGE IT AFTER YOUR FIRST LOGIN ==');
|
||||
console.log('=====================================================');
|
||||
console.log();
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
setTimeout(() => {}, 1000);
|
||||
}
|
||||
|
||||
start();
|
||||
void start();
|
|
@ -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;
|
|
@ -1,77 +0,0 @@
|
|||
const JWT = require('jsonwebtoken');
|
||||
const db = require('./Database');
|
||||
const moment = require('moment');
|
||||
const log = require('../utils/Log');
|
||||
const Util = require('../utils/Util');
|
||||
|
||||
class Route {
|
||||
constructor(path, method, options) {
|
||||
if (!path) throw new Error('Every route needs a URL associated with it.');
|
||||
if (!method) throw new Error('Every route needs its method specified.');
|
||||
|
||||
this.path = path;
|
||||
this.method = method;
|
||||
this.options = options || {};
|
||||
}
|
||||
|
||||
async authorize(req, res) {
|
||||
const banned = await db
|
||||
.table('bans')
|
||||
.where({ ip: req.ip })
|
||||
.first();
|
||||
if (banned) return res.status(401).json({ message: 'This IP has been banned from using the service.' });
|
||||
|
||||
if (this.options.bypassAuth) return this.run(req, res, db);
|
||||
// The only reason I call it token here and not Api Key is to be backwards compatible
|
||||
// with the uploader and sharex
|
||||
// Small price to pay.
|
||||
if (req.headers.token) return this.authorizeApiKey(req, res, req.headers.token);
|
||||
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' });
|
||||
|
||||
return JWT.verify(token, Util.config.secret, async (error, decoded) => {
|
||||
if (error) {
|
||||
log.error(error);
|
||||
return res.status(401).json({ message: 'Invalid token' });
|
||||
}
|
||||
const id = decoded ? decoded.sub : '';
|
||||
const iat = decoded ? decoded.iat : '';
|
||||
|
||||
const user = await db
|
||||
.table('users')
|
||||
.where({ id })
|
||||
.first();
|
||||
if (!user) return res.status(401).json({ message: 'Invalid authorization' });
|
||||
if (iat && iat < moment(user.passwordEditedAt).format('x')) {
|
||||
return res.status(401).json({ message: 'Token expired' });
|
||||
}
|
||||
if (!user.enabled) return res.status(401).json({ message: 'This account has been disabled' });
|
||||
if (this.options.adminOnly && !user.isAdmin) { return res.status(401).json({ message: 'Invalid authorization' }); }
|
||||
|
||||
return this.run(req, res, db, user);
|
||||
});
|
||||
}
|
||||
|
||||
async authorizeApiKey(req, res, apiKey) {
|
||||
if (!this.options.canApiKey) return res.status(401).json({ message: 'Api Key not allowed for this resource' });
|
||||
const user = await db
|
||||
.table('users')
|
||||
.where({ apiKey })
|
||||
.first();
|
||||
if (!user) return res.status(401).json({ message: 'Invalid authorization' });
|
||||
if (!user.enabled) return res.status(401).json({ message: 'This account has been disabled' });
|
||||
|
||||
return this.run(req, res, db, user);
|
||||
}
|
||||
|
||||
run() {}
|
||||
|
||||
error(res, error) {
|
||||
log.error(error);
|
||||
return res.status(500).json({ message: 'There was a problem parsing the request' });
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Route;
|
|
@ -1,123 +0,0 @@
|
|||
require('dotenv').config();
|
||||
|
||||
if (!process.env.SERVER_PORT) {
|
||||
console.log('Run the setup script first or fill the .env file manually before starting');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
if (!process.env.DOMAIN) {
|
||||
console.log('You failed to provide a domain for your instance. Edit the .env file manually and fix it.');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const { loadNuxt, build } = require('nuxt');
|
||||
const express = require('express');
|
||||
const helmet = require('helmet');
|
||||
const cors = require('cors');
|
||||
const RateLimit = require('express-rate-limit');
|
||||
const bodyParser = require('body-parser');
|
||||
const jetpack = require('fs-jetpack');
|
||||
const path = require('path');
|
||||
const morgan = require('morgan');
|
||||
const rfs = require('rotating-file-stream');
|
||||
const CronJob = require('cron').CronJob;
|
||||
const log = require('../utils/Log');
|
||||
|
||||
const Util = require('../utils/Util');
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const rateLimiter = new RateLimit({
|
||||
windowMs: parseInt(Util.config.rateLimitWindow, 10),
|
||||
max: parseInt(Util.config.rateLimitMax, 10),
|
||||
delayMs: 0
|
||||
});
|
||||
|
||||
class Server {
|
||||
constructor() {
|
||||
this.port = parseInt(process.env.SERVER_PORT, 10);
|
||||
this.server = express();
|
||||
this.server.set('trust proxy', 1);
|
||||
this.server.use(helmet());
|
||||
this.server.use(cors({ allowedHeaders: ['Accept', 'Authorization', 'Cache-Control', 'X-Requested-With', 'Content-Type', 'albumId', 'finishedChunks'] }));
|
||||
this.server.use((req, res, 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 && req.headers.accept.includes('application/vnd.chibisafe.json')) return next();
|
||||
return res.status(405).json({ message: 'Incorrect `Accept` header provided' });
|
||||
});
|
||||
this.server.use(bodyParser.urlencoded({ extended: true }));
|
||||
this.server.use(bodyParser.json());
|
||||
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
const accessLogStream = rfs.createStream('access.log', {
|
||||
interval: '1d', // rotate daily
|
||||
path: path.join(__dirname, '../../../logs', 'log')
|
||||
});
|
||||
this.server.use(morgan('combined', { stream: accessLogStream }));
|
||||
}
|
||||
|
||||
// Apply rate limiting to the api only
|
||||
this.server.use('/api/', rateLimiter);
|
||||
|
||||
// Serve the uploads
|
||||
this.server.use(express.static(path.join(__dirname, '../../../uploads')));
|
||||
this.routesFolder = path.join(__dirname, '../routes');
|
||||
|
||||
// Save the cron job instances in case we want to stop them later
|
||||
this.jobs = {};
|
||||
}
|
||||
|
||||
registerAllTheRoutes() {
|
||||
jetpack.find(this.routesFolder, { matching: '*.js' }).forEach(routeFile => {
|
||||
const RouteClass = require(path.join('../../../', routeFile));
|
||||
let routes = [RouteClass];
|
||||
if (Array.isArray(RouteClass)) routes = RouteClass;
|
||||
for (const File of routes) {
|
||||
try {
|
||||
const route = new File();
|
||||
this.server[route.method](`/api${route.path}`, route.authorize.bind(route));
|
||||
log.info(`Found route ${route.method.toUpperCase()} /api${route.path}`);
|
||||
} catch (e) {
|
||||
log.error(`Failed loading route from file ${routeFile} with error: ${e.message}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async serveNuxt() {
|
||||
const isProd = process.env.NODE_ENV === 'production';
|
||||
const nuxt = await loadNuxt(isProd ? 'start' : 'dev');
|
||||
this.server.use(nuxt.render);
|
||||
if (!isProd) {
|
||||
build(nuxt);
|
||||
}
|
||||
}
|
||||
|
||||
createJobs() {
|
||||
// TODO: move into the database config. (we can just show the crontab line for start, later on we can add dropdowns and stuff)
|
||||
this.jobs.stats = new CronJob('0 0 * * * *', Util.saveStatsToDb, null, true);
|
||||
}
|
||||
|
||||
start() {
|
||||
jetpack.dir('uploads/chunks');
|
||||
jetpack.dir('uploads/thumbs/square');
|
||||
jetpack.dir('uploads/thumbs/preview');
|
||||
this.registerAllTheRoutes();
|
||||
this.serveNuxt();
|
||||
const server = this.server.listen(this.port, () => {
|
||||
log.success(`Backend ready and listening on port ${this.port}`);
|
||||
});
|
||||
server.setTimeout(600000);
|
||||
|
||||
this.createJobs();
|
||||
}
|
||||
}
|
||||
|
||||
const start = async () => {
|
||||
const conf = await Util.config;
|
||||
new Server().start();
|
||||
};
|
||||
|
||||
start();
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
import { PrismaClient } from '@prisma/client';
|
||||
const prisma = new PrismaClient();
|
||||
export default prisma;
|
|
@ -0,0 +1,90 @@
|
|||
import type { FastifyRequest } from 'fastify';
|
||||
|
||||
export interface RequestWithUser extends FastifyRequest {
|
||||
user: {
|
||||
id: number;
|
||||
username: string;
|
||||
isAdmin: boolean;
|
||||
apiKey?: string | null | undefined;
|
||||
};
|
||||
}
|
||||
|
||||
export interface User {
|
||||
id: number;
|
||||
username: string;
|
||||
password: string;
|
||||
enabled: boolean;
|
||||
isAdmin: boolean;
|
||||
apiKey: string;
|
||||
passwordEditedAt: string;
|
||||
apiKeyEditedAt: string;
|
||||
createdAt: string;
|
||||
editedAt: string;
|
||||
}
|
||||
|
||||
export interface File {
|
||||
id: number;
|
||||
userId?: number | null;
|
||||
name: string;
|
||||
original: string;
|
||||
type: string;
|
||||
size: number;
|
||||
hash: string;
|
||||
ip: string;
|
||||
createdAt: Date;
|
||||
editedAt: Date | null;
|
||||
}
|
||||
|
||||
export interface ExtendedFile extends File {
|
||||
url?: string;
|
||||
thumb?: string;
|
||||
thumbSquare?: string;
|
||||
preview?: string;
|
||||
}
|
||||
|
||||
export interface ExtendedFileWithData extends ExtendedFile {
|
||||
data: {
|
||||
hash: string;
|
||||
size: number;
|
||||
filename: string;
|
||||
originalName: string;
|
||||
mimeType: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface Album {
|
||||
id: number;
|
||||
userId: number;
|
||||
name: string;
|
||||
zippedAt: Date | null;
|
||||
createdAt: Date;
|
||||
editedAt: Date | null;
|
||||
nsfw: boolean;
|
||||
}
|
||||
|
||||
export interface Settings {
|
||||
domain: string;
|
||||
routePrefix: string;
|
||||
rateLimitWindow: number;
|
||||
rateLimitMax: number;
|
||||
secret: string;
|
||||
serviceName: string;
|
||||
chunkSize: number;
|
||||
maxSize: number;
|
||||
generateZips: boolean;
|
||||
generatedFilenameLength: number;
|
||||
generatedAlbumLength: number;
|
||||
blockedExtensions: string[];
|
||||
publicMode: boolean;
|
||||
userAccounts: boolean;
|
||||
metaThemeColor: string;
|
||||
metaDescription: string;
|
||||
metaKeywords: string;
|
||||
metaTwitterHandle: string;
|
||||
backgroundImageURL: string;
|
||||
logoURL: string;
|
||||
statisticsCron: string;
|
||||
enabledStatistics: string[];
|
||||
savedStatistics: string[];
|
||||
[key: string]: string | number | string[] | boolean;
|
||||
}
|
|
@ -1,54 +1,47 @@
|
|||
import dotenv from 'dotenv/config';
|
||||
import { NuxtConfig } from '@nuxt/types';
|
||||
import { getConfig } from '../utils/Util';
|
||||
import autoprefixer from 'autoprefixer';
|
||||
|
||||
const Util = require('./src/api/utils/Util');
|
||||
|
||||
export default async () => {
|
||||
/*
|
||||
FIXME:
|
||||
Since Util.config is not populated during production env because it needs to grab the values from the db
|
||||
we need to use this hack to populate it before we can access the properties without await like we do in the export below.
|
||||
This will be solved once the TypeScript rewrite is complete as we can can simply pass a config object to express
|
||||
and build from there, but for now the build needs to be triggered before the API is started.
|
||||
*/
|
||||
await Util.config;
|
||||
return {
|
||||
const settings = await getConfig();
|
||||
const config: NuxtConfig = {
|
||||
ssr: true,
|
||||
srcDir: 'src/site/',
|
||||
head: {
|
||||
title: Util.config.serviceName,
|
||||
titleTemplate: `%s | ${Util.config.serviceName}`,
|
||||
title: settings.serviceName,
|
||||
titleTemplate: `%s | ${settings.serviceName}`,
|
||||
// TODO: Add the directory with pictures for favicon and stuff
|
||||
meta: [
|
||||
{ charset: 'utf-8' },
|
||||
{ name: 'viewport', content: 'width=device-width, initial-scale=1' },
|
||||
{ hid: 'theme-color', name: 'theme-color', content: `${Util.config.metaThemeColor}` },
|
||||
{ hid: 'description', name: 'description', content: `${Util.config.metaDescription}` },
|
||||
{ hid: 'keywords', name: 'keywords', content: `${Util.config.metaKeywords}` },
|
||||
{ hid: 'theme-color', name: 'theme-color', content: `${settings.metaThemeColor}` },
|
||||
{ hid: 'description', name: 'description', content: `${settings.metaDescription}` },
|
||||
{ hid: 'keywords', name: 'keywords', content: `${settings.metaKeywords}` },
|
||||
{
|
||||
hid: 'apple-mobile-web-app-title',
|
||||
name: 'apple-mobile-web-app-title',
|
||||
content: `${Util.config.serviceName}`
|
||||
content: `${settings.serviceName}`
|
||||
},
|
||||
{ hid: 'application-name', name: 'application-name', content: `${Util.config.serviceName}` },
|
||||
{ hid: 'application-name', name: 'application-name', content: `${settings.serviceName}` },
|
||||
{ hid: 'twitter:card', name: 'twitter:card', content: 'summary' },
|
||||
{ hid: 'twitter:site', name: 'twitter:site', content: `${Util.config.metaTwitterHandle}` },
|
||||
{ hid: 'twitter:creator', name: 'twitter:creator', content: `${Util.config.metaTwitterHandle}` },
|
||||
{ hid: 'twitter:title', name: 'twitter:title', content: `${Util.config.serviceName}` },
|
||||
{ hid: 'twitter:description', name: 'twitter:description', content: `${Util.config.metaDescription}` },
|
||||
{ hid: 'twitter:site', name: 'twitter:site', content: `${settings.metaTwitterHandle}` },
|
||||
{ hid: 'twitter:creator', name: 'twitter:creator', content: `${settings.metaTwitterHandle}` },
|
||||
{ hid: 'twitter:title', name: 'twitter:title', content: `${settings.serviceName}` },
|
||||
{ hid: 'twitter:description', name: 'twitter:description', content: `${settings.metaDescription}` },
|
||||
{ hid: 'twitter:image', name: 'twitter:image', content: `/logo.png` },
|
||||
{ hid: 'og:url', property: 'og:url', content: `/` },
|
||||
{ hid: 'og:type', property: 'og:type', content: 'website' },
|
||||
{ hid: 'og:title', property: 'og:title', content: `${Util.config.serviceName}` },
|
||||
{ hid: 'og:description', property: 'og:description', content: `${Util.config.metaDescription}` },
|
||||
{ hid: 'og:title', property: 'og:title', content: `${settings.serviceName}` },
|
||||
{ hid: 'og:description', property: 'og:description', content: `${settings.metaDescription}` },
|
||||
{ hid: 'og:image', property: 'og:image', content: `/logo.png` },
|
||||
{ hid: 'og:image:secure_url', property: 'og:image:secure_url', content: `/logo.png` },
|
||||
{ hid: 'og:site_name', property: 'og:site_name', content: `${Util.config.serviceName}` }
|
||||
{ hid: 'og:site_name', property: 'og:site_name', content: `${settings.serviceName}` }
|
||||
],
|
||||
link: [
|
||||
{ rel: 'stylesheet', href: 'https://fonts.googleapis.com/css?family=Nunito:300,400,600,700' },
|
||||
|
||||
// This one is a pain in the ass to make it customizable, so you should edit it manually
|
||||
// @ts-ignore
|
||||
{ type: 'application/json+oembed', href: `/oembed.json` }
|
||||
]
|
||||
},
|
||||
|
@ -69,13 +62,14 @@ export default async () => {
|
|||
linkExactActiveClass: 'is-active'
|
||||
},
|
||||
env: {
|
||||
development: process.env.NODE_ENV !== 'production'
|
||||
development: Boolean(process.env.NODE_ENV !== 'production').toString()
|
||||
},
|
||||
axios: {
|
||||
baseURL: `${process.env.NODE_ENV === 'production' ? process.env.DOMAIN : 'http://localhost:5000'}/api`
|
||||
baseURL: `${process.env.NODE_ENV === 'production' ? process.env.DOMAIN as string : 'http://localhost:5000'}/api`
|
||||
},
|
||||
build: {
|
||||
extractCSS: process.env.NODE_ENV === 'production',
|
||||
// @ts-ignore
|
||||
postcss: {
|
||||
preset: {
|
||||
autoprefixer
|
||||
|
@ -89,4 +83,5 @@ export default async () => {
|
|||
}
|
||||
}
|
||||
};
|
||||
return config;
|
||||
};
|
|
@ -0,0 +1,67 @@
|
|||
import type { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify';
|
||||
import jetpack from 'fs-jetpack';
|
||||
import path from 'path';
|
||||
|
||||
export default {
|
||||
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}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];
|
||||
|
||||
// Get rid of the filename
|
||||
paths.pop();
|
||||
|
||||
// Get rid of the src/api/routes part
|
||||
paths.splice(0, 3);
|
||||
|
||||
let url: string = paths.join(slash);
|
||||
|
||||
// Transform path variables to express variables
|
||||
url = url.replace('_', ':');
|
||||
|
||||
// Append the missing /
|
||||
url = `/${url}`;
|
||||
|
||||
// Build final route
|
||||
url = `${route.options?.ignoreRoutePrefix ? '' : '/api'}${url}`;
|
||||
|
||||
// Run middlewares if any, and in order of execution
|
||||
const middlewares: any[] = [];
|
||||
if (route.middlewares?.length) {
|
||||
// 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 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
|
||||
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,36 +0,0 @@
|
|||
const chalk = require('chalk');
|
||||
const { dump } = require('dumper.js');
|
||||
|
||||
class Log {
|
||||
static info(args) {
|
||||
if (Log.checkIfArrayOrObject(args)) dump(args);
|
||||
else console.log(args); // eslint-disable-line no-console
|
||||
}
|
||||
|
||||
static success(args) {
|
||||
if (Log.checkIfArrayOrObject(args)) dump(args);
|
||||
else console.log(chalk.green(args)); // eslint-disable-line no-console
|
||||
}
|
||||
|
||||
static warn(args) {
|
||||
if (Log.checkIfArrayOrObject(args)) dump(args);
|
||||
else console.log(chalk.yellow(args)); // eslint-disable-line no-console
|
||||
}
|
||||
|
||||
static error(args) {
|
||||
if (Log.checkIfArrayOrObject(args)) dump(args);
|
||||
else console.log(chalk.red(args)); // eslint-disable-line no-console
|
||||
}
|
||||
|
||||
static debug(args) {
|
||||
if (Log.checkIfArrayOrObject(args)) dump(args);
|
||||
else console.log(chalk.gray(args)); // eslint-disable-line no-console
|
||||
}
|
||||
|
||||
static checkIfArrayOrObject(thing) {
|
||||
if (typeof thing === typeof [] || typeof thing === typeof {}) return true;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Log;
|
|
@ -1,225 +0,0 @@
|
|||
const si = require('systeminformation');
|
||||
|
||||
class StatsGenerator {
|
||||
// symbols would be better because they're unique, but harder to serialize them
|
||||
static Type = Object.freeze({
|
||||
// should contain key value: number
|
||||
TIME: 'time',
|
||||
// should contain key value: number
|
||||
BYTE: 'byte',
|
||||
// should contain key value: { used: number, total: number }
|
||||
BYTE_USAGE: 'byteUsage',
|
||||
// should contain key data: Array<{ key: string, value: number | string }>
|
||||
// and optionally a count/total
|
||||
DETAILED: 'detailed',
|
||||
// hidden type should be skipped during iteration, can contain anything
|
||||
// these should be treated on a case by case basis on the frontend
|
||||
HIDDEN: 'hidden'
|
||||
});
|
||||
|
||||
static statGenerators = {
|
||||
system: StatsGenerator.getSystemInfo,
|
||||
fileSystems: StatsGenerator.getFileSystemsInfo,
|
||||
uploads: StatsGenerator.getUploadsInfo,
|
||||
users: StatsGenerator.getUsersInfo,
|
||||
albums: StatsGenerator.getAlbumStats
|
||||
};
|
||||
|
||||
static keyOrder = Object.keys(StatsGenerator.statGenerators);
|
||||
|
||||
static async getSystemInfo() {
|
||||
const os = await si.osInfo();
|
||||
|
||||
const currentLoad = await si.currentLoad();
|
||||
const mem = await si.mem();
|
||||
const time = si.time();
|
||||
const nodeUptime = process.uptime();
|
||||
|
||||
return {
|
||||
'Platform': `${os.platform} ${os.arch}`,
|
||||
'Distro': `${os.distro} ${os.release}`,
|
||||
'Kernel': os.kernel,
|
||||
'CPU Load': `${currentLoad.currentload.toFixed(1)}%`,
|
||||
'CPUs Load': currentLoad.cpus.map(cpu => `${cpu.load.toFixed(1)}%`).join(', '),
|
||||
'System Memory': {
|
||||
value: {
|
||||
used: mem.active,
|
||||
total: mem.total
|
||||
},
|
||||
type: StatsGenerator.Type.BYTE_USAGE
|
||||
},
|
||||
'Memory Usage': {
|
||||
value: process.memoryUsage().rss,
|
||||
type: StatsGenerator.Type.BYTE
|
||||
},
|
||||
'System Uptime': {
|
||||
value: time.uptime,
|
||||
type: StatsGenerator.Type.TIME
|
||||
},
|
||||
'Node.js': `${process.versions.node}`,
|
||||
'Service Uptime': {
|
||||
value: Math.floor(nodeUptime),
|
||||
type: StatsGenerator.Type.TIME
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
static async getFileSystemsInfo() {
|
||||
const stats = {};
|
||||
|
||||
const fsSize = await si.fsSize();
|
||||
for (const fs of fsSize) {
|
||||
stats[`${fs.fs} (${fs.type}) on ${fs.mount}`] = {
|
||||
value: {
|
||||
total: fs.size,
|
||||
used: fs.used
|
||||
},
|
||||
type: StatsGenerator.Type.BYTE_USAGE
|
||||
};
|
||||
}
|
||||
|
||||
return stats;
|
||||
}
|
||||
|
||||
static async getUploadsInfo(db) {
|
||||
const stats = {
|
||||
'Total': 0,
|
||||
'Images': 0,
|
||||
'Videos': 0,
|
||||
'Others': {
|
||||
data: {},
|
||||
count: 0,
|
||||
type: StatsGenerator.Type.DETAILED
|
||||
},
|
||||
'Size in DB': {
|
||||
value: 0,
|
||||
type: StatsGenerator.Type.BYTE
|
||||
}
|
||||
};
|
||||
|
||||
const getFilesCountAndSize = async () => {
|
||||
const uploads = await db.table('files').select('size');
|
||||
|
||||
return {
|
||||
'Total': uploads.length,
|
||||
'Size in DB': {
|
||||
value: uploads.reduce((acc, upload) => acc + parseInt(upload.size, 10), 0),
|
||||
type: StatsGenerator.Type.BYTE
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const getImagesCount = async () => {
|
||||
const Images = await db.table('files')
|
||||
.where('type', 'like', `image/%`)
|
||||
.count('id as count')
|
||||
.then(rows => rows[0].count);
|
||||
|
||||
return { Images };
|
||||
};
|
||||
|
||||
const getVideosCount = async () => {
|
||||
const Videos = await db.table('files')
|
||||
.where('type', 'like', `video/%`)
|
||||
.count('id as count')
|
||||
.then(rows => rows[0].count);
|
||||
|
||||
return { Videos };
|
||||
};
|
||||
|
||||
const getOthersCount = async () => {
|
||||
// rename to key, value from type, count
|
||||
const data = await db.table('files')
|
||||
.select('type as key')
|
||||
.count('id as value')
|
||||
.whereNot('type', 'like', `image/%`)
|
||||
.whereNot('type', 'like', `video/%`)
|
||||
.groupBy('key')
|
||||
.orderBy('value', 'desc');
|
||||
|
||||
const count = data.reduce((acc, val) => acc + val.value, 0);
|
||||
|
||||
return {
|
||||
Others: {
|
||||
data,
|
||||
count,
|
||||
type: StatsGenerator.Type.DETAILED
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const result = await Promise.all([getFilesCountAndSize(), getImagesCount(), getVideosCount(), getOthersCount()]);
|
||||
|
||||
return { ...stats, ...Object.assign({}, ...result) };
|
||||
}
|
||||
|
||||
static async getUsersInfo(db) {
|
||||
const stats = {
|
||||
Total: 0,
|
||||
Admins: 0,
|
||||
Disabled: 0
|
||||
};
|
||||
|
||||
const users = await db.table('users');
|
||||
stats.Total = users.length;
|
||||
|
||||
for (const user of users) {
|
||||
if (!user.enabled) {
|
||||
stats.Disabled++;
|
||||
}
|
||||
|
||||
if (user.isAdmin) {
|
||||
stats.Admins++;
|
||||
}
|
||||
}
|
||||
|
||||
return stats;
|
||||
}
|
||||
|
||||
static async getAlbumStats(db) {
|
||||
const stats = {
|
||||
'Total': 0,
|
||||
'NSFW': 0,
|
||||
'Generated archives': 0,
|
||||
'Generated identifiers': 0,
|
||||
'Files in albums': 0
|
||||
};
|
||||
|
||||
const albums = await db.table('albums');
|
||||
stats.Total = albums.length;
|
||||
for (const album of albums) {
|
||||
if (album.nsfw) stats.NSFW++;
|
||||
if (album.zipGeneratedAt) stats['Generated archives']++; // XXX: Bobby checks each after if a zip really exists on the disk. Is it really needed?
|
||||
}
|
||||
|
||||
stats['Generated identifiers'] = await db.table('albumsLinks').count('id as count').then(rows => rows[0].count);
|
||||
stats['Files in albums'] = await db.table('albumsFiles')
|
||||
.whereNotNull('albumId')
|
||||
.count('id as count')
|
||||
.then(rows => rows[0].count);
|
||||
|
||||
return stats;
|
||||
}
|
||||
|
||||
static async getStats(db) {
|
||||
const res = {};
|
||||
|
||||
for (const [name, funct] of Object.entries(StatsGenerator.statGenerators)) {
|
||||
res[name] = await funct(db);
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
static async getMissingStats(db, existingStats) {
|
||||
const res = {};
|
||||
|
||||
for (const [name, funct] of Object.entries(StatsGenerator.statGenerators)) {
|
||||
if (existingStats.indexOf(name) === -1) res[name] = await funct(db);
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = StatsGenerator;
|
|
@ -0,0 +1,276 @@
|
|||
import si from 'systeminformation';
|
||||
import prisma from '../structures/database';
|
||||
|
||||
export const Type = Object.freeze({
|
||||
// should contain key value: number
|
||||
TIME: 'time',
|
||||
// should contain key value: number
|
||||
BYTE: 'byte',
|
||||
// should contain key value: { used: number, total: number }
|
||||
BYTE_USAGE: 'byteUsage',
|
||||
// should contain key data: Array<{ key: string, value: number | string }>
|
||||
// and optionally a count/total
|
||||
DETAILED: 'detailed',
|
||||
// hidden type should be skipped during iteration, can contain anything
|
||||
// these should be treated on a case by case basis on the frontend
|
||||
HIDDEN: 'hidden'
|
||||
});
|
||||
|
||||
export const getSystemInfo = async () => {
|
||||
const os = await si.osInfo();
|
||||
|
||||
const currentLoad = await si.currentLoad();
|
||||
const mem = await si.mem();
|
||||
const time = si.time();
|
||||
const nodeUptime = process.uptime();
|
||||
|
||||
return {
|
||||
'Platform': `${os.platform} ${os.arch}`,
|
||||
'Distro': `${os.distro} ${os.release}`,
|
||||
'Kernel': os.kernel,
|
||||
'CPU Load': `${currentLoad.currentLoad.toFixed(1)}%`,
|
||||
'CPUs Load': currentLoad.cpus.map(cpu => `${cpu.load.toFixed(1)}%`).join(', '),
|
||||
'System Memory': {
|
||||
value: {
|
||||
used: mem.active,
|
||||
total: mem.total
|
||||
},
|
||||
type: Type.BYTE_USAGE
|
||||
},
|
||||
'Memory Usage': {
|
||||
value: process.memoryUsage().rss,
|
||||
type: Type.BYTE
|
||||
},
|
||||
'System Uptime': {
|
||||
value: time.uptime,
|
||||
type: Type.TIME
|
||||
},
|
||||
'Node.js': `${process.versions.node}`,
|
||||
'Service Uptime': {
|
||||
value: Math.floor(nodeUptime),
|
||||
type: Type.TIME
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
export const getFileSystemsInfo = async () => {
|
||||
const stats: Record<string, { value: { total: number; used: number }; type: string }> = {};
|
||||
|
||||
const fsSize = await si.fsSize();
|
||||
for (const fs of fsSize) {
|
||||
stats[`${fs.fs} (${fs.type}) on ${fs.mount}`] = {
|
||||
value: {
|
||||
total: fs.size,
|
||||
used: fs.used
|
||||
},
|
||||
type: Type.BYTE_USAGE
|
||||
};
|
||||
}
|
||||
|
||||
return stats;
|
||||
};
|
||||
|
||||
export const getUploadsInfo = async () => {
|
||||
const stats = {
|
||||
'Total': 0,
|
||||
'Images': 0,
|
||||
'Videos': 0,
|
||||
'Others': {
|
||||
data: {},
|
||||
count: 0,
|
||||
type: Type.DETAILED
|
||||
},
|
||||
'Size in DB': {
|
||||
value: 0,
|
||||
type: Type.BYTE
|
||||
}
|
||||
};
|
||||
|
||||
const getFilesCountAndSize = async () => {
|
||||
const uploads = await prisma.files.findMany({
|
||||
select: {
|
||||
size: true
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
'Total': uploads.length,
|
||||
'Size in DB': {
|
||||
value: uploads.reduce((acc, upload) => acc + upload.size, 0),
|
||||
type: Type.BYTE
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const getImagesCount = async () => {
|
||||
const Images = await prisma.files.count({
|
||||
select: {
|
||||
id: true
|
||||
},
|
||||
where: {
|
||||
type: {
|
||||
contains: 'image/'
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { Images: Images.id };
|
||||
};
|
||||
|
||||
const getVideosCount = async () => {
|
||||
const Videos = await prisma.files.count({
|
||||
select: {
|
||||
id: true
|
||||
},
|
||||
where: {
|
||||
type: {
|
||||
contains: 'video/'
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { Videos: Videos.id };
|
||||
};
|
||||
|
||||
const getOthersCount = async () => {
|
||||
// TODO: This needs additional testing as the returned object is no the same one as
|
||||
// before the typescript rewrite due to Prisma constraints.
|
||||
// https://www.prisma.io/docs/concepts/components/prisma-client/aggregation-grouping-summarizing
|
||||
const data = await prisma.files.findMany({
|
||||
select: {
|
||||
type: true
|
||||
},
|
||||
where: {
|
||||
NOT: [
|
||||
{
|
||||
type: {
|
||||
contains: 'image/'
|
||||
}
|
||||
},
|
||||
{
|
||||
type: {
|
||||
contains: 'video/'
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
orderBy: {
|
||||
type: 'desc'
|
||||
}
|
||||
});
|
||||
/*
|
||||
// rename to key, value from type, count
|
||||
const data = await db.table('files')
|
||||
.select('type as key')
|
||||
.count('id as value')
|
||||
.whereNot('type', 'like', `image/%`)
|
||||
.whereNot('type', 'like', `video/%`)
|
||||
.groupBy('key')
|
||||
.orderBy('value', 'desc');
|
||||
|
||||
const count = data.reduce((acc, val) => acc + val.value, 0);
|
||||
|
||||
return {
|
||||
Others: {
|
||||
data,
|
||||
count,
|
||||
type: Type.DETAILED
|
||||
}
|
||||
};
|
||||
*/
|
||||
};
|
||||
|
||||
const result = await Promise.all([getFilesCountAndSize(), getImagesCount(), getVideosCount(), getOthersCount()]);
|
||||
|
||||
return { ...stats, ...Object.assign({}, ...result) };
|
||||
};
|
||||
|
||||
export const getUsersInfo = async () => {
|
||||
const stats = {
|
||||
Total: 0,
|
||||
Admins: 0,
|
||||
Disabled: 0
|
||||
};
|
||||
|
||||
const users = await prisma.users.findMany({
|
||||
select: {
|
||||
enabled: true,
|
||||
isAdmin: true
|
||||
}
|
||||
});
|
||||
|
||||
stats.Total = users.length;
|
||||
|
||||
for (const user of users) {
|
||||
if (!user.enabled) {
|
||||
stats.Disabled++;
|
||||
}
|
||||
|
||||
if (user.isAdmin) {
|
||||
stats.Admins++;
|
||||
}
|
||||
}
|
||||
|
||||
return stats;
|
||||
};
|
||||
|
||||
export const getAlbumStats = async () => {
|
||||
const stats = {
|
||||
'Total': 0,
|
||||
'NSFW': 0,
|
||||
'Generated archives': 0,
|
||||
'Generated identifiers': 0,
|
||||
'Files in albums': 0
|
||||
};
|
||||
|
||||
const albums = await prisma.albums.findMany({
|
||||
select: {
|
||||
nsfw: true,
|
||||
zippedAt: true
|
||||
}
|
||||
});
|
||||
stats.Total = albums.length;
|
||||
for (const album of albums) {
|
||||
if (album.nsfw) stats.NSFW++;
|
||||
if (album.zippedAt) stats['Generated archives']++; // XXX: Bobby checks each after if a zip really exists on the disk. Is it really needed?
|
||||
}
|
||||
|
||||
stats['Generated identifiers'] = await prisma.albumsLinks.count();
|
||||
stats['Files in albums'] = await prisma.albumsFiles.count();
|
||||
|
||||
return stats;
|
||||
};
|
||||
|
||||
// TODO: Finish this as it does nothing rn
|
||||
export const getStats = () => ({});
|
||||
/*
|
||||
export const getStats = async () => {
|
||||
const res = {};
|
||||
|
||||
for (const [name, funct] of Object.entries(statGenerators)) {
|
||||
res[name] = await funct(db);
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
export const getMissingStats = async (db, existingStats) => {
|
||||
const res = {};
|
||||
|
||||
for (const [name, funct] of Object.entries(statGenerators)) {
|
||||
if (existingStats.indexOf(name) === -1) res[name] = await funct(db);
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
||||
*/
|
||||
|
||||
export const statGenerators = {
|
||||
system: getSystemInfo,
|
||||
fileSystems: getFileSystemsInfo,
|
||||
uploads: getUploadsInfo,
|
||||
users: getUsersInfo,
|
||||
albums: getAlbumStats
|
||||
};
|
||||
|
||||
export const keyOrder = Object.keys(statGenerators);
|
|
@ -1,105 +0,0 @@
|
|||
const jetpack = require('fs-jetpack');
|
||||
const path = require('path');
|
||||
const sharp = require('sharp');
|
||||
const ffmpeg = require('fluent-ffmpeg');
|
||||
const previewUtil = require('./videoPreview/FragmentPreview');
|
||||
|
||||
const log = require('./Log');
|
||||
|
||||
class ThumbUtil {
|
||||
static imageExtensions = ['.jpg', '.jpeg', '.gif', '.png', '.webp'];
|
||||
static videoExtensions = ['.webm', '.mp4', '.wmv', '.avi', '.mov'];
|
||||
|
||||
static thumbPath = path.join(__dirname, '../../../', 'uploads', 'thumbs');
|
||||
static squareThumbPath = path.join(__dirname, '../../../', 'uploads', 'thumbs', 'square');
|
||||
static videoPreviewPath = path.join(__dirname, '../../../', 'uploads', 'thumbs', 'preview');
|
||||
|
||||
static generateThumbnails(filename) {
|
||||
if (!filename) return;
|
||||
const ext = path.extname(filename).toLowerCase();
|
||||
const output = `${filename.slice(0, -ext.length)}.webp`;
|
||||
const previewOutput = `${filename.slice(0, -ext.length)}.webm`;
|
||||
|
||||
// eslint-disable-next-line max-len
|
||||
if (ThumbUtil.imageExtensions.includes(ext)) return ThumbUtil.generateThumbnailForImage(filename, output);
|
||||
// eslint-disable-next-line max-len
|
||||
if (ThumbUtil.videoExtensions.includes(ext)) return ThumbUtil.generateThumbnailForVideo(filename, previewOutput);
|
||||
return null;
|
||||
}
|
||||
|
||||
static async generateThumbnailForImage(filename, output) {
|
||||
const filePath = path.join(__dirname, '../../../', 'uploads', filename);
|
||||
|
||||
const file = await jetpack.readAsync(filePath, 'buffer');
|
||||
await sharp(file)
|
||||
.resize(64, 64)
|
||||
.toFormat('webp')
|
||||
.toFile(path.join(ThumbUtil.squareThumbPath, output));
|
||||
await sharp(file)
|
||||
.resize(225, null)
|
||||
.toFormat('webp')
|
||||
.toFile(path.join(ThumbUtil.thumbPath, output));
|
||||
}
|
||||
|
||||
static async generateThumbnailForVideo(filename, output) {
|
||||
const filePath = path.join(__dirname, '../../../', 'uploads', filename);
|
||||
|
||||
ffmpeg(filePath)
|
||||
.thumbnail({
|
||||
timestamps: [0],
|
||||
filename: '%b.webp',
|
||||
folder: ThumbUtil.squareThumbPath,
|
||||
size: '64x64'
|
||||
})
|
||||
.on('error', error => log.error(error.message));
|
||||
|
||||
ffmpeg(filePath)
|
||||
.thumbnail({
|
||||
timestamps: [0],
|
||||
filename: '%b.webp',
|
||||
folder: ThumbUtil.thumbPath,
|
||||
size: '150x?'
|
||||
})
|
||||
.on('error', error => log.error(error.message));
|
||||
|
||||
try {
|
||||
await previewUtil({
|
||||
input: filePath,
|
||||
width: 150,
|
||||
output: path.join(ThumbUtil.videoPreviewPath, output)
|
||||
});
|
||||
} catch (e) {
|
||||
log.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
static getFileThumbnail(filename) {
|
||||
if (!filename) return null;
|
||||
const ext = path.extname(filename).toLowerCase();
|
||||
|
||||
const isImage = ThumbUtil.imageExtensions.includes(ext);
|
||||
const isVideo = ThumbUtil.videoExtensions.includes(ext);
|
||||
|
||||
if (isImage) return { thumb: `${filename.slice(0, -ext.length)}.webp` };
|
||||
if (isVideo) {
|
||||
return {
|
||||
thumb: `${filename.slice(0, -ext.length)}.webp`,
|
||||
preview: `${filename.slice(0, -ext.length)}.webm`
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
static async removeThumbs({ thumb, preview }) {
|
||||
if (thumb) {
|
||||
await jetpack.removeAsync(path.join(ThumbUtil.thumbPath, thumb));
|
||||
await jetpack.removeAsync(path.join(ThumbUtil.squareThumbPath, thumb));
|
||||
}
|
||||
if (preview) {
|
||||
await jetpack.removeAsync(path.join(ThumbUtil.videoPreviewPath, preview));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ThumbUtil;
|
|
@ -0,0 +1,102 @@
|
|||
import jetpack from 'fs-jetpack';
|
||||
import path from 'path';
|
||||
import sharp from 'sharp';
|
||||
import ffmpeg from 'fluent-ffmpeg';
|
||||
import previewUtil from './videoPreview/FragmentPreview';
|
||||
|
||||
// TODO: Check that importing the log function works for routes and CLI (generateThumbs.ts)
|
||||
import { log } from '../main';
|
||||
|
||||
const imageExtensions = ['.jpg', '.jpeg', '.gif', '.png', '.webp'];
|
||||
const videoExtensions = ['.webm', '.mp4', '.wmv', '.avi', '.mov'];
|
||||
|
||||
const thumbPath = path.join(__dirname, '../../../', 'uploads', 'thumbs');
|
||||
const squareThumbPath = path.join(__dirname, '../../../', 'uploads', 'thumbs', 'square');
|
||||
const videoPreviewPath = path.join(__dirname, '../../../', 'uploads', 'thumbs', 'preview');
|
||||
|
||||
export const generateThumbnails = (filename: string) => {
|
||||
if (!filename) return;
|
||||
const ext = path.extname(filename).toLowerCase();
|
||||
const output = `${filename.slice(0, -ext.length)}.webp`;
|
||||
const previewOutput = `${filename.slice(0, -ext.length)}.webm`;
|
||||
|
||||
// eslint-disable-next-line max-len
|
||||
if (imageExtensions.includes(ext)) return generateThumbnailForImage(filename, output);
|
||||
// eslint-disable-next-line max-len
|
||||
if (videoExtensions.includes(ext)) return generateThumbnailForVideo(filename, previewOutput);
|
||||
return null;
|
||||
};
|
||||
|
||||
const generateThumbnailForImage = async (filename: string, output: string) => {
|
||||
const filePath = path.join(__dirname, '../../../', 'uploads', filename);
|
||||
|
||||
const file = await jetpack.readAsync(filePath, 'buffer');
|
||||
await sharp(file)
|
||||
.resize(64, 64)
|
||||
.toFormat('webp')
|
||||
.toFile(path.join(squareThumbPath, output));
|
||||
await sharp(file)
|
||||
.resize(225, null)
|
||||
.toFormat('webp')
|
||||
.toFile(path.join(thumbPath, output));
|
||||
};
|
||||
|
||||
const generateThumbnailForVideo = async (filename: string, output: string) => {
|
||||
const filePath = path.join(__dirname, '../../../', 'uploads', filename);
|
||||
|
||||
ffmpeg(filePath)
|
||||
.thumbnail({
|
||||
timestamps: [0],
|
||||
filename: '%b.webp',
|
||||
folder: squareThumbPath,
|
||||
size: '64x64'
|
||||
})
|
||||
.on('error', error => log.error(error.message));
|
||||
|
||||
ffmpeg(filePath)
|
||||
.thumbnail({
|
||||
timestamps: [0],
|
||||
filename: '%b.webp',
|
||||
folder: thumbPath,
|
||||
size: '150x?'
|
||||
})
|
||||
.on('error', error => log.error(error.message));
|
||||
|
||||
try {
|
||||
await previewUtil({
|
||||
input: filePath,
|
||||
width: 150,
|
||||
output: path.join(videoPreviewPath, output)
|
||||
});
|
||||
} catch (e) {
|
||||
log.error(e);
|
||||
}
|
||||
};
|
||||
|
||||
export const getFileThumbnail = (filename: string) => {
|
||||
if (!filename) return null;
|
||||
const ext = path.extname(filename).toLowerCase();
|
||||
|
||||
const isImage = imageExtensions.includes(ext);
|
||||
const isVideo = videoExtensions.includes(ext);
|
||||
|
||||
if (isImage) return { thumb: `${filename.slice(0, -ext.length)}.webp` };
|
||||
if (isVideo) {
|
||||
return {
|
||||
thumb: `${filename.slice(0, -ext.length)}.webp`,
|
||||
preview: `${filename.slice(0, -ext.length)}.webm`
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export const removeThumbs = async ({ thumb, preview }: { thumb?: string; preview?: string }) => {
|
||||
if (thumb) {
|
||||
await jetpack.removeAsync(path.join(thumbPath, thumb));
|
||||
await jetpack.removeAsync(path.join(squareThumbPath, thumb));
|
||||
}
|
||||
if (preview) {
|
||||
await jetpack.removeAsync(path.join(videoPreviewPath, preview));
|
||||
}
|
||||
};
|
|
@ -1,419 +0,0 @@
|
|||
/* eslint-disable no-await-in-loop */
|
||||
const jetpack = require('fs-jetpack');
|
||||
const randomstring = require('randomstring');
|
||||
const path = require('path');
|
||||
const JWT = require('jsonwebtoken');
|
||||
const db = require('../structures/Database');
|
||||
const moment = require('moment');
|
||||
const Zip = require('adm-zip');
|
||||
const uuidv4 = require('uuid/v4');
|
||||
|
||||
const log = require('./Log');
|
||||
const ThumbUtil = require('./ThumbUtil');
|
||||
const StatsGenerator = require('./StatsGenerator');
|
||||
|
||||
const preserveExtensions = ['.tar.gz', '.tar.z', '.tar.bz2', '.tar.lzma', '.tar.lzo', '.tar.xz'];
|
||||
|
||||
class Util {
|
||||
static uploadPath = path.join(__dirname, '../../../', 'uploads');
|
||||
static statsLastSavedTime = null;
|
||||
static _config = null;
|
||||
|
||||
static get config() {
|
||||
if (this._config) return this._config;
|
||||
return (async () => {
|
||||
if (this._config === null) {
|
||||
const conf = await db('settings').select('key', 'value');
|
||||
this._config = conf.reduce((obj, item) => (
|
||||
// eslint-disable-next-line no-sequences
|
||||
obj[item.key] = typeof item.value === 'string' || item.value instanceof String ? JSON.parse(item.value) : item.value, obj
|
||||
), {});
|
||||
}
|
||||
return this._config;
|
||||
})();
|
||||
}
|
||||
|
||||
static invalidateConfigCache() {
|
||||
this._config = null;
|
||||
}
|
||||
|
||||
static getEnvironmentDefaults() {
|
||||
return {
|
||||
domain: process.env.DOMAIN,
|
||||
routePrefix: '/api',
|
||||
rateLimitWindow: process.env.RATE_LIMIT_WINDOW || 2,
|
||||
rateLimitMax: process.env.RATE_LIMIT_MAX || 5,
|
||||
secret: process.env.SECRET || randomstring.generate(64),
|
||||
serviceName: process.env.SERVICE_NAME || 'change-me',
|
||||
chunkSize: process.env.CHUNK_SIZE || 90,
|
||||
maxSize: process.env.MAX_SIZE || 5000,
|
||||
// eslint-disable-next-line eqeqeq
|
||||
generateZips: process.env.GENERATE_ZIPS == undefined ? true : false,
|
||||
generatedFilenameLength: process.env.GENERATED_FILENAME_LENGTH || 12,
|
||||
generatedAlbumLength: process.env.GENERATED_ALBUM_LENGTH || 6,
|
||||
blockedExtensions: process.env.BLOCKED_EXTENSIONS ? process.env.BLOCKED_EXTENSIONS.split(',') : ['.jar', '.exe', '.msi', '.com', '.bat', '.cmd', '.scr', '.ps1', '.sh'],
|
||||
// eslint-disable-next-line eqeqeq
|
||||
publicMode: process.env.PUBLIC_MODE == undefined ? true : false,
|
||||
// eslint-disable-next-line eqeqeq
|
||||
userAccounts: process.env.USER_ACCOUNTS == undefined ? true : false,
|
||||
metaThemeColor: process.env.META_THEME_COLOR || '#20222b',
|
||||
metaDescription: process.env.META_DESCRIPTION || 'Blazing fast file uploader and bunker written in node! 🚀',
|
||||
metaKeywords: process.env.META_KEYWORDS || 'chibisafe,lolisafe,upload,uploader,file,vue,images,ssr,file uploader,free',
|
||||
metaTwitterHandle: process.env.META_TWITTER_HANDLE || '@your-handle',
|
||||
backgroundImageURL: process.env.BACKGROUND_IMAGE_URL || '',
|
||||
logoURL: process.env.LOGO_URL || '',
|
||||
statisticsCron: process.env.STATISTICS_CRON || '0 0 * * * *',
|
||||
enabledStatistics: process.env.ENABLED_STATISTICS ? process.env.ENABLED_STATISTICS.split(',') : ['system', 'fileSystems', 'uploads', 'users', 'albums'],
|
||||
savedStatistics: process.env.SAVED_STATISTICS ? process.env.SAVED_STATISTICS.split(',') : ['system', 'fileSystems', 'uploads', 'users', 'albums']
|
||||
};
|
||||
}
|
||||
|
||||
static async wipeConfigDb() {
|
||||
try {
|
||||
await db.table('settings').del();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
static async writeConfigToDb(config, wipe = false) {
|
||||
// TODO: Check that the config passes the joi schema validation
|
||||
if (!config || !config.key) return;
|
||||
try {
|
||||
config.value = JSON.stringify(config.value);
|
||||
await db.table('settings').insert(config);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
this.invalidateConfigCache();
|
||||
}
|
||||
}
|
||||
|
||||
static uuid() {
|
||||
return uuidv4();
|
||||
}
|
||||
|
||||
static isExtensionBlocked(extension) {
|
||||
return this.config.blockedExtensions.includes(extension);
|
||||
}
|
||||
|
||||
static getMimeFromType(fileTypeMimeObj) {
|
||||
return fileTypeMimeObj ? fileTypeMimeObj.mime : undefined;
|
||||
}
|
||||
|
||||
static constructFilePublicLink(req, file) {
|
||||
/*
|
||||
TODO: This wont work without a reverse proxy serving both
|
||||
the site and the API under the same domain. Pls fix.
|
||||
*/
|
||||
const host = this.getHost(req);
|
||||
file.url = `${host}/${file.name}`;
|
||||
const { thumb, preview } = ThumbUtil.getFileThumbnail(file.name) || {};
|
||||
if (thumb) {
|
||||
file.thumb = `${host}/thumbs/${thumb}`;
|
||||
file.thumbSquare = `${host}/thumbs/square/${thumb}`;
|
||||
file.preview = preview && `${host}/thumbs/preview/${preview}`;
|
||||
}
|
||||
return file;
|
||||
}
|
||||
|
||||
static getUniqueFilename(extension) {
|
||||
const retry = (i = 0) => {
|
||||
const filename = randomstring.generate({
|
||||
length: this.config.generatedFilenameLength,
|
||||
capitalization: 'lowercase'
|
||||
}) + extension;
|
||||
|
||||
// TODO: Change this to look for the file in the db instead of in the filesystem
|
||||
const exists = jetpack.exists(path.join(Util.uploadPath, filename));
|
||||
if (!exists) return filename;
|
||||
if (i < 5) return retry(i + 1);
|
||||
log.error('Couldnt allocate identifier for file');
|
||||
return null;
|
||||
};
|
||||
return retry();
|
||||
}
|
||||
|
||||
static getUniqueAlbumIdentifier() {
|
||||
const retry = async (i = 0) => {
|
||||
const identifier = randomstring.generate({
|
||||
length: this.config.generatedAlbumLength,
|
||||
capitalization: 'lowercase'
|
||||
});
|
||||
const exists = await db
|
||||
.table('links')
|
||||
.where({ identifier })
|
||||
.first();
|
||||
if (!exists) return identifier;
|
||||
/*
|
||||
It's funny but if you do i++ the asignment never gets done resulting in an infinite loop
|
||||
*/
|
||||
if (i < 5) return retry(i + 1);
|
||||
log.error('Couldnt allocate identifier for album');
|
||||
return null;
|
||||
};
|
||||
return retry();
|
||||
}
|
||||
|
||||
static getFilenameFromPath(fullPath) {
|
||||
return fullPath.replace(/^.*[\\\/]/, ''); // eslint-disable-line no-useless-escape
|
||||
}
|
||||
|
||||
static async deleteFile(filename, deleteFromDB = false) {
|
||||
const thumbName = ThumbUtil.getFileThumbnail(filename);
|
||||
try {
|
||||
await jetpack.removeAsync(path.join(Util.uploadPath, filename));
|
||||
if (thumbName) await ThumbUtil.removeThumbs(thumbName);
|
||||
|
||||
if (deleteFromDB) {
|
||||
await db
|
||||
.table('files')
|
||||
.where('name', filename)
|
||||
.delete()
|
||||
.wasMutated();
|
||||
}
|
||||
} catch (error) {
|
||||
log.error(`There was an error removing the file < ${filename} >`);
|
||||
log.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
static async deleteAllFilesFromAlbum(id) {
|
||||
try {
|
||||
const fileAlbums = await db.table('albumsFiles').where({ albumId: id });
|
||||
for (const fileAlbum of fileAlbums) {
|
||||
const file = await db
|
||||
.table('files')
|
||||
.where({ id: fileAlbum.fileId })
|
||||
.first();
|
||||
|
||||
if (!file) continue;
|
||||
|
||||
await this.deleteFile(file.name, true);
|
||||
}
|
||||
} catch (error) {
|
||||
log.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
static async deleteAllFilesFromUser(id) {
|
||||
try {
|
||||
const files = await db.table('files').where({ userId: id });
|
||||
for (const file of files) {
|
||||
await this.deleteFile(file.name, true);
|
||||
}
|
||||
} catch (error) {
|
||||
log.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
static async deleteAllFilesFromTag(id) {
|
||||
try {
|
||||
const fileTags = await db.table('fileTags').where({ tagId: id });
|
||||
for (const fileTag of fileTags) {
|
||||
const file = await db
|
||||
.table('files')
|
||||
.where({ id: fileTag.fileId })
|
||||
.first();
|
||||
if (!file) continue;
|
||||
await this.deleteFile(file.name, true);
|
||||
}
|
||||
} catch (error) {
|
||||
log.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
static async isAuthorized(req) {
|
||||
if (req.headers.token) {
|
||||
const user = await db.table('users').where({ apiKey: req.headers.token }).first();
|
||||
if (!user || !user.enabled) return false;
|
||||
return user;
|
||||
}
|
||||
|
||||
if (!req.headers.authorization) return false;
|
||||
const token = req.headers.authorization.split(' ')[1];
|
||||
if (!token) return false;
|
||||
|
||||
return JWT.verify(token, this.config.secret, async (error, decoded) => {
|
||||
if (error) {
|
||||
log.error(error);
|
||||
return false;
|
||||
}
|
||||
const id = decoded ? decoded.sub : '';
|
||||
const iat = decoded ? decoded.iat : '';
|
||||
|
||||
const user = await db
|
||||
.table('users')
|
||||
.where({ id })
|
||||
.first();
|
||||
if (!user || !user.enabled) return false;
|
||||
if (iat && iat < moment(user.passwordEditedAt).format('x')) return false;
|
||||
|
||||
return user;
|
||||
});
|
||||
}
|
||||
|
||||
static createZip(files, album) {
|
||||
try {
|
||||
const zip = new Zip();
|
||||
for (const file of files) {
|
||||
zip.addLocalFile(path.join(Util.uploadPath, file));
|
||||
}
|
||||
zip.writeZip(
|
||||
path.join(__dirname, '../../../', 'uploads', 'zips', `${album.userId}-${album.id}.zip`)
|
||||
);
|
||||
} catch (error) {
|
||||
log.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
static generateThumbnails = ThumbUtil.generateThumbnails;
|
||||
|
||||
static async fileExists(req, res, exists, filename) {
|
||||
exists = Util.constructFilePublicLink(req, exists);
|
||||
res.json({
|
||||
message: 'Successfully uploaded the file.',
|
||||
name: exists.name,
|
||||
hash: exists.hash,
|
||||
size: exists.size,
|
||||
url: exists.url,
|
||||
thumb: exists.thumb,
|
||||
deleteUrl: `${this.getHost(req)}/api/file/${exists.id}`,
|
||||
repeated: true
|
||||
});
|
||||
|
||||
return this.deleteFile(filename);
|
||||
}
|
||||
|
||||
static async storeFileToDb(req, res, user, file, db) {
|
||||
const dbFile = await db.table('files')
|
||||
// eslint-disable-next-line func-names
|
||||
.where(function() {
|
||||
if (user === undefined) {
|
||||
this.whereNull('userId');
|
||||
} else {
|
||||
this.where('userId', user.id);
|
||||
}
|
||||
})
|
||||
.where({
|
||||
hash: file.data.hash,
|
||||
size: file.data.size
|
||||
})
|
||||
.first();
|
||||
|
||||
if (dbFile) {
|
||||
await this.fileExists(req, res, dbFile, file.data.filename);
|
||||
return;
|
||||
}
|
||||
|
||||
const now = moment.utc().toDate();
|
||||
const data = {
|
||||
userId: user ? user.id : null,
|
||||
name: file.data.filename,
|
||||
original: file.data.originalname,
|
||||
type: file.data.mimetype,
|
||||
size: file.data.size,
|
||||
hash: file.data.hash,
|
||||
ip: req.ip,
|
||||
createdAt: now,
|
||||
editedAt: now
|
||||
};
|
||||
Util.generateThumbnails(file.data.filename);
|
||||
|
||||
let fileId;
|
||||
if (process.env.DB_CLIENT === 'sqlite3') {
|
||||
fileId = await db.table('files').insert(data).wasMutated();
|
||||
} else {
|
||||
fileId = await db.table('files').insert(data, 'id').wasMutated();
|
||||
}
|
||||
|
||||
return {
|
||||
file: data,
|
||||
id: fileId
|
||||
};
|
||||
}
|
||||
|
||||
static async saveFileToAlbum(db, albumId, insertedId) {
|
||||
if (!albumId) return;
|
||||
|
||||
const now = moment.utc().toDate();
|
||||
try {
|
||||
await db.table('albumsFiles').insert({ albumId, fileId: insertedId[0] }).wasMutated();
|
||||
await db.table('albums').where('id', albumId).update('editedAt', now);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
static getExtension(filename) {
|
||||
// Always return blank string if the filename does not seem to have a valid extension
|
||||
// Files such as .DS_Store (anything that starts with a dot, without any extension after) will still be accepted
|
||||
if (!/\../.test(filename)) return '';
|
||||
|
||||
let lower = filename.toLowerCase(); // due to this, the returned extname will always be lower case
|
||||
let multi = '';
|
||||
let extname = '';
|
||||
|
||||
// check for multi-archive extensions (.001, .002, and so on)
|
||||
if (/\.\d{3}$/.test(lower)) {
|
||||
multi = lower.slice(lower.lastIndexOf('.') - lower.length);
|
||||
lower = lower.slice(0, lower.lastIndexOf('.'));
|
||||
}
|
||||
|
||||
// check against extensions that must be preserved
|
||||
for (const extPreserve of preserveExtensions) {
|
||||
if (lower.endsWith(extPreserve)) {
|
||||
extname = extPreserve;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!extname) {
|
||||
extname = lower.slice(lower.lastIndexOf('.') - lower.length); // path.extname(lower)
|
||||
}
|
||||
|
||||
return extname + multi;
|
||||
}
|
||||
|
||||
// TODO: Allow choosing what to save to db and what stats we care about in general
|
||||
// TODO: if a stat is not saved to db but selected to be shows on the dashboard, it will be generated during the request
|
||||
static async saveStatsToDb(force) {
|
||||
// If there were no changes since the instance started, don't generate new stats
|
||||
// OR
|
||||
// if we alredy saved a stats to the db, and there were no new changes to the db since then
|
||||
// skip generating and saving new stats.
|
||||
if (!force &&
|
||||
(!db.userParams.lastMutationTime ||
|
||||
(Util.statsLastSavedTime && Util.statsLastSavedTime > db.userParams.lastMutationTime)
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const now = moment.utc().toDate();
|
||||
const stats = await StatsGenerator.getStats(db);
|
||||
|
||||
let batchId = 1;
|
||||
|
||||
const res = (await db('statistics').max({ lastBatch: 'batchId' }))[0];
|
||||
if (res && res.lastBatch) {
|
||||
batchId = res.lastBatch + 1;
|
||||
}
|
||||
|
||||
try {
|
||||
for (const [type, data] of Object.entries(stats)) {
|
||||
await db.table('statistics').insert({ type, data: JSON.stringify(data), createdAt: now, batchId });
|
||||
}
|
||||
|
||||
Util.statsLastSavedTime = now.getTime();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
static getHost(req) {
|
||||
return `${req.protocol}://${req.headers.host}`;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Util;
|
|
@ -0,0 +1,401 @@
|
|||
import jetpack from 'fs-jetpack';
|
||||
import randomstring from 'randomstring';
|
||||
import path from 'path';
|
||||
import prisma from '../structures/database';
|
||||
import { utc } from 'moment';
|
||||
import Zip from 'adm-zip';
|
||||
import { generateThumbnails, getFileThumbnail, removeThumbs } from './ThumbUtil';
|
||||
// import { getStats } from './StatsGenerator';
|
||||
import type { FastifyRequest, FastifyReply } from 'fastify';
|
||||
import type { File, ExtendedFile, ExtendedFileWithData, Album, User, Settings } from '../structures/interfaces';
|
||||
|
||||
// TODO: Check that importing the log function works for routes and CLI (generateThumbs.ts)
|
||||
import { log } from '../main';
|
||||
|
||||
export { generateThumbnails } from './ThumbUtil';
|
||||
export { v4 as uuid } from 'uuid';
|
||||
|
||||
const preserveExtensions = ['.tar.gz', '.tar.z', '.tar.bz2', '.tar.lzma', '.tar.lzo', '.tar.xz'];
|
||||
|
||||
export const uploadPath = path.join(__dirname, '../../../', 'uploads');
|
||||
export const statsLastSavedTime = null;
|
||||
export const _config = null;
|
||||
|
||||
/*
|
||||
TODO: Ask crawl how to properly type this.
|
||||
I want that if I call getConfig() to know the properties that will come back and their types
|
||||
to use them in nuxt.ts for example
|
||||
*/
|
||||
export const getConfig = async () => {
|
||||
const config = await prisma.settings.findMany();
|
||||
return config.reduce((conf, item) => {
|
||||
if (typeof item.value === 'string') {
|
||||
conf[item.key] = JSON.parse(item.value);
|
||||
} else {
|
||||
conf[item.key] = item.value;
|
||||
}
|
||||
return config;
|
||||
}, {} as Record<string, any>) as Settings;
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||
export const getEnvironmentDefaults = () => ({
|
||||
domain: process.env.DOMAIN,
|
||||
routePrefix: '/api',
|
||||
rateLimitWindow: process.env.RATE_LIMIT_WINDOW ?? 2,
|
||||
rateLimitMax: process.env.RATE_LIMIT_MAX ?? 5,
|
||||
secret: process.env.SECRET ?? randomstring.generate(64),
|
||||
serviceName: process.env.SERVICE_NAME ?? 'change-me',
|
||||
chunkSize: process.env.CHUNK_SIZE ?? 90,
|
||||
maxSize: process.env.MAX_SIZE ?? 5000,
|
||||
// eslint-disable-next-line eqeqeq
|
||||
generateZips: process.env.GENERATE_ZIPS == undefined ? true : false,
|
||||
generatedFilenameLength: process.env.GENERATED_FILENAME_LENGTH ?? 12,
|
||||
generatedAlbumLength: process.env.GENERATED_ALBUM_LENGTH ?? 6,
|
||||
blockedExtensions: process.env.BLOCKED_EXTENSIONS ? process.env.BLOCKED_EXTENSIONS.split(',') : ['.jar', '.exe', '.msi', '.com', '.bat', '.cmd', '.scr', '.ps1', '.sh'],
|
||||
// eslint-disable-next-line eqeqeq
|
||||
publicMode: process.env.PUBLIC_MODE == undefined ? true : false,
|
||||
// eslint-disable-next-line eqeqeq
|
||||
userAccounts: process.env.USER_ACCOUNTS == undefined ? true : false,
|
||||
metaThemeColor: process.env.META_THEME_COLOR ?? '#20222b',
|
||||
metaDescription: process.env.META_DESCRIPTION ?? 'Blazing fast file uploader and bunker written in node! 🚀',
|
||||
metaKeywords: process.env.META_KEYWORDS ?? 'chibisafe,lolisafe,upload,uploader,file,vue,images,ssr,file uploader,free',
|
||||
metaTwitterHandle: process.env.META_TWITTER_HANDLE ?? '@your-handle',
|
||||
backgroundImageURL: process.env.BACKGROUND_IMAGE_URL ?? '',
|
||||
logoURL: process.env.LOGO_URL ?? '',
|
||||
statisticsCron: process.env.STATISTICS_CRON ?? '0 0 * * * *',
|
||||
enabledStatistics: process.env.ENABLED_STATISTICS ? process.env.ENABLED_STATISTICS.split(',') : ['system', 'fileSystems', 'uploads', 'users', 'albums'],
|
||||
savedStatistics: process.env.SAVED_STATISTICS ? process.env.SAVED_STATISTICS.split(',') : ['system', 'fileSystems', 'uploads', 'users', 'albums']
|
||||
} as Settings);
|
||||
|
||||
export const wipeConfigDb = async () => {
|
||||
try {
|
||||
await prisma.settings.deleteMany();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
export const writeConfigToDb = async (config: { key: string; value: string | number | string[] | boolean }) => {
|
||||
if (!config.key) return;
|
||||
try {
|
||||
const data = {
|
||||
key: config.key,
|
||||
value: JSON.stringify(config.value)
|
||||
};
|
||||
await prisma.settings.create({
|
||||
data
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
export const isExtensionBlocked = async (extension: string) => (await getConfig()).blockedExtensions.includes(extension);
|
||||
export const getMimeFromType = (fileTypeMimeObj: Record<string, null>) => fileTypeMimeObj.mime;
|
||||
|
||||
export const getUniqueFilename = (extension: string) => {
|
||||
const retry: any = async (i = 0) => {
|
||||
const filename = randomstring.generate({
|
||||
length: (await getConfig()).generatedFilenameLength,
|
||||
capitalization: 'lowercase'
|
||||
}) + extension;
|
||||
|
||||
// TODO: Change this to look for the file in the db instead of in the filesystem
|
||||
const exists = jetpack.exists(path.join(uploadPath, filename));
|
||||
if (!exists) return filename;
|
||||
if (i < 5) return retry(i + 1);
|
||||
log.error('Couldnt allocate identifier for file');
|
||||
return null;
|
||||
};
|
||||
return retry();
|
||||
};
|
||||
|
||||
export const getUniqueAlbumIdentifier = () => {
|
||||
const retry: any = async (i = 0) => {
|
||||
const identifier = randomstring.generate({
|
||||
length: (await getConfig()).generatedAlbumLength,
|
||||
capitalization: 'lowercase'
|
||||
});
|
||||
const exists = await prisma.links.findFirst({
|
||||
where: {
|
||||
identifier
|
||||
}
|
||||
});
|
||||
if (!exists) return identifier;
|
||||
/*
|
||||
It's funny but if you do i++ the asignment never gets done resulting in an infinite loop
|
||||
*/
|
||||
if (i < 5) return retry(i + 1);
|
||||
log.error('Couldnt allocate identifier for album');
|
||||
return null;
|
||||
};
|
||||
return retry();
|
||||
};
|
||||
|
||||
export const getFilenameFromPath = (fullPath: string) => fullPath.replace(/^.*[\\\/]/, ''); // eslint-disable-line no-useless-escape
|
||||
|
||||
export const deleteFile = async (filename: string, deleteFromDB = false) => {
|
||||
const thumbName = getFileThumbnail(filename);
|
||||
try {
|
||||
await jetpack.removeAsync(path.join(uploadPath, filename));
|
||||
if (thumbName) await removeThumbs(thumbName);
|
||||
|
||||
if (deleteFromDB) {
|
||||
await prisma.files.deleteMany({
|
||||
where: {
|
||||
name: filename
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
log.error(`There was an error removing the file < ${filename} >`);
|
||||
log.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
export const deleteAllFilesFromAlbum = async (id: number) => {
|
||||
try {
|
||||
const fileAlbums = await prisma.albumsFiles.findMany({
|
||||
where: {
|
||||
albumId: id
|
||||
},
|
||||
select: {
|
||||
fileId: true
|
||||
}
|
||||
});
|
||||
|
||||
for (const fileAlbum of fileAlbums) {
|
||||
const file = await prisma.files.findUnique({
|
||||
where: {
|
||||
id: fileAlbum.fileId
|
||||
}
|
||||
});
|
||||
|
||||
if (!file?.name) continue;
|
||||
|
||||
await deleteFile(file.name, true);
|
||||
}
|
||||
} catch (error) {
|
||||
log.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
export const deleteAllFilesFromUser = async (id: number) => {
|
||||
try {
|
||||
const files = await prisma.files.findMany({
|
||||
where: {
|
||||
userId: id
|
||||
}
|
||||
});
|
||||
|
||||
for (const file of files) {
|
||||
await deleteFile(file.name, true);
|
||||
}
|
||||
} catch (error) {
|
||||
log.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
export const deleteAllFilesFromTag = async (id: number) => {
|
||||
try {
|
||||
const fileTags = await prisma.fileTags.findMany({
|
||||
where: {
|
||||
tagId: id
|
||||
}
|
||||
});
|
||||
for (const fileTag of fileTags) {
|
||||
const file = await prisma.files.findFirst({
|
||||
where: {
|
||||
id: fileTag.fileId
|
||||
},
|
||||
select: {
|
||||
name: true
|
||||
}
|
||||
});
|
||||
if (!file) continue;
|
||||
await deleteFile(file.name, true);
|
||||
}
|
||||
} catch (error) {
|
||||
log.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
export const createZip = (files: string[], album: Album) => {
|
||||
try {
|
||||
const zip = new Zip();
|
||||
for (const file of files) {
|
||||
zip.addLocalFile(path.join(uploadPath, file));
|
||||
}
|
||||
zip.writeZip(
|
||||
path.join(__dirname, '../../../', 'uploads', 'zips', `${album.userId}-${album.id}.zip`)
|
||||
);
|
||||
} catch (error) {
|
||||
log.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
export const constructFilePublicLink = (req: FastifyRequest, file: File) => {
|
||||
/*
|
||||
TODO: This wont work without a reverse proxy serving both
|
||||
the site and the API under the same domain. Pls fix.
|
||||
*/
|
||||
const extended: ExtendedFile = { ...file };
|
||||
const host = getHost(req);
|
||||
extended.url = `${host}/${extended.name}`;
|
||||
const { thumb, preview } = getFileThumbnail(extended.name) ?? {};
|
||||
if (thumb) {
|
||||
extended.thumb = `${host}/thumbs/${thumb}`;
|
||||
extended.thumbSquare = `${host}/thumbs/square/${thumb}`;
|
||||
extended.preview = preview && `${host}/thumbs/preview/${preview}`;
|
||||
}
|
||||
return extended;
|
||||
};
|
||||
|
||||
export const fileExists = (req: FastifyRequest, res: FastifyReply, exists: File, filename: string) => {
|
||||
const file = constructFilePublicLink(req, exists);
|
||||
void res.send({
|
||||
message: 'Successfully uploaded the file.',
|
||||
name: file.name,
|
||||
hash: file.hash,
|
||||
size: file.size,
|
||||
url: file.url,
|
||||
thumb: file.thumb,
|
||||
deleteUrl: `${getHost(req)}/api/file/${file.id}`,
|
||||
repeated: true
|
||||
});
|
||||
|
||||
return deleteFile(filename);
|
||||
};
|
||||
|
||||
export const storeFileToDb = async (req: FastifyRequest, res: FastifyReply, user: User, file: ExtendedFileWithData) => {
|
||||
const dbFile = await prisma.files.findFirst({
|
||||
where: {
|
||||
hash: file.data.hash,
|
||||
size: file.data.size,
|
||||
userId: user.id ? user.id : undefined
|
||||
}
|
||||
});
|
||||
|
||||
if (dbFile) {
|
||||
await fileExists(req, res, dbFile, file.data.filename);
|
||||
return;
|
||||
}
|
||||
|
||||
const now = utc().toDate();
|
||||
const data = {
|
||||
userId: user.id ? user.id : undefined,
|
||||
name: file.data.filename,
|
||||
original: file.data.originalName,
|
||||
type: file.data.mimeType,
|
||||
size: file.data.size,
|
||||
hash: file.data.hash,
|
||||
ip: req.ip,
|
||||
createdAt: now,
|
||||
editedAt: now
|
||||
};
|
||||
void generateThumbnails(file.data.filename);
|
||||
|
||||
const fileId = await prisma.files.create({
|
||||
data
|
||||
});
|
||||
|
||||
return {
|
||||
file: data,
|
||||
id: fileId.id
|
||||
};
|
||||
};
|
||||
|
||||
export const saveFileToAlbum = async (albumId: number, insertedId: number) => {
|
||||
if (!albumId) return;
|
||||
|
||||
const now = utc().toDate();
|
||||
try {
|
||||
await prisma.albumsFiles.create({
|
||||
data: {
|
||||
albumId,
|
||||
fileId: insertedId
|
||||
}
|
||||
});
|
||||
await prisma.albums.update({
|
||||
where: {
|
||||
id: albumId
|
||||
},
|
||||
data: {
|
||||
editedAt: now
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
export const getExtension = (filename: string) => {
|
||||
// Always return blank string if the filename does not seem to have a valid extension
|
||||
// Files such as .DS_Store (anything that starts with a dot, without any extension after) will still be accepted
|
||||
if (!/\../.test(filename)) return '';
|
||||
|
||||
let lower = filename.toLowerCase(); // due to this, the returned extname will always be lower case
|
||||
let multi = '';
|
||||
let extname = '';
|
||||
|
||||
// check for multi-archive extensions (.001, .002, and so on)
|
||||
if (/\.\d{3}$/.test(lower)) {
|
||||
multi = lower.slice(lower.lastIndexOf('.') - lower.length);
|
||||
lower = lower.slice(0, lower.lastIndexOf('.'));
|
||||
}
|
||||
|
||||
// check against extensions that must be preserved
|
||||
for (const extPreserve of preserveExtensions) {
|
||||
if (lower.endsWith(extPreserve)) {
|
||||
extname = extPreserve;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!extname) {
|
||||
extname = lower.slice(lower.lastIndexOf('.') - lower.length); // path.extname(lower)
|
||||
}
|
||||
|
||||
return extname + multi;
|
||||
};
|
||||
|
||||
/*
|
||||
// TODO: Allow choosing what to save to db and what stats we care about in general
|
||||
// TODO: if a stat is not saved to db but selected to be shows on the dashboard, it will be generated during the request
|
||||
export const saveStatsToDb = async (force: boolean) => {
|
||||
// If there were no changes since the instance started, don't generate new stats
|
||||
// OR
|
||||
// if we alredy saved a stats to the db, and there were no new changes to the db since then
|
||||
// skip generating and saving new stats.
|
||||
if (!force &&
|
||||
(!db.userParams.lastMutationTime ||
|
||||
(Util.statsLastSavedTime && Util.statsLastSavedTime > db.userParams.lastMutationTime)
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const now = utc().toDate();
|
||||
const stats = await getStats(db);
|
||||
|
||||
let batchId = 1;
|
||||
|
||||
const res = (await db('statistics').max({ lastBatch: 'batchId' }))[0];
|
||||
if (res && res.lastBatch) {
|
||||
batchId = res.lastBatch + 1;
|
||||
}
|
||||
|
||||
try {
|
||||
for (const [type, data] of Object.entries(stats)) {
|
||||
await db.table('statistics').insert({ type, data: JSON.stringify(data), createdAt: now, batchId });
|
||||
}
|
||||
|
||||
Util.statsLastSavedTime = now.getTime();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
*/
|
||||
|
||||
export const getHost = (req: FastifyRequest) => `${req.protocol}://${req.headers.host as string}`;
|
|
@ -1,17 +1,17 @@
|
|||
require('dotenv').config();
|
||||
import dotenv from 'dotenv';
|
||||
dotenv.config();
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const ThumbUtil = require('./ThumbUtil');
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { generateThumbnails } from './ThumbUtil';
|
||||
|
||||
const start = async () => {
|
||||
const files = fs.readdirSync(path.join(__dirname, '../../../uploads'));
|
||||
for (const fileName of files) {
|
||||
console.log(`Generating thumb for '${fileName}`);
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await ThumbUtil.generateThumbnails(fileName);
|
||||
await generateThumbnails(fileName);
|
||||
}
|
||||
};
|
||||
|
||||
start();
|
||||
void start();
|
|
@ -1,18 +1,20 @@
|
|||
require('dotenv').config();
|
||||
const blake3 = require('blake3');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const db = require('knex')({
|
||||
client: 'sqlite3',
|
||||
connection: {
|
||||
filename: path.join(__dirname, '../../../database/', 'database.sqlite')
|
||||
}
|
||||
});
|
||||
import dotenv from 'dotenv';
|
||||
dotenv.config();
|
||||
|
||||
import blake3 from 'blake3';
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
import prisma from '../structures/database';
|
||||
|
||||
const start = async () => {
|
||||
const hrstart = process.hrtime();
|
||||
const uploads = await db.table('files')
|
||||
.select('id', 'name', 'hash');
|
||||
const uploads = await prisma.files.findMany({
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
hash: true
|
||||
}
|
||||
});
|
||||
console.log(`Uploads : ${uploads.length}`);
|
||||
|
||||
|
||||
|
@ -26,21 +28,30 @@ const start = async () => {
|
|||
|
||||
for (const upload of uploads) {
|
||||
await new Promise((resolve, reject) => {
|
||||
// TODO: Check that this return works as a continue and doesnt break the loop
|
||||
if (!upload.name) return;
|
||||
fs.createReadStream(path.join(__dirname, '../../../uploads', upload.name))
|
||||
.on('error', reject)
|
||||
.pipe(blake3.createHash())
|
||||
.on('error', reject)
|
||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||
.on('data', async source => {
|
||||
const hash = source.toString('hex');
|
||||
console.log(`${upload.name}: ${hash}`);
|
||||
await db.table('files')
|
||||
.update('hash', hash)
|
||||
.where('id', upload.id);
|
||||
const hash = source.toStrin('hex');
|
||||
console.log(`${upload.name as string}: ${hash as string}`);
|
||||
await prisma.files.update({
|
||||
where: {
|
||||
id: upload.id
|
||||
},
|
||||
data: {
|
||||
hash
|
||||
}
|
||||
});
|
||||
done++;
|
||||
resolve();
|
||||
// TODO: Removed parenthesis here, check if it still works as expected
|
||||
resolve;
|
||||
});
|
||||
}).catch(error => {
|
||||
console.log(`${upload.name}: ${error.toString()}`);
|
||||
console.log(`${upload.name as string}: ${error.toString() as string}`);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -52,4 +63,4 @@ const start = async () => {
|
|||
process.exit(0);
|
||||
};
|
||||
|
||||
start();
|
||||
void start();
|
|
@ -1,10 +1,11 @@
|
|||
/* eslint-disable no-bitwise */
|
||||
const ffmpeg = require('fluent-ffmpeg');
|
||||
const probe = require('ffmpeg-probe');
|
||||
import ffmpeg from 'fluent-ffmpeg';
|
||||
// @ts-ignore - no typings for this package
|
||||
import probe from 'ffmpeg-probe';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
const noop = () => {};
|
||||
|
||||
const getRandomInt = (min, max) => {
|
||||
const getRandomInt = (min: number, max: number) => {
|
||||
const minInt = Math.ceil(min);
|
||||
const maxInt = Math.floor(max);
|
||||
|
||||
|
@ -12,7 +13,7 @@ const getRandomInt = (min, max) => {
|
|||
return Math.floor(Math.random() * (maxInt - minInt + 1) + minInt);
|
||||
};
|
||||
|
||||
const getStartTime = (vDuration, fDuration, ignoreBeforePercent, ignoreAfterPercent) => {
|
||||
const getStartTime = (vDuration: number, fDuration: number, ignoreBeforePercent: number, ignoreAfterPercent: number) => {
|
||||
// by subtracting the fragment duration we can be sure that the resulting
|
||||
// start time + fragment duration will be less than the video duration
|
||||
const safeVDuration = vDuration - fDuration;
|
||||
|
@ -25,7 +26,7 @@ const getStartTime = (vDuration, fDuration, ignoreBeforePercent, ignoreAfterPerc
|
|||
return getRandomInt(ignoreBeforePercent * safeVDuration, ignoreAfterPercent * safeVDuration);
|
||||
};
|
||||
|
||||
module.exports = async opts => {
|
||||
export default async (opts: any) => {
|
||||
const {
|
||||
log = noop,
|
||||
|
||||
|
@ -48,7 +49,7 @@ module.exports = async opts => {
|
|||
|
||||
const startTime = getStartTime(duration, fragmentDurationSecond, ignoreBeforePercent, ignoreAfterPercent);
|
||||
|
||||
const result = { startTime, duration };
|
||||
const result: any = { startTime, duration };
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
let scale = null;
|
||||
|
@ -56,15 +57,15 @@ module.exports = async opts => {
|
|||
if (width && height) {
|
||||
result.width = width | 0;
|
||||
result.height = height | 0;
|
||||
scale = `scale=${width}:${height}`;
|
||||
scale = `scale=${width as number}:${height as number}`;
|
||||
} else if (width) {
|
||||
result.width = width | 0;
|
||||
result.height = ((info.height * width) / info.width) | 0;
|
||||
scale = `scale=${width}:-1`;
|
||||
scale = `scale=${width as number}:-1`;
|
||||
} else if (height) {
|
||||
result.height = height | 0;
|
||||
result.width = ((info.width * height) / info.height) | 0;
|
||||
scale = `scale=-1:${height}`;
|
||||
scale = `scale=-1:${height as number}`;
|
||||
} else {
|
||||
result.width = info.width;
|
||||
result.height = info.height;
|
||||
|
@ -75,10 +76,10 @@ module.exports = async opts => {
|
|||
.inputOptions([`-ss ${startTime}`])
|
||||
.outputOptions(['-vsync', 'vfr'])
|
||||
.outputOptions(['-q:v', quality, '-vf', scale])
|
||||
.outputOptions([`-t ${fragmentDurationSecond}`])
|
||||
.outputOptions([`-t ${fragmentDurationSecond as number}`])
|
||||
.noAudio()
|
||||
.output(output)
|
||||
.on('start', cmd => log && log({ cmd }))
|
||||
.on('start', cmd => log?.({ cmd }))
|
||||
.on('end', resolve)
|
||||
.on('error', reject)
|
||||
.run();
|
|
@ -1,10 +1,12 @@
|
|||
/* eslint-disable no-bitwise */
|
||||
const ffmpeg = require('fluent-ffmpeg');
|
||||
const probe = require('ffmpeg-probe');
|
||||
import ffmpeg from 'fluent-ffmpeg';
|
||||
// @ts-ignore - no typings for this package
|
||||
import probe from 'ffmpeg-probe';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
const noop = () => {};
|
||||
|
||||
module.exports = async opts => {
|
||||
module.exports = async (opts: any) => {
|
||||
const {
|
||||
log = noop,
|
||||
|
||||
|
@ -22,7 +24,7 @@ module.exports = async opts => {
|
|||
const info = await probe(input);
|
||||
// const numFramesTotal = parseInt(info.streams[0].nb_frames, 10);
|
||||
const { avg_frame_rate: avgFrameRate, duration } = info.streams[0];
|
||||
const [frames, time] = avgFrameRate.split('/').map(e => parseInt(e, 10));
|
||||
const [frames, time] = avgFrameRate.split('/').map((e: string) => parseInt(e, 10));
|
||||
|
||||
const numFramesTotal = (frames / time) * duration;
|
||||
|
||||
|
@ -30,26 +32,26 @@ module.exports = async opts => {
|
|||
numFramesToCapture = Math.max(1, Math.min(numFramesTotal, numFramesToCapture)) | 0;
|
||||
const nthFrame = (numFramesTotal / numFramesToCapture) | 0;
|
||||
|
||||
const result = {
|
||||
const result: any = {
|
||||
output,
|
||||
numFrames: numFramesToCapture
|
||||
};
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
let scale = null;
|
||||
|
||||
if (width && height) {
|
||||
result.width = width | 0;
|
||||
result.height = height | 0;
|
||||
scale = `scale=${width}:${height}`;
|
||||
scale = `scale=${width as number}:${height as number}`;
|
||||
} else if (width) {
|
||||
result.width = width | 0;
|
||||
result.height = ((info.height * width) / info.width) | 0;
|
||||
scale = `scale=${width}:-1`;
|
||||
scale = `scale=${width as number}:-1`;
|
||||
} else if (height) {
|
||||
result.height = height | 0;
|
||||
result.width = ((info.width * height) / info.height) | 0;
|
||||
scale = `scale=-1:${height}`;
|
||||
scale = `scale=-1:${height as number}`;
|
||||
} else {
|
||||
result.width = info.width;
|
||||
result.height = info.height;
|
||||
|
@ -63,7 +65,7 @@ module.exports = async opts => {
|
|||
.noAudio()
|
||||
.outputFormat('webm')
|
||||
.output(output)
|
||||
.on('start', cmd => log && log({ cmd }))
|
||||
.on('start', cmd => log?.({ cmd }))
|
||||
.on('end', () => resolve())
|
||||
.on('error', err => reject(err))
|
||||
.run();
|
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"moduleResolution": "Node",
|
||||
"esModuleInterop": true,
|
||||
"sourceMap": true,
|
||||
"strict": true,
|
||||
"target": "esnext",
|
||||
"module": "commonjs",
|
||||
"jsx": "preserve",
|
||||
"lib": ["esnext", "dom"],
|
||||
"rootDir": "./src/api",
|
||||
"outDir": "./dist",
|
||||
"sourceRoot": "./",
|
||||
"types": [
|
||||
"@types/node",
|
||||
"@nuxt/types"
|
||||
]
|
||||
},
|
||||
"include": ["**/*.ts", "**/*.d.ts"]
|
||||
}
|
Loading…
Reference in New Issue