feat(v3.3): ratelimit
This commit is contained in:
parent
7eff77ccc4
commit
7194c53891
12 changed files with 1088 additions and 1088 deletions
|
@ -18,7 +18,7 @@
|
|||
"@mui/icons-material": "^5.0.0",
|
||||
"@mui/material": "^5.0.2",
|
||||
"@mui/styles": "^5.0.1",
|
||||
"@prisma/client": "^3.1.1",
|
||||
"@prisma/client": "^3.7.0",
|
||||
"@reduxjs/toolkit": "^1.6.0",
|
||||
"argon2": "^0.28.2",
|
||||
"colorette": "^1.2.2",
|
||||
|
@ -27,9 +27,10 @@
|
|||
"fecha": "^4.2.1",
|
||||
"formik": "^2.2.9",
|
||||
"multer": "^1.4.2",
|
||||
"next": "11.1.1",
|
||||
"prisma": "^3.1.1",
|
||||
"next": "^12.0.4",
|
||||
"prisma": "^3.7.0",
|
||||
"react": "17.0.2",
|
||||
"react-color": "^2.19.3",
|
||||
"react-dom": "17.0.2",
|
||||
"react-dropzone": "^11.3.2",
|
||||
"react-redux": "^7.2.4",
|
||||
|
|
11
prisma/migrations/20211128031800_ratelimit/migration.sql
Normal file
11
prisma/migrations/20211128031800_ratelimit/migration.sql
Normal file
|
@ -0,0 +1,11 @@
|
|||
-- AlterTable
|
||||
ALTER TABLE "User" ADD COLUMN "ratelimited" BOOLEAN NOT NULL DEFAULT false;
|
||||
|
||||
-- RenameIndex
|
||||
ALTER INDEX "InvisibleImage_imageId_unique" RENAME TO "InvisibleImage_imageId_key";
|
||||
|
||||
-- RenameIndex
|
||||
ALTER INDEX "InvisibleUrl_urlId_unique" RENAME TO "InvisibleUrl_urlId_key";
|
||||
|
||||
-- RenameIndex
|
||||
ALTER INDEX "Theme_userId_unique" RENAME TO "Theme_userId_key";
|
|
@ -18,6 +18,7 @@ model User {
|
|||
embedTitle String?
|
||||
embedColor String @default("#2f3136")
|
||||
embedSiteName String? @default("{image.file} • {user.name}")
|
||||
ratelimited Boolean @default(false)
|
||||
images Image[]
|
||||
urls Url[]
|
||||
}
|
||||
|
|
|
@ -20,7 +20,7 @@ const dev = process.env.NODE_ENV === 'development';
|
|||
|
||||
function log(url, status) {
|
||||
if (url.startsWith('/_next') || url.startsWith('/__nextjs')) return;
|
||||
return Logger.get('url').info(`${status === 200 ? bold(green(status)) : bold(red(status))}: ${url}`);
|
||||
return Logger.get('url').info(url);
|
||||
}
|
||||
|
||||
function shouldUseYarn() {
|
||||
|
@ -111,7 +111,7 @@ function shouldUseYarn() {
|
|||
handle(req, res);
|
||||
}
|
||||
|
||||
log(req.url, res.statusCode);
|
||||
if (config.core.logger) log(req.url, res.statusCode);
|
||||
});
|
||||
|
||||
srv.on('error', (e) => {
|
||||
|
|
|
@ -9,6 +9,7 @@ const validator = yup.object({
|
|||
host: yup.string().default('0.0.0.0'),
|
||||
port: yup.number().default(3000),
|
||||
database_url: yup.string().required(),
|
||||
logger: yup.boolean().default(true),
|
||||
}).required(),
|
||||
uploader: yup.object({
|
||||
route: yup.string().required(),
|
||||
|
@ -22,6 +23,10 @@ const validator = yup.object({
|
|||
route: yup.string().required(),
|
||||
length: yup.number().default(6),
|
||||
}).required(),
|
||||
ratelimit: yup.object({
|
||||
user: yup.number().default(0),
|
||||
admin: yup.number().default(0),
|
||||
}),
|
||||
});
|
||||
|
||||
|
||||
|
|
|
@ -125,9 +125,9 @@ export default function Dashboard() {
|
|||
return (
|
||||
<>
|
||||
<Typography variant='h4'>Welcome back {user?.username}</Typography>
|
||||
<Typography color='GrayText' pb={2}>You have <b>{images.length ? images.length : '...'}</b> images</Typography>
|
||||
<Typography color='GrayText' pb={2}>You have <b>{images.length ? images.length : '...'}</b> files</Typography>
|
||||
|
||||
<Typography variant='h4'>Recent Images</Typography>
|
||||
<Typography variant='h4'>Recent Files</Typography>
|
||||
<Grid container spacing={4} py={2}>
|
||||
{recent.length ? recent.map(image => (
|
||||
<Grid item xs={12} sm={3} key={image.id}>
|
||||
|
@ -172,7 +172,7 @@ export default function Dashboard() {
|
|||
</Grid>
|
||||
</Grid>
|
||||
<Card name='Images' sx={{ my: 2 }} elevation={0} variant='outlined'>
|
||||
<Link href='/dashboard/images' pb={2}>View Gallery</Link>
|
||||
<Link href='/dashboard/files' pb={2}>View Files</Link>
|
||||
<TableContainer sx={{ maxHeight: 440 }}>
|
||||
<Table size='small'>
|
||||
<TableHead>
|
||||
|
@ -225,11 +225,11 @@ export default function Dashboard() {
|
|||
onPageChange={handleChangePage}
|
||||
onRowsPerPageChange={handleChangeRowsPerPage} />
|
||||
</Card>
|
||||
<Card name='Images per User' sx={{ height: '100%', my: 2 }} elevation={0} variant='outlined'>
|
||||
<Card name='Files per User' sx={{ height: '100%', my: 2 }} elevation={0} variant='outlined'>
|
||||
<StatTable
|
||||
columns={[
|
||||
{ id: 'username', name: 'Name' },
|
||||
{ id: 'count', name: 'Images' },
|
||||
{ id: 'count', name: 'Files' },
|
||||
]}
|
||||
rows={stats ? stats.count_by_user : []} />
|
||||
</Card>
|
||||
|
|
|
@ -38,20 +38,16 @@ export type NextApiRes = NextApiResponse & {
|
|||
forbid: (message: string) => void;
|
||||
bad: (message: string) => void;
|
||||
json: (json: any) => void;
|
||||
ratelimited: () => void;
|
||||
setCookie: (name: string, value: unknown, options: CookieSerializeOptions) => void;
|
||||
}
|
||||
|
||||
// {
|
||||
// 'Access-Control-Allow-Origin': '*',
|
||||
// 'Access-Control-Allow-Methods': 'GET,HEAD,POST,OPTIONS',
|
||||
// 'Access-Control-Max-Age': '86400'
|
||||
// }
|
||||
|
||||
export const withZipline = (handler: (req: NextApiRequest, res: NextApiResponse) => unknown) => (req: NextApiReq, res: NextApiRes) => {
|
||||
res.setHeader('Content-Type', 'application/json');
|
||||
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||
res.setHeader('Access-Content-Allow-Methods', 'GET,HEAD,POST,OPTIONS');
|
||||
res.setHeader('Access-Control-Max-Age', '86400');
|
||||
|
||||
res.error = (message: string) => {
|
||||
res.json({
|
||||
error: message,
|
||||
|
@ -73,6 +69,14 @@ export const withZipline = (handler: (req: NextApiRequest, res: NextApiResponse)
|
|||
});
|
||||
};
|
||||
|
||||
res.ratelimited = () => {
|
||||
res.status(429);
|
||||
|
||||
res.json({
|
||||
error: '429: ratelimited',
|
||||
});
|
||||
};
|
||||
|
||||
res.json = (json: any) => {
|
||||
res.end(JSON.stringify(json));
|
||||
};
|
||||
|
|
|
@ -10,6 +10,7 @@ const envValues = [
|
|||
e('HOST', 'string', (c, v) => c.core.host = v),
|
||||
e('PORT', 'number', (c, v) => c.core.port = v),
|
||||
e('DATABASE_URL', 'string', (c, v) => c.core.database_url = v),
|
||||
e('LOGGER', 'boolean', (c, v) => c.core.logger = v ?? true),
|
||||
|
||||
e('UPLOADER_ROUTE', 'string', (c, v) => c.uploader.route = v),
|
||||
e('UPLOADER_LENGTH', 'number', (c, v) => c.uploader.length = v),
|
||||
|
@ -20,6 +21,9 @@ const envValues = [
|
|||
|
||||
e('URLS_ROUTE', 'string', (c, v) => c.urls.route = v),
|
||||
e('URLS_LENGTH', 'number', (c, v) => c.urls.length = v),
|
||||
|
||||
e('RATELIMIT_USER', 'number', (c, v) => c.ratelimit.user = v ?? 0),
|
||||
e('RATELIMIT_ADMIN', 'number', (c, v) => c.ratelimit.user = v ?? 0),
|
||||
];
|
||||
|
||||
module.exports = () => {
|
||||
|
@ -43,6 +47,7 @@ function tryReadEnv() {
|
|||
host: undefined,
|
||||
port: undefined,
|
||||
database_url: undefined,
|
||||
logger: undefined,
|
||||
},
|
||||
uploader: {
|
||||
route: undefined,
|
||||
|
@ -56,6 +61,10 @@ function tryReadEnv() {
|
|||
route: undefined,
|
||||
length: undefined,
|
||||
},
|
||||
ratelimit: {
|
||||
user: undefined,
|
||||
admin: undefined,
|
||||
},
|
||||
};
|
||||
|
||||
for (let i = 0, L = envValues.length; i !== L; ++i) {
|
||||
|
|
|
@ -13,6 +13,9 @@ export interface ConfigCore {
|
|||
|
||||
// The PostgreSQL database url
|
||||
database_url: string
|
||||
|
||||
// Whether or not to log stuff
|
||||
logger: boolean;
|
||||
}
|
||||
|
||||
export interface ConfigUploader {
|
||||
|
@ -43,8 +46,18 @@ export interface ConfigUrls {
|
|||
length: number;
|
||||
}
|
||||
|
||||
// Ratelimiting for users/admins, setting them to 0 disables ratelimiting
|
||||
export interface ConfigRatelimit {
|
||||
// Ratelimit for users
|
||||
user: number;
|
||||
|
||||
// Ratelimit for admins
|
||||
admin: number;
|
||||
}
|
||||
|
||||
export interface Config {
|
||||
core: ConfigCore;
|
||||
uploader: ConfigUploader;
|
||||
urls: ConfigUrls;
|
||||
}
|
||||
ratelimit: ConfigRatelimit;
|
||||
}
|
||||
|
|
|
@ -12,7 +12,7 @@ const uploader = multer({
|
|||
});
|
||||
|
||||
async function handler(req: NextApiReq, res: NextApiRes) {
|
||||
if (req.method !== 'POST') return res.forbid('no allow');
|
||||
if (req.method !== 'POST') return res.forbid('invalid method');
|
||||
if (!req.headers.authorization) return res.forbid('no authorization');
|
||||
|
||||
const user = await prisma.user.findFirst({
|
||||
|
@ -21,8 +21,10 @@ async function handler(req: NextApiReq, res: NextApiRes) {
|
|||
},
|
||||
});
|
||||
|
||||
|
||||
if (!user) return res.forbid('authorization incorect');
|
||||
|
||||
if (user.ratelimited) return res.ratelimited();
|
||||
|
||||
if (!req.files) return res.error('no files');
|
||||
if (req.files && req.files.length === 0) return res.error('no files');
|
||||
|
||||
|
@ -53,8 +55,31 @@ async function handler(req: NextApiReq, res: NextApiRes) {
|
|||
files.push(`${zconfig.core.secure ? 'https' : 'http'}://${req.headers.host}${zconfig.uploader.route}/${invis ? invis.invis : image.file}`);
|
||||
}
|
||||
|
||||
// url will be deprecated soon
|
||||
return res.json({ files, url: files[0] });
|
||||
if (user.administrator && zconfig.ratelimit.admin !== 0) {
|
||||
await prisma.user.update({
|
||||
where: {
|
||||
id: user.id,
|
||||
},
|
||||
data: {
|
||||
ratelimited: true,
|
||||
},
|
||||
});
|
||||
setTimeout(async () => await prisma.user.update({ where: { id: user.id }, data: { ratelimited: false } }), zconfig.ratelimit.admin * 1000).unref();
|
||||
}
|
||||
|
||||
if (!user.administrator && zconfig.ratelimit.user !== 0) {
|
||||
await prisma.user.update({
|
||||
where: {
|
||||
id: user.id,
|
||||
},
|
||||
data: {
|
||||
ratelimited: true,
|
||||
},
|
||||
});
|
||||
setTimeout(async () => await prisma.user.update({ where: { id: user.id }, data: { ratelimited: false } }), zconfig.ratelimit.user * 1000).unref();
|
||||
}
|
||||
|
||||
return res.json({ files });
|
||||
}
|
||||
|
||||
function run(middleware: any) {
|
||||
|
|
1
zip-env.d.ts
vendored
1
zip-env.d.ts
vendored
|
@ -6,6 +6,7 @@ declare global {
|
|||
interface Global {
|
||||
prisma: PrismaClient;
|
||||
config: Config;
|
||||
ratelimit: Set<string>;
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue