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",
|
"name": "zip3",
|
||||||
"version": "3.4.1",
|
"version": "3.4.2",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "node esbuild.config.js && REACT_EDITOR=code-insiders NODE_ENV=development node dist/server",
|
"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[];
|
|
@ -8,16 +8,17 @@ generator client {
|
||||||
}
|
}
|
||||||
|
|
||||||
model User {
|
model User {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
username String
|
username String
|
||||||
password String
|
password String
|
||||||
token String
|
token String
|
||||||
administrator Boolean @default(false)
|
administrator Boolean @default(false)
|
||||||
systemTheme String @default("system")
|
systemTheme String @default("system")
|
||||||
embedTitle String?
|
embedTitle String?
|
||||||
embedColor String @default("#2f3136")
|
embedColor String @default("#2f3136")
|
||||||
embedSiteName String? @default("{image.file} • {user.name}")
|
embedSiteName String? @default("{image.file} • {user.name}")
|
||||||
ratelimited Boolean @default(false)
|
ratelimited Boolean @default(false)
|
||||||
|
domains String[]
|
||||||
images Image[]
|
images Image[]
|
||||||
urls Url[]
|
urls Url[]
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,13 @@
|
||||||
import React from 'react';
|
import React, { useState } from 'react';
|
||||||
|
|
||||||
import useFetch from 'hooks/useFetch';
|
import useFetch from 'hooks/useFetch';
|
||||||
import Link from 'components/Link';
|
import Link from 'components/Link';
|
||||||
import { useStoreDispatch, useStoreSelector } from 'lib/redux/store';
|
import { useStoreDispatch, useStoreSelector } from 'lib/redux/store';
|
||||||
import { updateUser } from 'lib/redux/reducers/user';
|
import { updateUser } from 'lib/redux/reducers/user';
|
||||||
import { useForm } from '@mantine/hooks';
|
import { randomId, useForm } from '@mantine/hooks';
|
||||||
import { Tooltip, TextInput, Button, Text, Title, Group, ColorInput } from '@mantine/core';
|
import { Tooltip, TextInput, Button, Text, Title, Group, ColorInput, MultiSelect, Space } from '@mantine/core';
|
||||||
import { DownloadIcon } from '@modulz/radix-icons';
|
import { DownloadIcon, Cross1Icon } from '@modulz/radix-icons';
|
||||||
|
import { useNotifications } from '@mantine/notifications';
|
||||||
|
|
||||||
function VarsTooltip({ children }) {
|
function VarsTooltip({ children }) {
|
||||||
return (
|
return (
|
||||||
|
@ -27,6 +28,9 @@ function VarsTooltip({ children }) {
|
||||||
export default function Manage() {
|
export default function Manage() {
|
||||||
const user = useStoreSelector(state => state.user);
|
const user = useStoreSelector(state => state.user);
|
||||||
const dispatch = useStoreDispatch();
|
const dispatch = useStoreDispatch();
|
||||||
|
const notif = useNotifications();
|
||||||
|
|
||||||
|
const [domains, setDomains] = useState(user.domains ?? []);
|
||||||
|
|
||||||
const genShareX = (withEmbed: boolean = false, withZws: boolean = false) => {
|
const genShareX = (withEmbed: boolean = false, withZws: boolean = false) => {
|
||||||
const config = {
|
const config = {
|
||||||
|
@ -61,6 +65,7 @@ export default function Manage() {
|
||||||
embedTitle: user.embedTitle ?? '',
|
embedTitle: user.embedTitle ?? '',
|
||||||
embedColor: user.embedColor,
|
embedColor: user.embedColor,
|
||||||
embedSiteName: user.embedSiteName ?? '',
|
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');
|
if (cleanUsername === '') return form.setFieldError('username', 'Username can\'t be nothing');
|
||||||
|
|
||||||
|
const id = notif.showNotification({
|
||||||
|
title: 'Updating user...',
|
||||||
|
message: '',
|
||||||
|
loading: true,
|
||||||
|
});
|
||||||
|
|
||||||
const data = {
|
const data = {
|
||||||
username: cleanUsername,
|
username: cleanUsername,
|
||||||
password: cleanPassword === '' ? null : cleanPassword,
|
password: cleanPassword === '' ? null : cleanPassword,
|
||||||
embedTitle: cleanEmbedTitle === '' ? null : cleanEmbedTitle,
|
embedTitle: cleanEmbedTitle === '' ? null : cleanEmbedTitle,
|
||||||
embedColor: cleanEmbedColor === '' ? null : cleanEmbedColor,
|
embedColor: cleanEmbedColor === '' ? null : cleanEmbedColor,
|
||||||
embedSiteName: cleanEmbedSiteName === '' ? null : cleanEmbedSiteName,
|
embedSiteName: cleanEmbedSiteName === '' ? null : cleanEmbedSiteName,
|
||||||
|
domains,
|
||||||
};
|
};
|
||||||
|
|
||||||
const newUser = await useFetch('/api/user', 'PATCH', data);
|
const newUser = await useFetch('/api/user', 'PATCH', data);
|
||||||
|
|
||||||
if (newUser.error) {
|
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 {
|
} else {
|
||||||
dispatch(updateUser(newUser));
|
dispatch(updateUser(newUser));
|
||||||
|
notif.updateNotification(id, {
|
||||||
|
title: 'Saved User',
|
||||||
|
message: '',
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -97,10 +134,23 @@ export default function Manage() {
|
||||||
</VarsTooltip>
|
</VarsTooltip>
|
||||||
<form onSubmit={form.onSubmit((v) => onSubmit(v))}>
|
<form onSubmit={form.onSubmit((v) => onSubmit(v))}>
|
||||||
<TextInput id='username' label='Username' {...form.getInputProps('username')} />
|
<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')} />
|
<TextInput id='embedTitle' label='Embed Title' {...form.getInputProps('embedTitle')} />
|
||||||
<ColorInput id='embedColor' label='Embed Color' {...form.getInputProps('embedColor')} />
|
<ColorInput id='embedColor' label='Embed Color' {...form.getInputProps('embedColor')} />
|
||||||
<TextInput id='embedSiteName' label='Embed Site Name' {...form.getInputProps('embedSiteName')} />
|
<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 }}>
|
<Group position='right' sx={{ paddingTop: 12 }}>
|
||||||
<Button
|
<Button
|
||||||
type='submit'
|
type='submit'
|
||||||
|
|
|
@ -25,6 +25,7 @@ export type NextApiReq = NextApiRequest & {
|
||||||
administrator: boolean;
|
administrator: boolean;
|
||||||
id: number;
|
id: number;
|
||||||
password: string;
|
password: string;
|
||||||
|
domains: string[];
|
||||||
} | null | void>;
|
} | null | void>;
|
||||||
getCookie: (name: string) => string | null;
|
getCookie: (name: string) => string | null;
|
||||||
cleanCookie: (name: string) => void;
|
cleanCookie: (name: string) => void;
|
||||||
|
@ -33,7 +34,7 @@ export type NextApiReq = NextApiRequest & {
|
||||||
|
|
||||||
export type NextApiRes = NextApiResponse & {
|
export type NextApiRes = NextApiResponse & {
|
||||||
error: (message: string) => void;
|
error: (message: string) => void;
|
||||||
forbid: (message: string) => void;
|
forbid: (message: string, extra?: any) => void;
|
||||||
bad: (message: string) => void;
|
bad: (message: string) => void;
|
||||||
json: (json: any) => void;
|
json: (json: any) => void;
|
||||||
ratelimited: () => 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.setHeader('Content-Type', 'application/json');
|
||||||
res.status(403);
|
res.status(403);
|
||||||
res.json({
|
res.json({
|
||||||
error: '403: ' + message,
|
error: '403: ' + message,
|
||||||
|
...extra,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -93,6 +95,7 @@ export const withZipline = (handler: (req: NextApiRequest, res: NextApiResponse)
|
||||||
maxAge: undefined,
|
maxAge: undefined,
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
req.user = async () => {
|
req.user = async () => {
|
||||||
try {
|
try {
|
||||||
const userId = req.getCookie('user');
|
const userId = req.getCookie('user');
|
||||||
|
@ -111,6 +114,7 @@ export const withZipline = (handler: (req: NextApiRequest, res: NextApiResponse)
|
||||||
systemTheme: true,
|
systemTheme: true,
|
||||||
token: true,
|
token: true,
|
||||||
username: true,
|
username: true,
|
||||||
|
domains: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,7 @@ export interface User {
|
||||||
embedColor: string;
|
embedColor: string;
|
||||||
embedSiteName: string;
|
embedSiteName: string;
|
||||||
systemTheme: string;
|
systemTheme: string;
|
||||||
|
domains: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const initialState: User = null;
|
const initialState: User = null;
|
||||||
|
|
|
@ -71,7 +71,12 @@ async function handler(req: NextApiReq, res: NextApiRes) {
|
||||||
|
|
||||||
await datasource.save(image.file, file.buffer);
|
await datasource.save(image.file, file.buffer);
|
||||||
Logger.get('image').info(`User ${user.username} (${user.id}) uploaded an image ${image.file} (${image.id})`);
|
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) {
|
if (user.administrator && zconfig.ratelimit.admin !== 0) {
|
||||||
|
|
|
@ -2,6 +2,7 @@ import prisma from 'lib/prisma';
|
||||||
import { hashPassword } from 'lib/util';
|
import { hashPassword } from 'lib/util';
|
||||||
import { NextApiReq, NextApiRes, withZipline } from 'middleware/withZipline';
|
import { NextApiReq, NextApiRes, withZipline } from 'middleware/withZipline';
|
||||||
import Logger from 'lib/logger';
|
import Logger from 'lib/logger';
|
||||||
|
import pkg from '../../../../package.json';
|
||||||
|
|
||||||
async function handler(req: NextApiReq, res: NextApiRes) {
|
async function handler(req: NextApiReq, res: NextApiRes) {
|
||||||
const user = await req.user();
|
const user = await req.user();
|
||||||
|
@ -51,6 +52,36 @@ async function handler(req: NextApiReq, res: NextApiRes) {
|
||||||
data: { systemTheme: req.body.systemTheme },
|
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({
|
const newUser = await prisma.user.findFirst({
|
||||||
where: {
|
where: {
|
||||||
id: Number(user.id),
|
id: Number(user.id),
|
||||||
|
@ -66,6 +97,7 @@ async function handler(req: NextApiReq, res: NextApiRes) {
|
||||||
systemTheme: true,
|
systemTheme: true,
|
||||||
token: true,
|
token: true,
|
||||||
username: true,
|
username: true,
|
||||||
|
domains: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue