This commit is contained in:
Pitu 2021-06-25 01:28:59 +09:00
parent 7114830582
commit 9321750461
17 changed files with 283 additions and 215 deletions

Binary file not shown.

View File

@ -1,92 +0,0 @@
import autoprefixer from 'autoprefixer';
const Util = require('./src/api/utils/Util');
export default {
ssr: true,
srcDir: 'src/site/',
head: {
title: Util.config.serviceName,
titleTemplate: `%s | ${Util.config.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: 'apple-mobile-web-app-title',
name: 'apple-mobile-web-app-title',
content: `${Util.config.serviceName}`
},
{ hid: 'application-name', name: 'application-name', content: `${Util.config.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: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: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}` }
],
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
{ type: 'application/json+oembed', href: `/oembed.json` }
]
},
plugins: [
'~/plugins/axios',
'~/plugins/buefy',
'~/plugins/v-clipboard',
'~/plugins/vue-isyourpasswordsafe',
'~/plugins/vue-timeago',
'~/plugins/vuebar',
'~/plugins/notifier',
'~/plugins/handler',
],
css: [],
modules: ['@nuxtjs/axios', 'cookie-universal-nuxt',],
router: {
linkActiveClass: 'is-active',
linkExactActiveClass: 'is-active',
},
env: {
development: process.env.NODE_ENV !== 'production'
},
axios: {
baseURL: `${process.env.NODE_ENV === 'production' ? process.env.DOMAIN : 'http://localhost:5000'}/api`
},
build: {
extractCSS: process.env.NODE_ENV === 'production',
postcss: {
preset: {
autoprefixer,
},
},
axios: {
baseURL: `${process.env.NODE_ENV === 'production' ? process.env.DOMAIN : 'http://localhost:5000'}/api`
},
build: {
extractCSS: process.env.NODE_ENV === 'production',
postcss: {
preset: {
autoprefixer
}
},
extend(config, { isDev }) {
// Extend only webpack config for client-bundle
if (isDev) {
config.devtool = 'source-map';
}
}
},
},
};

View File

@ -9,7 +9,8 @@
"url": "https://github.com/Pitu"
},
"scripts": {
"setup": "node src/setup.js && npm run migrate && npm run seed",
"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",

View File

@ -9,7 +9,8 @@ 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 { loadNuxt, build } from 'nuxt';
import { Nuxt, Builder } from 'nuxt';
import nuxtDefaults from './structures/nuxt';
import Routes from './structures/routes';
@ -91,7 +92,19 @@ const start = async () => {
root: path.join(__dirname, '../../uploads')
});
// TODO: Enable this after Utils is ported to TypeScript
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');

View File

@ -1,5 +1,5 @@
import type { FastifyReply, HookHandlerDoneFunction } from 'fastify';
import type { RequestWithUser } from './auth';
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' });

View File

@ -1,13 +1,7 @@
import type { FastifyRequest, FastifyReply, HookHandlerDoneFunction } from 'fastify';
import type { FastifyReply, HookHandlerDoneFunction } from 'fastify';
import type { RequestWithUser } from '../structures/interfaces';
import prisma from '../structures/database';
export interface RequestWithUser extends FastifyRequest {
user: {
id: number;
username: string | null;
isAdmin: boolean | null;
};
}
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

View File

@ -11,12 +11,12 @@ datasource db {
model albums {
id Int @id @default(autoincrement())
userId Int?
name String?
userId Int
name String
zippedAt DateTime?
createdAt DateTime @default(now())
editedAt DateTime?
nsfw Boolean? @default(false)
nsfw Boolean @default(false)
@@unique([userId, name], name: "albums_userid_name_unique")
}

View File

@ -8,7 +8,9 @@ interface params {
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' });

View File

@ -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
});
};

View File

@ -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;

View File

@ -0,0 +1,45 @@
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 ip provided' });
const link = await prisma.links.findFirst({
where: {
identifier,
userId: req.user.id
}
});
await prisma.links.delete({
where: {
links_userid_albumid_identifier_unique: {
}
}
});
const albumLink = await prisma.albumsLinks.findFirst({
where: {
linkId: link.id
}
});
await prisma.albumsLinks.delete({
where: {
id: albumLink.id
}
});
return res.send({
message: 'Successfully banned the ip'
});
};

View File

@ -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;

View File

@ -5,7 +5,7 @@ export interface RequestWithUser extends FastifyRequest {
id: number;
username: string;
isAdmin: boolean;
apiKey?: string;
apiKey?: string | null | undefined;
};
}
@ -24,15 +24,15 @@ export interface User {
export interface File {
id: number;
userId?: number;
userId?: number | null;
name: string;
original: string;
type: string;
size: number;
hash: string;
ip: string;
createdAt: string;
editedAt: string;
createdAt: Date;
editedAt: Date | null;
}
export interface ExtendedFile extends File {
@ -56,8 +56,35 @@ export interface Album {
id: number;
userId: number;
name: string;
zippedAt: string;
createdAt: string;
editedAt: 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;
}

View File

@ -0,0 +1,87 @@
import { NuxtConfig } from '@nuxt/types';
import { getConfig } from '../utils/Util';
import autoprefixer from 'autoprefixer';
export default async () => {
const settings = await getConfig();
const config: NuxtConfig = {
ssr: true,
srcDir: 'src/site/',
head: {
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: `${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: `${settings.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: `${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: `${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: `${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` }
]
},
plugins: [
'~/plugins/axios',
'~/plugins/buefy',
'~/plugins/v-clipboard',
'~/plugins/vue-isyourpasswordsafe',
'~/plugins/vue-timeago',
'~/plugins/vuebar',
'~/plugins/notifier',
'~/plugins/handler'
],
css: [],
modules: ['@nuxtjs/axios', 'cookie-universal-nuxt'],
router: {
linkActiveClass: 'is-active',
linkExactActiveClass: 'is-active'
},
env: {
development: Boolean(process.env.NODE_ENV !== 'production').toString()
},
axios: {
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
}
},
extend(config, { isDev }) {
// Extend only webpack config for client-bundle
if (isDev) {
config.devtool = 'source-map';
}
}
}
};
return config;
};

View File

@ -54,7 +54,7 @@ export const getSystemInfo = async () => {
};
export const getFileSystemsInfo = async () => {
const stats = {};
const stats: Record<string, { value: { total: number; used: number }; type: string }> = {};
const fsSize = await si.fsSize();
for (const fs of fsSize) {

View File

@ -5,9 +5,9 @@ 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 { getStats } from './StatsGenerator';
import type { FastifyRequest, FastifyReply } from 'fastify';
import type { File, ExtendedFile, ExtendedFileWithData, Album, User } from '../structures/interfaces';
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';
@ -21,6 +21,11 @@ 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) => {
@ -30,9 +35,10 @@ export const getConfig = async () => {
conf[item.key] = item.value;
}
return config;
}, {} as Record<string, any>);
}, {} as Record<string, any>) as Settings;
};
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
export const getEnvironmentDefaults = () => ({
domain: process.env.DOMAIN,
routePrefix: '/api',
@ -60,7 +66,7 @@ export const getEnvironmentDefaults = () => ({
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 {
@ -70,12 +76,15 @@ export const wipeConfigDb = async () => {
}
};
export const writeConfigToDb = async (config: { key: string; value: string }) => {
export const writeConfigToDb = async (config: { key: string; value: string | number | string[] | boolean }) => {
if (!config.key) return;
try {
config.value = JSON.stringify(config.value);
const data = {
key: config.key,
value: JSON.stringify(config.value)
};
await prisma.settings.create({
data: config
data
});
} catch (error) {
console.error(error);
@ -85,23 +94,6 @@ export const writeConfigToDb = async (config: { key: string; value: string }) =>
export const isExtensionBlocked = async (extension: string) => (await getConfig()).blockedExtensions.includes(extension);
export const getMimeFromType = (fileTypeMimeObj: Record<string, null>) => fileTypeMimeObj.mime;
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 = file as ExtendedFile;
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 getUniqueFilename = (extension: string) => {
const retry: any = async (i = 0) => {
const filename = randomstring.generate({
@ -243,6 +235,23 @@ export const createZip = (files: string[], album: Album) => {
}
};
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({