feat(v3.4.2): random domain selection #129
This commit is contained in:
parent
99e92e4594
commit
083040e300
8 changed files with 110 additions and 15 deletions
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "zip3",
|
||||
"version": "3.4.1",
|
||||
"version": "3.4.2",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"dev": "node esbuild.config.js && REACT_EDITOR=code-insiders NODE_ENV=development node dist/server",
|
||||
|
|
2
prisma/migrations/20220304004623_domains/migration.sql
Normal file
2
prisma/migrations/20220304004623_domains/migration.sql
Normal file
|
@ -0,0 +1,2 @@
|
|||
-- AlterTable
|
||||
ALTER TABLE "User" ADD COLUMN "domains" TEXT[];
|
|
@ -18,6 +18,7 @@ model User {
|
|||
embedColor String @default("#2f3136")
|
||||
embedSiteName String? @default("{image.file} • {user.name}")
|
||||
ratelimited Boolean @default(false)
|
||||
domains String[]
|
||||
images Image[]
|
||||
urls Url[]
|
||||
}
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
import React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import useFetch from 'hooks/useFetch';
|
||||
import Link from 'components/Link';
|
||||
import { useStoreDispatch, useStoreSelector } from 'lib/redux/store';
|
||||
import { updateUser } from 'lib/redux/reducers/user';
|
||||
import { useForm } from '@mantine/hooks';
|
||||
import { Tooltip, TextInput, Button, Text, Title, Group, ColorInput } from '@mantine/core';
|
||||
import { DownloadIcon } from '@modulz/radix-icons';
|
||||
import { randomId, useForm } from '@mantine/hooks';
|
||||
import { Tooltip, TextInput, Button, Text, Title, Group, ColorInput, MultiSelect, Space } from '@mantine/core';
|
||||
import { DownloadIcon, Cross1Icon } from '@modulz/radix-icons';
|
||||
import { useNotifications } from '@mantine/notifications';
|
||||
|
||||
function VarsTooltip({ children }) {
|
||||
return (
|
||||
|
@ -27,6 +28,9 @@ function VarsTooltip({ children }) {
|
|||
export default function Manage() {
|
||||
const user = useStoreSelector(state => state.user);
|
||||
const dispatch = useStoreDispatch();
|
||||
const notif = useNotifications();
|
||||
|
||||
const [domains, setDomains] = useState(user.domains ?? []);
|
||||
|
||||
const genShareX = (withEmbed: boolean = false, withZws: boolean = false) => {
|
||||
const config = {
|
||||
|
@ -61,6 +65,7 @@ export default function Manage() {
|
|||
embedTitle: user.embedTitle ?? '',
|
||||
embedColor: user.embedColor,
|
||||
embedSiteName: user.embedSiteName ?? '',
|
||||
domains: user.domains ?? [],
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -73,19 +78,51 @@ export default function Manage() {
|
|||
|
||||
if (cleanUsername === '') return form.setFieldError('username', 'Username can\'t be nothing');
|
||||
|
||||
const id = notif.showNotification({
|
||||
title: 'Updating user...',
|
||||
message: '',
|
||||
loading: true,
|
||||
});
|
||||
|
||||
const data = {
|
||||
username: cleanUsername,
|
||||
password: cleanPassword === '' ? null : cleanPassword,
|
||||
embedTitle: cleanEmbedTitle === '' ? null : cleanEmbedTitle,
|
||||
embedColor: cleanEmbedColor === '' ? null : cleanEmbedColor,
|
||||
embedSiteName: cleanEmbedSiteName === '' ? null : cleanEmbedSiteName,
|
||||
domains,
|
||||
};
|
||||
|
||||
const newUser = await useFetch('/api/user', 'PATCH', data);
|
||||
|
||||
if (newUser.error) {
|
||||
if (newUser.invalidDomains) {
|
||||
notif.updateNotification(id, {
|
||||
message: <>
|
||||
<Text mt='xs'>The following domains are invalid:</Text>
|
||||
{newUser.invalidDomains.map(err => (
|
||||
<>
|
||||
<Text color='gray' key={randomId()}>{err.domain}: {err.reason}</Text>
|
||||
<Space h='md' />
|
||||
</>
|
||||
))}
|
||||
</>,
|
||||
color: 'red',
|
||||
icon: <Cross1Icon />,
|
||||
});
|
||||
}
|
||||
notif.updateNotification(id, {
|
||||
title: 'Couldn\'t save user',
|
||||
message: newUser.error,
|
||||
color: 'red',
|
||||
icon: <Cross1Icon />,
|
||||
});
|
||||
} else {
|
||||
dispatch(updateUser(newUser));
|
||||
notif.updateNotification(id, {
|
||||
title: 'Saved User',
|
||||
message: '',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -101,6 +138,19 @@ export default function Manage() {
|
|||
<TextInput id='embedTitle' label='Embed Title' {...form.getInputProps('embedTitle')} />
|
||||
<ColorInput id='embedColor' label='Embed Color' {...form.getInputProps('embedColor')} />
|
||||
<TextInput id='embedSiteName' label='Embed Site Name' {...form.getInputProps('embedSiteName')} />
|
||||
<MultiSelect
|
||||
id='domains'
|
||||
label='Domains'
|
||||
data={domains}
|
||||
placeholder='Leave blank if you dont want random domain selection.'
|
||||
creatable
|
||||
searchable
|
||||
clearable
|
||||
getCreateLabel={query => `Add ${query}`}
|
||||
onCreate={query => setDomains((current) => [...current, query])}
|
||||
{...form.getInputProps('domains')}
|
||||
/>
|
||||
|
||||
<Group position='right' sx={{ paddingTop: 12 }}>
|
||||
<Button
|
||||
type='submit'
|
||||
|
|
|
@ -25,6 +25,7 @@ export type NextApiReq = NextApiRequest & {
|
|||
administrator: boolean;
|
||||
id: number;
|
||||
password: string;
|
||||
domains: string[];
|
||||
} | null | void>;
|
||||
getCookie: (name: string) => string | null;
|
||||
cleanCookie: (name: string) => void;
|
||||
|
@ -33,7 +34,7 @@ export type NextApiReq = NextApiRequest & {
|
|||
|
||||
export type NextApiRes = NextApiResponse & {
|
||||
error: (message: string) => void;
|
||||
forbid: (message: string) => void;
|
||||
forbid: (message: string, extra?: any) => void;
|
||||
bad: (message: string) => void;
|
||||
json: (json: any) => void;
|
||||
ratelimited: () => void;
|
||||
|
@ -52,11 +53,12 @@ export const withZipline = (handler: (req: NextApiRequest, res: NextApiResponse)
|
|||
});
|
||||
};
|
||||
|
||||
res.forbid = (message: string) => {
|
||||
res.forbid = (message: string, extra: any = {}) => {
|
||||
res.setHeader('Content-Type', 'application/json');
|
||||
res.status(403);
|
||||
res.json({
|
||||
error: '403: ' + message,
|
||||
...extra,
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -93,6 +95,7 @@ export const withZipline = (handler: (req: NextApiRequest, res: NextApiResponse)
|
|||
maxAge: undefined,
|
||||
}));
|
||||
};
|
||||
|
||||
req.user = async () => {
|
||||
try {
|
||||
const userId = req.getCookie('user');
|
||||
|
@ -111,6 +114,7 @@ export const withZipline = (handler: (req: NextApiRequest, res: NextApiResponse)
|
|||
systemTheme: true,
|
||||
token: true,
|
||||
username: true,
|
||||
domains: true,
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -7,6 +7,7 @@ export interface User {
|
|||
embedColor: string;
|
||||
embedSiteName: string;
|
||||
systemTheme: string;
|
||||
domains: string[];
|
||||
}
|
||||
|
||||
const initialState: User = null;
|
||||
|
|
|
@ -71,8 +71,13 @@ async function handler(req: NextApiReq, res: NextApiRes) {
|
|||
|
||||
await datasource.save(image.file, file.buffer);
|
||||
Logger.get('image').info(`User ${user.username} (${user.id}) uploaded an image ${image.file} (${image.id})`);
|
||||
if (user.domains.length) {
|
||||
const domain = user.domains[Math.floor(Math.random() * user.domains.length)];
|
||||
files.push(`${domain}${zconfig.uploader.route}/${invis ? invis.invis : image.file}`);
|
||||
} else {
|
||||
files.push(`${zconfig.core.secure ? 'https' : 'http'}://${req.headers.host}${zconfig.uploader.route}/${invis ? invis.invis : image.file}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (user.administrator && zconfig.ratelimit.admin !== 0) {
|
||||
await prisma.user.update({
|
||||
|
|
|
@ -2,6 +2,7 @@ import prisma from 'lib/prisma';
|
|||
import { hashPassword } from 'lib/util';
|
||||
import { NextApiReq, NextApiRes, withZipline } from 'middleware/withZipline';
|
||||
import Logger from 'lib/logger';
|
||||
import pkg from '../../../../package.json';
|
||||
|
||||
async function handler(req: NextApiReq, res: NextApiRes) {
|
||||
const user = await req.user();
|
||||
|
@ -51,6 +52,36 @@ async function handler(req: NextApiReq, res: NextApiRes) {
|
|||
data: { systemTheme: req.body.systemTheme },
|
||||
});
|
||||
|
||||
if (req.body.domains) {
|
||||
if (!req.body.domains) await prisma.user.update({
|
||||
where: { id: user.id },
|
||||
data: { domains: [] },
|
||||
});
|
||||
|
||||
const invalidDomains = [];
|
||||
|
||||
for (const domain of req.body.domains) {
|
||||
try {
|
||||
const url = new URL(domain);
|
||||
url.pathname = '/api/version';
|
||||
const res = await fetch(url.toString());
|
||||
if (!res.ok) invalidDomains.push({ domain, reason: 'Got a non OK response' });
|
||||
else {
|
||||
const body = await res.json();
|
||||
if (body?.local !== pkg.version) invalidDomains.push({ domain, reason: 'Version mismatch' });
|
||||
else await prisma.user.update({
|
||||
where: { id: user.id },
|
||||
data: { domains: { push: url.origin } },
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
invalidDomains.push({ domain, reason: e.message });
|
||||
}
|
||||
}
|
||||
|
||||
if (invalidDomains.length) return res.forbid('Invalid domains', { invalidDomains });
|
||||
}
|
||||
|
||||
const newUser = await prisma.user.findFirst({
|
||||
where: {
|
||||
id: Number(user.id),
|
||||
|
@ -66,6 +97,7 @@ async function handler(req: NextApiReq, res: NextApiRes) {
|
|||
systemTheme: true,
|
||||
token: true,
|
||||
username: true,
|
||||
domains: true,
|
||||
},
|
||||
});
|
||||
|
||||
|
|
Loading…
Reference in a new issue