mirror of https://github.com/AlphaNecron/Void.git
feat(api): url generator for shortener
This commit is contained in:
parent
02c40e0568
commit
2eba13ef48
|
@ -23,4 +23,5 @@ route = '/go'
|
||||||
raw_route = '/r'
|
raw_route = '/r'
|
||||||
length = 6
|
length = 6
|
||||||
directory = './uploads'
|
directory = './uploads'
|
||||||
|
max_size = 104857600
|
||||||
blacklisted = ['exe']
|
blacklisted = ['exe']
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "void",
|
"name": "void",
|
||||||
"version": "0.5.0",
|
"version": "0.5.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=14"
|
"node": ">=14"
|
||||||
|
|
|
@ -91,7 +91,8 @@
|
||||||
raw_route = '/r' # Route to serve raw contents
|
raw_route = '/r' # Route to serve raw contents
|
||||||
length = 6 # Slug length
|
length = 6 # Slug length
|
||||||
directory = './uploads' # The directory where images are stored
|
directory = './uploads' # The directory where images are stored
|
||||||
blacklisted = ['exe'] # Blacklisted extensions
|
max_size = 104857600 # Max upload size (users only), in bytes
|
||||||
|
blacklisted = ['exe'] # Blacklisted file extensions (users only)
|
||||||
```
|
```
|
||||||
|
|
||||||
### Features
|
### Features
|
||||||
|
|
|
@ -1,98 +0,0 @@
|
||||||
import { PrismaClient } from '@prisma/client';
|
|
||||||
import { mkdir, readFile, stat } from 'fs/promises';
|
|
||||||
import { createServer } from 'http';
|
|
||||||
import next from 'next';
|
|
||||||
import { extname, join } from 'path';
|
|
||||||
import deployDb from '../scripts/deployDb';
|
|
||||||
import prismaRun from '../scripts/prismaRun';
|
|
||||||
import { error, info } from '../src/lib/logger';
|
|
||||||
import mimetypes from '../src/lib/mimetypes';
|
|
||||||
import validate from '../src/lib/validateConfig';
|
|
||||||
import readConfig from '../src/lib/configReader';
|
|
||||||
import start from '../twilight/twilight';
|
|
||||||
import { name, version } from '../package.json';
|
|
||||||
|
|
||||||
info('SERVER', `Starting ${name}@${version}`);
|
|
||||||
|
|
||||||
const dev = process.env.NODE_ENV === 'development';
|
|
||||||
|
|
||||||
(async () => {
|
|
||||||
try {
|
|
||||||
const config = await validate(readConfig());
|
|
||||||
const data = await prismaRun(config.core.database_url, ['migrate', 'status'], true);
|
|
||||||
if (data.match(/Following migrations? have not yet been applied/)) {
|
|
||||||
info('DB', 'Some migrations are not applied, applying them now...');
|
|
||||||
await deployDb(config);
|
|
||||||
info('DB', 'Finished applying migrations');
|
|
||||||
await prismaRun(config.core.database_url, ['db', 'seed'])
|
|
||||||
}
|
|
||||||
process.env.DATABASE_URL = config.core.database_url;
|
|
||||||
if (config.bot.enabled) {
|
|
||||||
if (!config.bot.token) error('BOT', 'Token is not specified');
|
|
||||||
else start(config);
|
|
||||||
}
|
|
||||||
await stat('./.next');
|
|
||||||
await mkdir(config.uploader.directory, { recursive: true });
|
|
||||||
const app = next({
|
|
||||||
dir: '.',
|
|
||||||
dev,
|
|
||||||
quiet: dev
|
|
||||||
});
|
|
||||||
await app.prepare();
|
|
||||||
const handle = app.getRequestHandler();
|
|
||||||
const prisma = new PrismaClient();
|
|
||||||
const srv = createServer(async (req, res) => {
|
|
||||||
if (req.url.startsWith(config.uploader.raw_route)) {
|
|
||||||
const parts = req.url.split('/');
|
|
||||||
if (!parts[2] || parts[2] === '') return;
|
|
||||||
let data;
|
|
||||||
try {
|
|
||||||
data = await readFile(join(process.cwd(), config.uploader.directory, parts[2]));
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
app.render404(req, res);
|
|
||||||
}
|
|
||||||
if (!data) {
|
|
||||||
app.render404(req, res);
|
|
||||||
} else {
|
|
||||||
let file = await prisma.file.findFirst({
|
|
||||||
where: {
|
|
||||||
fileName: parts[2],
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (file) {
|
|
||||||
res.setHeader('Content-Type', file.mimetype);
|
|
||||||
} else {
|
|
||||||
const mimetype = mimetypes[extname(parts[2])] ?? 'application/octet-stream';
|
|
||||||
res.setHeader('Content-Type', mimetype);
|
|
||||||
}
|
|
||||||
res.setHeader('Content-Length', data.byteLength);
|
|
||||||
res.end(data);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
handle(req, res);
|
|
||||||
}
|
|
||||||
if (!(req.url.startsWith('/_next') || req.url.startsWith('/__nextjs'))) {
|
|
||||||
res.statusCode === 200 ? info('ROUTER', `${res.statusCode} ${req.url}`) : error('URL', `${res.statusCode} ${req.url}`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
srv.on('error', (e) => {
|
|
||||||
error('SERVER', e);
|
|
||||||
process.exit(1);
|
|
||||||
});
|
|
||||||
srv.on('listening', async () => {
|
|
||||||
info('SERVER', `Listening on ${config.core.host}:${config.core.port}`);
|
|
||||||
});
|
|
||||||
srv.listen(config.core.port, config.core.host);
|
|
||||||
} catch (e) {
|
|
||||||
if (e.message && e.message.startsWith('Could not find a production')) {
|
|
||||||
console.log(e.message);
|
|
||||||
error('WEB', 'There is no production build - run yarn build');
|
|
||||||
} else if (e.code && e.code === 'ENOENT') {
|
|
||||||
if (e.path === './.next') error('WEB', 'There is no production build - run yarn build');
|
|
||||||
} else {
|
|
||||||
error('SERVER', e);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})();
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { Button, ButtonGroup, Heading, Input, Modal, ModalBody, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalOverlay, Select, Switch } from '@chakra-ui/react';
|
import { Button, ButtonGroup, Tab, TabList, TabPanels, TabPanel, Tabs, Heading, Input, Modal, ModalBody, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalOverlay, Select, Switch } from '@chakra-ui/react';
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { Download, X } from 'react-feather';
|
import { Download, X } from 'react-feather';
|
||||||
|
|
||||||
|
@ -6,8 +6,10 @@ export default function ShareXDialog({ open, onClose, token }) {
|
||||||
const ref = React.useRef();
|
const ref = React.useRef();
|
||||||
const [name, setName] = useState('Void');
|
const [name, setName] = useState('Void');
|
||||||
const [generator, setGenerator] = useState('random');
|
const [generator, setGenerator] = useState('random');
|
||||||
|
const [tab, setTab] = useState(0);
|
||||||
|
const [usePassword, setUsePassword] = useState(false);
|
||||||
const [preserveFileName, setPreserveFileName] = useState(false);
|
const [preserveFileName, setPreserveFileName] = useState(false);
|
||||||
const generateConfig = shortener => {
|
const downloadConfig = () => {
|
||||||
const apiUrl = `${window.location.protocol + '//' + window.location.hostname + (window.location.port ? ':' + window.location.port : '')}/api`;
|
const apiUrl = `${window.location.protocol + '//' + window.location.hostname + (window.location.port ? ':' + window.location.port : '')}/api`;
|
||||||
const uploaderConfig = {
|
const uploaderConfig = {
|
||||||
Version: '13.2.1',
|
Version: '13.2.1',
|
||||||
|
@ -34,7 +36,9 @@ export default function ShareXDialog({ open, onClose, token }) {
|
||||||
RequestMethod: 'POST',
|
RequestMethod: 'POST',
|
||||||
RequestURL: `${apiUrl}/shorten`,
|
RequestURL: `${apiUrl}/shorten`,
|
||||||
Headers: {
|
Headers: {
|
||||||
Authorization: token
|
Authorization: token,
|
||||||
|
Generator: generator,
|
||||||
|
...(usePassword && { Password: '$prompt:Password$' })
|
||||||
},
|
},
|
||||||
Body: 'FormURLEncoded',
|
Body: 'FormURLEncoded',
|
||||||
Arguments: {
|
Arguments: {
|
||||||
|
@ -44,8 +48,8 @@ export default function ShareXDialog({ open, onClose, token }) {
|
||||||
ErrorMessage: '$json:error$'
|
ErrorMessage: '$json:error$'
|
||||||
};
|
};
|
||||||
const a = document.createElement('a');
|
const a = document.createElement('a');
|
||||||
a.setAttribute('href', 'data:application/json;charset=utf-8,' + encodeURIComponent(JSON.stringify(shortener ? shortenerConfig : uploaderConfig, null, '\t')));
|
a.setAttribute('href', 'data:application/json;charset=utf-8,' + encodeURIComponent(JSON.stringify(tab === 0 ? uploaderConfig : shortenerConfig, null, '\t')));
|
||||||
a.setAttribute('download', `${name.replaceAll(' ', '_')}.sxcu`);
|
a.setAttribute('download', `${name.replaceAll(' ', '_')}_${tab === 0 ? 'Uploader' : 'Shortener'}.sxcu`);
|
||||||
a.click();
|
a.click();
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
|
@ -55,36 +59,66 @@ export default function ShareXDialog({ open, onClose, token }) {
|
||||||
isOpen={open}
|
isOpen={open}
|
||||||
scrollBehavior='inside'
|
scrollBehavior='inside'
|
||||||
>
|
>
|
||||||
<ModalOverlay/>
|
<ModalOverlay />
|
||||||
<ModalContent>
|
<ModalContent>
|
||||||
<ModalHeader>ShareX config generator</ModalHeader>
|
<ModalHeader>ShareX config generator</ModalHeader>
|
||||||
<ModalCloseButton/>
|
<ModalCloseButton />
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
<Heading mb={1} size='sm'>Config name</Heading>
|
<Tabs index={tab} onChange={index => setTab(index)} size='sm'>
|
||||||
<Input
|
<TabList>
|
||||||
value={name}
|
<Tab>Uploader</Tab>
|
||||||
onChange={n => setName(n.target.value)}
|
<Tab>Shortener</Tab>
|
||||||
placeholder='Void'
|
</TabList>
|
||||||
size='sm'
|
<TabPanels>
|
||||||
/>
|
<TabPanel>
|
||||||
<Heading mt={2} mb={1} size='sm'>URL generator</Heading>
|
<Heading mb={1} size='sm'>Config name</Heading>
|
||||||
<Select
|
<Input
|
||||||
value={generator}
|
value={name}
|
||||||
onChange={g => setGenerator(g.target.value)}
|
onChange={n => setName(n.target.value)}
|
||||||
size='sm'
|
placeholder='Void'
|
||||||
>
|
size='sm'
|
||||||
<option value='random'>Random</option>
|
/>
|
||||||
<option value='zws'>Invisible</option>
|
<Heading mt={2} mb={1} size='sm'>URL generator</Heading>
|
||||||
<option value='emoji'>Emoji</option>
|
<Select
|
||||||
</Select>
|
value={generator}
|
||||||
<Heading mt={2} mb={1} size='sm'>Preserve file name</Heading>
|
onChange={g => setGenerator(g.target.value)}
|
||||||
<Switch isChecked={preserveFileName} onChange={p => setPreserveFileName(p.target.checked)}/>
|
size='sm'
|
||||||
|
>
|
||||||
|
<option value='random'>Random</option>
|
||||||
|
<option value='zws'>Invisible</option>
|
||||||
|
<option value='emoji'>Emoji</option>
|
||||||
|
</Select>
|
||||||
|
<Heading mt={2} mb={1} size='sm'>Preserve file name</Heading>
|
||||||
|
<Switch isChecked={preserveFileName} onChange={p => setPreserveFileName(p.target.checked)} />
|
||||||
|
</TabPanel>
|
||||||
|
<TabPanel>
|
||||||
|
<Heading mb={1} size='sm'>Config name</Heading>
|
||||||
|
<Input
|
||||||
|
value={name}
|
||||||
|
onChange={n => setName(n.target.value)}
|
||||||
|
placeholder='Void'
|
||||||
|
size='sm'
|
||||||
|
/>
|
||||||
|
<Heading mt={2} mb={1} size='sm'>URL generator</Heading>
|
||||||
|
<Select
|
||||||
|
value={generator}
|
||||||
|
onChange={g => setGenerator(g.target.value)}
|
||||||
|
size='sm'
|
||||||
|
>
|
||||||
|
<option value='random'>Random</option>
|
||||||
|
<option value='zws'>Invisible</option>
|
||||||
|
<option value='emoji'>Emoji</option>
|
||||||
|
</Select>
|
||||||
|
<Heading mt={2} mb={1} size='sm'>Use password</Heading>
|
||||||
|
<Switch isChecked={usePassword} onChange={p => setUsePassword(p.target.checked)} />
|
||||||
|
</TabPanel>
|
||||||
|
</TabPanels>
|
||||||
|
</Tabs>
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
<ButtonGroup size='sm'>
|
<ButtonGroup size='sm'>
|
||||||
<Button onClick={onClose} leftIcon={<X size={16}/>}>Cancel</Button>
|
<Button onClick={onClose} leftIcon={<X size={16} />}>Cancel</Button>
|
||||||
<Button colorScheme='purple' leftIcon={<Download size={16}/>} onClick={() => generateConfig(true)}>Shortener</Button>
|
<Button colorScheme='purple' leftIcon={<Download size={16} />} onClick={() => downloadConfig()} ref={ref}>Download</Button>
|
||||||
<Button colorScheme='purple' leftIcon={<Download size={16}/>} onClick={() => generateConfig(false)} ref={ref}>Uploader</Button>
|
|
||||||
</ButtonGroup>
|
</ButtonGroup>
|
||||||
</ModalFooter>
|
</ModalFooter>
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
|
|
|
@ -36,13 +36,13 @@ export default function Upload() {
|
||||||
body
|
body
|
||||||
});
|
});
|
||||||
const json = await res.json();
|
const json = await res.json();
|
||||||
if (res.ok && !json.error) {
|
if (!json.error) {
|
||||||
showToast('success', 'File uploaded', json.url);
|
showToast('success', 'File uploaded', json.url);
|
||||||
if (copy(json.url))
|
if (copy(json.url))
|
||||||
showToast('info', 'Copied the URL to your clipboard');
|
showToast('info', 'Copied the URL to your clipboard');
|
||||||
else
|
|
||||||
showToast('error', 'Couldn\'t upload the file', json.error);
|
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
showToast('error', 'Couldn\'t upload the file', json.error);
|
||||||
}
|
}
|
||||||
catch (error) {
|
catch (error) {
|
||||||
showToast('error', 'Error while uploading the file', error.message);
|
showToast('error', 'Error while uploading the file', error.message);
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { Button, ButtonGroup, FormControl, FormLabel, HStack, IconButton, Input, Link, Popover, PopoverArrow, PopoverBody, PopoverCloseButton, PopoverContent, PopoverFooter, PopoverHeader, PopoverTrigger, Skeleton, Table, Tbody, Td, Th, Thead, Tr, useDisclosure, useToast } from '@chakra-ui/react';
|
import { Button, ButtonGroup, FormControl, FormLabel, HStack, Select, IconButton, Input, Link, Popover, PopoverArrow, PopoverBody, PopoverCloseButton, PopoverContent, PopoverFooter, PopoverHeader, PopoverTrigger, Skeleton, Table, Tbody, Td, Th, Thead, Tr, useDisclosure, useToast } from '@chakra-ui/react';
|
||||||
import copy from 'copy-to-clipboard';
|
import copy from 'copy-to-clipboard';
|
||||||
import { Field, Form, Formik } from 'formik';
|
import { Field, Form, Formik } from 'formik';
|
||||||
import useFetch from 'lib/hooks/useFetch';
|
import useFetch from 'lib/hooks/useFetch';
|
||||||
|
@ -16,6 +16,7 @@ export default function URLs() {
|
||||||
const schema = yup.object({
|
const schema = yup.object({
|
||||||
destination: yup.string().matches(/((?:(?:http?|ftp)[s]*:\/\/)?[a-z0-9-%\/\&=?\.]+\.[a-z]{2,4}\/?([^\s<>\#%"\,\{\}\\|\\\^\[\]`]+)?)/i).min(3).required(),
|
destination: yup.string().matches(/((?:(?:http?|ftp)[s]*:\/\/)?[a-z0-9-%\/\&=?\.]+\.[a-z]{2,4}\/?([^\s<>\#%"\,\{\}\\|\\\^\[\]`]+)?)/i).min(3).required(),
|
||||||
vanity: yup.string(),
|
vanity: yup.string(),
|
||||||
|
generator: yup.string(),
|
||||||
urlPassword: yup.string()
|
urlPassword: yup.string()
|
||||||
});
|
});
|
||||||
const handleDelete = async u => {
|
const handleDelete = async u => {
|
||||||
|
@ -45,7 +46,8 @@ export default function URLs() {
|
||||||
const data = {
|
const data = {
|
||||||
destination: schemify(values.destination.trim()),
|
destination: schemify(values.destination.trim()),
|
||||||
vanity: values.vanity.trim(),
|
vanity: values.vanity.trim(),
|
||||||
password: values.urlPassword.trim()
|
password: values.urlPassword.trim(),
|
||||||
|
generator: values.generator
|
||||||
};
|
};
|
||||||
setBusy(true);
|
setBusy(true);
|
||||||
const res = await useFetch('/api/shorten', 'POST', data);
|
const res = await useFetch('/api/shorten', 'POST', data);
|
||||||
|
@ -82,7 +84,7 @@ export default function URLs() {
|
||||||
</PopoverHeader>
|
</PopoverHeader>
|
||||||
<PopoverArrow/>
|
<PopoverArrow/>
|
||||||
<PopoverCloseButton/>
|
<PopoverCloseButton/>
|
||||||
<Formik validationSchema={schema} initialValues={{ destination: '', vanity: '', urlPassword: '' }} onSubmit={(values, actions) => { handleSubmit(values, actions); }}>
|
<Formik validationSchema={schema} initialValues={{ destination: '', vanity: '', generator: 'random', urlPassword: '' }} onSubmit={(values, actions) => { handleSubmit(values, actions); }}>
|
||||||
{props => (
|
{props => (
|
||||||
<Form>
|
<Form>
|
||||||
<PopoverBody>
|
<PopoverBody>
|
||||||
|
@ -90,7 +92,7 @@ export default function URLs() {
|
||||||
{({ field, form }) => (
|
{({ field, form }) => (
|
||||||
<FormControl isInvalid={form.errors.destination && form.touched.destination} isRequired>
|
<FormControl isInvalid={form.errors.destination && form.touched.destination} isRequired>
|
||||||
<FormLabel htmlFor='destination'>Destination</FormLabel>
|
<FormLabel htmlFor='destination'>Destination</FormLabel>
|
||||||
<Input {...field} size='sm' id='destination' mb={4} placeholder='Destination'/>
|
<Input {...field} size='sm' id='destination' mb={2} placeholder='Destination'/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
)}
|
)}
|
||||||
</Field>
|
</Field>
|
||||||
|
@ -98,7 +100,19 @@ export default function URLs() {
|
||||||
{({ field }) => (
|
{({ field }) => (
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<FormLabel htmlFor='vanity'>Vanity URL</FormLabel>
|
<FormLabel htmlFor='vanity'>Vanity URL</FormLabel>
|
||||||
<Input {...field} size='sm' id='vanity' mb={4} placeholder='Leave blank for random'/>
|
<Input {...field} size='sm' id='vanity' mb={2} placeholder='Leave blank for random'/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
</Field>
|
||||||
|
<Field name='generator'>
|
||||||
|
{({ field }) => (
|
||||||
|
<FormControl>
|
||||||
|
<FormLabel htmlFor='generator'>URL generator</FormLabel>
|
||||||
|
<Select {...field} size='sm' id='generator' mb={2}>
|
||||||
|
<option value='random'>Random</option>
|
||||||
|
<option value='zws'>Invisible</option>
|
||||||
|
<option value='emoji'>Emoji</option>
|
||||||
|
</Select>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
)}
|
)}
|
||||||
</Field>
|
</Field>
|
||||||
|
@ -106,7 +120,7 @@ export default function URLs() {
|
||||||
{({ field }) => (
|
{({ field }) => (
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<FormLabel htmlFor='urlPassword'>Password</FormLabel>
|
<FormLabel htmlFor='urlPassword'>Password</FormLabel>
|
||||||
<Input {...field} size='sm' id='urlPassword' mb={4} placeholder='Password'/>
|
<Input {...field} size='sm' id='urlPassword' mb={2} placeholder='Password'/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
)}
|
)}
|
||||||
</Field>
|
</Field>
|
||||||
|
|
|
@ -15,6 +15,7 @@ const envValues = [
|
||||||
e('UPLOADER_RAW_ROUTE', 'string', (c, v) => c.uploader.raw_route = v),
|
e('UPLOADER_RAW_ROUTE', 'string', (c, v) => c.uploader.raw_route = v),
|
||||||
e('UPLOADER_LENGTH', 'number', (c, v) => c.uploader.length = v),
|
e('UPLOADER_LENGTH', 'number', (c, v) => c.uploader.length = v),
|
||||||
e('UPLOADER_DIRECTORY', 'string', (c, v) => c.uploader.directory = v),
|
e('UPLOADER_DIRECTORY', 'string', (c, v) => c.uploader.directory = v),
|
||||||
|
e('UPLOADER_MAX_SIZE', 'number', (c, v) => c.uploader.max_size = v),
|
||||||
e('UPLOADER_BLACKLISTED', 'array', (c, v) => v ? c.uploader.blacklisted = v : c.uploader.blacklisted = []),
|
e('UPLOADER_BLACKLISTED', 'array', (c, v) => v ? c.uploader.blacklisted = v : c.uploader.blacklisted = []),
|
||||||
|
|
||||||
e('BOT_ENABLED', 'boolean', (c, v) => c.bot.enabled = v),
|
e('BOT_ENABLED', 'boolean', (c, v) => c.bot.enabled = v),
|
||||||
|
|
|
@ -26,6 +26,7 @@ export interface Uploader {
|
||||||
raw_route: string;
|
raw_route: string;
|
||||||
length: number;
|
length: number;
|
||||||
directory: string;
|
directory: string;
|
||||||
|
max_size: number;
|
||||||
blacklisted: string[];
|
blacklisted: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,7 @@ const validator = yup.object({
|
||||||
secret: yup.string().min(8).required(),
|
secret: yup.string().min(8).required(),
|
||||||
host: yup.string().default('0.0.0.0'),
|
host: yup.string().default('0.0.0.0'),
|
||||||
port: yup.number().default(3000),
|
port: yup.number().default(3000),
|
||||||
database_url: yup.string().required(),
|
database_url: yup.string().required()
|
||||||
}).required(),
|
}).required(),
|
||||||
bot: yup.object({
|
bot: yup.object({
|
||||||
enabled: yup.bool().default(false),
|
enabled: yup.bool().default(false),
|
||||||
|
@ -26,7 +26,8 @@ const validator = yup.object({
|
||||||
raw_route: yup.string().required(),
|
raw_route: yup.string().required(),
|
||||||
length: yup.number().default(6),
|
length: yup.number().default(6),
|
||||||
directory: yup.string().required(),
|
directory: yup.string().required(),
|
||||||
blacklisted: yup.array().default([]),
|
max_size: yup.number().default(104857600),
|
||||||
|
blacklisted: yup.array().default([])
|
||||||
}).required(),
|
}).required(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -115,6 +115,7 @@ export const getServerSideProps: GetServerSideProps = async context => {
|
||||||
const slug = context.params.id[0];
|
const slug = context.params.id[0];
|
||||||
if (slug === config.shortener.route.split('/').pop()) {
|
if (slug === config.shortener.route.split('/').pop()) {
|
||||||
const short = context.params.id[1];
|
const short = context.params.id[1];
|
||||||
|
if ((short ?? '').trim() === '') return { notFound: true };
|
||||||
const url = await prisma.url.findFirst({
|
const url = await prisma.url.findFirst({
|
||||||
where: {
|
where: {
|
||||||
short
|
short
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { verifyPassword } from 'lib/utils';
|
||||||
import { NextApiReq, NextApiRes, withVoid } from 'middleware/withVoid';
|
import { NextApiReq, NextApiRes, withVoid } from 'middleware/withVoid';
|
||||||
|
|
||||||
async function handler(req: NextApiReq, res: NextApiRes) {
|
async function handler(req: NextApiReq, res: NextApiRes) {
|
||||||
if (req.method !== 'POST') return res.status(405).end();
|
if (req.method !== 'POST') return res.forbid('Invalid method');
|
||||||
const { username, password } = req.body as { username: string, password: string };
|
const { username, password } = req.body as { username: string, password: string };
|
||||||
const user = await prisma.user.findFirst({
|
const user = await prisma.user.findFirst({
|
||||||
where: {
|
where: {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { default as config } from 'lib/config';
|
import config from 'lib/config';
|
||||||
import generate from 'lib/generators';
|
import generate, { emoji, zws } from 'lib/generators';
|
||||||
import { info } from 'lib/logger';
|
import { info } from 'lib/logger';
|
||||||
import { NextApiReq, NextApiRes, withVoid } from 'lib/middleware/withVoid';
|
import { NextApiReq, NextApiRes, withVoid } from 'lib/middleware/withVoid';
|
||||||
import prisma from 'lib/prisma';
|
import prisma from 'lib/prisma';
|
||||||
|
@ -25,11 +25,12 @@ async function handler(req: NextApiReq, res: NextApiRes) {
|
||||||
});
|
});
|
||||||
if (existing) return res.error('Vanity is already taken');
|
if (existing) return res.error('Vanity is already taken');
|
||||||
}
|
}
|
||||||
const rand = generate(config.shortener.length);
|
const generator = req.headers.generator || req.body.generator;
|
||||||
|
const rand = generator === 'zws' ? zws(config.shortener.length) : generator === 'emoji' ? emoji(config.shortener.length) : generate(config.shortener.length);
|
||||||
if (req.body.password) var password = await hashPassword(req.body.password);
|
if (req.body.password) var password = await hashPassword(req.body.password);
|
||||||
const url = await prisma.url.create({
|
const url = await prisma.url.create({
|
||||||
data: {
|
data: {
|
||||||
short: req.body.vanity ? req.body.vanity : rand,
|
short: req.body.vanity || rand,
|
||||||
destination: req.body.destination,
|
destination: req.body.destination,
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
password
|
password
|
||||||
|
|
|
@ -23,23 +23,10 @@ async function handler(req: NextApiReq, res: NextApiRes) {
|
||||||
if (!user) return res.forbid('Unauthorized');
|
if (!user) return res.forbid('Unauthorized');
|
||||||
if (!req.file) return res.error('No file specified');
|
if (!req.file) return res.error('No file specified');
|
||||||
const ext = req.file.originalname.includes('.') ? req.file.originalname.split('.').pop() : req.file.originalname;
|
const ext = req.file.originalname.includes('.') ? req.file.originalname.split('.').pop() : req.file.originalname;
|
||||||
if (cfg.uploader.blacklisted.includes(ext)) return res.error('Blacklisted extension received: ' + ext);
|
if (cfg.uploader.blacklisted.includes(ext) && !user.isAdmin) return res.error(`Blacklisted extension received: ${ext}`);
|
||||||
|
if (req.file.size > cfg.uploader.max_size && !user.isAdmin) return res.error('The file is too big');
|
||||||
const rand = generate(cfg.uploader.length);
|
const rand = generate(cfg.uploader.length);
|
||||||
let slug;
|
const slug = req.headers.generator === 'zws' ? zws(cfg.uploader.length) : req.headers.generator === 'emoji' ? emoji(cfg.uploader.length) : rand;
|
||||||
switch (req.headers.generator) {
|
|
||||||
case 'zws': {
|
|
||||||
slug = zws(cfg.uploader.length);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'emoji': {
|
|
||||||
slug = emoji(cfg.uploader.length);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
default: {
|
|
||||||
slug = rand;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const deletionToken = generate(15);
|
const deletionToken = generate(15);
|
||||||
function getMimetype(current, ext) {
|
function getMimetype(current, ext) {
|
||||||
if (current === 'application/octet-stream') {
|
if (current === 'application/octet-stream') {
|
||||||
|
|
|
@ -28,10 +28,9 @@ const shorten = {
|
||||||
id: 1
|
id: 1
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
const rand = generate(config.shortener.length);
|
|
||||||
const url = await prisma.url.create({
|
const url = await prisma.url.create({
|
||||||
data: {
|
data: {
|
||||||
short: vanity ? vanity : rand,
|
short: vanity ?? generate(config.shortener.length),
|
||||||
destination: schemify(dest),
|
destination: schemify(dest),
|
||||||
userId: config.bot.default_uid,
|
userId: config.bot.default_uid,
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,21 +29,7 @@ const upload = {
|
||||||
const fileName = url.parse(args[0]).pathname;
|
const fileName = url.parse(args[0]).pathname;
|
||||||
const ext = extname(fileName);
|
const ext = extname(fileName);
|
||||||
const rand = generate(config.uploader.length);
|
const rand = generate(config.uploader.length);
|
||||||
let slug;
|
const slug = args[1] === 'zws' ? zws(config.uploader.length) : args[1] === 'emoji' ? emoji(config.uploader.length) : rand;
|
||||||
switch (args[1] ?? 'normal') {
|
|
||||||
case 'zws': {
|
|
||||||
slug = zws(config.uploader.length);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'emoji': {
|
|
||||||
slug = emoji(config.uploader.length);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
default: {
|
|
||||||
slug = rand;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const deletionToken = generate(15);
|
const deletionToken = generate(15);
|
||||||
function getMimetype(current, ext) {
|
function getMimetype(current, ext) {
|
||||||
if (current === 'application/octet-stream') {
|
if (current === 'application/octet-stream') {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import Discord, { Message, MessageEmbed } from 'discord.js';
|
import Discord, { Message, MessageEmbed } from 'discord.js';
|
||||||
import { readdir } from 'fs';
|
import { readdirSync } from 'fs';
|
||||||
import { error, info } from '../src/lib/logger';
|
import { error, info } from '../src/lib/logger';
|
||||||
import { Logger } from './utils/logger';
|
import { Logger } from './utils/logger';
|
||||||
|
|
||||||
|
@ -16,14 +16,13 @@ client.once('ready', () => {
|
||||||
avatarUrl = client.user.displayAvatarURL();
|
avatarUrl = client.user.displayAvatarURL();
|
||||||
global.logger = new Logger(client);
|
global.logger = new Logger(client);
|
||||||
global.logger.log('Twilight is ready');
|
global.logger.log('Twilight is ready');
|
||||||
readdir(`${__dirname}/commands`, (err, files) => {
|
readdirSync(`${__dirname}/commands`)
|
||||||
if(err) error('BOT', err.message);
|
.map(file => file.toString())
|
||||||
files.forEach(file => {
|
.filter(file => file.endsWith('.ts'))
|
||||||
if (file.toString().includes('.ts')) {
|
.forEach(file => {
|
||||||
import(`${__dirname}/commands/${file.toString()}`).then(command => commands.push(command.default));
|
import(`${__dirname}/commands/${file.toString()}`).then(command => commands.push(command.default));
|
||||||
info('COMMAND', `Loaded command: ${file.toString().split('.').shift()}`);}
|
info('COMMAND', `Loaded command: ${file.toString().split('.').shift()}`);
|
||||||
});
|
});
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
client.on('message', (msg: Message) => {
|
client.on('message', (msg: Message) => {
|
||||||
|
|
Loading…
Reference in New Issue