feat(v3.4.2): random domain selection #129

This commit is contained in:
diced 2022-03-03 17:52:34 -08:00
parent 99e92e4594
commit 083040e300
No known key found for this signature in database
GPG key ID: 85AB64C74535D76E
8 changed files with 110 additions and 15 deletions

View file

@ -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",

View file

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "User" ADD COLUMN "domains" TEXT[];

View file

@ -8,16 +8,17 @@ generator client {
}
model User {
id Int @id @default(autoincrement())
id Int @id @default(autoincrement())
username String
password String
token String
administrator Boolean @default(false)
systemTheme String @default("system")
administrator Boolean @default(false)
systemTheme String @default("system")
embedTitle String?
embedColor String @default("#2f3136")
embedSiteName String? @default("{image.file} • {user.name}")
ratelimited Boolean @default(false)
embedColor String @default("#2f3136")
embedSiteName String? @default("{image.file} • {user.name}")
ratelimited Boolean @default(false)
domains String[]
images Image[]
urls Url[]
}

View file

@ -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: '',
});
}
};
@ -97,10 +134,23 @@ export default function Manage() {
</VarsTooltip>
<form onSubmit={form.onSubmit((v) => onSubmit(v))}>
<TextInput id='username' label='Username' {...form.getInputProps('username')} />
<TextInput id='password' label='Password'type='password' {...form.getInputProps('password')} />
<TextInput id='password' label='Password' type='password' {...form.getInputProps('password')} />
<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'

View file

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

View file

@ -7,6 +7,7 @@ export interface User {
embedColor: string;
embedSiteName: string;
systemTheme: string;
domains: string[];
}
const initialState: User = null;

View file

@ -71,7 +71,12 @@ 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})`);
files.push(`${zconfig.core.secure ? 'https' : 'http'}://${req.headers.host}${zconfig.uploader.route}/${invis ? invis.invis : image.file}`);
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) {

View file

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