feat: prettier run (#200)
* feat: prettier run * fix: whatever that was * chore: format more files * chore: make format command better
This commit is contained in:
parent
cb7dacd089
commit
af0cd26ea0
165 changed files with 3635 additions and 10100 deletions
|
@ -1,36 +1,18 @@
|
||||||
{
|
{
|
||||||
"extends": [
|
"extends": ["next", "next/core-web-vitals", "plugin:prettier/recommended"],
|
||||||
"next",
|
|
||||||
"next/core-web-vitals"
|
|
||||||
],
|
|
||||||
"rules": {
|
"rules": {
|
||||||
"indent": [
|
"linebreak-style": ["error", "unix"],
|
||||||
"error",
|
|
||||||
2,
|
|
||||||
{
|
|
||||||
"SwitchCase": 1
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"linebreak-style": [
|
|
||||||
"error",
|
|
||||||
"unix"
|
|
||||||
],
|
|
||||||
"quotes": [
|
"quotes": [
|
||||||
"error",
|
"error",
|
||||||
"single"
|
"single",
|
||||||
],
|
{
|
||||||
"semi": [
|
"avoidEscape": true
|
||||||
"error",
|
}
|
||||||
"always"
|
|
||||||
],
|
|
||||||
"comma-dangle": [
|
|
||||||
"error",
|
|
||||||
"always-multiline"
|
|
||||||
],
|
|
||||||
"jsx-quotes": [
|
|
||||||
"error",
|
|
||||||
"prefer-single"
|
|
||||||
],
|
],
|
||||||
|
"semi": ["error", "always"],
|
||||||
|
"comma-dangle": ["error", "always-multiline"],
|
||||||
|
"jsx-quotes": ["error", "prefer-single"],
|
||||||
|
"indent": "off",
|
||||||
"react/prop-types": "off",
|
"react/prop-types": "off",
|
||||||
"react-hooks/rules-of-hooks": "off",
|
"react-hooks/rules-of-hooks": "off",
|
||||||
"react-hooks/exhaustive-deps": "off",
|
"react-hooks/exhaustive-deps": "off",
|
||||||
|
|
5
.prettierrc.json
Normal file
5
.prettierrc.json
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"singleQuote": true,
|
||||||
|
"jsxSingleQuote": true,
|
||||||
|
"printWidth": 110
|
||||||
|
}
|
|
@ -2,8 +2,8 @@ nodeLinker: node-modules
|
||||||
|
|
||||||
plugins:
|
plugins:
|
||||||
- path: .yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs
|
- path: .yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs
|
||||||
spec: "@yarnpkg/plugin-interactive-tools"
|
spec: '@yarnpkg/plugin-interactive-tools'
|
||||||
|
|
||||||
yarnPath: .yarn/releases/yarn-3.2.1.cjs
|
yarnPath: .yarn/releases/yarn-3.2.1.cjs
|
||||||
|
|
||||||
checksumBehavior: "update"
|
checksumBehavior: 'update'
|
||||||
|
|
|
@ -1,18 +1,23 @@
|
||||||
# Contributing
|
# Contributing
|
||||||
|
|
||||||
## Bug reports
|
## Bug reports
|
||||||
|
|
||||||
Create an issue on GitHub, please include the following (if one of them is not applicable to the issue then it's not needed):
|
Create an issue on GitHub, please include the following (if one of them is not applicable to the issue then it's not needed):
|
||||||
* The steps to reproduce the bug
|
|
||||||
* Logs of Zipline
|
- The steps to reproduce the bug
|
||||||
* The version of Zipline
|
- Logs of Zipline
|
||||||
* Your OS & Browser including server OS
|
- The version of Zipline
|
||||||
* What you were expecting to see
|
- Your OS & Browser including server OS
|
||||||
|
- What you were expecting to see
|
||||||
|
|
||||||
## Feature requests
|
## Feature requests
|
||||||
|
|
||||||
Create an issue on GitHub, please include the following:
|
Create an issue on GitHub, please include the following:
|
||||||
* Breif explanation of the feature in the title (very breif please)
|
|
||||||
* How it would work (detailed, but optional)
|
- Breif explanation of the feature in the title (very breif please)
|
||||||
|
- How it would work (detailed, but optional)
|
||||||
|
|
||||||
## Pull Requests (contributions to the codebase)
|
## Pull Requests (contributions to the codebase)
|
||||||
|
|
||||||
Create a pull request on GitHub. If your PR does not pass the action checks, then please fix the errors. If your PR was submitted before a release, and I have pushed a new release, please make sure to update your PR to reflect any changes, usually this is handled by GitHub.
|
Create a pull request on GitHub. If your PR does not pass the action checks, then please fix the errors. If your PR was submitted before a release, and I have pushed a new release, please make sure to update your PR to reflect any changes, usually this is handled by GitHub.
|
||||||
Please make sure your code also reflects the style of the rest of the code.
|
Please make sure your code also reflects the style of the rest of the code.
|
||||||
|
|
28
README.md
28
README.md
|
@ -15,6 +15,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- Configurable
|
- Configurable
|
||||||
- Fast
|
- Fast
|
||||||
- Built with Next.js & React
|
- Built with Next.js & React
|
||||||
|
@ -35,6 +36,7 @@
|
||||||
# Usage
|
# Usage
|
||||||
|
|
||||||
## Install & run with Docker
|
## Install & run with Docker
|
||||||
|
|
||||||
This section requires [Docker](https://docs.docker.com/get-docker/) and [docker-compose](https://docs.docker.com/compose/install/).
|
This section requires [Docker](https://docs.docker.com/get-docker/) and [docker-compose](https://docs.docker.com/compose/install/).
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
|
@ -45,11 +47,14 @@ docker-compose up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
### After installing
|
### After installing
|
||||||
|
|
||||||
After installing, please edit the `docker-compose.yml` file and find the line that says `SECRET=changethis` and replace `changethis` with a random string.
|
After installing, please edit the `docker-compose.yml` file and find the line that says `SECRET=changethis` and replace `changethis` with a random string.
|
||||||
Ways you could generate the string could be from a password managers generator, or you could just slam your keyboard and hope for the best.
|
Ways you could generate the string could be from a password managers generator, or you could just slam your keyboard and hope for the best.
|
||||||
|
|
||||||
## Building & running from source
|
## Building & running from source
|
||||||
|
|
||||||
This section requires [nodejs](https://nodejs.org), [yarn](https://yarnpkg.com/) or [npm](https://npmjs.com).
|
This section requires [nodejs](https://nodejs.org), [yarn](https://yarnpkg.com/) or [npm](https://npmjs.com).
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
git clone https://github.com/diced/zipline
|
git clone https://github.com/diced/zipline
|
||||||
cd zipline
|
cd zipline
|
||||||
|
@ -63,6 +68,7 @@ yarn start
|
||||||
```
|
```
|
||||||
|
|
||||||
# NGINX Proxy
|
# NGINX Proxy
|
||||||
|
|
||||||
This section requires [NGINX](https://nginx.org/).
|
This section requires [NGINX](https://nginx.org/).
|
||||||
|
|
||||||
```nginx
|
```nginx
|
||||||
|
@ -81,14 +87,17 @@ server {
|
||||||
```
|
```
|
||||||
|
|
||||||
# Website
|
# Website
|
||||||
|
|
||||||
The default port is `3000`, once you have accessed it you can see a login screen. The default credentials are "administrator" and "password". Once you login please immediately change the details to something more secure. You can do this by clicking on the top right corner where it says "administrator" with a gear icon and clicking Manage Account.
|
The default port is `3000`, once you have accessed it you can see a login screen. The default credentials are "administrator" and "password". Once you login please immediately change the details to something more secure. You can do this by clicking on the top right corner where it says "administrator" with a gear icon and clicking Manage Account.
|
||||||
|
|
||||||
# ShareX (Windows)
|
# ShareX (Windows)
|
||||||
|
|
||||||
This section requires [ShareX](https://www.getsharex.com/).
|
This section requires [ShareX](https://www.getsharex.com/).
|
||||||
|
|
||||||
After navigating to Zipline, click on the top right corner where it says your username and click Manage Account. Scroll down to see "ShareX Config", select the one you would prefer using. After this you can import the .sxcu into sharex. [More information here](https://zipl.vercel.app/docs/guides/uploaders/sharex)
|
After navigating to Zipline, click on the top right corner where it says your username and click Manage Account. Scroll down to see "ShareX Config", select the one you would prefer using. After this you can import the .sxcu into sharex. [More information here](https://zipl.vercel.app/docs/guides/uploaders/sharex)
|
||||||
|
|
||||||
# Flameshot (Linux)
|
# Flameshot (Linux)
|
||||||
|
|
||||||
This section requires [Flameshot](https://www.flameshot.org/), [jq](https://stedolan.github.io/jq/), and [xsel](https://github.com/kfish/xsel).
|
This section requires [Flameshot](https://www.flameshot.org/), [jq](https://stedolan.github.io/jq/), and [xsel](https://github.com/kfish/xsel).
|
||||||
|
|
||||||
You can either use the script below, or generate one directly from Zipline (just like how you can generate a ShareX config).
|
You can either use the script below, or generate one directly from Zipline (just like how you can generate a ShareX config).
|
||||||
|
@ -104,17 +113,22 @@ curl -H "Content-Type: multipart/form-data" -H "authorization: $TOKEN" -F file=@
|
||||||
# Contributing
|
# Contributing
|
||||||
|
|
||||||
## Bug reports
|
## Bug reports
|
||||||
|
|
||||||
Create an issue on GitHub, please include the following (if one of them is not applicable to the issue then it's not needed):
|
Create an issue on GitHub, please include the following (if one of them is not applicable to the issue then it's not needed):
|
||||||
* The steps to reproduce the bug
|
|
||||||
* Logs of Zipline
|
- The steps to reproduce the bug
|
||||||
* The version of Zipline
|
- Logs of Zipline
|
||||||
* Your OS & Browser including server OS
|
- The version of Zipline
|
||||||
* What you were expecting to see
|
- Your OS & Browser including server OS
|
||||||
|
- What you were expecting to see
|
||||||
|
|
||||||
## Feature requests
|
## Feature requests
|
||||||
|
|
||||||
Create an issue on GitHub, please include the following:
|
Create an issue on GitHub, please include the following:
|
||||||
* Breif explanation of the feature in the title (very breif please)
|
|
||||||
* How it would work (detailed, but optional)
|
- Breif explanation of the feature in the title (very breif please)
|
||||||
|
- How it would work (detailed, but optional)
|
||||||
|
|
||||||
## Pull Requests (contributions to the codebase)
|
## Pull Requests (contributions to the codebase)
|
||||||
|
|
||||||
Create a pull request on GitHub. If your PR does not pass the action checks, then please fix the errors. If your PR was submitted before a release, and I have pushed a new release, please make sure to update your PR to reflect any changes, usually this is handled by GitHub.
|
Create a pull request on GitHub. If your PR does not pass the action checks, then please fix the errors. If your PR was submitted before a release, and I have pushed a new release, please make sure to update your PR to reflect any changes, usually this is handled by GitHub.
|
|
@ -9,4 +9,5 @@
|
||||||
| < 2 | :x: |
|
| < 2 | :x: |
|
||||||
|
|
||||||
## Reporting a Vulnerability
|
## Reporting a Vulnerability
|
||||||
|
|
||||||
Report a Vulnerability by issuing a bug report, with exact details with how the vulnerability happened, what "exploits" can happen, and possible fixes (optional). Vulnerability reports are treated with high priority and will be resolved most of the time quickly.
|
Report a Vulnerability by issuing a bug report, with exact details with how the vulnerability happened, what "exploits" can happen, and possible fixes (optional). Vulnerability reports are treated with high priority and will be resolved most of the time quickly.
|
||||||
|
|
9678
mimes.json
9678
mimes.json
File diff suppressed because it is too large
Load diff
|
@ -8,6 +8,7 @@
|
||||||
"build-ci": "cross-env ZIPLINE_DOCKER_BUILD=1 npm-run-all build:schema build:next",
|
"build-ci": "cross-env ZIPLINE_DOCKER_BUILD=1 npm-run-all build:schema build:next",
|
||||||
"build:next": "next build",
|
"build:next": "next build",
|
||||||
"build:schema": "prisma generate --schema=prisma/schema.prisma",
|
"build:schema": "prisma generate --schema=prisma/schema.prisma",
|
||||||
|
"format": "prettier --write ./src/**/*.{ts,tsx} ./*.{md,js,json,yml}",
|
||||||
"migrate:dev": "prisma migrate dev --create-only",
|
"migrate:dev": "prisma migrate dev --create-only",
|
||||||
"start": "tsx src/server",
|
"start": "tsx src/server",
|
||||||
"lint": "next lint",
|
"lint": "next lint",
|
||||||
|
@ -69,8 +70,10 @@
|
||||||
"esbuild": "^0.14.44",
|
"esbuild": "^0.14.44",
|
||||||
"eslint": "^7.32.0",
|
"eslint": "^7.32.0",
|
||||||
"eslint-config-next": "12.1.6",
|
"eslint-config-next": "12.1.6",
|
||||||
|
"eslint-config-prettier": "^8.5.0",
|
||||||
"eslint-plugin-prettier": "^4.2.1",
|
"eslint-plugin-prettier": "^4.2.1",
|
||||||
"npm-run-all": "^4.1.5",
|
"npm-run-all": "^4.1.5",
|
||||||
|
"prettier": "^2.7.1",
|
||||||
"ts-node": "^10.8.1",
|
"ts-node": "^10.8.1",
|
||||||
"tsx": "^3.8.0",
|
"tsx": "^3.8.0",
|
||||||
"typescript": "^4.7.3"
|
"typescript": "^4.7.3"
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import { Card as MCard, Title } from '@mantine/core';
|
import { Card as MCard, Title } from '@mantine/core';
|
||||||
|
|
||||||
|
|
||||||
export default function Card({ name, children, ...other }) {
|
export default function Card({ name, children, ...other }) {
|
||||||
return (
|
return (
|
||||||
<MCard p='md' shadow='sm' {...other}>
|
<MCard p='md' shadow='sm' {...other}>
|
||||||
|
|
|
@ -11,11 +11,5 @@ const useStyles = createStyles((theme, { size }: { size: MantineSize }) => ({
|
||||||
export default function CodeInput({ ...props }) {
|
export default function CodeInput({ ...props }) {
|
||||||
const { classes } = useStyles({ size: 'md' }, { name: 'CodeInput' });
|
const { classes } = useStyles({ size: 'md' }, { name: 'CodeInput' });
|
||||||
|
|
||||||
return (
|
return <Textarea classNames={{ input: classes.input }} autoComplete='nope' {...props} />;
|
||||||
<Textarea
|
|
||||||
classNames={{ input: classes.input }}
|
|
||||||
autoComplete='nope'
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,19 @@ import { showNotification } from '@mantine/notifications';
|
||||||
import { relativeTime } from 'lib/utils/client';
|
import { relativeTime } from 'lib/utils/client';
|
||||||
import { useFileDelete, useFileFavorite } from 'lib/queries/files';
|
import { useFileDelete, useFileFavorite } from 'lib/queries/files';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { CalendarIcon, ClockIcon, CopyIcon, CrossIcon, DeleteIcon, ExternalLinkIcon, FileIcon, HashIcon, ImageIcon, StarIcon, EyeIcon } from './icons';
|
import {
|
||||||
|
CalendarIcon,
|
||||||
|
ClockIcon,
|
||||||
|
CopyIcon,
|
||||||
|
CrossIcon,
|
||||||
|
DeleteIcon,
|
||||||
|
ExternalLinkIcon,
|
||||||
|
FileIcon,
|
||||||
|
HashIcon,
|
||||||
|
ImageIcon,
|
||||||
|
StarIcon,
|
||||||
|
EyeIcon,
|
||||||
|
} from './icons';
|
||||||
import MutedText from './MutedText';
|
import MutedText from './MutedText';
|
||||||
import Type from './Type';
|
import Type from './Type';
|
||||||
import Link from './Link';
|
import Link from './Link';
|
||||||
|
@ -76,7 +88,9 @@ export default function File({ image, updateImages, disableMediaPreview }) {
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleFavorite = async () => {
|
const handleFavorite = async () => {
|
||||||
favoriteFile.mutate({ id: image.id, favorite: !image.favorite }, {
|
favoriteFile.mutate(
|
||||||
|
{ id: image.id, favorite: !image.favorite },
|
||||||
|
{
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
showNotification({
|
showNotification({
|
||||||
title: 'Image is now ' + (!image.favorite ? 'favorited' : 'unfavorited'),
|
title: 'Image is now ' + (!image.favorite ? 'favorited' : 'unfavorited'),
|
||||||
|
@ -93,18 +107,14 @@ export default function File({ image, updateImages, disableMediaPreview }) {
|
||||||
icon: <CrossIcon />,
|
icon: <CrossIcon />,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
}
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log(image);
|
console.log(image);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Modal
|
<Modal opened={open} onClose={() => setOpen(false)} title={<Title>{image.file}</Title>} size='xl'>
|
||||||
opened={open}
|
|
||||||
onClose={() => setOpen(false)}
|
|
||||||
title={<Title>{image.file}</Title>}
|
|
||||||
size='xl'
|
|
||||||
>
|
|
||||||
<LoadingOverlay visible={loading} />
|
<LoadingOverlay visible={loading} />
|
||||||
<Stack>
|
<Stack>
|
||||||
<Type
|
<Type
|
||||||
|
@ -120,13 +130,19 @@ export default function File({ image, updateImages, disableMediaPreview }) {
|
||||||
<FileMeta Icon={FileIcon} title='Name' subtitle={image.file} />
|
<FileMeta Icon={FileIcon} title='Name' subtitle={image.file} />
|
||||||
<FileMeta Icon={ImageIcon} title='Type' subtitle={image.mimetype} />
|
<FileMeta Icon={ImageIcon} title='Type' subtitle={image.mimetype} />
|
||||||
<FileMeta Icon={EyeIcon} title='Views' subtitle={image.views} />
|
<FileMeta Icon={EyeIcon} title='Views' subtitle={image.views} />
|
||||||
<FileMeta Icon={CalendarIcon} title='Uploaded at' subtitle={new Date(image.created_at).toLocaleString()} />
|
<FileMeta
|
||||||
{image.expires_at && <FileMeta
|
Icon={CalendarIcon}
|
||||||
|
title='Uploaded at'
|
||||||
|
subtitle={new Date(image.created_at).toLocaleString()}
|
||||||
|
/>
|
||||||
|
{image.expires_at && (
|
||||||
|
<FileMeta
|
||||||
Icon={ClockIcon}
|
Icon={ClockIcon}
|
||||||
title='Expires'
|
title='Expires'
|
||||||
subtitle={relativeTime(new Date(image.expires_at))}
|
subtitle={relativeTime(new Date(image.expires_at))}
|
||||||
tooltip={new Date(image.expires_at).toLocaleString()}
|
tooltip={new Date(image.expires_at).toLocaleString()}
|
||||||
/>}
|
/>
|
||||||
|
)}
|
||||||
<FileMeta Icon={HashIcon} title='ID' subtitle={image.id} />
|
<FileMeta Icon={HashIcon} title='ID' subtitle={image.id} />
|
||||||
</Stack>
|
</Stack>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
@ -145,8 +161,20 @@ export default function File({ image, updateImages, disableMediaPreview }) {
|
||||||
<LoadingOverlay visible={loading} />
|
<LoadingOverlay visible={loading} />
|
||||||
<Type
|
<Type
|
||||||
file={image}
|
file={image}
|
||||||
sx={{ minHeight: 200, maxHeight: 320, fontSize: 70, width: '100%', cursor: 'pointer' }}
|
sx={{
|
||||||
style={{ minHeight: 200, maxHeight: 320, fontSize: 70, width: '100%', cursor: 'pointer' }}
|
minHeight: 200,
|
||||||
|
maxHeight: 320,
|
||||||
|
fontSize: 70,
|
||||||
|
width: '100%',
|
||||||
|
cursor: 'pointer',
|
||||||
|
}}
|
||||||
|
style={{
|
||||||
|
minHeight: 200,
|
||||||
|
maxHeight: 320,
|
||||||
|
fontSize: 70,
|
||||||
|
width: '100%',
|
||||||
|
cursor: 'pointer',
|
||||||
|
}}
|
||||||
src={image.url}
|
src={image.url}
|
||||||
alt={image.file}
|
alt={image.file}
|
||||||
onClick={() => setOpen(true)}
|
onClick={() => setOpen(true)}
|
||||||
|
|
|
@ -1,4 +1,27 @@
|
||||||
import { AppShell, Box, Burger, Button, Divider, Header, MediaQuery, Navbar, NavLink, Paper, Popover, ScrollArea, Select, Stack, Text, Title, UnstyledButton, useMantineTheme, Group, Image, Tooltip, Badge } from '@mantine/core';
|
import {
|
||||||
|
AppShell,
|
||||||
|
Box,
|
||||||
|
Burger,
|
||||||
|
Button,
|
||||||
|
Divider,
|
||||||
|
Header,
|
||||||
|
MediaQuery,
|
||||||
|
Navbar,
|
||||||
|
NavLink,
|
||||||
|
Paper,
|
||||||
|
Popover,
|
||||||
|
ScrollArea,
|
||||||
|
Select,
|
||||||
|
Stack,
|
||||||
|
Text,
|
||||||
|
Title,
|
||||||
|
UnstyledButton,
|
||||||
|
useMantineTheme,
|
||||||
|
Group,
|
||||||
|
Image,
|
||||||
|
Tooltip,
|
||||||
|
Badge,
|
||||||
|
} from '@mantine/core';
|
||||||
import { useClipboard } from '@mantine/hooks';
|
import { useClipboard } from '@mantine/hooks';
|
||||||
import { useModals } from '@mantine/modals';
|
import { useModals } from '@mantine/modals';
|
||||||
import { showNotification } from '@mantine/notifications';
|
import { showNotification } from '@mantine/notifications';
|
||||||
|
@ -9,7 +32,24 @@ import { useRecoilState } from 'recoil';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { ExternalLinkIcon, ActivityIcon, CheckIcon, CopyIcon, CrossIcon, DeleteIcon, FileIcon, HomeIcon, LinkIcon, LogoutIcon, PencilIcon, SettingsIcon, TagIcon, TypeIcon, UploadIcon, UserIcon } from './icons';
|
import {
|
||||||
|
ExternalLinkIcon,
|
||||||
|
ActivityIcon,
|
||||||
|
CheckIcon,
|
||||||
|
CopyIcon,
|
||||||
|
CrossIcon,
|
||||||
|
DeleteIcon,
|
||||||
|
FileIcon,
|
||||||
|
HomeIcon,
|
||||||
|
LinkIcon,
|
||||||
|
LogoutIcon,
|
||||||
|
PencilIcon,
|
||||||
|
SettingsIcon,
|
||||||
|
TagIcon,
|
||||||
|
TypeIcon,
|
||||||
|
UploadIcon,
|
||||||
|
UserIcon,
|
||||||
|
} from './icons';
|
||||||
import { friendlyThemeName, themes } from './Theming';
|
import { friendlyThemeName, themes } from './Theming';
|
||||||
|
|
||||||
function MenuItemLink(props) {
|
function MenuItemLink(props) {
|
||||||
|
@ -23,7 +63,7 @@ function MenuItemLink(props) {
|
||||||
function MenuItem(props) {
|
function MenuItem(props) {
|
||||||
return (
|
return (
|
||||||
<UnstyledButton
|
<UnstyledButton
|
||||||
sx={theme => ({
|
sx={(theme) => ({
|
||||||
display: 'block',
|
display: 'block',
|
||||||
width: '100%',
|
width: '100%',
|
||||||
padding: 5,
|
padding: 5,
|
||||||
|
@ -47,14 +87,16 @@ function MenuItem(props) {
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<Group noWrap>
|
<Group noWrap>
|
||||||
<Box sx={theme => ({
|
<Box
|
||||||
|
sx={(theme) => ({
|
||||||
marginRight: theme.spacing.xs / 4,
|
marginRight: theme.spacing.xs / 4,
|
||||||
paddingLeft: theme.spacing.xs / 2,
|
paddingLeft: theme.spacing.xs / 2,
|
||||||
|
|
||||||
'& *': {
|
'& *': {
|
||||||
display: 'block',
|
display: 'block',
|
||||||
},
|
},
|
||||||
})}>
|
})}
|
||||||
|
>
|
||||||
{props.icon}
|
{props.icon}
|
||||||
</Box>
|
</Box>
|
||||||
<Text size='sm'>{props.children}</Text>
|
<Text size='sm'>{props.children}</Text>
|
||||||
|
@ -101,13 +143,13 @@ const admin_items = [
|
||||||
icon: <UserIcon size={18} />,
|
icon: <UserIcon size={18} />,
|
||||||
text: 'Users',
|
text: 'Users',
|
||||||
link: '/dashboard/users',
|
link: '/dashboard/users',
|
||||||
if: props => true,
|
if: (props) => true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: <TagIcon size={18} />,
|
icon: <TagIcon size={18} />,
|
||||||
text: 'Invites',
|
text: 'Invites',
|
||||||
link: '/dashboard/invites',
|
link: '/dashboard/invites',
|
||||||
if: props => props.invites,
|
if: (props) => props.invites,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -129,7 +171,7 @@ export default function Layout({ children, props }) {
|
||||||
const modals = useModals();
|
const modals = useModals();
|
||||||
const clipboard = useClipboard();
|
const clipboard = useClipboard();
|
||||||
|
|
||||||
const handleUpdateTheme = async value => {
|
const handleUpdateTheme = async (value) => {
|
||||||
const newUser = await useFetch('/api/user', 'PATCH', {
|
const newUser = await useFetch('/api/user', 'PATCH', {
|
||||||
systemTheme: value || 'dark_blue',
|
systemTheme: value || 'dark_blue',
|
||||||
});
|
});
|
||||||
|
@ -146,7 +188,8 @@ export default function Layout({ children, props }) {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const openResetToken = () => modals.openConfirmModal({
|
const openResetToken = () =>
|
||||||
|
modals.openConfirmModal({
|
||||||
title: 'Reset Token',
|
title: 'Reset Token',
|
||||||
children: (
|
children: (
|
||||||
<Text size='sm'>
|
<Text size='sm'>
|
||||||
|
@ -167,7 +210,8 @@ export default function Layout({ children, props }) {
|
||||||
} else {
|
} else {
|
||||||
showNotification({
|
showNotification({
|
||||||
title: 'Token Reset',
|
title: 'Token Reset',
|
||||||
message: 'Your token has been reset. You will need to update any uploaders to use this new token.',
|
message:
|
||||||
|
'Your token has been reset. You will need to update any uploaders to use this new token.',
|
||||||
color: 'green',
|
color: 'green',
|
||||||
icon: <CheckIcon />,
|
icon: <CheckIcon />,
|
||||||
});
|
});
|
||||||
|
@ -177,11 +221,13 @@ export default function Layout({ children, props }) {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const openCopyToken = () => modals.openConfirmModal({
|
const openCopyToken = () =>
|
||||||
|
modals.openConfirmModal({
|
||||||
title: 'Copy Token',
|
title: 'Copy Token',
|
||||||
children: (
|
children: (
|
||||||
<Text size='sm'>
|
<Text size='sm'>
|
||||||
Make sure you don't share this token with anyone as they will be able to upload files on your behalf.
|
Make sure you don't share this token with anyone as they will be able to upload files on your
|
||||||
|
behalf.
|
||||||
</Text>
|
</Text>
|
||||||
),
|
),
|
||||||
labels: { confirm: 'Copy', cancel: 'Cancel' },
|
labels: { confirm: 'Copy', cancel: 'Cancel' },
|
||||||
|
@ -204,16 +250,8 @@ export default function Layout({ children, props }) {
|
||||||
navbarOffsetBreakpoint='sm'
|
navbarOffsetBreakpoint='sm'
|
||||||
fixed
|
fixed
|
||||||
navbar={
|
navbar={
|
||||||
<Navbar
|
<Navbar pt='sm' hiddenBreakpoint='sm' hidden={!opened} width={{ sm: 200, lg: 230 }}>
|
||||||
pt='sm'
|
<Navbar.Section grow component={ScrollArea}>
|
||||||
hiddenBreakpoint='sm'
|
|
||||||
hidden={!opened}
|
|
||||||
width={{ sm: 200, lg: 230 }}
|
|
||||||
>
|
|
||||||
<Navbar.Section
|
|
||||||
grow
|
|
||||||
component={ScrollArea}
|
|
||||||
>
|
|
||||||
{items.map(({ icon, text, link }) => (
|
{items.map(({ icon, text, link }) => (
|
||||||
<Link href={link} key={text} passHref>
|
<Link href={link} key={text} passHref>
|
||||||
<NavLink
|
<NavLink
|
||||||
|
@ -230,9 +268,11 @@ export default function Layout({ children, props }) {
|
||||||
label='Administration'
|
label='Administration'
|
||||||
icon={<SettingsIcon />}
|
icon={<SettingsIcon />}
|
||||||
childrenOffset={28}
|
childrenOffset={28}
|
||||||
defaultOpened={admin_items.map(x => x.link).includes(router.pathname)}
|
defaultOpened={admin_items.map((x) => x.link).includes(router.pathname)}
|
||||||
>
|
>
|
||||||
{admin_items.filter(x => x.if(props)).map(({ icon, text, link }) => (
|
{admin_items
|
||||||
|
.filter((x) => x.if(props))
|
||||||
|
.map(({ icon, text, link }) => (
|
||||||
<Link href={link} key={text} passHref>
|
<Link href={link} key={text} passHref>
|
||||||
<NavLink
|
<NavLink
|
||||||
component='a'
|
component='a'
|
||||||
|
@ -247,7 +287,8 @@ export default function Layout({ children, props }) {
|
||||||
)}
|
)}
|
||||||
</Navbar.Section>
|
</Navbar.Section>
|
||||||
<Navbar.Section>
|
<Navbar.Section>
|
||||||
{external_links.length ? external_links.map(({ label, link }, i) => (
|
{external_links.length
|
||||||
|
? external_links.map(({ label, link }, i) => (
|
||||||
<Link href={link} passHref key={i}>
|
<Link href={link} passHref key={i}>
|
||||||
<NavLink
|
<NavLink
|
||||||
label={label}
|
label={label}
|
||||||
|
@ -257,7 +298,8 @@ export default function Layout({ children, props }) {
|
||||||
icon={<ExternalLinkIcon />}
|
icon={<ExternalLinkIcon />}
|
||||||
/>
|
/>
|
||||||
</Link>
|
</Link>
|
||||||
)) : null}
|
))
|
||||||
|
: null}
|
||||||
</Navbar.Section>
|
</Navbar.Section>
|
||||||
{version.isSuccess ? (
|
{version.isSuccess ? (
|
||||||
<Navbar.Section>
|
<Navbar.Section>
|
||||||
|
@ -295,16 +337,12 @@ export default function Layout({ children, props }) {
|
||||||
</MediaQuery>
|
</MediaQuery>
|
||||||
<Title ml='sm'>{title}</Title>
|
<Title ml='sm'>{title}</Title>
|
||||||
<Box sx={{ marginLeft: 'auto', marginRight: 0 }}>
|
<Box sx={{ marginLeft: 'auto', marginRight: 0 }}>
|
||||||
<Popover
|
<Popover position='bottom-end' opened={open} onClose={() => setOpen(false)}>
|
||||||
position='bottom-end'
|
|
||||||
opened={open}
|
|
||||||
onClose={() => setOpen(false)}
|
|
||||||
>
|
|
||||||
<Popover.Target>
|
<Popover.Target>
|
||||||
<Button
|
<Button
|
||||||
leftIcon={avatar ? <Image src={avatar} height={32} radius='md' /> : <SettingsIcon />}
|
leftIcon={avatar ? <Image src={avatar} height={32} radius='md' /> : <SettingsIcon />}
|
||||||
onClick={() => setOpen((o) => !o)}
|
onClick={() => setOpen((o) => !o)}
|
||||||
sx={t => ({
|
sx={(t) => ({
|
||||||
backgroundColor: 'inherit',
|
backgroundColor: 'inherit',
|
||||||
'&:hover': {
|
'&:hover': {
|
||||||
backgroundColor: t.other.hover,
|
backgroundColor: t.other.hover,
|
||||||
|
@ -320,7 +358,8 @@ export default function Layout({ children, props }) {
|
||||||
|
|
||||||
<Popover.Dropdown p={4} mr='md' sx={{ minWidth: '200px' }}>
|
<Popover.Dropdown p={4} mr='md' sx={{ minWidth: '200px' }}>
|
||||||
<Stack spacing={2}>
|
<Stack spacing={2}>
|
||||||
<Text sx={{
|
<Text
|
||||||
|
sx={{
|
||||||
color: theme.colorScheme === 'dark' ? theme.colors.dark[2] : theme.colors.gray[6],
|
color: theme.colorScheme === 'dark' ? theme.colors.dark[2] : theme.colors.gray[6],
|
||||||
fontWeight: 500,
|
fontWeight: 500,
|
||||||
fontSize: theme.fontSizes.sm,
|
fontSize: theme.fontSizes.sm,
|
||||||
|
@ -330,23 +369,48 @@ export default function Layout({ children, props }) {
|
||||||
>
|
>
|
||||||
{user.username}
|
{user.username}
|
||||||
</Text>
|
</Text>
|
||||||
<MenuItemLink icon={<SettingsIcon />} href='/dashboard/manage'>Manage Account</MenuItemLink>
|
<MenuItemLink icon={<SettingsIcon />} href='/dashboard/manage'>
|
||||||
<MenuItem icon={<CopyIcon />} onClick={() => { setOpen(false); openCopyToken(); }}>Copy Token</MenuItem>
|
Manage Account
|
||||||
<MenuItem icon={<DeleteIcon />} onClick={() => { setOpen(false); openResetToken(); }} color='red'>Reset Token</MenuItem>
|
</MenuItemLink>
|
||||||
<MenuItemLink icon={<LogoutIcon />} href='/auth/logout' color='red'>Logout</MenuItemLink>
|
<MenuItem
|
||||||
|
icon={<CopyIcon />}
|
||||||
|
onClick={() => {
|
||||||
|
setOpen(false);
|
||||||
|
openCopyToken();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Copy Token
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem
|
||||||
|
icon={<DeleteIcon />}
|
||||||
|
onClick={() => {
|
||||||
|
setOpen(false);
|
||||||
|
openResetToken();
|
||||||
|
}}
|
||||||
|
color='red'
|
||||||
|
>
|
||||||
|
Reset Token
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItemLink icon={<LogoutIcon />} href='/auth/logout' color='red'>
|
||||||
|
Logout
|
||||||
|
</MenuItemLink>
|
||||||
<Divider
|
<Divider
|
||||||
variant='solid'
|
variant='solid'
|
||||||
my={theme.spacing.xs / 2}
|
my={theme.spacing.xs / 2}
|
||||||
sx={theme => ({
|
sx={(theme) => ({
|
||||||
width: '110%',
|
width: '110%',
|
||||||
borderTopColor: theme.colorScheme === 'dark' ? theme.colors.dark[7] : theme.colors.gray[2],
|
borderTopColor:
|
||||||
|
theme.colorScheme === 'dark' ? theme.colors.dark[7] : theme.colors.gray[2],
|
||||||
margin: `${theme.spacing.xs / 2}px -4px`,
|
margin: `${theme.spacing.xs / 2}px -4px`,
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
<MenuItem icon={<PencilIcon />}>
|
<MenuItem icon={<PencilIcon />}>
|
||||||
<Select
|
<Select
|
||||||
size='xs'
|
size='xs'
|
||||||
data={Object.keys(themes).map(t => ({ value: t, label: friendlyThemeName[t] }))}
|
data={Object.keys(themes).map((t) => ({
|
||||||
|
value: t,
|
||||||
|
label: friendlyThemeName[t],
|
||||||
|
}))}
|
||||||
value={systemTheme}
|
value={systemTheme}
|
||||||
onChange={handleUpdateTheme}
|
onChange={handleUpdateTheme}
|
||||||
/>
|
/>
|
||||||
|
@ -363,7 +427,7 @@ export default function Layout({ children, props }) {
|
||||||
withBorder
|
withBorder
|
||||||
p='md'
|
p='md'
|
||||||
shadow='xs'
|
shadow='xs'
|
||||||
sx={t => ({
|
sx={(t) => ({
|
||||||
borderColor: t.colorScheme === 'dark' ? t.colors.dark[5] : t.colors.dark[0],
|
borderColor: t.colorScheme === 'dark' ? t.colors.dark[5] : t.colors.dark[0],
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
|
|
|
@ -1,5 +1,9 @@
|
||||||
import { Text } from '@mantine/core';
|
import { Text } from '@mantine/core';
|
||||||
|
|
||||||
export default function MutedText({ children, ...props }) {
|
export default function MutedText({ children, ...props }) {
|
||||||
return <Text color='dimmed' size='xl' {...props}>{children}</Text>;
|
return (
|
||||||
|
<Text color='dimmed' size='xl' {...props}>
|
||||||
|
{children}
|
||||||
|
</Text>
|
||||||
|
);
|
||||||
}
|
}
|
|
@ -6,12 +6,7 @@ import { CheckIcon, CrossIcon } from './icons';
|
||||||
|
|
||||||
function PasswordRequirement({ meets, label }: { meets: boolean; label: string }) {
|
function PasswordRequirement({ meets, label }: { meets: boolean; label: string }) {
|
||||||
return (
|
return (
|
||||||
<Text
|
<Text color={meets ? 'teal' : 'red'} sx={{ display: 'flex', alignItems: 'center' }} mt='sm' size='sm'>
|
||||||
color={meets ? 'teal' : 'red'}
|
|
||||||
sx={{ display: 'flex', alignItems: 'center' }}
|
|
||||||
mt='sm'
|
|
||||||
size='sm'
|
|
||||||
>
|
|
||||||
{meets ? <CheckIcon /> : <CrossIcon />} <Box ml='md'>{label}</Box>
|
{meets ? <CheckIcon /> : <CrossIcon />} <Box ml='md'>{label}</Box>
|
||||||
</Text>
|
</Text>
|
||||||
);
|
);
|
||||||
|
@ -60,10 +55,7 @@ export default function PasswordStrength({ value, setValue, setStrength, ...prop
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Popover.Target>
|
<Popover.Target>
|
||||||
<div
|
<div onFocusCapture={() => setPopoverOpened(true)} onBlurCapture={() => setPopoverOpened(false)}>
|
||||||
onFocusCapture={() => setPopoverOpened(true)}
|
|
||||||
onBlurCapture={() => setPopoverOpened(false)}
|
|
||||||
>
|
|
||||||
<PasswordInput
|
<PasswordInput
|
||||||
label='Password'
|
label='Password'
|
||||||
description='A strong password should include letters in lower and uppercase, at least 1 number, at least 1 special symbol'
|
description='A strong password should include letters in lower and uppercase, at least 1 number, at least 1 special symbol'
|
||||||
|
|
|
@ -7,18 +7,16 @@ export function SmallTable({ rows, columns }) {
|
||||||
<Table highlightOnHover>
|
<Table highlightOnHover>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
{columns.map(col => (
|
{columns.map((col) => (
|
||||||
<th key={randomId()}>{col.name}</th>
|
<th key={randomId()}>{col.name}</th>
|
||||||
))}
|
))}
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{rows.map(row => (
|
{rows.map((row) => (
|
||||||
<tr key={randomId()}>
|
<tr key={randomId()}>
|
||||||
{columns.map(col => (
|
{columns.map((col) => (
|
||||||
<td key={randomId()}>
|
<td key={randomId()}>{col.format ? col.format(row[col.id]) : row[col.id]}</td>
|
||||||
{col.format ? col.format(row[col.id]) : row[col.id]}
|
|
||||||
</td>
|
|
||||||
))}
|
))}
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
|
|
|
@ -31,7 +31,7 @@ const useStyles = createStyles((theme) => ({
|
||||||
interface StatsGridProps {
|
interface StatsGridProps {
|
||||||
stat: {
|
stat: {
|
||||||
title: string;
|
title: string;
|
||||||
icon: React.ReactNode,
|
icon: React.ReactNode;
|
||||||
value: string;
|
value: string;
|
||||||
desc: string;
|
desc: string;
|
||||||
diff?: number;
|
diff?: number;
|
||||||
|
@ -53,27 +53,14 @@ export default function StatCard({ stat }: StatsGridProps) {
|
||||||
|
|
||||||
<Group align='flex-end' spacing='xs' mt={25}>
|
<Group align='flex-end' spacing='xs' mt={25}>
|
||||||
<Text className={classes.value}>{stat.value}</Text>
|
<Text className={classes.value}>{stat.value}</Text>
|
||||||
{
|
{typeof stat.diff == 'number' && (
|
||||||
typeof stat.diff == 'number' && (
|
|
||||||
<>
|
<>
|
||||||
<Text
|
<Text color={stat.diff >= 0 ? 'teal' : 'red'} size='sm' weight={500} className={classes.diff}>
|
||||||
color={stat.diff >= 0 ? 'teal' : 'red'}
|
|
||||||
size='sm'
|
|
||||||
weight={500}
|
|
||||||
className={classes.diff}
|
|
||||||
>
|
|
||||||
<span>{stat.diff === Infinity ? '∞' : stat.diff}%</span>
|
<span>{stat.diff === Infinity ? '∞' : stat.diff}%</span>
|
||||||
{
|
{stat.diff >= 0 ? <ArrowUpRight size={16} /> : <ArrowDownRight size={16} />}
|
||||||
stat.diff >= 0 ? (
|
|
||||||
<ArrowUpRight size={16} />
|
|
||||||
) : (
|
|
||||||
<ArrowDownRight size={16} />
|
|
||||||
)
|
|
||||||
}
|
|
||||||
</Text>
|
</Text>
|
||||||
</>
|
</>
|
||||||
)
|
)}
|
||||||
}
|
|
||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
<Text size='xs' color='dimmed' mt={7}>
|
<Text size='xs' color='dimmed' mt={7}>
|
||||||
|
|
|
@ -20,7 +20,7 @@ import { useRecoilValue } from 'recoil';
|
||||||
import { userSelector } from 'lib/recoil/user';
|
import { userSelector } from 'lib/recoil/user';
|
||||||
|
|
||||||
export const themes = {
|
export const themes = {
|
||||||
system: (colorScheme: 'dark' | 'light') => colorScheme === 'dark' ? dark_blue : light_blue,
|
system: (colorScheme: 'dark' | 'light') => (colorScheme === 'dark' ? dark_blue : light_blue),
|
||||||
dark_blue,
|
dark_blue,
|
||||||
light_blue,
|
light_blue,
|
||||||
dark,
|
dark,
|
||||||
|
@ -34,17 +34,17 @@ export const themes = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const friendlyThemeName = {
|
export const friendlyThemeName = {
|
||||||
'system': 'System Theme',
|
system: 'System Theme',
|
||||||
'dark_blue': 'Dark Blue',
|
dark_blue: 'Dark Blue',
|
||||||
'light_blue': 'Light Blue',
|
light_blue: 'Light Blue',
|
||||||
'dark': 'Very Dark',
|
dark: 'Very Dark',
|
||||||
'ayu_dark': 'Ayu Dark',
|
ayu_dark: 'Ayu Dark',
|
||||||
'ayu_mirage': 'Ayu Mirage',
|
ayu_mirage: 'Ayu Mirage',
|
||||||
'ayu_light': 'Ayu Light',
|
ayu_light: 'Ayu Light',
|
||||||
'nord': 'Nord',
|
nord: 'Nord',
|
||||||
'dracula': 'Dracula',
|
dracula: 'Dracula',
|
||||||
'matcha_dark_azul': 'Matcha Dark Azul',
|
matcha_dark_azul: 'Matcha Dark Azul',
|
||||||
'qogir_dark': 'Qogir Dark',
|
qogir_dark: 'Qogir Dark',
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function ZiplineTheming({ Component, pageProps, ...props }) {
|
export default function ZiplineTheming({ Component, pageProps, ...props }) {
|
||||||
|
@ -69,14 +69,14 @@ export default function ZiplineTheming({ Component, pageProps, ...props }) {
|
||||||
...theme,
|
...theme,
|
||||||
components: {
|
components: {
|
||||||
AppShell: {
|
AppShell: {
|
||||||
styles: t => ({
|
styles: (t) => ({
|
||||||
root: {
|
root: {
|
||||||
backgroundColor: t.other.AppShell_backgroundColor,
|
backgroundColor: t.other.AppShell_backgroundColor,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
NavLink: {
|
NavLink: {
|
||||||
styles: t => ({
|
styles: (t) => ({
|
||||||
icon: {
|
icon: {
|
||||||
paddingLeft: t.spacing.sm,
|
paddingLeft: t.spacing.sm,
|
||||||
},
|
},
|
||||||
|
@ -101,14 +101,14 @@ export default function ZiplineTheming({ Component, pageProps, ...props }) {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Card: {
|
Card: {
|
||||||
styles: t => ({
|
styles: (t) => ({
|
||||||
root: {
|
root: {
|
||||||
backgroundColor: t.colorScheme === 'dark' ? t.colors.dark[6] : t.colors.gray[0],
|
backgroundColor: t.colorScheme === 'dark' ? t.colors.dark[6] : t.colors.gray[0],
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
Image: {
|
Image: {
|
||||||
styles: t => ({
|
styles: (t) => ({
|
||||||
placeholder: {
|
placeholder: {
|
||||||
backgroundColor: t.colorScheme === 'dark' ? t.colors.dark[6] : t.colors.gray[0],
|
backgroundColor: t.colorScheme === 'dark' ? t.colors.dark[6] : t.colors.gray[0],
|
||||||
},
|
},
|
||||||
|
|
|
@ -7,18 +7,23 @@ function Placeholder({ text, Icon, ...props }) {
|
||||||
if (props.disableResolve) props.src = null;
|
if (props.disableResolve) props.src = null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Image height={200} withPlaceholder placeholder={
|
<Image
|
||||||
|
height={200}
|
||||||
|
withPlaceholder
|
||||||
|
placeholder={
|
||||||
<Group>
|
<Group>
|
||||||
<Icon size={48} />
|
<Icon size={48} />
|
||||||
<Text size='md'>{text}</Text>
|
<Text size='md'>{text}</Text>
|
||||||
</Group>
|
</Group>
|
||||||
} {...props} />
|
}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Type({ file, popup = false, disableMediaPreview, ...props }) {
|
export default function Type({ file, popup = false, disableMediaPreview, ...props }) {
|
||||||
const type = (file.type || file.mimetype).split('/')[0];
|
const type = (file.type || file.mimetype).split('/')[0];
|
||||||
const name = (file.name || file.file);
|
const name = file.name || file.file;
|
||||||
|
|
||||||
const media = /^(video|audio|image|text)/.test(type);
|
const media = /^(video|audio|image|text)/.test(type);
|
||||||
|
|
||||||
|
@ -36,18 +41,34 @@ export default function Type({ file, popup = false, disableMediaPreview, ...prop
|
||||||
}
|
}
|
||||||
|
|
||||||
if (media && disableMediaPreview) {
|
if (media && disableMediaPreview) {
|
||||||
return <Placeholder Icon={FileIcon} text={`Click to view file (${name})`} disableResolve={true} {...props} />;
|
return (
|
||||||
};
|
<Placeholder Icon={FileIcon} text={`Click to view file (${name})`} disableResolve={true} {...props} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return popup ? (media ? {
|
return popup ? (
|
||||||
'video': <video width='100%' autoPlay controls {...props} />,
|
media ? (
|
||||||
'image': <Image {...props} />,
|
{
|
||||||
'audio': <audio autoPlay controls {...props} style={{ width: '100%' }}/>,
|
video: <video width='100%' autoPlay controls {...props} />,
|
||||||
'text': <Prism withLineNumbers language={name.split('.').pop()} {...props} style={{}} sx={{}}>{text}</Prism>,
|
image: <Image {...props} />,
|
||||||
}[type]: <Text>Can't preview {file.type || file.mimetype}</Text>) : (media ? {
|
audio: <audio autoPlay controls {...props} style={{ width: '100%' }} />,
|
||||||
'video': <Placeholder Icon={PlayIcon} text={`Click to view video (${name})`} {...props} />,
|
text: (
|
||||||
'image': <Image {...props} />,
|
<Prism withLineNumbers language={name.split('.').pop()} {...props} style={{}} sx={{}}>
|
||||||
'audio': <Placeholder Icon={AudioIcon} text={`Click to view audio (${name})`} {...props}/>,
|
{text}
|
||||||
'text': <Placeholder Icon={FileIcon} text={`Click to view text file (${name})`} {...props}/>,
|
</Prism>
|
||||||
}[type] : <Placeholder Icon={FileIcon} text={`Click to view file (${name})`} {...props}/>);
|
),
|
||||||
};
|
}[type]
|
||||||
|
) : (
|
||||||
|
<Text>Can't preview {file.type || file.mimetype}</Text>
|
||||||
|
)
|
||||||
|
) : media ? (
|
||||||
|
{
|
||||||
|
video: <Placeholder Icon={PlayIcon} text={`Click to view video (${name})`} {...props} />,
|
||||||
|
image: <Image {...props} />,
|
||||||
|
audio: <Placeholder Icon={AudioIcon} text={`Click to view audio (${name})`} {...props} />,
|
||||||
|
text: <Placeholder Icon={FileIcon} text={`Click to view text file (${name})`} {...props} />,
|
||||||
|
}[type]
|
||||||
|
) : (
|
||||||
|
<Placeholder Icon={FileIcon} text={`Click to view file (${name})`} {...props} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
|
@ -16,9 +16,7 @@ export default function Dropzone({ loading, onDrop, children }) {
|
||||||
</Text>
|
</Text>
|
||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
<div style={{ pointerEvents: 'all' }}>
|
<div style={{ pointerEvents: 'all' }}>{children}</div>
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
</MantineDropzone>
|
</MantineDropzone>
|
||||||
);
|
);
|
||||||
}
|
}
|
|
@ -46,9 +46,7 @@ export default function FileDropzone({ file }: { file: File }) {
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Badge size='lg'>
|
<Badge size='lg'>{file.name}</Badge>
|
||||||
{file.name}
|
|
||||||
</Badge>
|
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
);
|
);
|
||||||
}
|
}
|
|
@ -3,5 +3,12 @@
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
|
|
||||||
export default function DiscordIcon({ ...props }) {
|
export default function DiscordIcon({ ...props }) {
|
||||||
return <Image src='https://assets-global.website-files.com/6257adef93867e50d84d30e2/62595384f934b806f37f4956_145dc557845548a36a82337912ca3ac5.svg' width={24} height={24} {...props} />;
|
return (
|
||||||
|
<Image
|
||||||
|
src='https://assets-global.website-files.com/6257adef93867e50d84d30e2/62595384f934b806f37f4956_145dc557845548a36a82337912ca3ac5.svg'
|
||||||
|
width={24}
|
||||||
|
height={24}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
|
@ -3,5 +3,12 @@
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
|
|
||||||
export default function FlameshotIcon({ ...props }) {
|
export default function FlameshotIcon({ ...props }) {
|
||||||
return <Image src='https://raw.githubusercontent.com/flameshot-org/flameshot/master/data/img/app/flameshot.svg' width={24} height={24} {...props} />;
|
return (
|
||||||
|
<Image
|
||||||
|
src='https://raw.githubusercontent.com/flameshot-org/flameshot/master/data/img/app/flameshot.svg'
|
||||||
|
width={24}
|
||||||
|
height={24}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
|
@ -12,19 +12,19 @@ export default function RecentFiles({ disableMediaPreview }) {
|
||||||
<>
|
<>
|
||||||
<Title>Recent Files</Title>
|
<Title>Recent Files</Title>
|
||||||
<SimpleGrid
|
<SimpleGrid
|
||||||
cols={(recent.isSuccess && recent.data.length === 0) ? 1 : 4}
|
cols={recent.isSuccess && recent.data.length === 0 ? 1 : 4}
|
||||||
spacing='lg'
|
spacing='lg'
|
||||||
breakpoints={[
|
breakpoints={[{ maxWidth: 'sm', cols: 1, spacing: 'sm' }]}
|
||||||
{ maxWidth: 'sm', cols: 1, spacing: 'sm' },
|
|
||||||
]}
|
|
||||||
>
|
>
|
||||||
{
|
{recent.isSuccess ? (
|
||||||
recent.isSuccess
|
recent.data.length > 0 ? (
|
||||||
? (
|
recent.data.map((image) => (
|
||||||
recent.data.length > 0
|
<File
|
||||||
? (
|
key={randomId()}
|
||||||
recent.data.map(image => (
|
image={image}
|
||||||
<File key={randomId()} image={image} updateImages={invalidateFiles} disableMediaPreview={disableMediaPreview} />
|
updateImages={invalidateFiles}
|
||||||
|
disableMediaPreview={disableMediaPreview}
|
||||||
|
/>
|
||||||
))
|
))
|
||||||
) : (
|
) : (
|
||||||
<MantineCard shadow='md'>
|
<MantineCard shadow='md'>
|
||||||
|
@ -42,13 +42,12 @@ export default function RecentFiles({ disableMediaPreview }) {
|
||||||
</MantineCard>
|
</MantineCard>
|
||||||
)
|
)
|
||||||
) : (
|
) : (
|
||||||
[1, 2, 3, 4].map(x => (
|
[1, 2, 3, 4].map((x) => (
|
||||||
<div key={x}>
|
<div key={x}>
|
||||||
<Skeleton width='100%' height={220} sx={{ borderRadius: 1 }} />
|
<Skeleton width='100%' height={220} sx={{ borderRadius: 1 }} />
|
||||||
</div>
|
</div>
|
||||||
))
|
))
|
||||||
)
|
)}
|
||||||
}
|
|
||||||
</SimpleGrid>
|
</SimpleGrid>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
@ -18,44 +18,51 @@ export function StatCards() {
|
||||||
{ maxWidth: 'xs', cols: 1 },
|
{ maxWidth: 'xs', cols: 1 },
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<StatCard stat={{
|
<StatCard
|
||||||
|
stat={{
|
||||||
title: 'UPLOADED FILES',
|
title: 'UPLOADED FILES',
|
||||||
value: stats.isSuccess ? latest.data.count.toLocaleString() : '...',
|
value: stats.isSuccess ? latest.data.count.toLocaleString() : '...',
|
||||||
desc: 'files have been uploaded',
|
desc: 'files have been uploaded',
|
||||||
icon: (
|
icon: <FileIcon />,
|
||||||
<FileIcon />
|
diff:
|
||||||
),
|
stats.isSuccess && before?.data ? percentChange(before.data.count, latest.data.count) : undefined,
|
||||||
diff: stats.isSuccess && before?.data ? percentChange(before.data.count, latest.data.count) : undefined,
|
}}
|
||||||
}}/>
|
/>
|
||||||
|
|
||||||
<StatCard stat={{
|
<StatCard
|
||||||
|
stat={{
|
||||||
title: 'STORAGE',
|
title: 'STORAGE',
|
||||||
value: stats.isSuccess ? latest.data.size : '...',
|
value: stats.isSuccess ? latest.data.size : '...',
|
||||||
desc: 'of storage used',
|
desc: 'of storage used',
|
||||||
icon: (
|
icon: <Database size={15} />,
|
||||||
<Database size={15} />
|
diff:
|
||||||
),
|
stats.isSuccess && before?.data
|
||||||
diff: stats.isSuccess && before?.data ? percentChange(before.data.size_num, latest.data.size_num) : undefined,
|
? percentChange(before.data.size_num, latest.data.size_num)
|
||||||
}}/>
|
: undefined,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
<StatCard stat={{
|
<StatCard
|
||||||
|
stat={{
|
||||||
title: 'VIEWS',
|
title: 'VIEWS',
|
||||||
value: stats.isSuccess ? latest.data.views_count.toLocaleString() : '...',
|
value: stats.isSuccess ? latest.data.views_count.toLocaleString() : '...',
|
||||||
desc: 'total page views',
|
desc: 'total page views',
|
||||||
icon: (
|
icon: <Eye size={15} />,
|
||||||
<Eye size={15} />
|
diff:
|
||||||
),
|
stats.isSuccess && before?.data
|
||||||
diff: stats.isSuccess && before?.data ? percentChange(before.data.views_count, latest.data.views_count) : undefined,
|
? percentChange(before.data.views_count, latest.data.views_count)
|
||||||
}}/>
|
: undefined,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
<StatCard stat={{
|
<StatCard
|
||||||
|
stat={{
|
||||||
title: 'USERS',
|
title: 'USERS',
|
||||||
value: stats.isSuccess ? latest.data.count_users.toLocaleString() : '...',
|
value: stats.isSuccess ? latest.data.count_users.toLocaleString() : '...',
|
||||||
desc: 'total registered users',
|
desc: 'total registered users',
|
||||||
icon: (
|
icon: <Users size={15} />,
|
||||||
<Users size={15} />
|
}}
|
||||||
),
|
/>
|
||||||
}}/>
|
|
||||||
</SimpleGrid>
|
</SimpleGrid>
|
||||||
);
|
);
|
||||||
}
|
}
|
|
@ -29,7 +29,9 @@ export default function Dashboard({ disableMediaPreview }) {
|
||||||
};
|
};
|
||||||
|
|
||||||
const deleteImage = async ({ original }) => {
|
const deleteImage = async ({ original }) => {
|
||||||
const res = await useFetch('/api/user/files', 'DELETE', { id: original.id });
|
const res = await useFetch('/api/user/files', 'DELETE', {
|
||||||
|
id: original.id,
|
||||||
|
});
|
||||||
if (!res.error) {
|
if (!res.error) {
|
||||||
updateImages();
|
updateImages();
|
||||||
showNotification({
|
showNotification({
|
||||||
|
@ -46,7 +48,6 @@ export default function Dashboard({ disableMediaPreview }) {
|
||||||
icon: <CrossIcon />,
|
icon: <CrossIcon />,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const copyImage = async ({ original }) => {
|
const copyImage = async ({ original }) => {
|
||||||
|
@ -65,7 +66,9 @@ export default function Dashboard({ disableMediaPreview }) {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Title>Welcome back, {user?.username}</Title>
|
<Title>Welcome back, {user?.username}</Title>
|
||||||
<MutedText size='md'>You have <b>{images.isSuccess ? images.data.length : '...'}</b> files</MutedText>
|
<MutedText size='md'>
|
||||||
|
You have <b>{images.isSuccess ? images.data.length : '...'}</b> files
|
||||||
|
</MutedText>
|
||||||
|
|
||||||
<StatCards />
|
<StatCards />
|
||||||
|
|
||||||
|
@ -73,7 +76,9 @@ export default function Dashboard({ disableMediaPreview }) {
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
<Title>Files</Title>
|
<Title>Files</Title>
|
||||||
<MutedText size='md'>View your gallery <Link href='/dashboard/files'>here</Link>.</MutedText>
|
<MutedText size='md'>
|
||||||
|
View your gallery <Link href='/dashboard/files'>here</Link>.
|
||||||
|
</MutedText>
|
||||||
<DataGrid
|
<DataGrid
|
||||||
data={images.data ?? []}
|
data={images.data ?? []}
|
||||||
loading={images.isLoading}
|
loading={images.isLoading}
|
||||||
|
@ -124,7 +129,6 @@ export default function Dashboard({ disableMediaPreview }) {
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
empty={<></>}
|
empty={<></>}
|
||||||
|
|
||||||
columns={[
|
columns={[
|
||||||
{
|
{
|
||||||
accessorKey: 'file',
|
accessorKey: 'file',
|
||||||
|
|
|
@ -29,35 +29,26 @@ export default function FilePagation({ disableMediaPreview }) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<SimpleGrid
|
<SimpleGrid cols={3} spacing='lg' breakpoints={[{ maxWidth: 'sm', cols: 1, spacing: 'sm' }]}>
|
||||||
cols={3}
|
{pages.isSuccess
|
||||||
spacing='lg'
|
|
||||||
breakpoints={[
|
|
||||||
{ maxWidth: 'sm', cols: 1, spacing: 'sm' },
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
{
|
|
||||||
(pages.isSuccess)
|
|
||||||
? pages.data.length
|
? pages.data.length
|
||||||
? (
|
? pages.data[page - 1 ?? 0].map((image) => (
|
||||||
pages.data[(page - 1) ?? 0].map(image => (
|
|
||||||
<div key={image.id}>
|
<div key={image.id}>
|
||||||
<File image={image} updateImages={() => pages.refetch()} disableMediaPreview={disableMediaPreview} />
|
<File
|
||||||
|
image={image}
|
||||||
|
updateImages={() => pages.refetch()}
|
||||||
|
disableMediaPreview={disableMediaPreview}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
))
|
))
|
||||||
) : (
|
: null
|
||||||
null
|
: [1, 2, 3, 4].map((x) => (
|
||||||
)
|
|
||||||
: (
|
|
||||||
[1,2,3,4].map(x => (
|
|
||||||
<div key={x}>
|
<div key={x}>
|
||||||
<Skeleton width='100%' height={220} sx={{ borderRadius: 1 }} />
|
<Skeleton width='100%' height={220} sx={{ borderRadius: 1 }} />
|
||||||
</div>
|
</div>
|
||||||
))
|
))}
|
||||||
)
|
|
||||||
}
|
|
||||||
</SimpleGrid>
|
</SimpleGrid>
|
||||||
{(pages.isSuccess && pages.data.length) ? (
|
{pages.isSuccess && pages.data.length ? (
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
|
@ -69,7 +60,11 @@ export default function FilePagation({ disableMediaPreview }) {
|
||||||
>
|
>
|
||||||
<div></div>
|
<div></div>
|
||||||
<Pagination total={pages.data?.length ?? 0} page={page} onChange={setPage} />
|
<Pagination total={pages.data?.length ?? 0} page={page} onChange={setPage} />
|
||||||
<Checkbox label='Show non-media files' checked={checked} onChange={(event) => setChecked(event.currentTarget.checked)} />
|
<Checkbox
|
||||||
|
label='Show non-media files'
|
||||||
|
checked={checked}
|
||||||
|
onChange={(event) => setChecked(event.currentTarget.checked)}
|
||||||
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
) : null}
|
) : null}
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -11,7 +11,7 @@ export default function Files({ disableMediaPreview }) {
|
||||||
const favoritePages = usePaginatedFiles({ favorite: 'media' });
|
const favoritePages = usePaginatedFiles({ favorite: 'media' });
|
||||||
const [favoritePage, setFavoritePage] = useState(1);
|
const [favoritePage, setFavoritePage] = useState(1);
|
||||||
|
|
||||||
const updatePages = async favorite => {
|
const updatePages = async (favorite) => {
|
||||||
pages.refetch();
|
pages.refetch();
|
||||||
|
|
||||||
if (favorite) {
|
if (favorite) {
|
||||||
|
@ -24,31 +24,28 @@ export default function Files({ disableMediaPreview }) {
|
||||||
<Group mb='md'>
|
<Group mb='md'>
|
||||||
<Title>Files</Title>
|
<Title>Files</Title>
|
||||||
<Link href='/dashboard/upload' passHref>
|
<Link href='/dashboard/upload' passHref>
|
||||||
<ActionIcon component='a' variant='filled' color='primary'><PlusIcon/></ActionIcon>
|
<ActionIcon component='a' variant='filled' color='primary'>
|
||||||
|
<PlusIcon />
|
||||||
|
</ActionIcon>
|
||||||
</Link>
|
</Link>
|
||||||
</Group>
|
</Group>
|
||||||
{
|
{favoritePages.isSuccess && favoritePages.data.length ? (
|
||||||
(favoritePages.isSuccess && favoritePages.data.length)
|
<Accordion variant='contained' mb='sm'>
|
||||||
? (
|
|
||||||
<Accordion
|
|
||||||
variant='contained'
|
|
||||||
mb='sm'
|
|
||||||
>
|
|
||||||
<Accordion.Item value='favorite'>
|
<Accordion.Item value='favorite'>
|
||||||
<Accordion.Control>Favorite Files</Accordion.Control>
|
<Accordion.Control>Favorite Files</Accordion.Control>
|
||||||
<Accordion.Panel>
|
<Accordion.Panel>
|
||||||
<SimpleGrid
|
<SimpleGrid cols={3} spacing='lg' breakpoints={[{ maxWidth: 'sm', cols: 1, spacing: 'sm' }]}>
|
||||||
cols={3}
|
{favoritePages.isSuccess && favoritePages.data.length
|
||||||
spacing='lg'
|
? favoritePages.data[favoritePage - 1 ?? 0].map((image) => (
|
||||||
breakpoints={[
|
|
||||||
{ maxWidth: 'sm', cols: 1, spacing: 'sm' },
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
{(favoritePages.isSuccess && favoritePages.data.length) ? favoritePages.data[(favoritePage - 1) ?? 0].map(image => (
|
|
||||||
<div key={image.id}>
|
<div key={image.id}>
|
||||||
<File image={image} updateImages={() => updatePages(true)} disableMediaPreview={disableMediaPreview} />
|
<File
|
||||||
|
image={image}
|
||||||
|
updateImages={() => updatePages(true)}
|
||||||
|
disableMediaPreview={disableMediaPreview}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)) : null}
|
))
|
||||||
|
: null}
|
||||||
</SimpleGrid>
|
</SimpleGrid>
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
|
@ -59,13 +56,16 @@ export default function Files({ disableMediaPreview }) {
|
||||||
paddingBottom: 3,
|
paddingBottom: 3,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Pagination total={favoritePages.data.length} page={favoritePage} onChange={setFavoritePage} />
|
<Pagination
|
||||||
|
total={favoritePages.data.length}
|
||||||
|
page={favoritePage}
|
||||||
|
onChange={setFavoritePage}
|
||||||
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
</Accordion.Panel>
|
</Accordion.Panel>
|
||||||
</Accordion.Item>
|
</Accordion.Item>
|
||||||
</Accordion>
|
</Accordion>
|
||||||
) : null
|
) : null}
|
||||||
}
|
|
||||||
|
|
||||||
<FilePagation disableMediaPreview={disableMediaPreview} />
|
<FilePagation disableMediaPreview={disableMediaPreview} />
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -1,4 +1,17 @@
|
||||||
import { ActionIcon, Avatar, Button, Card, Group, Modal, NumberInput, Select, SimpleGrid, Skeleton, Stack, Title } from '@mantine/core';
|
import {
|
||||||
|
ActionIcon,
|
||||||
|
Avatar,
|
||||||
|
Button,
|
||||||
|
Card,
|
||||||
|
Group,
|
||||||
|
Modal,
|
||||||
|
NumberInput,
|
||||||
|
Select,
|
||||||
|
SimpleGrid,
|
||||||
|
Skeleton,
|
||||||
|
Stack,
|
||||||
|
Title,
|
||||||
|
} from '@mantine/core';
|
||||||
import { useClipboard } from '@mantine/hooks';
|
import { useClipboard } from '@mantine/hooks';
|
||||||
import { useForm } from '@mantine/form';
|
import { useForm } from '@mantine/form';
|
||||||
import { useModals } from '@mantine/modals';
|
import { useModals } from '@mantine/modals';
|
||||||
|
@ -9,17 +22,7 @@ import useFetch from 'hooks/useFetch';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
const expires = [
|
const expires = ['30m', '1h', '6h', '12h', '1d', '3d', '5d', '7d', 'never'];
|
||||||
'30m',
|
|
||||||
'1h',
|
|
||||||
'6h',
|
|
||||||
'12h',
|
|
||||||
'1d',
|
|
||||||
'3d',
|
|
||||||
'5d',
|
|
||||||
'7d',
|
|
||||||
'never',
|
|
||||||
];
|
|
||||||
|
|
||||||
function CreateInviteModal({ open, setOpen, updateInvites }) {
|
function CreateInviteModal({ open, setOpen, updateInvites }) {
|
||||||
const form = useForm({
|
const form = useForm({
|
||||||
|
@ -29,10 +32,15 @@ function CreateInviteModal({ open, setOpen, updateInvites }) {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const onSubmit = async values => {
|
const onSubmit = async (values) => {
|
||||||
if (!expires.includes(values.expires)) return form.setFieldError('expires', 'Invalid expiration');
|
if (!expires.includes(values.expires)) return form.setFieldError('expires', 'Invalid expiration');
|
||||||
if (values.count < 1 || values.count > 100) return form.setFieldError('count', 'Must be between 1 and 100');
|
if (values.count < 1 || values.count > 100)
|
||||||
const expires_at = values.expires === 'never' ? null : new Date({
|
return form.setFieldError('count', 'Must be between 1 and 100');
|
||||||
|
const expires_at =
|
||||||
|
values.expires === 'never'
|
||||||
|
? null
|
||||||
|
: new Date(
|
||||||
|
{
|
||||||
'30m': Date.now() + 30 * 60 * 1000,
|
'30m': Date.now() + 30 * 60 * 1000,
|
||||||
'1h': Date.now() + 60 * 60 * 1000,
|
'1h': Date.now() + 60 * 60 * 1000,
|
||||||
'6h': Date.now() + 6 * 60 * 60 * 1000,
|
'6h': Date.now() + 6 * 60 * 60 * 1000,
|
||||||
|
@ -40,7 +48,8 @@ function CreateInviteModal({ open, setOpen, updateInvites }) {
|
||||||
'1d': Date.now() + 24 * 60 * 60 * 1000,
|
'1d': Date.now() + 24 * 60 * 60 * 1000,
|
||||||
'5d': Date.now() + 5 * 24 * 60 * 60 * 1000,
|
'5d': Date.now() + 5 * 24 * 60 * 60 * 1000,
|
||||||
'7d': Date.now() + 7 * 24 * 60 * 60 * 1000,
|
'7d': Date.now() + 7 * 24 * 60 * 60 * 1000,
|
||||||
}[values.expires]);
|
}[values.expires]
|
||||||
|
);
|
||||||
|
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
|
|
||||||
|
@ -69,12 +78,8 @@ function CreateInviteModal({ open, setOpen, updateInvites }) {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal opened={open} onClose={() => setOpen(false)} title={<Title>Create Invite</Title>}>
|
||||||
opened={open}
|
<form onSubmit={form.onSubmit((v) => onSubmit(v))}>
|
||||||
onClose={() => setOpen(false)}
|
|
||||||
title={<Title>Create Invite</Title>}
|
|
||||||
>
|
|
||||||
<form onSubmit={form.onSubmit(v => onSubmit(v))}>
|
|
||||||
<Select
|
<Select
|
||||||
label='Expires'
|
label='Expires'
|
||||||
id='expires'
|
id='expires'
|
||||||
|
@ -120,7 +125,8 @@ export default function Users() {
|
||||||
const [invites, setInvites] = useState([]);
|
const [invites, setInvites] = useState([]);
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
const openDeleteModal = invite => modals.openConfirmModal({
|
const openDeleteModal = (invite) =>
|
||||||
|
modals.openConfirmModal({
|
||||||
title: `Delete ${invite.code}?`,
|
title: `Delete ${invite.code}?`,
|
||||||
centered: true,
|
centered: true,
|
||||||
overlayBlur: 3,
|
overlayBlur: 3,
|
||||||
|
@ -147,7 +153,7 @@ export default function Users() {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleCopy = async invite => {
|
const handleCopy = async (invite) => {
|
||||||
clipboard.copy(`${window.location.protocol}//${window.location.host}/invite/${invite.code}`);
|
clipboard.copy(`${window.location.protocol}//${window.location.host}/invite/${invite.code}`);
|
||||||
showNotification({
|
showNotification({
|
||||||
title: 'Copied to clipboard',
|
title: 'Copied to clipboard',
|
||||||
|
@ -162,7 +168,7 @@ export default function Users() {
|
||||||
setInvites(us);
|
setInvites(us);
|
||||||
} else {
|
} else {
|
||||||
router.push('/dashboard');
|
router.push('/dashboard');
|
||||||
};
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -174,24 +180,28 @@ export default function Users() {
|
||||||
<CreateInviteModal open={open} setOpen={setOpen} updateInvites={updateInvites} />
|
<CreateInviteModal open={open} setOpen={setOpen} updateInvites={updateInvites} />
|
||||||
<Group mb='md'>
|
<Group mb='md'>
|
||||||
<Title>Invites</Title>
|
<Title>Invites</Title>
|
||||||
<ActionIcon variant='filled' color='primary' onClick={() => setOpen(true)}><PlusIcon /></ActionIcon>
|
<ActionIcon variant='filled' color='primary' onClick={() => setOpen(true)}>
|
||||||
|
<PlusIcon />
|
||||||
|
</ActionIcon>
|
||||||
</Group>
|
</Group>
|
||||||
<SimpleGrid
|
<SimpleGrid cols={3} spacing='lg' breakpoints={[{ maxWidth: 'sm', cols: 1, spacing: 'sm' }]}>
|
||||||
cols={3}
|
{invites.length
|
||||||
spacing='lg'
|
? invites.map((invite) => (
|
||||||
breakpoints={[
|
|
||||||
{ maxWidth: 'sm', cols: 1, spacing: 'sm' },
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
{invites.length ? invites.map(invite => (
|
|
||||||
<Card key={invite.id} sx={{ maxWidth: '100%' }}>
|
<Card key={invite.id} sx={{ maxWidth: '100%' }}>
|
||||||
<Group position='apart'>
|
<Group position='apart'>
|
||||||
<Group position='left'>
|
<Group position='left'>
|
||||||
<Avatar size='lg' color={invite.used ? 'dark' : 'primary'}>{invite.id}</Avatar>
|
<Avatar size='lg' color={invite.used ? 'dark' : 'primary'}>
|
||||||
|
{invite.id}
|
||||||
|
</Avatar>
|
||||||
<Stack spacing={0}>
|
<Stack spacing={0}>
|
||||||
<Title>{invite.code}{invite.used && <> (Used)</>}</Title>
|
<Title>
|
||||||
|
{invite.code}
|
||||||
|
{invite.used && <> (Used)</>}
|
||||||
|
</Title>
|
||||||
<MutedText size='sm'>Created: {new Date(invite.created_at).toLocaleString()}</MutedText>
|
<MutedText size='sm'>Created: {new Date(invite.created_at).toLocaleString()}</MutedText>
|
||||||
<MutedText size='sm'>Expires: {invite.expires_at ? new Date(invite.expires_at).toLocaleString() : 'Never'}</MutedText>
|
<MutedText size='sm'>
|
||||||
|
Expires: {invite.expires_at ? new Date(invite.expires_at).toLocaleString() : 'Never'}
|
||||||
|
</MutedText>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Group>
|
</Group>
|
||||||
<Group position='right'>
|
<Group position='right'>
|
||||||
|
@ -204,9 +214,8 @@ export default function Users() {
|
||||||
</Group>
|
</Group>
|
||||||
</Group>
|
</Group>
|
||||||
</Card>
|
</Card>
|
||||||
)) : [1, 2, 3].map(x => (
|
))
|
||||||
<Skeleton key={x} width='100%' height={100} radius='sm' />
|
: [1, 2, 3].map((x) => <Skeleton key={x} width='100%' height={100} radius='sm' />)}
|
||||||
))}
|
|
||||||
</SimpleGrid>
|
</SimpleGrid>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { GeneratorModal } from './GeneratorModal';
|
import { GeneratorModal } from './GeneratorModal';
|
||||||
|
|
||||||
export default function Flameshot({ user, open, setOpen }) {
|
export default function Flameshot({ user, open, setOpen }) {
|
||||||
const onSubmit = values => {
|
const onSubmit = (values) => {
|
||||||
const curl = [
|
const curl = [
|
||||||
'curl',
|
'curl',
|
||||||
'-H',
|
'-H',
|
||||||
|
@ -10,7 +10,12 @@ export default function Flameshot({ user, open, setOpen }) {
|
||||||
`"authorization: ${user?.token}"`,
|
`"authorization: ${user?.token}"`,
|
||||||
'-F',
|
'-F',
|
||||||
'file=@/tmp/ss.png',
|
'file=@/tmp/ss.png',
|
||||||
`${window.location.protocol + '//' + window.location.hostname + (window.location.port ? ':' + window.location.port : '')}/api/upload`,
|
`${
|
||||||
|
window.location.protocol +
|
||||||
|
'//' +
|
||||||
|
window.location.hostname +
|
||||||
|
(window.location.port ? ':' + window.location.port : '')
|
||||||
|
}/api/upload`,
|
||||||
];
|
];
|
||||||
|
|
||||||
const extraHeaders = {};
|
const extraHeaders = {};
|
||||||
|
@ -58,11 +63,13 @@ ${curl.join(' ')} | jq -r '.files[0]' | tr -d '\n' | xsel -ib;
|
||||||
pseudoElement.parentNode.removeChild(pseudoElement);
|
pseudoElement.parentNode.removeChild(pseudoElement);
|
||||||
};
|
};
|
||||||
|
|
||||||
return <GeneratorModal
|
return (
|
||||||
|
<GeneratorModal
|
||||||
opened={open}
|
opened={open}
|
||||||
onClose={() => setOpen(false)}
|
onClose={() => setOpen(false)}
|
||||||
title='Flameshot'
|
title='Flameshot'
|
||||||
desc='To use this script, you need Flameshot, curl, jq, and xsel installed. This script is intended for use on Linux only.'
|
desc='To use this script, you need Flameshot, curl, jq, and xsel installed. This script is intended for use on Linux only.'
|
||||||
onSubmit={onSubmit}
|
onSubmit={onSubmit}
|
||||||
/>;
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
|
@ -13,14 +13,9 @@ export function GeneratorModal({ opened, onClose, title, onSubmit, ...other }) {
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal opened={opened} onClose={onClose} title={<Title order={3}>{title}</Title>} size='lg'>
|
||||||
opened={opened}
|
|
||||||
onClose={onClose}
|
|
||||||
title={<Title order={3}>{title}</Title>}
|
|
||||||
size='lg'
|
|
||||||
>
|
|
||||||
{other.desc && <Text>{other.desc}</Text>}
|
{other.desc && <Text>{other.desc}</Text>}
|
||||||
<form onSubmit={form.onSubmit(values => onSubmit(values))}>
|
<form onSubmit={form.onSubmit((values) => onSubmit(values))}>
|
||||||
<Select
|
<Select
|
||||||
label='Select file name format'
|
label='Select file name format'
|
||||||
data={[
|
data={[
|
||||||
|
@ -34,7 +29,7 @@ export function GeneratorModal({ opened, onClose, title, onSubmit, ...other }) {
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<NumberInput
|
<NumberInput
|
||||||
label={'Image Compression (leave at 0 if you don\'t want to compress)'}
|
label={"Image Compression (leave at 0 if you don't want to compress)"}
|
||||||
max={100}
|
max={100}
|
||||||
min={0}
|
min={0}
|
||||||
mt='md'
|
mt='md'
|
||||||
|
@ -48,26 +43,15 @@ export function GeneratorModal({ opened, onClose, title, onSubmit, ...other }) {
|
||||||
id='zeroWidthSpace'
|
id='zeroWidthSpace'
|
||||||
{...form.getInputProps('zeroWidthSpace', { type: 'checkbox' })}
|
{...form.getInputProps('zeroWidthSpace', { type: 'checkbox' })}
|
||||||
/>
|
/>
|
||||||
<Checkbox
|
<Checkbox label='Embed' id='embed' {...form.getInputProps('embed', { type: 'checkbox' })} />
|
||||||
label='Embed'
|
|
||||||
id='embed'
|
|
||||||
{...form.getInputProps('embed', { type: 'checkbox' })}
|
|
||||||
/>
|
|
||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
<Group grow>
|
<Group grow>
|
||||||
<Button
|
<Button mt='md' onClick={form.reset}>
|
||||||
mt='md'
|
|
||||||
onClick={form.reset}
|
|
||||||
>
|
|
||||||
Reset
|
Reset
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button
|
<Button mt='md' rightIcon={<DownloadIcon />} type='submit'>
|
||||||
mt='md'
|
|
||||||
rightIcon={<DownloadIcon />}
|
|
||||||
type='submit'
|
|
||||||
>
|
|
||||||
Download
|
Download
|
||||||
</Button>
|
</Button>
|
||||||
</Group>
|
</Group>
|
||||||
|
|
|
@ -7,7 +7,12 @@ export default function ShareX({ user, open, setOpen }) {
|
||||||
Name: 'Zipline',
|
Name: 'Zipline',
|
||||||
DestinationType: 'ImageUploader, TextUploader',
|
DestinationType: 'ImageUploader, TextUploader',
|
||||||
RequestMethod: 'POST',
|
RequestMethod: 'POST',
|
||||||
RequestURL: `${window.location.protocol + '//' + window.location.hostname + (window.location.port ? ':' + window.location.port : '')}/api/upload`,
|
RequestURL: `${
|
||||||
|
window.location.protocol +
|
||||||
|
'//' +
|
||||||
|
window.location.hostname +
|
||||||
|
(window.location.port ? ':' + window.location.port : '')
|
||||||
|
}/api/upload`,
|
||||||
Headers: {
|
Headers: {
|
||||||
Authorization: user?.token,
|
Authorization: user?.token,
|
||||||
},
|
},
|
||||||
|
@ -16,7 +21,7 @@ export default function ShareX({ user, open, setOpen }) {
|
||||||
FileFormName: 'file',
|
FileFormName: 'file',
|
||||||
});
|
});
|
||||||
|
|
||||||
const onSubmit = values => {
|
const onSubmit = (values) => {
|
||||||
if (values.format !== 'RANDOM') {
|
if (values.format !== 'RANDOM') {
|
||||||
config.Headers['Format'] = values.format;
|
config.Headers['Format'] = values.format;
|
||||||
setConfig(config);
|
setConfig(config);
|
||||||
|
@ -50,7 +55,10 @@ export default function ShareX({ user, open, setOpen }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const pseudoElement = document.createElement('a');
|
const pseudoElement = document.createElement('a');
|
||||||
pseudoElement.setAttribute('href', 'data:application/json;charset=utf-8,' + encodeURIComponent(JSON.stringify(config, null, '\t')));
|
pseudoElement.setAttribute(
|
||||||
|
'href',
|
||||||
|
'data:application/json;charset=utf-8,' + encodeURIComponent(JSON.stringify(config, null, '\t'))
|
||||||
|
);
|
||||||
pseudoElement.setAttribute('download', 'zipline.sxcu');
|
pseudoElement.setAttribute('download', 'zipline.sxcu');
|
||||||
pseudoElement.style.display = 'none';
|
pseudoElement.style.display = 'none';
|
||||||
document.body.appendChild(pseudoElement);
|
document.body.appendChild(pseudoElement);
|
||||||
|
@ -58,10 +66,5 @@ export default function ShareX({ user, open, setOpen }) {
|
||||||
pseudoElement.parentNode.removeChild(pseudoElement);
|
pseudoElement.parentNode.removeChild(pseudoElement);
|
||||||
};
|
};
|
||||||
|
|
||||||
return <GeneratorModal
|
return <GeneratorModal opened={open} onClose={() => setOpen(false)} title='ShareX' onSubmit={onSubmit} />;
|
||||||
opened={open}
|
|
||||||
onClose={() => setOpen(false)}
|
|
||||||
title='ShareX'
|
|
||||||
onSubmit={onSubmit}
|
|
||||||
/>;
|
|
||||||
}
|
}
|
|
@ -1,9 +1,30 @@
|
||||||
import { Box, Button, Card, ColorInput, FileInput, Group, Image, PasswordInput, Space, Text, TextInput, Title, Tooltip } from '@mantine/core';
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Card,
|
||||||
|
ColorInput,
|
||||||
|
FileInput,
|
||||||
|
Group,
|
||||||
|
Image,
|
||||||
|
PasswordInput,
|
||||||
|
Space,
|
||||||
|
Text,
|
||||||
|
TextInput,
|
||||||
|
Title,
|
||||||
|
Tooltip,
|
||||||
|
} from '@mantine/core';
|
||||||
import { useForm } from '@mantine/form';
|
import { useForm } from '@mantine/form';
|
||||||
import { randomId, useInterval } from '@mantine/hooks';
|
import { randomId, useInterval } from '@mantine/hooks';
|
||||||
import { useModals } from '@mantine/modals';
|
import { useModals } from '@mantine/modals';
|
||||||
import { showNotification, updateNotification } from '@mantine/notifications';
|
import { showNotification, updateNotification } from '@mantine/notifications';
|
||||||
import { CrossIcon, DeleteIcon, FlameshotIcon, RefreshIcon, SettingsIcon, ShareXIcon } from 'components/icons';
|
import {
|
||||||
|
CrossIcon,
|
||||||
|
DeleteIcon,
|
||||||
|
FlameshotIcon,
|
||||||
|
RefreshIcon,
|
||||||
|
SettingsIcon,
|
||||||
|
ShareXIcon,
|
||||||
|
} from 'components/icons';
|
||||||
import DownloadIcon from 'components/icons/DownloadIcon';
|
import DownloadIcon from 'components/icons/DownloadIcon';
|
||||||
import Link from 'components/Link';
|
import Link from 'components/Link';
|
||||||
import MutedText from 'components/MutedText';
|
import MutedText from 'components/MutedText';
|
||||||
|
@ -17,7 +38,15 @@ import Flameshot from './Flameshot';
|
||||||
import ShareX from './ShareX';
|
import ShareX from './ShareX';
|
||||||
|
|
||||||
function ExportDataTooltip({ children }) {
|
function ExportDataTooltip({ children }) {
|
||||||
return <Tooltip position='top' color='' label='After clicking, if you have a lot of files the export can take a while to complete. A list of previous exports will be below to download.'>{children}</Tooltip>;
|
return (
|
||||||
|
<Tooltip
|
||||||
|
position='top'
|
||||||
|
color=''
|
||||||
|
label='After clicking, if you have a lot of files the export can take a while to complete. A list of previous exports will be below to download.'
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Manage() {
|
export default function Manage() {
|
||||||
|
@ -70,7 +99,7 @@ export default function Manage() {
|
||||||
if (newUser.error) {
|
if (newUser.error) {
|
||||||
updateNotification({
|
updateNotification({
|
||||||
id: 'update-user',
|
id: 'update-user',
|
||||||
title: 'Couldn\'t save user',
|
title: "Couldn't save user",
|
||||||
message: newUser.error,
|
message: newUser.error,
|
||||||
color: 'red',
|
color: 'red',
|
||||||
icon: <CrossIcon />,
|
icon: <CrossIcon />,
|
||||||
|
@ -96,14 +125,14 @@ export default function Manage() {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const onSubmit = async values => {
|
const onSubmit = async (values) => {
|
||||||
const cleanUsername = values.username.trim();
|
const cleanUsername = values.username.trim();
|
||||||
const cleanPassword = values.password.trim();
|
const cleanPassword = values.password.trim();
|
||||||
const cleanEmbedTitle = values.embedTitle.trim();
|
const cleanEmbedTitle = values.embedTitle.trim();
|
||||||
const cleanEmbedColor = values.embedColor.trim();
|
const cleanEmbedColor = values.embedColor.trim();
|
||||||
const cleanEmbedSiteName = values.embedSiteName.trim();
|
const cleanEmbedSiteName = values.embedSiteName.trim();
|
||||||
|
|
||||||
if (cleanUsername === '') return form.setFieldError('username', 'Username can\'t be nothing');
|
if (cleanUsername === '') return form.setFieldError('username', "Username can't be nothing");
|
||||||
|
|
||||||
showNotification({
|
showNotification({
|
||||||
id: 'update-user',
|
id: 'update-user',
|
||||||
|
@ -119,7 +148,10 @@ export default function Manage() {
|
||||||
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: values.domains.split(/\s?,\s?/).map(x => x.trim()).filter(x => x !== ''),
|
domains: values.domains
|
||||||
|
.split(/\s?,\s?/)
|
||||||
|
.map((x) => x.trim())
|
||||||
|
.filter((x) => x !== ''),
|
||||||
};
|
};
|
||||||
|
|
||||||
const newUser = await useFetch('/api/user', 'PATCH', data);
|
const newUser = await useFetch('/api/user', 'PATCH', data);
|
||||||
|
@ -128,22 +160,26 @@ export default function Manage() {
|
||||||
if (newUser.invalidDomains) {
|
if (newUser.invalidDomains) {
|
||||||
updateNotification({
|
updateNotification({
|
||||||
id: 'update-user',
|
id: 'update-user',
|
||||||
message: <>
|
message: (
|
||||||
<Text mt='xs'>The following domains are invalid:</Text>
|
|
||||||
{newUser.invalidDomains.map(err => (
|
|
||||||
<>
|
<>
|
||||||
<Text color='gray' key={randomId()}>{err.domain}: {err.reason}</Text>
|
<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' />
|
<Space h='md' />
|
||||||
</>
|
</>
|
||||||
))}
|
))}
|
||||||
</>,
|
</>
|
||||||
|
),
|
||||||
color: 'red',
|
color: 'red',
|
||||||
icon: <CrossIcon />,
|
icon: <CrossIcon />,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
updateNotification({
|
updateNotification({
|
||||||
id: 'update-user',
|
id: 'update-user',
|
||||||
title: 'Couldn\'t save user',
|
title: "Couldn't save user",
|
||||||
message: newUser.error,
|
message: newUser.error,
|
||||||
color: 'red',
|
color: 'red',
|
||||||
icon: <CrossIcon />,
|
icon: <CrossIcon />,
|
||||||
|
@ -164,7 +200,8 @@ export default function Manage() {
|
||||||
showNotification({
|
showNotification({
|
||||||
title: 'Export started...',
|
title: 'Export started...',
|
||||||
loading: true,
|
loading: true,
|
||||||
message: 'If you have a lot of files, the export may take a while. The list of exports will be updated every 30s.',
|
message:
|
||||||
|
'If you have a lot of files, the export may take a while. The list of exports will be updated every 30s.',
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
showNotification({
|
showNotification({
|
||||||
|
@ -179,11 +216,15 @@ export default function Manage() {
|
||||||
const getExports = async () => {
|
const getExports = async () => {
|
||||||
const res = await useFetch('/api/user/export');
|
const res = await useFetch('/api/user/export');
|
||||||
|
|
||||||
setExports(res.exports.map(s => ({
|
setExports(
|
||||||
|
res.exports
|
||||||
|
.map((s) => ({
|
||||||
date: new Date(Number(s.name.split('_')[3].slice(0, -4))),
|
date: new Date(Number(s.name.split('_')[3].slice(0, -4))),
|
||||||
size: s.size,
|
size: s.size,
|
||||||
full: s.name,
|
full: s.name,
|
||||||
})).sort((a, b) => a.date.getTime() - b.date.getTime()));
|
}))
|
||||||
|
.sort((a, b) => a.date.getTime() - b.date.getTime())
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDelete = async () => {
|
const handleDelete = async () => {
|
||||||
|
@ -193,7 +234,7 @@ export default function Manage() {
|
||||||
|
|
||||||
if (!res.count) {
|
if (!res.count) {
|
||||||
showNotification({
|
showNotification({
|
||||||
title: 'Couldn\'t delete files',
|
title: "Couldn't delete files",
|
||||||
message: res.error,
|
message: res.error,
|
||||||
color: 'red',
|
color: 'red',
|
||||||
icon: <CrossIcon />,
|
icon: <CrossIcon />,
|
||||||
|
@ -208,7 +249,8 @@ export default function Manage() {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const openDeleteModal = () => modals.openConfirmModal({
|
const openDeleteModal = () =>
|
||||||
|
modals.openConfirmModal({
|
||||||
title: 'Are you sure you want to delete all of your files?',
|
title: 'Are you sure you want to delete all of your files?',
|
||||||
closeOnConfirm: false,
|
closeOnConfirm: false,
|
||||||
labels: { confirm: 'Yes', cancel: 'No' },
|
labels: { confirm: 'Yes', cancel: 'No' },
|
||||||
|
@ -236,30 +278,49 @@ export default function Manage() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Title>Manage User</Title>
|
<Title>Manage User</Title>
|
||||||
<MutedText size='md'>Want to use variables in embed text? Visit <Link href='https://zipline.diced.tech/docs/guides/variables'>the docs</Link> for variables</MutedText>
|
<MutedText size='md'>
|
||||||
|
Want to use variables in embed text? Visit{' '}
|
||||||
|
<Link href='https://zipline.diced.tech/docs/guides/variables'>the docs</Link> for variables
|
||||||
|
</MutedText>
|
||||||
<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')} />
|
||||||
<PasswordInput id='password' label='Password' description='Leave blank to keep your old password' {...form.getInputProps('password')} />
|
<PasswordInput
|
||||||
|
id='password'
|
||||||
|
label='Password'
|
||||||
|
description='Leave blank to keep your old 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')} />
|
||||||
<TextInput id='domains' label='Domains' description='A list of domains separated by commas. These domains will be used to randomly output a domain when uploading. This is optional.' placeholder='https://example.com, https://example2.com' {...form.getInputProps('domains')} />
|
<TextInput
|
||||||
|
id='domains'
|
||||||
|
label='Domains'
|
||||||
|
description='A list of domains separated by commas. These domains will be used to randomly output a domain when uploading. This is optional.'
|
||||||
|
placeholder='https://example.com, https://example2.com'
|
||||||
|
{...form.getInputProps('domains')}
|
||||||
|
/>
|
||||||
|
|
||||||
<Group position='right' mt='md'>
|
<Group position='right' mt='md'>
|
||||||
<Button
|
<Button type='submit'>Save User</Button>
|
||||||
type='submit'
|
|
||||||
>Save User</Button>
|
|
||||||
</Group>
|
</Group>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<Box mb='md'>
|
<Box mb='md'>
|
||||||
<Title>Avatar</Title>
|
<Title>Avatar</Title>
|
||||||
<FileInput placeholder='Click to upload a file' id='file' description='Add a custom avatar or leave blank for none' accept='image/png,image/jpeg,image/gif' value={file} onChange={handleAvatarChange} />
|
<FileInput
|
||||||
|
placeholder='Click to upload a file'
|
||||||
|
id='file'
|
||||||
|
description='Add a custom avatar or leave blank for none'
|
||||||
|
accept='image/png,image/jpeg,image/gif'
|
||||||
|
value={file}
|
||||||
|
onChange={handleAvatarChange}
|
||||||
|
/>
|
||||||
<Card mt='md'>
|
<Card mt='md'>
|
||||||
<Text>Preview:</Text>
|
<Text>Preview:</Text>
|
||||||
<Button
|
<Button
|
||||||
leftIcon={fileDataURL ? <Image src={fileDataURL} height={32} radius='md' /> : <SettingsIcon />}
|
leftIcon={fileDataURL ? <Image src={fileDataURL} height={32} radius='md' /> : <SettingsIcon />}
|
||||||
sx={t => ({
|
sx={(t) => ({
|
||||||
backgroundColor: '#00000000',
|
backgroundColor: '#00000000',
|
||||||
'&:hover': {
|
'&:hover': {
|
||||||
backgroundColor: t.other.hover,
|
backgroundColor: t.other.hover,
|
||||||
|
@ -273,7 +334,15 @@ export default function Manage() {
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Group position='right' mt='md'>
|
<Group position='right' mt='md'>
|
||||||
<Button onClick={() => { setFile(null); setFileDataURL(null); }} color='red'>Reset</Button>
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
setFile(null);
|
||||||
|
setFileDataURL(null);
|
||||||
|
}}
|
||||||
|
color='red'
|
||||||
|
>
|
||||||
|
Reset
|
||||||
|
</Button>
|
||||||
<Button onClick={saveAvatar}>Save Avatar</Button>
|
<Button onClick={saveAvatar}>Save Avatar</Button>
|
||||||
</Group>
|
</Group>
|
||||||
</Box>
|
</Box>
|
||||||
|
@ -284,9 +353,17 @@ export default function Manage() {
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Group>
|
<Group>
|
||||||
<Button onClick={openDeleteModal} rightIcon={<DeleteIcon />} color='red'>Delete All Data</Button>
|
<Button onClick={openDeleteModal} rightIcon={<DeleteIcon />} color='red'>
|
||||||
<ExportDataTooltip><Button onClick={exportData} rightIcon={<DownloadIcon />}>Export Data</Button></ExportDataTooltip>
|
Delete All Data
|
||||||
<Button onClick={getExports} rightIcon={<RefreshIcon />}>Refresh</Button>
|
</Button>
|
||||||
|
<ExportDataTooltip>
|
||||||
|
<Button onClick={exportData} rightIcon={<DownloadIcon />}>
|
||||||
|
Export Data
|
||||||
|
</Button>
|
||||||
|
</ExportDataTooltip>
|
||||||
|
<Button onClick={getExports} rightIcon={<RefreshIcon />}>
|
||||||
|
Refresh
|
||||||
|
</Button>
|
||||||
</Group>
|
</Group>
|
||||||
<Card mt={22}>
|
<Card mt={22}>
|
||||||
{exports && exports.length ? (
|
{exports && exports.length ? (
|
||||||
|
@ -296,11 +373,16 @@ export default function Manage() {
|
||||||
{ id: 'date', name: 'Date' },
|
{ id: 'date', name: 'Date' },
|
||||||
{ id: 'size', name: 'Size' },
|
{ id: 'size', name: 'Size' },
|
||||||
]}
|
]}
|
||||||
rows={exports ? exports.map((x, i) => ({
|
rows={
|
||||||
|
exports
|
||||||
|
? exports.map((x, i) => ({
|
||||||
name: <Link href={'/api/user/export?name=' + x.full}>Export {i + 1}</Link>,
|
name: <Link href={'/api/user/export?name=' + x.full}>Export {i + 1}</Link>,
|
||||||
date: x.date.toLocaleString(),
|
date: x.date.toLocaleString(),
|
||||||
size: bytesToRead(x.size),
|
size: bytesToRead(x.size),
|
||||||
})) : []} />
|
}))
|
||||||
|
: []
|
||||||
|
}
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Text>No exports yet</Text>
|
<Text>No exports yet</Text>
|
||||||
)}
|
)}
|
||||||
|
@ -308,8 +390,12 @@ export default function Manage() {
|
||||||
|
|
||||||
<Title my='md'>Uploaders</Title>
|
<Title my='md'>Uploaders</Title>
|
||||||
<Group>
|
<Group>
|
||||||
<Button size='xl' onClick={() => setShareXOpen(true)} rightIcon={<ShareXIcon />}>Generate ShareX Config</Button>
|
<Button size='xl' onClick={() => setShareXOpen(true)} rightIcon={<ShareXIcon />}>
|
||||||
<Button size='xl' onClick={() => setFlameshotOpen(true)} rightIcon={<FlameshotIcon />}>Generate Flameshot Script</Button>
|
Generate ShareX Config
|
||||||
|
</Button>
|
||||||
|
<Button size='xl' onClick={() => setFlameshotOpen(true)} rightIcon={<FlameshotIcon />}>
|
||||||
|
Generate Flameshot Script
|
||||||
|
</Button>
|
||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
<ShareX user={user} open={shareXOpen} setOpen={setShareXOpen} />
|
<ShareX user={user} open={shareXOpen} setOpen={setShareXOpen} />
|
||||||
|
|
|
@ -1,5 +1,16 @@
|
||||||
import { Box, Card, Grid, LoadingOverlay, MantineTheme, Title, useMantineTheme } from '@mantine/core';
|
import { Box, Card, Grid, LoadingOverlay, MantineTheme, Title, useMantineTheme } from '@mantine/core';
|
||||||
import { ArcElement, CategoryScale, Chart as ChartJS, ChartData, ChartOptions, LinearScale, LineController, LineElement, PointElement, Tooltip } from 'chart.js';
|
import {
|
||||||
|
ArcElement,
|
||||||
|
CategoryScale,
|
||||||
|
Chart as ChartJS,
|
||||||
|
ChartData,
|
||||||
|
ChartOptions,
|
||||||
|
LinearScale,
|
||||||
|
LineController,
|
||||||
|
LineElement,
|
||||||
|
PointElement,
|
||||||
|
Tooltip,
|
||||||
|
} from 'chart.js';
|
||||||
import ChartDataLabels from 'chartjs-plugin-datalabels';
|
import ChartDataLabels from 'chartjs-plugin-datalabels';
|
||||||
import ColorHash from 'color-hash';
|
import ColorHash from 'color-hash';
|
||||||
import { bytesToRead } from 'lib/utils/client';
|
import { bytesToRead } from 'lib/utils/client';
|
||||||
|
@ -49,13 +60,12 @@ const CHART_OPTIONS = (theme: MantineTheme): ChartOptions => ({
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
type LineChartData = ChartData<'line', number[], string>;
|
type LineChartData = ChartData<'line', number[], string>;
|
||||||
type ChartDataMemo = {
|
type ChartDataMemo = {
|
||||||
views: LineChartData,
|
views: LineChartData;
|
||||||
uploads: LineChartData,
|
uploads: LineChartData;
|
||||||
uploadTypes: ChartData<'pie', number[], string>,
|
uploadTypes: ChartData<'pie', number[], string>;
|
||||||
storage: LineChartData,
|
storage: LineChartData;
|
||||||
} | void;
|
} | void;
|
||||||
|
|
||||||
export default function Graphs() {
|
export default function Graphs() {
|
||||||
|
@ -77,41 +87,49 @@ export default function Graphs() {
|
||||||
return {
|
return {
|
||||||
views: {
|
views: {
|
||||||
labels,
|
labels,
|
||||||
datasets: [{
|
datasets: [
|
||||||
|
{
|
||||||
label: 'Views',
|
label: 'Views',
|
||||||
data: viewData,
|
data: viewData,
|
||||||
borderColor: theme.colors.blue[6],
|
borderColor: theme.colors.blue[6],
|
||||||
backgroundColor: theme.colors.blue[0],
|
backgroundColor: theme.colors.blue[0],
|
||||||
}],
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
|
|
||||||
uploads: {
|
uploads: {
|
||||||
labels,
|
labels,
|
||||||
datasets: [{
|
datasets: [
|
||||||
|
{
|
||||||
label: 'Uploads',
|
label: 'Uploads',
|
||||||
data: uploadData,
|
data: uploadData,
|
||||||
borderColor: theme.colors.blue[6],
|
borderColor: theme.colors.blue[6],
|
||||||
backgroundColor: theme.colors.blue[0],
|
backgroundColor: theme.colors.blue[0],
|
||||||
}],
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
|
|
||||||
uploadTypes: {
|
uploadTypes: {
|
||||||
labels: latest?.data.types_count.map((x) => x.mimetype),
|
labels: latest?.data.types_count.map((x) => x.mimetype),
|
||||||
datasets: [{
|
datasets: [
|
||||||
|
{
|
||||||
data: latest?.data.types_count.map((x) => x.count),
|
data: latest?.data.types_count.map((x) => x.count),
|
||||||
label: 'Upload Types',
|
label: 'Upload Types',
|
||||||
backgroundColor: latest?.data.types_count.map((x) => hash.hex(x.mimetype)),
|
backgroundColor: latest?.data.types_count.map((x) => hash.hex(x.mimetype)),
|
||||||
}],
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
|
|
||||||
storage: {
|
storage: {
|
||||||
labels,
|
labels,
|
||||||
datasets: [{
|
datasets: [
|
||||||
|
{
|
||||||
label: 'Storage',
|
label: 'Storage',
|
||||||
data: storageData,
|
data: storageData,
|
||||||
borderColor: theme.colors.blue[6],
|
borderColor: theme.colors.blue[6],
|
||||||
backgroundColor: theme.colors.blue[0],
|
backgroundColor: theme.colors.blue[0],
|
||||||
}],
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}, [historicalStats]);
|
}, [historicalStats]);
|
||||||
|
@ -125,11 +143,9 @@ export default function Graphs() {
|
||||||
<Grid.Col md={12} lg={4}>
|
<Grid.Col md={12} lg={4}>
|
||||||
<Card>
|
<Card>
|
||||||
<Title size='h4'>Upload Types</Title>
|
<Title size='h4'>Upload Types</Title>
|
||||||
{
|
{chartData && (
|
||||||
chartData && (
|
|
||||||
<Pie
|
<Pie
|
||||||
data={chartData.uploadTypes}
|
data={chartData.uploadTypes}
|
||||||
|
|
||||||
options={{
|
options={{
|
||||||
plugins: {
|
plugins: {
|
||||||
datalabels: {
|
datalabels: {
|
||||||
|
@ -148,8 +164,7 @@ export default function Graphs() {
|
||||||
}}
|
}}
|
||||||
style={{ maxHeight: '20vh' }}
|
style={{ maxHeight: '20vh' }}
|
||||||
/>
|
/>
|
||||||
)
|
)}
|
||||||
}
|
|
||||||
</Card>
|
</Card>
|
||||||
</Grid.Col>
|
</Grid.Col>
|
||||||
|
|
||||||
|
@ -157,16 +172,14 @@ export default function Graphs() {
|
||||||
<Grid.Col md={12} lg={8}>
|
<Grid.Col md={12} lg={8}>
|
||||||
<Card>
|
<Card>
|
||||||
<Title size='h4'>Total Views</Title>
|
<Title size='h4'>Total Views</Title>
|
||||||
{
|
{chartData && (
|
||||||
chartData && (
|
|
||||||
<Chart
|
<Chart
|
||||||
type='line'
|
type='line'
|
||||||
data={chartData.views}
|
data={chartData.views}
|
||||||
options={chartOptions}
|
options={chartOptions}
|
||||||
style={{ maxHeight: '20vh' }}
|
style={{ maxHeight: '20vh' }}
|
||||||
/>
|
/>
|
||||||
)
|
)}
|
||||||
}
|
|
||||||
</Card>
|
</Card>
|
||||||
</Grid.Col>
|
</Grid.Col>
|
||||||
|
|
||||||
|
@ -174,16 +187,14 @@ export default function Graphs() {
|
||||||
<Grid.Col md={12} lg={6}>
|
<Grid.Col md={12} lg={6}>
|
||||||
<Card>
|
<Card>
|
||||||
<Title size='h4'>Total Uploads</Title>
|
<Title size='h4'>Total Uploads</Title>
|
||||||
{
|
{chartData && (
|
||||||
chartData && (
|
|
||||||
<Chart
|
<Chart
|
||||||
type='line'
|
type='line'
|
||||||
data={chartData.uploads}
|
data={chartData.uploads}
|
||||||
options={chartOptions}
|
options={chartOptions}
|
||||||
style={{ maxHeight: '20vh' }}
|
style={{ maxHeight: '20vh' }}
|
||||||
/>
|
/>
|
||||||
)
|
)}
|
||||||
}
|
|
||||||
</Card>
|
</Card>
|
||||||
</Grid.Col>
|
</Grid.Col>
|
||||||
|
|
||||||
|
@ -191,8 +202,7 @@ export default function Graphs() {
|
||||||
<Grid.Col md={12} lg={6}>
|
<Grid.Col md={12} lg={6}>
|
||||||
<Card>
|
<Card>
|
||||||
<Title size='h4'>Storage Usage</Title>
|
<Title size='h4'>Storage Usage</Title>
|
||||||
{
|
{chartData && (
|
||||||
chartData && (
|
|
||||||
<Chart
|
<Chart
|
||||||
type='line'
|
type='line'
|
||||||
data={chartData.storage}
|
data={chartData.storage}
|
||||||
|
@ -226,8 +236,7 @@ export default function Graphs() {
|
||||||
}}
|
}}
|
||||||
style={{ maxHeight: '20vh' }}
|
style={{ maxHeight: '20vh' }}
|
||||||
/>
|
/>
|
||||||
)
|
)}
|
||||||
}
|
|
||||||
</Card>
|
</Card>
|
||||||
</Grid.Col>
|
</Grid.Col>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
|
@ -5,9 +5,7 @@ import { useStats } from 'lib/queries/stats';
|
||||||
export default function Types() {
|
export default function Types() {
|
||||||
const stats = useStats();
|
const stats = useStats();
|
||||||
|
|
||||||
if(stats.isLoading) return (
|
if (stats.isLoading) return <LoadingOverlay visible />;
|
||||||
<LoadingOverlay visible />
|
|
||||||
);
|
|
||||||
|
|
||||||
const latest = stats.data[0];
|
const latest = stats.data[0];
|
||||||
|
|
||||||
|
|
|
@ -13,7 +13,6 @@ export default function Stats() {
|
||||||
<Types />
|
<Types />
|
||||||
|
|
||||||
<Graphs />
|
<Graphs />
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -53,7 +53,7 @@ export default function Upload() {
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
window.addEventListener('paste', (e: ClipboardEvent) => {
|
window.addEventListener('paste', (e: ClipboardEvent) => {
|
||||||
const item = Array.from(e.clipboardData.items).find(x => /^image/.test(x.type));
|
const item = Array.from(e.clipboardData.items).find((x) => /^image/.test(x.type));
|
||||||
const file = item.getAsFile();
|
const file = item.getAsFile();
|
||||||
setFiles([...files, file]);
|
setFiles([...files, file]);
|
||||||
showNotification({
|
showNotification({
|
||||||
|
@ -64,7 +64,11 @@ export default function Upload() {
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleUpload = async () => {
|
const handleUpload = async () => {
|
||||||
const expires_at = expires === 'never' ? null : new Date({
|
const expires_at =
|
||||||
|
expires === 'never'
|
||||||
|
? null
|
||||||
|
: new Date(
|
||||||
|
{
|
||||||
'5min': Date.now() + 5 * 60 * 1000,
|
'5min': Date.now() + 5 * 60 * 1000,
|
||||||
'10min': Date.now() + 10 * 60 * 1000,
|
'10min': Date.now() + 10 * 60 * 1000,
|
||||||
'15min': Date.now() + 15 * 60 * 1000,
|
'15min': Date.now() + 15 * 60 * 1000,
|
||||||
|
@ -92,7 +96,8 @@ export default function Upload() {
|
||||||
'6m': Date.now() + 6 * 30 * 24 * 60 * 60 * 1000,
|
'6m': Date.now() + 6 * 30 * 24 * 60 * 60 * 1000,
|
||||||
'8m': Date.now() + 8 * 30 * 24 * 60 * 60 * 1000,
|
'8m': Date.now() + 8 * 30 * 24 * 60 * 60 * 1000,
|
||||||
'1y': Date.now() + 365 * 24 * 60 * 60 * 1000,
|
'1y': Date.now() + 365 * 24 * 60 * 60 * 1000,
|
||||||
}[expires]);
|
}[expires]
|
||||||
|
);
|
||||||
|
|
||||||
setProgress(0);
|
setProgress(0);
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
@ -108,13 +113,15 @@ export default function Upload() {
|
||||||
});
|
});
|
||||||
|
|
||||||
const req = new XMLHttpRequest();
|
const req = new XMLHttpRequest();
|
||||||
req.upload.addEventListener('progress', e => {
|
req.upload.addEventListener('progress', (e) => {
|
||||||
if (e.lengthComputable) {
|
if (e.lengthComputable) {
|
||||||
setProgress(Math.round(e.loaded / e.total * 100));
|
setProgress(Math.round((e.loaded / e.total) * 100));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
req.addEventListener('load', e => {
|
req.addEventListener(
|
||||||
|
'load',
|
||||||
|
(e) => {
|
||||||
// @ts-ignore not sure why it thinks response doesnt exist, but it does.
|
// @ts-ignore not sure why it thinks response doesnt exist, but it does.
|
||||||
const json = JSON.parse(e.target.response);
|
const json = JSON.parse(e.target.response);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
@ -123,7 +130,17 @@ export default function Upload() {
|
||||||
updateNotification({
|
updateNotification({
|
||||||
id: 'upload',
|
id: 'upload',
|
||||||
title: 'Upload Successful',
|
title: 'Upload Successful',
|
||||||
message: <>Copied first file to clipboard! <br />{json.files.map(x => (<Link key={x} href={x}>{x}<br /></Link>))}</>,
|
message: (
|
||||||
|
<>
|
||||||
|
Copied first file to clipboard! <br />
|
||||||
|
{json.files.map((x) => (
|
||||||
|
<Link key={x} href={x}>
|
||||||
|
{x}
|
||||||
|
<br />
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
),
|
||||||
color: 'green',
|
color: 'green',
|
||||||
icon: <UploadIcon />,
|
icon: <UploadIcon />,
|
||||||
});
|
});
|
||||||
|
@ -140,7 +157,9 @@ export default function Upload() {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
setProgress(0);
|
setProgress(0);
|
||||||
}, false);
|
},
|
||||||
|
false
|
||||||
|
);
|
||||||
|
|
||||||
req.open('POST', '/api/upload');
|
req.open('POST', '/api/upload');
|
||||||
req.setRequestHeader('Authorization', user.token);
|
req.setRequestHeader('Authorization', user.token);
|
||||||
|
@ -156,7 +175,9 @@ export default function Upload() {
|
||||||
|
|
||||||
<Dropzone loading={loading} onDrop={(f) => setFiles([...files, ...f])}>
|
<Dropzone loading={loading} onDrop={(f) => setFiles([...files, ...f])}>
|
||||||
<Group position='center' spacing='md'>
|
<Group position='center' spacing='md'>
|
||||||
{files.map(file => (<FileDropzone key={randomId()} file={file} />))}
|
{files.map((file) => (
|
||||||
|
<FileDropzone key={randomId()} file={file} />
|
||||||
|
))}
|
||||||
</Group>
|
</Group>
|
||||||
</Dropzone>
|
</Dropzone>
|
||||||
|
|
||||||
|
@ -210,7 +231,9 @@ export default function Upload() {
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Button leftIcon={<UploadIcon />} onClick={handleUpload} disabled={files.length === 0 ? true : false}>Upload</Button>
|
<Button leftIcon={<UploadIcon />} onClick={handleUpload} disabled={files.length === 0 ? true : false}>
|
||||||
|
Upload
|
||||||
|
</Button>
|
||||||
</Group>
|
</Group>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
@ -26,7 +26,7 @@ export default function Upload() {
|
||||||
});
|
});
|
||||||
|
|
||||||
const req = new XMLHttpRequest();
|
const req = new XMLHttpRequest();
|
||||||
req.addEventListener('load', e => {
|
req.addEventListener('load', (e) => {
|
||||||
// @ts-ignore not sure why it thinks response doesnt exist, but it does.
|
// @ts-ignore not sure why it thinks response doesnt exist, but it does.
|
||||||
const json = JSON.parse(e.target.response);
|
const json = JSON.parse(e.target.response);
|
||||||
|
|
||||||
|
@ -34,7 +34,17 @@ export default function Upload() {
|
||||||
updateNotification({
|
updateNotification({
|
||||||
id: 'upload-text',
|
id: 'upload-text',
|
||||||
title: 'Upload Successful',
|
title: 'Upload Successful',
|
||||||
message: <>Copied first file to clipboard! <br />{json.files.map(x => (<Link key={x} href={x}>{x}<br /></Link>))}</>,
|
message: (
|
||||||
|
<>
|
||||||
|
Copied first file to clipboard! <br />
|
||||||
|
{json.files.map((x) => (
|
||||||
|
<Link key={x} href={x}>
|
||||||
|
{x}
|
||||||
|
<br />
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -53,20 +63,23 @@ export default function Upload() {
|
||||||
<>
|
<>
|
||||||
<Title mb='md'>Upload Text</Title>
|
<Title mb='md'>Upload Text</Title>
|
||||||
|
|
||||||
<CodeInput
|
<CodeInput value={value} onChange={(e) => setValue(e.target.value)} />
|
||||||
value={value}
|
|
||||||
onChange={e => setValue(e.target.value)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Group position='right' mt='md'>
|
<Group position='right' mt='md'>
|
||||||
<Select
|
<Select
|
||||||
value={lang}
|
value={lang}
|
||||||
onChange={setLang}
|
onChange={setLang}
|
||||||
dropdownPosition='top'
|
dropdownPosition='top'
|
||||||
data={Object.keys(exts).map(x => ({ value: x, label: exts[x] }))}
|
data={Object.keys(exts).map((x) => ({ value: x, label: exts[x] }))}
|
||||||
icon={<TypeIcon />}
|
icon={<TypeIcon />}
|
||||||
/>
|
/>
|
||||||
<Button leftIcon={<UploadIcon />} onClick={handleUpload} disabled={value.trim().length === 0 ? true : false}>Upload</Button>
|
<Button
|
||||||
|
leftIcon={<UploadIcon />}
|
||||||
|
onClick={handleUpload}
|
||||||
|
disabled={value.trim().length === 0 ? true : false}
|
||||||
|
>
|
||||||
|
Upload
|
||||||
|
</Button>
|
||||||
</Group>
|
</Group>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
@ -5,13 +5,11 @@ import { CopyIcon, CrossIcon, DeleteIcon, ExternalLinkIcon } from 'components/ic
|
||||||
import TrashIcon from 'components/icons/TrashIcon';
|
import TrashIcon from 'components/icons/TrashIcon';
|
||||||
import { URLResponse, useURLDelete } from 'lib/queries/url';
|
import { URLResponse, useURLDelete } from 'lib/queries/url';
|
||||||
|
|
||||||
export default function URLCard({ url }: {
|
export default function URLCard({ url }: { url: URLResponse }) {
|
||||||
url: URLResponse
|
|
||||||
}) {
|
|
||||||
const clipboard = useClipboard();
|
const clipboard = useClipboard();
|
||||||
const urlDelete = useURLDelete();
|
const urlDelete = useURLDelete();
|
||||||
|
|
||||||
const copyURL = u => {
|
const copyURL = (u) => {
|
||||||
clipboard.copy(`${window.location.protocol}//${window.location.host}${u.url}`);
|
clipboard.copy(`${window.location.protocol}//${window.location.host}${u.url}`);
|
||||||
showNotification({
|
showNotification({
|
||||||
title: 'Copied to clipboard',
|
title: 'Copied to clipboard',
|
||||||
|
@ -20,7 +18,7 @@ export default function URLCard({ url }: {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const deleteURL = async u => {
|
const deleteURL = async (u) => {
|
||||||
urlDelete.mutate(u.id, {
|
urlDelete.mutate(u.id, {
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
showNotification({
|
showNotification({
|
||||||
|
|
|
@ -1,4 +1,15 @@
|
||||||
import { ActionIcon, Button, Group, Modal, SimpleGrid, Skeleton, TextInput, Title, Card, Center } from '@mantine/core';
|
import {
|
||||||
|
ActionIcon,
|
||||||
|
Button,
|
||||||
|
Group,
|
||||||
|
Modal,
|
||||||
|
SimpleGrid,
|
||||||
|
Skeleton,
|
||||||
|
TextInput,
|
||||||
|
Title,
|
||||||
|
Card,
|
||||||
|
Center,
|
||||||
|
} from '@mantine/core';
|
||||||
import { useForm } from '@mantine/form';
|
import { useForm } from '@mantine/form';
|
||||||
import { showNotification } from '@mantine/notifications';
|
import { showNotification } from '@mantine/notifications';
|
||||||
import { CrossIcon, LinkIcon, PlusIcon } from 'components/icons';
|
import { CrossIcon, LinkIcon, PlusIcon } from 'components/icons';
|
||||||
|
@ -24,11 +35,11 @@ export default function Urls() {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const onSubmit = async values => {
|
const onSubmit = async (values) => {
|
||||||
const cleanURL = values.url.trim();
|
const cleanURL = values.url.trim();
|
||||||
const cleanVanity = values.vanity.trim();
|
const cleanVanity = values.vanity.trim();
|
||||||
|
|
||||||
if (cleanURL === '') return form.setFieldError('url', 'URL can\'t be nothing');
|
if (cleanURL === '') return form.setFieldError('url', "URL can't be nothing");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
new URL(cleanURL);
|
new URL(cleanURL);
|
||||||
|
@ -45,7 +56,7 @@ export default function Urls() {
|
||||||
const res = await fetch('/api/shorten', {
|
const res = await fetch('/api/shorten', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Authorization': user.token,
|
Authorization: user.token,
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
body: JSON.stringify(data),
|
body: JSON.stringify(data),
|
||||||
|
@ -77,11 +88,7 @@ export default function Urls() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Modal
|
<Modal opened={createOpen} onClose={() => setCreateOpen(false)} title={<Title>Shorten URL</Title>}>
|
||||||
opened={createOpen}
|
|
||||||
onClose={() => setCreateOpen(false)}
|
|
||||||
title={<Title>Shorten URL</Title>}
|
|
||||||
>
|
|
||||||
<form onSubmit={form.onSubmit((v) => onSubmit(v))}>
|
<form onSubmit={form.onSubmit((v) => onSubmit(v))}>
|
||||||
<TextInput id='url' label='URL' {...form.getInputProps('url')} />
|
<TextInput id='url' label='URL' {...form.getInputProps('url')} />
|
||||||
<TextInput id='vanity' label='Vanity' {...form.getInputProps('vanity')} />
|
<TextInput id='vanity' label='Vanity' {...form.getInputProps('vanity')} />
|
||||||
|
@ -95,11 +102,12 @@ export default function Urls() {
|
||||||
|
|
||||||
<Group mb='md'>
|
<Group mb='md'>
|
||||||
<Title>URLs</Title>
|
<Title>URLs</Title>
|
||||||
<ActionIcon variant='filled' color='primary' onClick={() => setCreateOpen(true)}><PlusIcon /></ActionIcon>
|
<ActionIcon variant='filled' color='primary' onClick={() => setCreateOpen(true)}>
|
||||||
|
<PlusIcon />
|
||||||
|
</ActionIcon>
|
||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
{
|
{urls.data && urls.data.length === 0 && (
|
||||||
(urls.data && urls.data.length === 0) && (
|
|
||||||
<Card shadow='md'>
|
<Card shadow='md'>
|
||||||
<Center>
|
<Center>
|
||||||
<Group>
|
<Group>
|
||||||
|
@ -113,25 +121,12 @@ export default function Urls() {
|
||||||
</Group>
|
</Group>
|
||||||
</Center>
|
</Center>
|
||||||
</Card>
|
</Card>
|
||||||
)
|
)}
|
||||||
}
|
|
||||||
|
|
||||||
<SimpleGrid
|
<SimpleGrid cols={4} spacing='lg' breakpoints={[{ maxWidth: 'sm', cols: 1, spacing: 'sm' }]}>
|
||||||
cols={4}
|
{urls.isLoading || !urls.data
|
||||||
spacing='lg'
|
? [1, 2, 3, 4].map((x) => <Skeleton key={x} width='100%' height={80} radius='sm' />)
|
||||||
breakpoints={[
|
: urls.data.map((url) => <URLCard key={url.id} url={url} />)}
|
||||||
{ maxWidth: 'sm', cols: 1, spacing: 'sm' },
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
{
|
|
||||||
(urls.isLoading || !urls.data) ?
|
|
||||||
[1, 2, 3, 4].map(x => (
|
|
||||||
<Skeleton key={x} width='100%' height={80} radius='sm' />
|
|
||||||
))
|
|
||||||
: urls.data.map(url => (
|
|
||||||
<URLCard key={url.id} url={url} />
|
|
||||||
))
|
|
||||||
}
|
|
||||||
</SimpleGrid>
|
</SimpleGrid>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
@ -13,11 +13,11 @@ export function CreateUserModal({ open, setOpen, updateUsers }) {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const onSubmit = async values => {
|
const onSubmit = async (values) => {
|
||||||
const cleanUsername = values.username.trim();
|
const cleanUsername = values.username.trim();
|
||||||
const cleanPassword = values.password.trim();
|
const cleanPassword = values.password.trim();
|
||||||
if (cleanUsername === '') return form.setFieldError('username', 'Username can\'t be nothing');
|
if (cleanUsername === '') return form.setFieldError('username', "Username can't be nothing");
|
||||||
if (cleanPassword === '') return form.setFieldError('password', 'Password can\'t be nothing');
|
if (cleanPassword === '') return form.setFieldError('password', "Password can't be nothing");
|
||||||
|
|
||||||
const data = {
|
const data = {
|
||||||
username: cleanUsername,
|
username: cleanUsername,
|
||||||
|
@ -47,12 +47,8 @@ export function CreateUserModal({ open, setOpen, updateUsers }) {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal opened={open} onClose={() => setOpen(false)} title={<Title>Create User</Title>}>
|
||||||
opened={open}
|
<form onSubmit={form.onSubmit((v) => onSubmit(v))}>
|
||||||
onClose={() => setOpen(false)}
|
|
||||||
title={<Title>Create User</Title>}
|
|
||||||
>
|
|
||||||
<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')} />
|
||||||
<Switch mt={12} id='administrator' label='Administrator' {...form.getInputProps('administrator')} />
|
<Switch mt={12} id='administrator' label='Administrator' {...form.getInputProps('administrator')} />
|
||||||
|
|
|
@ -7,7 +7,8 @@ import useFetch from 'hooks/useFetch';
|
||||||
export function EditUserModal({ open, setOpen, updateUsers, user }) {
|
export function EditUserModal({ open, setOpen, updateUsers, user }) {
|
||||||
let form;
|
let form;
|
||||||
|
|
||||||
if (user) form = useForm({
|
if (user)
|
||||||
|
form = useForm({
|
||||||
initialValues: {
|
initialValues: {
|
||||||
username: user?.username,
|
username: user?.username,
|
||||||
password: '',
|
password: '',
|
||||||
|
@ -15,7 +16,7 @@ export function EditUserModal({ open, setOpen, updateUsers, user }) {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const onSubmit = async values => {
|
const onSubmit = async (values) => {
|
||||||
const cleanUsername = values.username.trim();
|
const cleanUsername = values.username.trim();
|
||||||
const cleanPassword = values.password.trim();
|
const cleanPassword = values.password.trim();
|
||||||
|
|
||||||
|
@ -28,7 +29,6 @@ export function EditUserModal({ open, setOpen, updateUsers, user }) {
|
||||||
if (cleanUsername !== '' && cleanUsername !== user.username) data.username = cleanUsername;
|
if (cleanUsername !== '' && cleanUsername !== user.username) data.username = cleanUsername;
|
||||||
if (cleanPassword !== '') data.password = cleanPassword;
|
if (cleanPassword !== '') data.password = cleanPassword;
|
||||||
|
|
||||||
|
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
const res = await useFetch('/api/user/' + user.id, 'PATCH', data);
|
const res = await useFetch('/api/user/' + user.id, 'PATCH', data);
|
||||||
if (res.error) {
|
if (res.error) {
|
||||||
|
@ -51,13 +51,9 @@ export function EditUserModal({ open, setOpen, updateUsers, user }) {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal opened={open} onClose={() => setOpen(false)} title={<Title>Edit User {user?.username}</Title>}>
|
||||||
opened={open}
|
|
||||||
onClose={() => setOpen(false)}
|
|
||||||
title={<Title>Edit User {user?.username}</Title>}
|
|
||||||
>
|
|
||||||
{user && (
|
{user && (
|
||||||
<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')} />
|
||||||
<Switch mt={12} id='administrator' label='Administrator' {...form.getInputProps('administrator')} />
|
<Switch mt={12} id='administrator' label='Administrator' {...form.getInputProps('administrator')} />
|
||||||
|
|
|
@ -45,7 +45,8 @@ export default function Users() {
|
||||||
};
|
};
|
||||||
|
|
||||||
// 2-step modal for deleting user if they want to delete their images too.
|
// 2-step modal for deleting user if they want to delete their images too.
|
||||||
const openDeleteModal = user => modals.openConfirmModal({
|
const openDeleteModal = (user) =>
|
||||||
|
modals.openConfirmModal({
|
||||||
title: `Delete ${user.username}?`,
|
title: `Delete ${user.username}?`,
|
||||||
closeOnConfirm: false,
|
closeOnConfirm: false,
|
||||||
labels: { confirm: 'Yes', cancel: 'No' },
|
labels: { confirm: 'Yes', cancel: 'No' },
|
||||||
|
@ -73,7 +74,7 @@ export default function Users() {
|
||||||
setUsers(us);
|
setUsers(us);
|
||||||
} else {
|
} else {
|
||||||
router.push('/dashboard');
|
router.push('/dashboard');
|
||||||
};
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -87,20 +88,25 @@ export default function Users() {
|
||||||
|
|
||||||
<Group mb='md'>
|
<Group mb='md'>
|
||||||
<Title>Users</Title>
|
<Title>Users</Title>
|
||||||
<ActionIcon variant='filled' color='primary' onClick={() => setCreateOpen(true)}><PlusIcon /></ActionIcon>
|
<ActionIcon variant='filled' color='primary' onClick={() => setCreateOpen(true)}>
|
||||||
|
<PlusIcon />
|
||||||
|
</ActionIcon>
|
||||||
</Group>
|
</Group>
|
||||||
<SimpleGrid
|
<SimpleGrid cols={3} spacing='lg' breakpoints={[{ maxWidth: 'sm', cols: 1, spacing: 'sm' }]}>
|
||||||
cols={3}
|
{users.length
|
||||||
spacing='lg'
|
? users
|
||||||
breakpoints={[
|
.filter((x) => x.username !== user.username)
|
||||||
{ maxWidth: 'sm', cols: 1, spacing: 'sm' },
|
.map((user) => (
|
||||||
]}
|
|
||||||
>
|
|
||||||
{users.length ? users.filter(x => x.username !== user.username).map(user => (
|
|
||||||
<Card key={user.id} sx={{ maxWidth: '100%' }}>
|
<Card key={user.id} sx={{ maxWidth: '100%' }}>
|
||||||
<Group position='apart'>
|
<Group position='apart'>
|
||||||
<Group position='left'>
|
<Group position='left'>
|
||||||
<Avatar size='lg' color={user.administrator ? 'primary' : 'dark'} src={user.avatar ?? null}>{user.username[0]}</Avatar>
|
<Avatar
|
||||||
|
size='lg'
|
||||||
|
color={user.administrator ? 'primary' : 'dark'}
|
||||||
|
src={user.avatar ?? null}
|
||||||
|
>
|
||||||
|
{user.username[0]}
|
||||||
|
</Avatar>
|
||||||
<Stack spacing={0}>
|
<Stack spacing={0}>
|
||||||
<Title>{user.username}</Title>
|
<Title>{user.username}</Title>
|
||||||
<MutedText size='sm'>ID: {user.id}</MutedText>
|
<MutedText size='sm'>ID: {user.id}</MutedText>
|
||||||
|
@ -110,7 +116,13 @@ export default function Users() {
|
||||||
<Group position='right'>
|
<Group position='right'>
|
||||||
{user.administrator ? null : (
|
{user.administrator ? null : (
|
||||||
<>
|
<>
|
||||||
<ActionIcon aria-label='edit' onClick={() => {setEditOpen(true); setSelectedUser(user);}}>
|
<ActionIcon
|
||||||
|
aria-label='edit'
|
||||||
|
onClick={() => {
|
||||||
|
setEditOpen(true);
|
||||||
|
setSelectedUser(user);
|
||||||
|
}}
|
||||||
|
>
|
||||||
<PencilIcon />
|
<PencilIcon />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
<ActionIcon aria-label='delete' onClick={() => openDeleteModal(user)}>
|
<ActionIcon aria-label='delete' onClick={() => openDeleteModal(user)}>
|
||||||
|
@ -118,13 +130,11 @@ export default function Users() {
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
</Group>
|
</Group>
|
||||||
</Group>
|
</Group>
|
||||||
</Card>
|
</Card>
|
||||||
)) : [1, 2, 3].map(x => (
|
))
|
||||||
<Skeleton key={x} width='100%' height={100} radius='sm' />
|
: [1, 2, 3].map((x) => <Skeleton key={x} width='100%' height={100} radius='sm' />)}
|
||||||
))}
|
|
||||||
</SimpleGrid>
|
</SimpleGrid>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
@ -63,7 +63,6 @@ export default function readConfig() {
|
||||||
map('DATASOURCE_S3_REGION', 'string', 'datasource.s3.region'),
|
map('DATASOURCE_S3_REGION', 'string', 'datasource.s3.region'),
|
||||||
map('DATASOURCE_S3_USE_SSL', 'boolean', 'datasource.s3.use_ssl'),
|
map('DATASOURCE_S3_USE_SSL', 'boolean', 'datasource.s3.use_ssl'),
|
||||||
|
|
||||||
|
|
||||||
map('DATASOURCE_SWIFT_USERNAME', 'string', 'datasource.swift.username'),
|
map('DATASOURCE_SWIFT_USERNAME', 'string', 'datasource.swift.username'),
|
||||||
map('DATASOURCE_SWIFT_PASSWORD', 'string', 'datasource.swift.password'),
|
map('DATASOURCE_SWIFT_PASSWORD', 'string', 'datasource.swift.password'),
|
||||||
map('DATASOURCE_SWIFT_AUTH_ENDPOINT', 'string', 'datasource.swift.auth_endpoint'),
|
map('DATASOURCE_SWIFT_AUTH_ENDPOINT', 'string', 'datasource.swift.auth_endpoint'),
|
||||||
|
@ -152,7 +151,7 @@ export default function readConfig() {
|
||||||
default:
|
default:
|
||||||
parsed = value;
|
parsed = value;
|
||||||
break;
|
break;
|
||||||
};
|
}
|
||||||
|
|
||||||
set(config, map.path, parsed);
|
set(config, map.path, parsed);
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,9 +3,11 @@ import { CombinedError, s, ValidationError } from '@sapphire/shapeshift';
|
||||||
import { inspect } from 'util';
|
import { inspect } from 'util';
|
||||||
import Logger from 'lib/logger';
|
import Logger from 'lib/logger';
|
||||||
|
|
||||||
const discord_content = s.object({
|
const discord_content = s
|
||||||
|
.object({
|
||||||
content: s.string.nullish.default(null),
|
content: s.string.nullish.default(null),
|
||||||
embed: s.object({
|
embed: s
|
||||||
|
.object({
|
||||||
title: s.string.nullish.default(null),
|
title: s.string.nullish.default(null),
|
||||||
description: s.string.nullish.default(null),
|
description: s.string.nullish.default(null),
|
||||||
footer: s.string.nullish.default(null),
|
footer: s.string.nullish.default(null),
|
||||||
|
@ -13,8 +15,10 @@ const discord_content = s.object({
|
||||||
thumbnail: s.boolean.default(false),
|
thumbnail: s.boolean.default(false),
|
||||||
image: s.boolean.default(true),
|
image: s.boolean.default(true),
|
||||||
timestamp: s.boolean.default(true),
|
timestamp: s.boolean.default(true),
|
||||||
}).default(null),
|
})
|
||||||
}).default(null);
|
.default(null),
|
||||||
|
})
|
||||||
|
.default(null);
|
||||||
|
|
||||||
const validator = s.object({
|
const validator = s.object({
|
||||||
core: s.object({
|
core: s.object({
|
||||||
|
@ -27,11 +31,14 @@ const validator = s.object({
|
||||||
stats_interval: s.number.default(1800),
|
stats_interval: s.number.default(1800),
|
||||||
invites_interval: s.number.default(1800),
|
invites_interval: s.number.default(1800),
|
||||||
}),
|
}),
|
||||||
datasource: s.object({
|
datasource: s
|
||||||
|
.object({
|
||||||
type: s.enum('local', 's3', 'swift').default('local'),
|
type: s.enum('local', 's3', 'swift').default('local'),
|
||||||
local: s.object({
|
local: s
|
||||||
|
.object({
|
||||||
directory: s.string.default('./uploads'),
|
directory: s.string.default('./uploads'),
|
||||||
}).default({
|
})
|
||||||
|
.default({
|
||||||
directory: './uploads',
|
directory: './uploads',
|
||||||
}),
|
}),
|
||||||
s3: s.object({
|
s3: s.object({
|
||||||
|
@ -52,7 +59,8 @@ const validator = s.object({
|
||||||
domain_id: s.string.default('default'),
|
domain_id: s.string.default('default'),
|
||||||
region_id: s.string.nullable,
|
region_id: s.string.nullable,
|
||||||
}).optional,
|
}).optional,
|
||||||
}).default({
|
})
|
||||||
|
.default({
|
||||||
type: 'local',
|
type: 'local',
|
||||||
local: {
|
local: {
|
||||||
directory: './uploads',
|
directory: './uploads',
|
||||||
|
@ -65,7 +73,8 @@ const validator = s.object({
|
||||||
domain_id: 'default',
|
domain_id: 'default',
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
uploader: s.object({
|
uploader: s
|
||||||
|
.object({
|
||||||
route: s.string.default('/u'),
|
route: s.string.default('/u'),
|
||||||
embed_route: s.string.default('/a'),
|
embed_route: s.string.default('/a'),
|
||||||
length: s.number.default(6),
|
length: s.number.default(6),
|
||||||
|
@ -73,7 +82,8 @@ const validator = s.object({
|
||||||
user_limit: s.number.default(104900000),
|
user_limit: s.number.default(104900000),
|
||||||
disabled_extensions: s.string.array.default([]),
|
disabled_extensions: s.string.array.default([]),
|
||||||
format_date: s.string.default('YYYY-MM-DD_HH:mm:ss'),
|
format_date: s.string.default('YYYY-MM-DD_HH:mm:ss'),
|
||||||
}).default({
|
})
|
||||||
|
.default({
|
||||||
route: '/u',
|
route: '/u',
|
||||||
embed_route: '/a',
|
embed_route: '/a',
|
||||||
length: 6,
|
length: 6,
|
||||||
|
@ -82,34 +92,44 @@ const validator = s.object({
|
||||||
disabled_extensions: [],
|
disabled_extensions: [],
|
||||||
format_date: 'YYYY-MM-DD_HH:mm:ss',
|
format_date: 'YYYY-MM-DD_HH:mm:ss',
|
||||||
}),
|
}),
|
||||||
urls: s.object({
|
urls: s
|
||||||
|
.object({
|
||||||
route: s.string.default('/go'),
|
route: s.string.default('/go'),
|
||||||
length: s.number.default(6),
|
length: s.number.default(6),
|
||||||
}).default({
|
})
|
||||||
|
.default({
|
||||||
route: '/go',
|
route: '/go',
|
||||||
length: 6,
|
length: 6,
|
||||||
}),
|
}),
|
||||||
ratelimit: s.object({
|
ratelimit: s
|
||||||
|
.object({
|
||||||
user: s.number.default(0),
|
user: s.number.default(0),
|
||||||
admin: s.number.default(0),
|
admin: s.number.default(0),
|
||||||
}).default({
|
})
|
||||||
|
.default({
|
||||||
user: 0,
|
user: 0,
|
||||||
admin: 0,
|
admin: 0,
|
||||||
}),
|
}),
|
||||||
website: s.object({
|
website: s
|
||||||
|
.object({
|
||||||
title: s.string.default('Zipline'),
|
title: s.string.default('Zipline'),
|
||||||
show_files_per_user: s.boolean.default(true),
|
show_files_per_user: s.boolean.default(true),
|
||||||
show_version: s.boolean.default(true),
|
show_version: s.boolean.default(true),
|
||||||
disable_media_preview: s.boolean.default(false),
|
disable_media_preview: s.boolean.default(false),
|
||||||
|
|
||||||
external_links: s.array(s.object({
|
external_links: s
|
||||||
|
.array(
|
||||||
|
s.object({
|
||||||
label: s.string,
|
label: s.string,
|
||||||
link: s.string,
|
link: s.string,
|
||||||
})).default([
|
})
|
||||||
|
)
|
||||||
|
.default([
|
||||||
{ label: 'Zipline', link: 'https://github.com/diced/zipline' },
|
{ label: 'Zipline', link: 'https://github.com/diced/zipline' },
|
||||||
{ label: 'Documentation', link: 'https://zipline.diced.tech/' },
|
{ label: 'Documentation', link: 'https://zipline.diced.tech/' },
|
||||||
]),
|
]),
|
||||||
}).default({
|
})
|
||||||
|
.default({
|
||||||
title: 'Zipline',
|
title: 'Zipline',
|
||||||
show_files_per_user: true,
|
show_files_per_user: true,
|
||||||
show_version: true,
|
show_version: true,
|
||||||
|
@ -123,21 +143,27 @@ const validator = s.object({
|
||||||
discord: s.object({
|
discord: s.object({
|
||||||
url: s.string,
|
url: s.string,
|
||||||
username: s.string.default('Zipline'),
|
username: s.string.default('Zipline'),
|
||||||
avatar_url: s.string.default('https://raw.githubusercontent.com/diced/zipline/9b60147e112ec5b70170500b85c75ea621f41d03/public/zipline.png'),
|
avatar_url: s.string.default(
|
||||||
|
'https://raw.githubusercontent.com/diced/zipline/9b60147e112ec5b70170500b85c75ea621f41d03/public/zipline.png'
|
||||||
|
),
|
||||||
upload: discord_content,
|
upload: discord_content,
|
||||||
shorten: discord_content,
|
shorten: discord_content,
|
||||||
}),
|
}),
|
||||||
oauth: s.object({
|
oauth: s
|
||||||
|
.object({
|
||||||
github_client_id: s.string.nullable.default(null),
|
github_client_id: s.string.nullable.default(null),
|
||||||
github_client_secret: s.string.nullable.default(null),
|
github_client_secret: s.string.nullable.default(null),
|
||||||
|
|
||||||
discord_client_id: s.string.nullable.default(null),
|
discord_client_id: s.string.nullable.default(null),
|
||||||
discord_client_secret: s.string.nullable.default(null),
|
discord_client_secret: s.string.nullable.default(null),
|
||||||
}).nullish.default(null),
|
})
|
||||||
features: s.object({
|
.nullish.default(null),
|
||||||
|
features: s
|
||||||
|
.object({
|
||||||
invites: s.boolean.default(true),
|
invites: s.boolean.default(true),
|
||||||
oauth_registration: s.boolean.default(false),
|
oauth_registration: s.boolean.default(false),
|
||||||
}).default({ invites: true, oauth_registration: false }),
|
})
|
||||||
|
.default({ invites: true, oauth_registration: false }),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default function validate(config): Config {
|
export default function validate(config): Config {
|
||||||
|
@ -150,10 +176,8 @@ export default function validate(config): Config {
|
||||||
errors.push('datasource.s3.access_key_id is a required field');
|
errors.push('datasource.s3.access_key_id is a required field');
|
||||||
if (!validated.datasource.s3.secret_access_key)
|
if (!validated.datasource.s3.secret_access_key)
|
||||||
errors.push('datasource.s3.secret_access_key is a required field');
|
errors.push('datasource.s3.secret_access_key is a required field');
|
||||||
if (!validated.datasource.s3.bucket)
|
if (!validated.datasource.s3.bucket) errors.push('datasource.s3.bucket is a required field');
|
||||||
errors.push('datasource.s3.bucket is a required field');
|
if (!validated.datasource.s3.endpoint) errors.push('datasource.s3.endpoint is a required field');
|
||||||
if (!validated.datasource.s3.endpoint)
|
|
||||||
errors.push('datasource.s3.endpoint is a required field');
|
|
||||||
if (errors.length) throw { errors };
|
if (errors.length) throw { errors };
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,9 +7,7 @@ export class S3 extends Datasource {
|
||||||
public name: string = 'S3';
|
public name: string = 'S3';
|
||||||
public s3: Client;
|
public s3: Client;
|
||||||
|
|
||||||
public constructor(
|
public constructor(public config: ConfigS3Datasource) {
|
||||||
public config: ConfigS3Datasource,
|
|
||||||
) {
|
|
||||||
super();
|
super();
|
||||||
this.s3 = new Client({
|
this.s3 = new Client({
|
||||||
endPoint: config.endpoint,
|
endPoint: config.endpoint,
|
||||||
|
@ -55,8 +53,8 @@ export class S3 extends Datasource {
|
||||||
const objects = this.s3.listObjectsV2(this.config.bucket, '', true);
|
const objects = this.s3.listObjectsV2(this.config.bucket, '', true);
|
||||||
let size = 0;
|
let size = 0;
|
||||||
|
|
||||||
objects.on('data', item => size += item.size);
|
objects.on('data', (item) => (size += item.size));
|
||||||
objects.on('end', err => {
|
objects.on('end', (err) => {
|
||||||
if (err) rej(err);
|
if (err) rej(err);
|
||||||
else res(size);
|
else res(size);
|
||||||
});
|
});
|
||||||
|
|
|
@ -44,9 +44,7 @@ class SwiftContainer {
|
||||||
const endpoint = catalogEntry.endpoints.find(
|
const endpoint = catalogEntry.endpoints.find(
|
||||||
(x: any) =>
|
(x: any) =>
|
||||||
x.interface === (this.options.credentials.interface || 'public') &&
|
x.interface === (this.options.credentials.interface || 'public') &&
|
||||||
(this.options.credentials.region_id
|
(this.options.credentials.region_id ? x.region_id == this.options.credentials.region_id : true)
|
||||||
? x.region_id == this.options.credentials.region_id
|
|
||||||
: true)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return endpoint ? endpoint.url : null;
|
return endpoint ? endpoint.url : null;
|
||||||
|
@ -94,7 +92,8 @@ class SwiftContainer {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (error || !json || !headers || json.error) throw new Error('Could not retrieve credentials from OpenStack, check your config file');
|
if (error || !json || !headers || json.error)
|
||||||
|
throw new Error('Could not retrieve credentials from OpenStack, check your config file');
|
||||||
|
|
||||||
const catalog = json.token.catalog;
|
const catalog = json.token.catalog;
|
||||||
// many Swift clouds use ceph radosgw to provide swift
|
// many Swift clouds use ceph radosgw to provide swift
|
||||||
|
@ -124,10 +123,15 @@ class SwiftContainer {
|
||||||
|
|
||||||
public async listObjects(query?: string): Promise<SwiftObject[]> {
|
public async listObjects(query?: string): Promise<SwiftObject[]> {
|
||||||
const auth = await this.authenticate();
|
const auth = await this.authenticate();
|
||||||
return await fetch(`${auth.swiftURL}/${this.options.credentials.container}${query ? `${query.startsWith('?') ? '' : '?'}${query}` : ''}`, {
|
return await fetch(
|
||||||
|
`${auth.swiftURL}/${this.options.credentials.container}${
|
||||||
|
query ? `${query.startsWith('?') ? '' : '?'}${query}` : ''
|
||||||
|
}`,
|
||||||
|
{
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: this.generateHeaders(auth.token),
|
headers: this.generateHeaders(auth.token),
|
||||||
}).then((e) => e.json());
|
}
|
||||||
|
).then((e) => e.json());
|
||||||
}
|
}
|
||||||
|
|
||||||
public async uploadObject(name: string, data: Buffer): Promise<any> {
|
public async uploadObject(name: string, data: Buffer): Promise<any> {
|
||||||
|
|
|
@ -15,7 +15,8 @@ function parse(str: string, args: Args) {
|
||||||
.replace(/{user\.name}/gi, args[0].username)
|
.replace(/{user\.name}/gi, args[0].username)
|
||||||
.replace(/{link}/gi, args[3]);
|
.replace(/{link}/gi, args[3]);
|
||||||
|
|
||||||
if (args[1]) str = str
|
if (args[1])
|
||||||
|
str = str
|
||||||
.replace(/{file\.id}/gi, args[1].id.toString())
|
.replace(/{file\.id}/gi, args[1].id.toString())
|
||||||
.replace(/{file\.mime}/gi, args[1].mimetype)
|
.replace(/{file\.mime}/gi, args[1].mimetype)
|
||||||
.replace(/{file\.file}/gi, args[1].file)
|
.replace(/{file\.file}/gi, args[1].file)
|
||||||
|
@ -23,7 +24,8 @@ function parse(str: string, args: Args) {
|
||||||
.replace(/{file\.created_at.time_string}/gi, args[1].created_at.toLocaleTimeString())
|
.replace(/{file\.created_at.time_string}/gi, args[1].created_at.toLocaleTimeString())
|
||||||
.replace(/{file\.created_at.date_string}/gi, args[1].created_at.toLocaleDateString());
|
.replace(/{file\.created_at.date_string}/gi, args[1].created_at.toLocaleDateString());
|
||||||
|
|
||||||
if (args[2]) str = str
|
if (args[2])
|
||||||
|
str = str
|
||||||
.replace(/{url\.id}/gi, args[2].id.toString())
|
.replace(/{url\.id}/gi, args[2].id.toString())
|
||||||
.replace(/{url\.vanity}/gi, args[2].vanity ? args[2].vanity : 'none')
|
.replace(/{url\.vanity}/gi, args[2].vanity ? args[2].vanity : 'none')
|
||||||
.replace(/{url\.destination}/gi, args[2].destination)
|
.replace(/{url\.destination}/gi, args[2].destination)
|
||||||
|
@ -34,10 +36,14 @@ function parse(str: string, args: Args) {
|
||||||
return str;
|
return str;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function parseContent(content: ConfigDiscordContent, args: Args): ConfigDiscordContent & { url: string } {
|
export function parseContent(
|
||||||
|
content: ConfigDiscordContent,
|
||||||
|
args: Args
|
||||||
|
): ConfigDiscordContent & { url: string } {
|
||||||
return {
|
return {
|
||||||
content: parse(content.content, args),
|
content: parse(content.content, args),
|
||||||
embed: content.embed ? {
|
embed: content.embed
|
||||||
|
? {
|
||||||
title: parse(content.embed.title, args),
|
title: parse(content.embed.title, args),
|
||||||
description: parse(content.embed.description, args),
|
description: parse(content.embed.description, args),
|
||||||
footer: parse(content.embed.footer, args),
|
footer: parse(content.embed.footer, args),
|
||||||
|
@ -45,7 +51,8 @@ export function parseContent(content: ConfigDiscordContent, args: Args): ConfigD
|
||||||
thumbnail: content.embed.thumbnail,
|
thumbnail: content.embed.thumbnail,
|
||||||
timestamp: content.embed.timestamp,
|
timestamp: content.embed.timestamp,
|
||||||
image: content.embed.image,
|
image: content.embed.image,
|
||||||
} : null,
|
}
|
||||||
|
: null,
|
||||||
url: args[3],
|
url: args[3],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -60,22 +67,34 @@ export async function sendUpload(user: User, image: Image, host: string) {
|
||||||
username: config.discord.username,
|
username: config.discord.username,
|
||||||
avatar_url: config.discord.avatar_url,
|
avatar_url: config.discord.avatar_url,
|
||||||
content: parsed.content ?? null,
|
content: parsed.content ?? null,
|
||||||
embeds: parsed.embed ? [{
|
embeds: parsed.embed
|
||||||
|
? [
|
||||||
|
{
|
||||||
title: parsed.embed.title ?? null,
|
title: parsed.embed.title ?? null,
|
||||||
description: parsed.embed.description ?? null,
|
description: parsed.embed.description ?? null,
|
||||||
url: parsed.url ?? null,
|
url: parsed.url ?? null,
|
||||||
timestamp: parsed.embed.timestamp ? image.created_at.toISOString() : null,
|
timestamp: parsed.embed.timestamp ? image.created_at.toISOString() : null,
|
||||||
color: parsed.embed.color ?? null,
|
color: parsed.embed.color ?? null,
|
||||||
footer: parsed.embed.footer ? {
|
footer: parsed.embed.footer
|
||||||
|
? {
|
||||||
text: parsed.embed.footer,
|
text: parsed.embed.footer,
|
||||||
} : null,
|
}
|
||||||
thumbnail: isImage && parsed.embed.thumbnail ? {
|
: null,
|
||||||
|
thumbnail:
|
||||||
|
isImage && parsed.embed.thumbnail
|
||||||
|
? {
|
||||||
url: parsed.url,
|
url: parsed.url,
|
||||||
} : null,
|
}
|
||||||
image: isImage && parsed.embed.image ? {
|
: null,
|
||||||
|
image:
|
||||||
|
isImage && parsed.embed.image
|
||||||
|
? {
|
||||||
url: parsed.url,
|
url: parsed.url,
|
||||||
} : null,
|
}
|
||||||
}] : null,
|
: null,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
const res = await fetch(config.discord.url, {
|
const res = await fetch(config.discord.url, {
|
||||||
|
@ -88,7 +107,9 @@ export async function sendUpload(user: User, image: Image, host: string) {
|
||||||
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
const text = await res.text();
|
const text = await res.text();
|
||||||
Logger.get('discord').error(`Failed to send upload notification to discord: ${res.status} ${res.statusText}`);
|
Logger.get('discord').error(
|
||||||
|
`Failed to send upload notification to discord: ${res.status} ${res.statusText}`
|
||||||
|
);
|
||||||
Logger.get('discord').error(`Received response: ${text}`);
|
Logger.get('discord').error(`Received response: ${text}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -104,16 +125,22 @@ export async function sendShorten(user: User, url: Url, host: string) {
|
||||||
username: config.discord.username,
|
username: config.discord.username,
|
||||||
avatar_url: config.discord.avatar_url,
|
avatar_url: config.discord.avatar_url,
|
||||||
content: parsed.content ?? null,
|
content: parsed.content ?? null,
|
||||||
embeds: parsed.embed ? [{
|
embeds: parsed.embed
|
||||||
|
? [
|
||||||
|
{
|
||||||
title: parsed.embed.title ?? null,
|
title: parsed.embed.title ?? null,
|
||||||
description: parsed.embed.description ?? null,
|
description: parsed.embed.description ?? null,
|
||||||
url: parsed.url ?? null,
|
url: parsed.url ?? null,
|
||||||
timestamp: parsed.embed.timestamp ? url.created_at.toISOString() : null,
|
timestamp: parsed.embed.timestamp ? url.created_at.toISOString() : null,
|
||||||
color: parsed.embed.color ?? null,
|
color: parsed.embed.color ?? null,
|
||||||
footer: parsed.embed.footer ? {
|
footer: parsed.embed.footer
|
||||||
|
? {
|
||||||
text: parsed.embed.footer,
|
text: parsed.embed.footer,
|
||||||
} : null,
|
}
|
||||||
}] : null,
|
: null,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
const res = await fetch(config.discord.url, {
|
const res = await fetch(config.discord.url, {
|
||||||
|
@ -125,7 +152,9 @@ export async function sendShorten(user: User, url: Url, host: string) {
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
Logger.get('discord').error(`Failed to send url shorten notification to discord: ${res.status} ${res.statusText}`);
|
Logger.get('discord').error(
|
||||||
|
`Failed to send url shorten notification to discord: ${res.status} ${res.statusText}`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return;
|
return;
|
||||||
|
|
|
@ -1,45 +1,45 @@
|
||||||
// https://github.com/toptal/haste-server/blob/master/static/application.js#L167-L174
|
// https://github.com/toptal/haste-server/blob/master/static/application.js#L167-L174
|
||||||
// Popular extension map
|
// Popular extension map
|
||||||
const exts = {
|
const exts = {
|
||||||
'md': 'Markdown',
|
md: 'Markdown',
|
||||||
'css': 'CSS',
|
css: 'CSS',
|
||||||
'js': 'JavaScript',
|
js: 'JavaScript',
|
||||||
'json': 'JSON',
|
json: 'JSON',
|
||||||
'html': 'HTML',
|
html: 'HTML',
|
||||||
'ts': 'TypeScript',
|
ts: 'TypeScript',
|
||||||
'java': 'Java',
|
java: 'Java',
|
||||||
'py': 'Python',
|
py: 'Python',
|
||||||
'rb': 'Ruby',
|
rb: 'Ruby',
|
||||||
'sh': 'Shell',
|
sh: 'Shell',
|
||||||
'php': 'PHP',
|
php: 'PHP',
|
||||||
'pl': 'Perl',
|
pl: 'Perl',
|
||||||
'sql': 'SQL',
|
sql: 'SQL',
|
||||||
'xml': 'XML',
|
xml: 'XML',
|
||||||
'yml': 'YAML',
|
yml: 'YAML',
|
||||||
'yaml': 'YAML',
|
yaml: 'YAML',
|
||||||
'c': 'C',
|
c: 'C',
|
||||||
'cpp': 'C++',
|
cpp: 'C++',
|
||||||
'cs': 'C#',
|
cs: 'C#',
|
||||||
'go': 'Go',
|
go: 'Go',
|
||||||
'h': 'C/C++ Header',
|
h: 'C/C++ Header',
|
||||||
'txt': 'Text',
|
txt: 'Text',
|
||||||
'dockerfile': 'Dockerfile',
|
dockerfile: 'Dockerfile',
|
||||||
'toml': 'TOML',
|
toml: 'TOML',
|
||||||
'ini': 'INI',
|
ini: 'INI',
|
||||||
'bat': 'Batch File',
|
bat: 'Batch File',
|
||||||
'tex': 'TeX',
|
tex: 'TeX',
|
||||||
'r': 'R',
|
r: 'R',
|
||||||
'lua': 'Lua',
|
lua: 'Lua',
|
||||||
'ps1': 'PowerShell',
|
ps1: 'PowerShell',
|
||||||
'rst': 'reStructuredText',
|
rst: 'reStructuredText',
|
||||||
'rs': 'Rust',
|
rs: 'Rust',
|
||||||
'swift': 'Swift',
|
swift: 'Swift',
|
||||||
'scss': 'SCSS',
|
scss: 'SCSS',
|
||||||
'less': 'LESS',
|
less: 'LESS',
|
||||||
'scala': 'Scala',
|
scala: 'Scala',
|
||||||
'kotlin': 'Kotlin',
|
kotlin: 'Kotlin',
|
||||||
'vb': 'Visual Basic',
|
vb: 'Visual Basic',
|
||||||
'vim': 'Vim Script',
|
vim: 'Vim Script',
|
||||||
};
|
};
|
||||||
|
|
||||||
export default exts;
|
export default exts;
|
|
@ -1,4 +1,8 @@
|
||||||
export default async function useFetch(url: string, method: 'GET' | 'POST' | 'PATCH' | 'DELETE' = 'GET', body: Record<string, any> = null) {
|
export default async function useFetch(
|
||||||
|
url: string,
|
||||||
|
method: 'GET' | 'POST' | 'PATCH' | 'DELETE' = 'GET',
|
||||||
|
body: Record<string, any> = null
|
||||||
|
) {
|
||||||
const headers = {};
|
const headers = {};
|
||||||
if (body) headers['content-type'] = 'application/json';
|
if (body) headers['content-type'] = 'application/json';
|
||||||
|
|
||||||
|
|
|
@ -10,8 +10,7 @@ export default class Logger {
|
||||||
public name: string;
|
public name: string;
|
||||||
|
|
||||||
static get(clas: any) {
|
static get(clas: any) {
|
||||||
if (typeof clas !== 'function')
|
if (typeof clas !== 'function') if (typeof clas !== 'string') throw new Error('not string/function');
|
||||||
if (typeof clas !== 'string') throw new Error('not string/function');
|
|
||||||
|
|
||||||
const name = clas.name ?? clas;
|
const name = clas.name ?? clas;
|
||||||
|
|
||||||
|
@ -28,11 +27,7 @@ export default class Logger {
|
||||||
|
|
||||||
error(...args: any[]) {
|
error(...args: any[]) {
|
||||||
console.log(
|
console.log(
|
||||||
this.formatMessage(
|
this.formatMessage(LoggerLevel.ERROR, this.name, args.map((error) => error.stack ?? error).join(' '))
|
||||||
LoggerLevel.ERROR,
|
|
||||||
this.name,
|
|
||||||
args.map((error) => error.stack ?? error).join(' ')
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,18 +3,20 @@ import { discord_auth, github_auth } from 'lib/oauth';
|
||||||
import { notNull } from 'lib/util';
|
import { notNull } from 'lib/util';
|
||||||
import { GetServerSideProps } from 'next';
|
import { GetServerSideProps } from 'next';
|
||||||
|
|
||||||
export const getServerSideProps: GetServerSideProps = async ctx => {
|
export const getServerSideProps: GetServerSideProps = async (ctx) => {
|
||||||
// this entire thing will also probably change before the stable release
|
// this entire thing will also probably change before the stable release
|
||||||
const ghEnabled = notNull(config.oauth?.github_client_id, config.oauth?.github_client_secret);
|
const ghEnabled = notNull(config.oauth?.github_client_id, config.oauth?.github_client_secret);
|
||||||
const discEnabled = notNull(config.oauth?.discord_client_id, config.oauth?.discord_client_secret);
|
const discEnabled = notNull(config.oauth?.discord_client_id, config.oauth?.discord_client_secret);
|
||||||
|
|
||||||
const oauth_providers = [];
|
const oauth_providers = [];
|
||||||
|
|
||||||
if (ghEnabled) oauth_providers.push({
|
if (ghEnabled)
|
||||||
|
oauth_providers.push({
|
||||||
name: 'GitHub',
|
name: 'GitHub',
|
||||||
url: '/api/auth/oauth/github',
|
url: '/api/auth/oauth/github',
|
||||||
});
|
});
|
||||||
if (discEnabled) oauth_providers.push({
|
if (discEnabled)
|
||||||
|
oauth_providers.push({
|
||||||
name: 'Discord',
|
name: 'Discord',
|
||||||
url: '/api/auth/oauth/discord',
|
url: '/api/auth/oauth/discord',
|
||||||
});
|
});
|
||||||
|
|
|
@ -21,7 +21,7 @@ export type NextApiReq = NextApiRequest & {
|
||||||
getCookie: (name: string) => string | null;
|
getCookie: (name: string) => string | null;
|
||||||
cleanCookie: (name: string) => void;
|
cleanCookie: (name: string) => void;
|
||||||
files?: NextApiFile[];
|
files?: NextApiFile[];
|
||||||
}
|
};
|
||||||
|
|
||||||
export type NextApiRes = NextApiResponse & {
|
export type NextApiRes = NextApiResponse & {
|
||||||
error: (message: string) => void;
|
error: (message: string) => void;
|
||||||
|
@ -30,31 +30,41 @@ export type NextApiRes = NextApiResponse & {
|
||||||
json: (json: Record<string, any>, status?: number) => void;
|
json: (json: Record<string, any>, status?: number) => void;
|
||||||
ratelimited: (remaining: number) => void;
|
ratelimited: (remaining: number) => void;
|
||||||
setCookie: (name: string, value: unknown, options: CookieSerializeOptions) => void;
|
setCookie: (name: string, value: unknown, options: CookieSerializeOptions) => void;
|
||||||
}
|
};
|
||||||
|
|
||||||
export const withZipline = (handler: (req: NextApiRequest, res: NextApiResponse) => unknown) => (req: NextApiReq, res: NextApiRes) => {
|
export const withZipline =
|
||||||
|
(handler: (req: NextApiRequest, res: NextApiResponse) => unknown) => (req: NextApiReq, res: NextApiRes) => {
|
||||||
res.setHeader('Content-Type', 'application/json');
|
res.setHeader('Content-Type', 'application/json');
|
||||||
res.setHeader('Access-Control-Allow-Origin', '*');
|
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||||
res.setHeader('Access-Content-Allow-Methods', 'GET,HEAD,POST,OPTIONS');
|
res.setHeader('Access-Content-Allow-Methods', 'GET,HEAD,POST,OPTIONS');
|
||||||
res.setHeader('Access-Control-Max-Age', '86400');
|
res.setHeader('Access-Control-Max-Age', '86400');
|
||||||
|
|
||||||
res.error = (message: string) => {
|
res.error = (message: string) => {
|
||||||
res.json({
|
res.json(
|
||||||
|
{
|
||||||
error: message,
|
error: message,
|
||||||
}, 500);
|
},
|
||||||
|
500
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
res.forbid = (message: string, extra: any = {}) => {
|
res.forbid = (message: string, extra: any = {}) => {
|
||||||
res.json({
|
res.json(
|
||||||
|
{
|
||||||
error: '403: ' + message,
|
error: '403: ' + message,
|
||||||
...extra,
|
...extra,
|
||||||
}, 403);
|
},
|
||||||
|
403
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
res.bad = (message: string) => {
|
res.bad = (message: string) => {
|
||||||
res.json({
|
res.json(
|
||||||
|
{
|
||||||
error: '401: ' + message,
|
error: '401: ' + message,
|
||||||
}, 401);
|
},
|
||||||
|
401
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
res.ratelimited = (remaining: number) => {
|
res.ratelimited = (remaining: number) => {
|
||||||
|
@ -79,11 +89,14 @@ export const withZipline = (handler: (req: NextApiRequest, res: NextApiResponse)
|
||||||
return unsigned ? unsigned : null;
|
return unsigned ? unsigned : null;
|
||||||
};
|
};
|
||||||
req.cleanCookie = (name: string) => {
|
req.cleanCookie = (name: string) => {
|
||||||
res.setHeader('Set-Cookie', serialize(name, '', {
|
res.setHeader(
|
||||||
|
'Set-Cookie',
|
||||||
|
serialize(name, '', {
|
||||||
path: '/',
|
path: '/',
|
||||||
expires: new Date(1),
|
expires: new Date(1),
|
||||||
maxAge: undefined,
|
maxAge: undefined,
|
||||||
}));
|
})
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
req.user = async () => {
|
req.user = async () => {
|
||||||
|
@ -107,7 +120,8 @@ export const withZipline = (handler: (req: NextApiRequest, res: NextApiResponse)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
res.setCookie = (name: string, value: unknown, options?: CookieSerializeOptions) => setCookie(res, name, value, options || {});
|
res.setCookie = (name: string, value: unknown, options?: CookieSerializeOptions) =>
|
||||||
|
setCookie(res, name, value, options || {});
|
||||||
|
|
||||||
return handler(req, res);
|
return handler(req, res);
|
||||||
};
|
};
|
||||||
|
@ -118,7 +132,6 @@ export const setCookie = (
|
||||||
value: unknown,
|
value: unknown,
|
||||||
options: CookieSerializeOptions = {}
|
options: CookieSerializeOptions = {}
|
||||||
) => {
|
) => {
|
||||||
|
|
||||||
if ('maxAge' in options) {
|
if ('maxAge' in options) {
|
||||||
options.expires = new Date(Date.now() + options.maxAge * 1000);
|
options.expires = new Date(Date.now() + options.maxAge * 1000);
|
||||||
options.maxAge /= 1000;
|
options.maxAge /= 1000;
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
import { readFile } from 'fs/promises';
|
import { readFile } from 'fs/promises';
|
||||||
|
|
||||||
export type Mimes = [string, string[]][]
|
export type Mimes = [string, string[]][];
|
||||||
|
|
||||||
export async function guess(extension: string): Promise<string> {
|
export async function guess(extension: string): Promise<string> {
|
||||||
const mimes: Mimes = JSON.parse(await readFile('./mimes.json', 'utf8'));
|
const mimes: Mimes = JSON.parse(await readFile('./mimes.json', 'utf8'));
|
||||||
|
|
||||||
const mime = mimes.find(x => x[0] === extension);
|
const mime = mimes.find((x) => x[0] === extension);
|
||||||
if (!mime) return 'application/octet-stream';
|
if (!mime) return 'application/octet-stream';
|
||||||
|
|
||||||
return mime[1][0];
|
return mime[1][0];
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
export const github_auth = {
|
export const github_auth = {
|
||||||
oauth_url: (clientId: string) => `https://github.com/login/oauth/authorize?client_id=${clientId}&scope=user`,
|
oauth_url: (clientId: string) =>
|
||||||
|
`https://github.com/login/oauth/authorize?client_id=${clientId}&scope=user`,
|
||||||
oauth_user: async (access_token: string) => {
|
oauth_user: async (access_token: string) => {
|
||||||
const res = await fetch('https://api.github.com/user', {
|
const res = await fetch('https://api.github.com/user', {
|
||||||
headers: {
|
headers: {
|
||||||
'Authorization': `Bearer ${access_token}`,
|
Authorization: `Bearer ${access_token}`,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
if (!res.ok) return null;
|
if (!res.ok) return null;
|
||||||
|
@ -13,11 +14,14 @@ export const github_auth = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const discord_auth = {
|
export const discord_auth = {
|
||||||
oauth_url: (clientId: string, origin: string) => `https://discord.com/api/oauth2/authorize?client_id=${clientId}&redirect_uri=${encodeURIComponent(`${origin}/api/auth/oauth/discord`)}&response_type=code&scope=identify`,
|
oauth_url: (clientId: string, origin: string) =>
|
||||||
|
`https://discord.com/api/oauth2/authorize?client_id=${clientId}&redirect_uri=${encodeURIComponent(
|
||||||
|
`${origin}/api/auth/oauth/discord`
|
||||||
|
)}&response_type=code&scope=identify`,
|
||||||
oauth_user: async (access_token: string) => {
|
oauth_user: async (access_token: string) => {
|
||||||
const res = await fetch('https://discord.com/api/users/@me', {
|
const res = await fetch('https://discord.com/api/users/@me', {
|
||||||
headers: {
|
headers: {
|
||||||
'Authorization': `Bearer ${access_token}`,
|
Authorization: `Bearer ${access_token}`,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
if (!res.ok) return null;
|
if (!res.ok) return null;
|
||||||
|
|
|
@ -2,6 +2,6 @@ import { PrismaClient } from '@prisma/client';
|
||||||
|
|
||||||
if (!global.prisma) {
|
if (!global.prisma) {
|
||||||
if (!process.env.ZIPLINE_DOCKER_BUILD) global.prisma = new PrismaClient();
|
if (!process.env.ZIPLINE_DOCKER_BUILD) global.prisma = new PrismaClient();
|
||||||
};
|
}
|
||||||
|
|
||||||
export default global.prisma;
|
export default global.prisma;
|
|
@ -9,7 +9,7 @@ export type UserFilesResponse = {
|
||||||
id: string;
|
id: string;
|
||||||
favorite: boolean;
|
favorite: boolean;
|
||||||
url: string;
|
url: string;
|
||||||
}
|
};
|
||||||
|
|
||||||
export const useFiles = (query: { [key: string]: string } = {}) => {
|
export const useFiles = (query: { [key: string]: string } = {}) => {
|
||||||
const queryBuilder = new URLSearchParams(query);
|
const queryBuilder = new URLSearchParams(query);
|
||||||
|
@ -17,11 +17,11 @@ export const useFiles = (query: { [key: string]: string } = {}) => {
|
||||||
|
|
||||||
return useQuery<UserFilesResponse[]>(['files', queryString], async () => {
|
return useQuery<UserFilesResponse[]>(['files', queryString], async () => {
|
||||||
return fetch('/api/user/files?' + queryString)
|
return fetch('/api/user/files?' + queryString)
|
||||||
.then(res => res.json() as Promise<UserFilesResponse[]>)
|
.then((res) => res.json() as Promise<UserFilesResponse[]>)
|
||||||
.then(data =>
|
.then((data) =>
|
||||||
query.paged === 'true'
|
query.paged === 'true'
|
||||||
? data
|
? data
|
||||||
: data.map(x => ({
|
: data.map((x) => ({
|
||||||
...x,
|
...x,
|
||||||
created_at: new Date(x.created_at).toLocaleString(),
|
created_at: new Date(x.created_at).toLocaleString(),
|
||||||
}))
|
}))
|
||||||
|
@ -31,57 +31,65 @@ export const useFiles = (query: { [key: string]: string } = {}) => {
|
||||||
|
|
||||||
export const usePaginatedFiles = (query: { [key: string]: string } = {}) => {
|
export const usePaginatedFiles = (query: { [key: string]: string } = {}) => {
|
||||||
query['paged'] = 'true';
|
query['paged'] = 'true';
|
||||||
const data = useFiles(query) as ReturnType<typeof useQuery> & { data: UserFilesResponse[][] };
|
const data = useFiles(query) as ReturnType<typeof useQuery> & {
|
||||||
|
data: UserFilesResponse[][];
|
||||||
|
};
|
||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useRecent = (filter?: string) => {
|
export const useRecent = (filter?: string) => {
|
||||||
return useQuery<UserFilesResponse[]>(['recent', filter], async () => {
|
return useQuery<UserFilesResponse[]>(['recent', filter], async () => {
|
||||||
return fetch(`/api/user/recent?filter=${encodeURIComponent(filter)}`)
|
return fetch(`/api/user/recent?filter=${encodeURIComponent(filter)}`)
|
||||||
.then(res => res.json())
|
.then((res) => res.json())
|
||||||
.then(data => data.map(x => ({
|
.then((data) =>
|
||||||
|
data.map((x) => ({
|
||||||
...x,
|
...x,
|
||||||
created_at: new Date(x.created_at).toLocaleString(),
|
created_at: new Date(x.created_at).toLocaleString(),
|
||||||
})));
|
}))
|
||||||
|
);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export function useFileDelete() {
|
export function useFileDelete() {
|
||||||
// '/api/user/files', 'DELETE', { id: image.id }
|
// '/api/user/files', 'DELETE', { id: image.id }
|
||||||
return useMutation(async (id: string) => {
|
return useMutation(
|
||||||
|
async (id: string) => {
|
||||||
return fetch('/api/user/files', {
|
return fetch('/api/user/files', {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
body: JSON.stringify({ id }),
|
body: JSON.stringify({ id }),
|
||||||
headers: {
|
headers: {
|
||||||
'content-type': 'application/json',
|
'content-type': 'application/json',
|
||||||
},
|
},
|
||||||
}).then(res => res.json());
|
}).then((res) => res.json());
|
||||||
}, {
|
},
|
||||||
|
{
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.refetchQueries(['files']);
|
queryClient.refetchQueries(['files']);
|
||||||
},
|
},
|
||||||
});
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useFileFavorite() {
|
export function useFileFavorite() {
|
||||||
// /api/user/files', 'PATCH', { id: image.id, favorite: !image.favorite }
|
// /api/user/files', 'PATCH', { id: image.id, favorite: !image.favorite }
|
||||||
return useMutation(async (data: { id: string, favorite: boolean }) => {
|
return useMutation(
|
||||||
|
async (data: { id: string; favorite: boolean }) => {
|
||||||
return fetch('/api/user/files', {
|
return fetch('/api/user/files', {
|
||||||
method: 'PATCH',
|
method: 'PATCH',
|
||||||
body: JSON.stringify(data),
|
body: JSON.stringify(data),
|
||||||
headers: {
|
headers: {
|
||||||
'content-type': 'application/json',
|
'content-type': 'application/json',
|
||||||
},
|
},
|
||||||
}).then(res => res.json());
|
}).then((res) => res.json());
|
||||||
}, {
|
},
|
||||||
|
{
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.refetchQueries(['files']);
|
queryClient.refetchQueries(['files']);
|
||||||
},
|
},
|
||||||
});
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function invalidateFiles() {
|
export function invalidateFiles() {
|
||||||
return queryClient.invalidateQueries(
|
return queryClient.invalidateQueries(['files', 'recent', 'stats']);
|
||||||
['files', 'recent', 'stats']
|
|
||||||
);
|
|
||||||
}
|
}
|
|
@ -16,14 +16,17 @@ export type Stats = {
|
||||||
size_num: number;
|
size_num: number;
|
||||||
types_count: StatsTypesCount[];
|
types_count: StatsTypesCount[];
|
||||||
views_count: number;
|
views_count: number;
|
||||||
}
|
};
|
||||||
}
|
};
|
||||||
|
|
||||||
export const useStats = (amount = 2) => {
|
export const useStats = (amount = 2) => {
|
||||||
return useQuery<Stats[]>(['stats', amount], async () => {
|
return useQuery<Stats[]>(
|
||||||
return fetch('/api/stats?amount=' + amount)
|
['stats', amount],
|
||||||
.then(res => res.json());
|
async () => {
|
||||||
}, {
|
return fetch('/api/stats?amount=' + amount).then((res) => res.json());
|
||||||
|
},
|
||||||
|
{
|
||||||
staleTime: 1000 * 60 * 5, // 5 minutes
|
staleTime: 1000 * 60 * 5, // 5 minutes
|
||||||
});
|
}
|
||||||
|
);
|
||||||
};
|
};
|
|
@ -7,17 +7,17 @@ export type URLResponse = {
|
||||||
id: string;
|
id: string;
|
||||||
url: string;
|
url: string;
|
||||||
vanity: string;
|
vanity: string;
|
||||||
}
|
};
|
||||||
|
|
||||||
export function useURLs() {
|
export function useURLs() {
|
||||||
return useQuery<URLResponse[]>(['urls'], async () => {
|
return useQuery<URLResponse[]>(['urls'], async () => {
|
||||||
return fetch('/api/user/urls')
|
return fetch('/api/user/urls').then((res) => res.json());
|
||||||
.then(res => res.json());
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useURLDelete() {
|
export function useURLDelete() {
|
||||||
return useMutation(async (id: string) => {
|
return useMutation(
|
||||||
|
async (id: string) => {
|
||||||
// '/api/user/urls', 'DELETE', { id: u.id }
|
// '/api/user/urls', 'DELETE', { id: u.id }
|
||||||
return fetch('/api/user/urls', {
|
return fetch('/api/user/urls', {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
|
@ -25,11 +25,15 @@ export function useURLDelete() {
|
||||||
headers: {
|
headers: {
|
||||||
'content-type': 'application/json',
|
'content-type': 'application/json',
|
||||||
},
|
},
|
||||||
}).then(res => res.json());
|
}).then((res) => res.json());
|
||||||
}, {
|
},
|
||||||
|
{
|
||||||
onSuccess: (data, variables) => {
|
onSuccess: (data, variables) => {
|
||||||
const dataWithoutDeleted = queryClient.getQueryData<URLResponse[]>(['urls'])?.filter(u => u.id !== variables);
|
const dataWithoutDeleted = queryClient
|
||||||
|
.getQueryData<URLResponse[]>(['urls'])
|
||||||
|
?.filter((u) => u.id !== variables);
|
||||||
queryClient.setQueryData(['urls'], dataWithoutDeleted);
|
queryClient.setQueryData(['urls'], dataWithoutDeleted);
|
||||||
},
|
},
|
||||||
});
|
}
|
||||||
|
);
|
||||||
}
|
}
|
|
@ -1,9 +1,13 @@
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
|
||||||
export const useVersion = () => {
|
export const useVersion = () => {
|
||||||
return useQuery<{ local: string, upstream: string }>(['version'], async () => {
|
return useQuery<{ local: string; upstream: string }>(
|
||||||
return fetch('/api/version').then(res => res.json());
|
['version'],
|
||||||
}, {
|
async () => {
|
||||||
|
return fetch('/api/version').then((res) => res.json());
|
||||||
|
},
|
||||||
|
{
|
||||||
staleTime: Infinity,
|
staleTime: Infinity,
|
||||||
});
|
}
|
||||||
|
);
|
||||||
};
|
};
|
|
@ -26,10 +26,7 @@ export function createToken() {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function sign(value: string, secret: string): string {
|
export function sign(value: string, secret: string): string {
|
||||||
const signed = value + ':' + createHmac('sha256', secret)
|
const signed = value + ':' + createHmac('sha256', secret).update(value).digest('base64').replace(/=+$/, '');
|
||||||
.update(value)
|
|
||||||
.digest('base64')
|
|
||||||
.replace(/=+$/, '');
|
|
||||||
|
|
||||||
return signed;
|
return signed;
|
||||||
}
|
}
|
||||||
|
@ -59,7 +56,7 @@ export function chunk<T>(arr: T[], size: number): Array<T[]> {
|
||||||
let i = 0;
|
let i = 0;
|
||||||
|
|
||||||
while (i < L) {
|
while (i < L) {
|
||||||
result.push(arr.slice(i, i += size));
|
result.push(arr.slice(i, (i += size)));
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
|
@ -93,7 +90,11 @@ export function randomInvis(length: number) {
|
||||||
// some parts from https://github.com/tycrek/ass/blob/master/generators/lengthGen.js
|
// some parts from https://github.com/tycrek/ass/blob/master/generators/lengthGen.js
|
||||||
const invisibleCharset = ['\u200B', '\u2060', '\u200C', '\u200D'];
|
const invisibleCharset = ['\u200B', '\u2060', '\u200C', '\u200D'];
|
||||||
|
|
||||||
return [...randomBytes(length)].map((byte) => invisibleCharset[Number(byte) % invisibleCharset.length]).join('').slice(1).concat(invisibleCharset[0]);
|
return [...randomBytes(length)]
|
||||||
|
.map((byte) => invisibleCharset[Number(byte) % invisibleCharset.length])
|
||||||
|
.join('')
|
||||||
|
.slice(1)
|
||||||
|
.concat(invisibleCharset[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createInvisImage(length: number, imageId: number) {
|
export function createInvisImage(length: number, imageId: number) {
|
||||||
|
|
|
@ -46,7 +46,10 @@ export function relativeTime(to: Date, from: Date = new Date()) {
|
||||||
|
|
||||||
for (const unit in units) {
|
for (const unit in units) {
|
||||||
if (time > units[unit]) {
|
if (time > units[unit]) {
|
||||||
return rtf.format(Math.floor(Math.round(time.getTime() / units[unit])), unit as Intl.RelativeTimeFormatUnit || 'second');
|
return rtf.format(
|
||||||
|
Math.floor(Math.round(time.getTime() / units[unit])),
|
||||||
|
(unit as Intl.RelativeTimeFormatUnit) || 'second'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,9 +15,11 @@ export default function FourOhFour() {
|
||||||
}}
|
}}
|
||||||
spacing='sm'
|
spacing='sm'
|
||||||
>
|
>
|
||||||
<Title sx={{ fontSize: 220, fontWeight: 900, lineHeight: .8 }}>404</Title>
|
<Title sx={{ fontSize: 220, fontWeight: 900, lineHeight: 0.8 }}>404</Title>
|
||||||
<MutedText sx={{ fontSize: 40, fontWeight: 500 }}>This page does not exist!</MutedText>
|
<MutedText sx={{ fontSize: 40, fontWeight: 500 }}>This page does not exist!</MutedText>
|
||||||
<Button component={Link} href='/dashboard'>Head to the Dashboard</Button>
|
<Button component={Link} href='/dashboard'>
|
||||||
|
Head to the Dashboard
|
||||||
|
</Button>
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,9 +15,11 @@ export default function FiveHundred() {
|
||||||
}}
|
}}
|
||||||
spacing='sm'
|
spacing='sm'
|
||||||
>
|
>
|
||||||
<Title sx={{ fontSize: 220, fontWeight: 900, lineHeight: .8 }}>500</Title>
|
<Title sx={{ fontSize: 220, fontWeight: 900, lineHeight: 0.8 }}>500</Title>
|
||||||
<MutedText sx={{ fontSize: 40, fontWeight: 500 }}>Internal Server Error</MutedText>
|
<MutedText sx={{ fontSize: 40, fontWeight: 500 }}>Internal Server Error</MutedText>
|
||||||
<Button component={Link} href='/dashboard'>Head to the Dashboard</Button>
|
<Button component={Link} href='/dashboard'>
|
||||||
|
Head to the Dashboard
|
||||||
|
</Button>
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,19 +30,21 @@ export default function EmbeddedImage({ image, user, pass }) {
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateImage = async (url?: string) => {
|
const updateImage = async (url?: string) => {
|
||||||
|
|
||||||
const imageEl = document.getElementById('image_content') as HTMLImageElement;
|
const imageEl = document.getElementById('image_content') as HTMLImageElement;
|
||||||
|
|
||||||
const img = new Image();
|
const img = new Image();
|
||||||
img.addEventListener('load', function () {
|
img.addEventListener('load', function () {
|
||||||
if (this.naturalWidth > innerWidth) imageEl.width = Math.floor(this.naturalWidth * Math.min((innerHeight / this.naturalHeight), (innerWidth / this.naturalWidth)));
|
if (this.naturalWidth > innerWidth)
|
||||||
|
imageEl.width = Math.floor(
|
||||||
|
this.naturalWidth * Math.min(innerHeight / this.naturalHeight, innerWidth / this.naturalWidth)
|
||||||
|
);
|
||||||
else imageEl.width = this.naturalWidth;
|
else imageEl.width = this.naturalWidth;
|
||||||
});
|
});
|
||||||
|
|
||||||
img.src = url || dataURL('/r');
|
img.src = url || dataURL('/r');
|
||||||
if (url) {
|
if (url) {
|
||||||
imageEl.src = url;
|
imageEl.src = url;
|
||||||
};
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -58,7 +60,9 @@ export default function EmbeddedImage({ image, user, pass }) {
|
||||||
<Head>
|
<Head>
|
||||||
{image.embed && (
|
{image.embed && (
|
||||||
<>
|
<>
|
||||||
{user.embedSiteName && <meta property='og:site_name' content={parse(user.embedSiteName, image, user)} />}
|
{user.embedSiteName && (
|
||||||
|
<meta property='og:site_name' content={parse(user.embedSiteName, image, user)} />
|
||||||
|
)}
|
||||||
{user.embedTitle && <meta property='og:title' content={parse(user.embedTitle, image, user)} />}
|
{user.embedTitle && <meta property='og:title' content={parse(user.embedTitle, image, user)} />}
|
||||||
<meta property='theme-color' content={user.embedColor} />
|
<meta property='theme-color' content={user.embedColor} />
|
||||||
</>
|
</>
|
||||||
|
@ -99,7 +103,13 @@ export default function EmbeddedImage({ image, user, pass }) {
|
||||||
closeOnClickOutside={false}
|
closeOnClickOutside={false}
|
||||||
overlayBlur={3}
|
overlayBlur={3}
|
||||||
>
|
>
|
||||||
<PasswordInput label='Password' placeholder='Password' error={error} value={password} onChange={e => setPassword(e.target.value)} />
|
<PasswordInput
|
||||||
|
label='Password'
|
||||||
|
placeholder='Password'
|
||||||
|
error={error}
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
/>
|
||||||
<Button fullWidth onClick={() => check()} mt='md'>
|
<Button fullWidth onClick={() => check()} mt='md'>
|
||||||
Submit
|
Submit
|
||||||
</Button>
|
</Button>
|
||||||
|
@ -117,12 +127,7 @@ export default function EmbeddedImage({ image, user, pass }) {
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{image.mimetype.startsWith('video') && (
|
{image.mimetype.startsWith('video') && (
|
||||||
<video
|
<video src={dataURL('/r')} controls={true} autoPlay={true} id='image_content' />
|
||||||
src={dataURL('/r')}
|
|
||||||
controls={true}
|
|
||||||
autoPlay={true}
|
|
||||||
id='image_content'
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
</>
|
</>
|
||||||
|
@ -139,11 +144,7 @@ export const getServerSideProps: GetServerSideProps = async (context) => {
|
||||||
if (route === config.urls.route.substring(1)) {
|
if (route === config.urls.route.substring(1)) {
|
||||||
const url = await prisma.url.findFirst({
|
const url = await prisma.url.findFirst({
|
||||||
where: {
|
where: {
|
||||||
OR: [
|
OR: [{ id }, { vanity: id }, { invisible: { invis: id } }],
|
||||||
{ id },
|
|
||||||
{ vanity: id },
|
|
||||||
{ invisible: { invis: id } },
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
select: {
|
select: {
|
||||||
destination: true,
|
destination: true,
|
||||||
|
@ -157,14 +158,10 @@ export const getServerSideProps: GetServerSideProps = async (context) => {
|
||||||
destination: url.destination,
|
destination: url.destination,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
} else if (uploader_route === '' ? /(^[^\\.]+\.[^\\.]+)/.test(route) : route === uploader_route) {
|
} else if (uploader_route === '' ? /(^[^\\.]+\.[^\\.]+)/.test(route) : route === uploader_route) {
|
||||||
const image = await prisma.image.findFirst({
|
const image = await prisma.image.findFirst({
|
||||||
where: {
|
where: {
|
||||||
OR: [
|
OR: [{ file: id }, { invisible: { invis: id } }],
|
||||||
{ file: id },
|
|
||||||
{ invisible: { invis: id } },
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
select: {
|
select: {
|
||||||
mimetype: true,
|
mimetype: true,
|
||||||
|
@ -196,7 +193,8 @@ export const getServerSideProps: GetServerSideProps = async (context) => {
|
||||||
image.created_at = image.created_at.toString();
|
image.created_at = image.created_at.toString();
|
||||||
|
|
||||||
const prismRender = Object.keys(exts).includes(image.file.split('.').pop());
|
const prismRender = Object.keys(exts).includes(image.file.split('.').pop());
|
||||||
if (prismRender) return {
|
if (prismRender)
|
||||||
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
destination: `/code/${image.file}`,
|
destination: `/code/${image.file}`,
|
||||||
permanent: true,
|
permanent: true,
|
||||||
|
@ -225,4 +223,3 @@ export const getServerSideProps: GetServerSideProps = async (context) => {
|
||||||
return { notFound: true };
|
return { notFound: true };
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -11,9 +11,7 @@ export default function MyApp({ Component, pageProps }) {
|
||||||
<Head>
|
<Head>
|
||||||
<meta name='viewport' content='minimum-scale=1, initial-scale=1, width=device-width' />
|
<meta name='viewport' content='minimum-scale=1, initial-scale=1, width=device-width' />
|
||||||
</Head>
|
</Head>
|
||||||
<QueryClientProvider
|
<QueryClientProvider client={queryClient}>
|
||||||
client={queryClient}
|
|
||||||
>
|
|
||||||
<ZiplineTheming Component={Component} pageProps={pageProps} />
|
<ZiplineTheming Component={Component} pageProps={pageProps} />
|
||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
</RecoilRoot>
|
</RecoilRoot>
|
||||||
|
|
|
@ -11,7 +11,10 @@ class MyDocument extends Document {
|
||||||
return (
|
return (
|
||||||
<Html lang='en'>
|
<Html lang='en'>
|
||||||
<Head>
|
<Head>
|
||||||
<link rel='stylesheet' href='https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap' />
|
<link
|
||||||
|
rel='stylesheet'
|
||||||
|
href='https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap'
|
||||||
|
/>
|
||||||
</Head>
|
</Head>
|
||||||
<body>
|
<body>
|
||||||
<Main />
|
<Main />
|
||||||
|
|
|
@ -15,9 +15,11 @@ export default function Error({ statusCode }) {
|
||||||
}}
|
}}
|
||||||
spacing='sm'
|
spacing='sm'
|
||||||
>
|
>
|
||||||
<Title sx={{ fontSize: 220, fontWeight: 900, lineHeight: .8 }}>{statusCode}</Title>
|
<Title sx={{ fontSize: 220, fontWeight: 900, lineHeight: 0.8 }}>{statusCode}</Title>
|
||||||
<MutedText sx={{ fontSize: 40, fontWeight: 500 }}>Something went wrong...</MutedText>
|
<MutedText sx={{ fontSize: 40, fontWeight: 500 }}>Something went wrong...</MutedText>
|
||||||
<Button component={Link} href='/dashboard'>Head to the Dashboard</Button>
|
<Button component={Link} href='/dashboard'>
|
||||||
|
Head to the Dashboard
|
||||||
|
</Button>
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,11 @@ async function handler(req: NextApiReq, res: NextApiRes) {
|
||||||
if (req.method === 'POST' && req.body && req.body.code) {
|
if (req.method === 'POST' && req.body && req.body.code) {
|
||||||
if (!config.features.invites) return res.forbid('invites are disabled');
|
if (!config.features.invites) return res.forbid('invites are disabled');
|
||||||
|
|
||||||
const { code, username, password } = req.body as { code: string; username: string, password: string };
|
const { code, username, password } = req.body as {
|
||||||
|
code: string;
|
||||||
|
username: string;
|
||||||
|
password: string;
|
||||||
|
};
|
||||||
const invite = await prisma.invite.findUnique({
|
const invite = await prisma.invite.findUnique({
|
||||||
where: { code },
|
where: { code },
|
||||||
});
|
});
|
||||||
|
@ -49,7 +53,11 @@ async function handler(req: NextApiReq, res: NextApiRes) {
|
||||||
|
|
||||||
if (req.method !== 'POST') return res.status(405).end();
|
if (req.method !== 'POST') return res.status(405).end();
|
||||||
|
|
||||||
const { username, password, administrator } = req.body as { username: string, password: string, administrator: boolean };
|
const { username, password, administrator } = req.body as {
|
||||||
|
username: string;
|
||||||
|
password: string;
|
||||||
|
administrator: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
if (!username) return res.bad('no username');
|
if (!username) return res.bad('no username');
|
||||||
if (!password) return res.bad('no auth');
|
if (!password) return res.bad('no auth');
|
||||||
|
|
|
@ -12,7 +12,10 @@ async function handler(req: NextApiReq, res: NextApiRes) {
|
||||||
if (!user.administrator) return res.forbid('you arent an administrator');
|
if (!user.administrator) return res.forbid('you arent an administrator');
|
||||||
|
|
||||||
if (req.method === 'POST') {
|
if (req.method === 'POST') {
|
||||||
const { expires_at, count } = req.body as { expires_at: string, count: number };
|
const { expires_at, count } = req.body as {
|
||||||
|
expires_at: string;
|
||||||
|
count: number;
|
||||||
|
};
|
||||||
|
|
||||||
const expiry = expires_at ? new Date(expires_at) : null;
|
const expiry = expires_at ? new Date(expires_at) : null;
|
||||||
if (expiry) {
|
if (expiry) {
|
||||||
|
@ -33,7 +36,11 @@ async function handler(req: NextApiReq, res: NextApiRes) {
|
||||||
|
|
||||||
await prisma.invite.createMany({ data });
|
await prisma.invite.createMany({ data });
|
||||||
|
|
||||||
Logger.get('invite').info(`${user.username} (${user.id}) created ${data.length} invites with codes ${data.map(invite => invite.code).join(', ')}`);
|
Logger.get('invite').info(
|
||||||
|
`${user.username} (${user.id}) created ${data.length} invites with codes ${data
|
||||||
|
.map((invite) => invite.code)
|
||||||
|
.join(', ')}`
|
||||||
|
);
|
||||||
|
|
||||||
return res.json(data);
|
return res.json(data);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -5,7 +5,10 @@ import Logger from 'lib/logger';
|
||||||
|
|
||||||
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.status(405).end();
|
||||||
const { username, password } = req.body as { username: string, password: string };
|
const { username, password } = req.body as {
|
||||||
|
username: string;
|
||||||
|
password: string;
|
||||||
|
};
|
||||||
|
|
||||||
const users = await prisma.user.findMany();
|
const users = await prisma.user.findMany();
|
||||||
if (users.length === 0) {
|
if (users.length === 0) {
|
||||||
|
@ -32,7 +35,11 @@ async function handler(req: NextApiReq, res: NextApiRes) {
|
||||||
const valid = await checkPassword(password, user.password);
|
const valid = await checkPassword(password, user.password);
|
||||||
if (!valid) return res.forbid('Wrong password');
|
if (!valid) return res.forbid('Wrong password');
|
||||||
|
|
||||||
res.setCookie('user', user.id, { sameSite: true, expires: new Date(Date.now() + (6.048e+8 * 2)), path: '/' });
|
res.setCookie('user', user.id, {
|
||||||
|
sameSite: true,
|
||||||
|
expires: new Date(Date.now() + 6.048e8 * 2),
|
||||||
|
path: '/',
|
||||||
|
});
|
||||||
|
|
||||||
Logger.get('user').info(`User ${user.username} (${user.id}) logged in`);
|
Logger.get('user').info(`User ${user.username} (${user.id}) logged in`);
|
||||||
|
|
||||||
|
|
|
@ -14,7 +14,13 @@ async function handler(req: NextApiReq, res: NextApiRes) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const { code } = req.query as { code: string };
|
const { code } = req.query as { code: string };
|
||||||
if (!code) return res.redirect(discord_auth.oauth_url(config.oauth.discord_client_id, `${config.core.https ? 'https' : 'http'}://${req.headers.host}`));
|
if (!code)
|
||||||
|
return res.redirect(
|
||||||
|
discord_auth.oauth_url(
|
||||||
|
config.oauth.discord_client_id,
|
||||||
|
`${config.core.https ? 'https' : 'http'}://${req.headers.host}`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
const resp = await fetch('https://discord.com/api/oauth2/token', {
|
const resp = await fetch('https://discord.com/api/oauth2/token', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
@ -38,7 +44,9 @@ async function handler(req: NextApiReq, res: NextApiRes) {
|
||||||
const userJson = await discord_auth.oauth_user(json.access_token);
|
const userJson = await discord_auth.oauth_user(json.access_token);
|
||||||
if (!userJson) return res.error('invalid user request');
|
if (!userJson) return res.error('invalid user request');
|
||||||
|
|
||||||
const avatar = userJson.avatar ? `https://cdn.discordapp.com/avatars/${userJson.id}/${userJson.avatar}.png` : `https://cdn.discordapp.com/embed/avatars/${userJson.discriminator % 5}.png`;
|
const avatar = userJson.avatar
|
||||||
|
? `https://cdn.discordapp.com/avatars/${userJson.id}/${userJson.avatar}.png`
|
||||||
|
: `https://cdn.discordapp.com/embed/avatars/${userJson.discriminator % 5}.png`;
|
||||||
const avatarBase64 = await getBase64URLFromURL(avatar);
|
const avatarBase64 = await getBase64URLFromURL(avatar);
|
||||||
|
|
||||||
const existing = await prisma.user.findFirst({
|
const existing = await prisma.user.findFirst({
|
||||||
|
@ -58,7 +66,11 @@ async function handler(req: NextApiReq, res: NextApiRes) {
|
||||||
});
|
});
|
||||||
|
|
||||||
req.cleanCookie('user');
|
req.cleanCookie('user');
|
||||||
res.setCookie('user', existing.id, { sameSite: true, expires: new Date(Date.now() + (6.048e+8 * 2)), path: '/' });
|
res.setCookie('user', existing.id, {
|
||||||
|
sameSite: true,
|
||||||
|
expires: new Date(Date.now() + 6.048e8 * 2),
|
||||||
|
path: '/',
|
||||||
|
});
|
||||||
Logger.get('user').info(`User ${existing.username} (${existing.id}) logged in via oauth(discord)`);
|
Logger.get('user').info(`User ${existing.username} (${existing.id}) logged in via oauth(discord)`);
|
||||||
|
|
||||||
return res.redirect('/dashboard');
|
return res.redirect('/dashboard');
|
||||||
|
@ -79,7 +91,11 @@ async function handler(req: NextApiReq, res: NextApiRes) {
|
||||||
Logger.get('user').info(`Created user ${user.username} via oauth(discord)`);
|
Logger.get('user').info(`Created user ${user.username} via oauth(discord)`);
|
||||||
|
|
||||||
req.cleanCookie('user');
|
req.cleanCookie('user');
|
||||||
res.setCookie('user', user.id, { sameSite: true, expires: new Date(Date.now() + (6.048e+8 * 2)), path: '/' });
|
res.setCookie('user', user.id, {
|
||||||
|
sameSite: true,
|
||||||
|
expires: new Date(Date.now() + 6.048e8 * 2),
|
||||||
|
path: '/',
|
||||||
|
});
|
||||||
Logger.get('user').info(`User ${user.username} (${user.id}) logged in via oauth(discord)`);
|
Logger.get('user').info(`User ${user.username} (${user.id}) logged in via oauth(discord)`);
|
||||||
|
|
||||||
return res.redirect('/dashboard');
|
return res.redirect('/dashboard');
|
||||||
|
|
|
@ -21,7 +21,7 @@ async function handler(req: NextApiReq, res: NextApiRes) {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
'Accept': 'application/json',
|
Accept: 'application/json',
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
client_id: config.oauth.github_client_id,
|
client_id: config.oauth.github_client_id,
|
||||||
|
@ -58,7 +58,11 @@ async function handler(req: NextApiReq, res: NextApiRes) {
|
||||||
});
|
});
|
||||||
|
|
||||||
req.cleanCookie('user');
|
req.cleanCookie('user');
|
||||||
res.setCookie('user', existing.id, { sameSite: true, expires: new Date(Date.now() + (6.048e+8 * 2)), path: '/' });
|
res.setCookie('user', existing.id, {
|
||||||
|
sameSite: true,
|
||||||
|
expires: new Date(Date.now() + 6.048e8 * 2),
|
||||||
|
path: '/',
|
||||||
|
});
|
||||||
Logger.get('user').info(`User ${existing.username} (${existing.id}) logged in via oauth(github)`);
|
Logger.get('user').info(`User ${existing.username} (${existing.id}) logged in via oauth(github)`);
|
||||||
|
|
||||||
return res.redirect('/dashboard');
|
return res.redirect('/dashboard');
|
||||||
|
@ -79,7 +83,11 @@ async function handler(req: NextApiReq, res: NextApiRes) {
|
||||||
Logger.get('user').info(`Created user ${user.username} via oauth(github)`);
|
Logger.get('user').info(`Created user ${user.username} via oauth(github)`);
|
||||||
|
|
||||||
req.cleanCookie('user');
|
req.cleanCookie('user');
|
||||||
res.setCookie('user', user.id, { sameSite: true, expires: new Date(Date.now() + (6.048e+8 * 2)), path: '/' });
|
res.setCookie('user', user.id, {
|
||||||
|
sameSite: true,
|
||||||
|
expires: new Date(Date.now() + 6.048e8 * 2),
|
||||||
|
path: '/',
|
||||||
|
});
|
||||||
Logger.get('user').info(`User ${user.username} (${user.id}) logged in via oauth(github)`);
|
Logger.get('user').info(`User ${user.username} (${user.id}) logged in via oauth(github)`);
|
||||||
|
|
||||||
return res.redirect('/dashboard');
|
return res.redirect('/dashboard');
|
||||||
|
|
|
@ -44,13 +44,25 @@ async function handler(req: NextApiReq, res: NextApiRes) {
|
||||||
|
|
||||||
if (req.headers.zws) invis = await createInvisURL(zconfig.urls.length, url.id);
|
if (req.headers.zws) invis = await createInvisURL(zconfig.urls.length, url.id);
|
||||||
|
|
||||||
Logger.get('url').info(`User ${user.username} (${user.id}) shortenned a url ${url.destination} (${url.id})`);
|
Logger.get('url').info(
|
||||||
|
`User ${user.username} (${user.id}) shortenned a url ${url.destination} (${url.id})`
|
||||||
|
);
|
||||||
|
|
||||||
if (config.discord?.shorten) {
|
if (config.discord?.shorten) {
|
||||||
await sendShorten(user, url, `${zconfig.core.https ? 'https' : 'http'}://${req.headers.host}${zconfig.urls.route}/${req.body.vanity ? req.body.vanity : invis ? invis.invis : url.id}`);
|
await sendShorten(
|
||||||
|
user,
|
||||||
|
url,
|
||||||
|
`${zconfig.core.https ? 'https' : 'http'}://${req.headers.host}${zconfig.urls.route}/${
|
||||||
|
req.body.vanity ? req.body.vanity : invis ? invis.invis : url.id
|
||||||
|
}`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return res.json({ url: `${zconfig.core.https ? 'https' : 'http'}://${req.headers.host}${zconfig.urls.route}/${req.body.vanity ? req.body.vanity : invis ? invis.invis : url.id}` });
|
return res.json({
|
||||||
|
url: `${zconfig.core.https ? 'https' : 'http'}://${req.headers.host}${zconfig.urls.route}/${
|
||||||
|
req.body.vanity ? req.body.vanity : invis ? invis.invis : url.id
|
||||||
|
}`,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withZipline(handler);
|
export default withZipline(handler);
|
|
@ -46,7 +46,7 @@ async function handler(req: NextApiReq, res: NextApiRes) {
|
||||||
if (!req.files) return res.error('no files');
|
if (!req.files) return res.error('no files');
|
||||||
if (req.files && req.files.length === 0) return res.error('no files');
|
if (req.files && req.files.length === 0) return res.error('no files');
|
||||||
|
|
||||||
const response: { files: string[], expires_at?: Date } = { files: [] };
|
const response: { files: string[]; expires_at?: Date } = { files: [] };
|
||||||
|
|
||||||
const expires_at = req.headers['expires-at'] as string;
|
const expires_at = req.headers['expires-at'] as string;
|
||||||
let expiry: Date;
|
let expiry: Date;
|
||||||
|
@ -62,15 +62,18 @@ async function handler(req: NextApiReq, res: NextApiRes) {
|
||||||
const rawFormat = ((req.headers.format || '') as string).toUpperCase() || 'RANDOM';
|
const rawFormat = ((req.headers.format || '') as string).toUpperCase() || 'RANDOM';
|
||||||
const format: ImageFormat = Object.keys(ImageFormat).includes(rawFormat) && ImageFormat[rawFormat];
|
const format: ImageFormat = Object.keys(ImageFormat).includes(rawFormat) && ImageFormat[rawFormat];
|
||||||
|
|
||||||
const imageCompressionPercent = req.headers['image-compression-percent'] ? Number(req.headers['image-compression-percent']) : null;
|
const imageCompressionPercent = req.headers['image-compression-percent']
|
||||||
|
? Number(req.headers['image-compression-percent'])
|
||||||
|
: null;
|
||||||
|
|
||||||
for (let i = 0; i !== req.files.length; ++i) {
|
for (let i = 0; i !== req.files.length; ++i) {
|
||||||
const file = req.files[i];
|
const file = req.files[i];
|
||||||
if (file.size > zconfig.uploader[user.administrator ? 'admin_limit' : 'user_limit']) return res.error(`file[${i}] size too big`);
|
if (file.size > zconfig.uploader[user.administrator ? 'admin_limit' : 'user_limit'])
|
||||||
|
return res.error(`file[${i}] size too big`);
|
||||||
|
|
||||||
const ext = file.originalname.split('.').pop();
|
const ext = file.originalname.split('.').pop();
|
||||||
if (zconfig.uploader.disabled_extensions.includes(ext)) return res.error('disabled extension recieved: ' + ext);
|
if (zconfig.uploader.disabled_extensions.includes(ext))
|
||||||
|
return res.error('disabled extension recieved: ' + ext);
|
||||||
let fileName: string;
|
let fileName: string;
|
||||||
|
|
||||||
switch (format) {
|
switch (format) {
|
||||||
|
@ -101,7 +104,7 @@ async function handler(req: NextApiReq, res: NextApiRes) {
|
||||||
const image = await prisma.image.create({
|
const image = await prisma.image.create({
|
||||||
data: {
|
data: {
|
||||||
file: `${fileName}.${compressionUsed ? 'jpg' : ext}`,
|
file: `${fileName}.${compressionUsed ? 'jpg' : ext}`,
|
||||||
mimetype: req.headers.uploadtext ? 'text/plain' : (compressionUsed ? 'image/jpeg' : file.mimetype),
|
mimetype: req.headers.uploadtext ? 'text/plain' : compressionUsed ? 'image/jpeg' : file.mimetype,
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
embed: !!req.headers.embed,
|
embed: !!req.headers.embed,
|
||||||
format,
|
format,
|
||||||
|
@ -115,21 +118,37 @@ async function handler(req: NextApiReq, res: NextApiRes) {
|
||||||
if (compressionUsed) {
|
if (compressionUsed) {
|
||||||
const buffer = await sharp(file.buffer).jpeg({ quality: imageCompressionPercent }).toBuffer();
|
const buffer = await sharp(file.buffer).jpeg({ quality: imageCompressionPercent }).toBuffer();
|
||||||
await datasource.save(image.file, buffer);
|
await datasource.save(image.file, buffer);
|
||||||
Logger.get('image').info(`User ${user.username} (${user.id}) compressed image from ${file.buffer.length} -> ${buffer.length} bytes`);
|
Logger.get('image').info(
|
||||||
|
`User ${user.username} (${user.id}) compressed image from ${file.buffer.length} -> ${buffer.length} bytes`
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
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})`
|
||||||
|
);
|
||||||
if (user.domains.length) {
|
if (user.domains.length) {
|
||||||
const domain = user.domains[Math.floor(Math.random() * user.domains.length)];
|
const domain = user.domains[Math.floor(Math.random() * user.domains.length)];
|
||||||
response.files.push(`${domain}${zconfig.uploader.route === '/' ? '' : zconfig.uploader.route}/${invis ? invis.invis : image.file}`);
|
response.files.push(
|
||||||
|
`${domain}${zconfig.uploader.route === '/' ? '' : zconfig.uploader.route}/${
|
||||||
|
invis ? invis.invis : image.file
|
||||||
|
}`
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
response.files.push(`${zconfig.core.https ? 'https' : 'http'}://${req.headers.host}${zconfig.uploader.route === '/' ? '' : zconfig.uploader.route}/${invis ? invis.invis : image.file}`);
|
response.files.push(
|
||||||
|
`${zconfig.core.https ? 'https' : 'http'}://${req.headers.host}${
|
||||||
|
zconfig.uploader.route === '/' ? '' : zconfig.uploader.route
|
||||||
|
}/${invis ? invis.invis : image.file}`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (zconfig.discord?.upload) {
|
if (zconfig.discord?.upload) {
|
||||||
await sendUpload(user, image, `${zconfig.core.https ? 'https' : 'http'}://${req.headers.host}/r/${invis ? invis.invis : image.file}`);
|
await sendUpload(
|
||||||
|
user,
|
||||||
|
image,
|
||||||
|
`${zconfig.core.https ? 'https' : 'http'}://${req.headers.host}/r/${invis ? invis.invis : image.file}`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -139,7 +158,7 @@ async function handler(req: NextApiReq, res: NextApiRes) {
|
||||||
id: user.id,
|
id: user.id,
|
||||||
},
|
},
|
||||||
data: {
|
data: {
|
||||||
ratelimit: new Date(Date.now() + (zconfig.ratelimit.admin * 1000)),
|
ratelimit: new Date(Date.now() + zconfig.ratelimit.admin * 1000),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
} else if (!user.administrator && zconfig.ratelimit.user > 0) {
|
} else if (!user.administrator && zconfig.ratelimit.user > 0) {
|
||||||
|
@ -149,7 +168,7 @@ async function handler(req: NextApiReq, res: NextApiRes) {
|
||||||
id: user.id,
|
id: user.id,
|
||||||
},
|
},
|
||||||
data: {
|
data: {
|
||||||
ratelimit: new Date(Date.now() + (zconfig.ratelimit.user * 1000)),
|
ratelimit: new Date(Date.now() + zconfig.ratelimit.user * 1000),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -170,7 +189,7 @@ function run(middleware: any) {
|
||||||
|
|
||||||
export default async function handlers(req, res) {
|
export default async function handlers(req, res) {
|
||||||
return withZipline(handler)(req, res);
|
return withZipline(handler)(req, res);
|
||||||
};
|
}
|
||||||
|
|
||||||
export const config = {
|
export const config = {
|
||||||
api: {
|
api: {
|
||||||
|
|
|
@ -65,33 +65,39 @@ async function handler(req: NextApiReq, res: NextApiRes) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (req.body.avatar) await prisma.user.update({
|
if (req.body.avatar)
|
||||||
|
await prisma.user.update({
|
||||||
where: { id: target.id },
|
where: { id: target.id },
|
||||||
data: { avatar: req.body.avatar },
|
data: { avatar: req.body.avatar },
|
||||||
});
|
});
|
||||||
|
|
||||||
if (req.body.embedTitle) await prisma.user.update({
|
if (req.body.embedTitle)
|
||||||
|
await prisma.user.update({
|
||||||
where: { id: target.id },
|
where: { id: target.id },
|
||||||
data: { embedTitle: req.body.embedTitle },
|
data: { embedTitle: req.body.embedTitle },
|
||||||
});
|
});
|
||||||
|
|
||||||
if (req.body.embedColor) await prisma.user.update({
|
if (req.body.embedColor)
|
||||||
|
await prisma.user.update({
|
||||||
where: { id: target.id },
|
where: { id: target.id },
|
||||||
data: { embedColor: req.body.embedColor },
|
data: { embedColor: req.body.embedColor },
|
||||||
});
|
});
|
||||||
|
|
||||||
if (req.body.embedSiteName) await prisma.user.update({
|
if (req.body.embedSiteName)
|
||||||
|
await prisma.user.update({
|
||||||
where: { id: target.id },
|
where: { id: target.id },
|
||||||
data: { embedSiteName: req.body.embedSiteName },
|
data: { embedSiteName: req.body.embedSiteName },
|
||||||
});
|
});
|
||||||
|
|
||||||
if (req.body.systemTheme) await prisma.user.update({
|
if (req.body.systemTheme)
|
||||||
|
await prisma.user.update({
|
||||||
where: { id: target.id },
|
where: { id: target.id },
|
||||||
data: { systemTheme: req.body.systemTheme },
|
data: { systemTheme: req.body.systemTheme },
|
||||||
});
|
});
|
||||||
|
|
||||||
if (req.body.domains) {
|
if (req.body.domains) {
|
||||||
if (!req.body.domains) await prisma.user.update({
|
if (!req.body.domains)
|
||||||
|
await prisma.user.update({
|
||||||
where: { id: target.id },
|
where: { id: target.id },
|
||||||
data: { domains: [] },
|
data: { domains: [] },
|
||||||
});
|
});
|
||||||
|
@ -138,7 +144,9 @@ async function handler(req: NextApiReq, res: NextApiRes) {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
Logger.get('user').info(`User ${user.username} (${user.id}) updated ${target.username} (${newUser.username}) (${newUser.id})`);
|
Logger.get('user').info(
|
||||||
|
`User ${user.username} (${user.id}) updated ${target.username} (${newUser.username}) (${newUser.id})`
|
||||||
|
);
|
||||||
|
|
||||||
return res.json(newUser);
|
return res.json(newUser);
|
||||||
}
|
}
|
||||||
|
|
|
@ -51,7 +51,7 @@ async function handler(req: NextApiReq, res: NextApiRes) {
|
||||||
} else {
|
} else {
|
||||||
Object.defineProperty(stream, 'ondata', {
|
Object.defineProperty(stream, 'ondata', {
|
||||||
get: () => ondataPatched,
|
get: () => ondataPatched,
|
||||||
set: cb => ondata = cb,
|
set: (cb) => (ondata = cb),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -68,14 +68,14 @@ async function handler(req: NextApiReq, res: NextApiRes) {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
zip.ondata = async (err, data, final) => {
|
zip.ondata = async (err, data, final) => {
|
||||||
if (!err) {
|
if (!err) {
|
||||||
write_stream.write(data);
|
write_stream.write(data);
|
||||||
if (final) {
|
if (final) {
|
||||||
write_stream.close();
|
write_stream.close();
|
||||||
Logger.get('user').info(`Export for ${user.username} (${user.id}) has completed and is available at ${export_name}`);
|
Logger.get('user').info(
|
||||||
|
`Export for ${user.username} (${user.id}) has completed and is available at ${export_name}`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
write_stream.close();
|
write_stream.close();
|
||||||
|
@ -91,14 +91,14 @@ async function handler(req: NextApiReq, res: NextApiRes) {
|
||||||
if (stream) {
|
if (stream) {
|
||||||
const def = new ZipPassThrough(file.file);
|
const def = new ZipPassThrough(file.file);
|
||||||
zip.add(def);
|
zip.add(def);
|
||||||
onBackpressure(def, stream, shouldApplyBackpressure => {
|
onBackpressure(def, stream, (shouldApplyBackpressure) => {
|
||||||
if (shouldApplyBackpressure) {
|
if (shouldApplyBackpressure) {
|
||||||
stream.pause();
|
stream.pause();
|
||||||
} else if (stream.isPaused()) {
|
} else if (stream.isPaused()) {
|
||||||
stream.resume();
|
stream.resume();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
stream.on('data', c => def.push(c));
|
stream.on('data', (c) => def.push(c));
|
||||||
stream.on('end', () => def.push(new Uint8Array(0), true));
|
stream.on('end', () => def.push(new Uint8Array(0), true));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -121,7 +121,7 @@ async function handler(req: NextApiReq, res: NextApiRes) {
|
||||||
stream.pipe(res);
|
stream.pipe(res);
|
||||||
} else {
|
} else {
|
||||||
const files = await readdir(tmpdir());
|
const files = await readdir(tmpdir());
|
||||||
const exp = files.filter(f => f.startsWith('zipline_export_'));
|
const exp = files.filter((f) => f.startsWith('zipline_export_'));
|
||||||
const exports = [];
|
const exports = [];
|
||||||
for (let i = 0; i !== exp.length; ++i) {
|
for (let i = 0; i !== exp.length; ++i) {
|
||||||
const name = exp[i];
|
const name = exp[i];
|
||||||
|
|
|
@ -39,7 +39,9 @@ async function handler(req: NextApiReq, res: NextApiRes) {
|
||||||
|
|
||||||
await datasource.delete(image.file);
|
await datasource.delete(image.file);
|
||||||
|
|
||||||
Logger.get('image').info(`User ${user.username} (${user.id}) deleted an image ${image.file} (${image.id})`);
|
Logger.get('image').info(
|
||||||
|
`User ${user.username} (${user.id}) deleted an image ${image.file} (${image.id})`
|
||||||
|
);
|
||||||
|
|
||||||
delete image.password;
|
delete image.password;
|
||||||
return res.json(image);
|
return res.json(image);
|
||||||
|
@ -49,7 +51,8 @@ async function handler(req: NextApiReq, res: NextApiRes) {
|
||||||
|
|
||||||
let image;
|
let image;
|
||||||
|
|
||||||
if (req.body.favorite !== null) image = await prisma.image.update({
|
if (req.body.favorite !== null)
|
||||||
|
image = await prisma.image.update({
|
||||||
where: { id: req.body.id },
|
where: { id: req.body.id },
|
||||||
data: {
|
data: {
|
||||||
favorite: req.body.favorite,
|
favorite: req.body.favorite,
|
||||||
|
@ -78,10 +81,10 @@ async function handler(req: NextApiReq, res: NextApiRes) {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
images.map(image => image.url = `/r/${image.file}`);
|
images.map((image) => (image.url = `/r/${image.file}`));
|
||||||
if (req.query.filter && req.query.filter === 'media') images = images.filter(x => /^(video|audio|image|text)/.test(x.mimetype));
|
if (req.query.filter && req.query.filter === 'media')
|
||||||
|
images = images.filter((x) => /^(video|audio|image|text)/.test(x.mimetype));
|
||||||
|
|
||||||
return res.json(req.query.paged ? chunk(images, 16) : images);
|
return res.json(req.query.paged ? chunk(images, 16) : images);
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,19 +17,28 @@ async function handler(req: NextApiReq, res: NextApiRes) {
|
||||||
req.cleanCookie('user');
|
req.cleanCookie('user');
|
||||||
Logger.get('user').info(`User ${user.username} (${user.id}) logged out (oauth token expired)`);
|
Logger.get('user').info(`User ${user.username} (${user.id}) logged out (oauth token expired)`);
|
||||||
|
|
||||||
return res.json({ error: 'oauth token expired', redirect_uri: github_auth.oauth_url(config.oauth.github_client_id) });
|
return res.json({
|
||||||
|
error: 'oauth token expired',
|
||||||
|
redirect_uri: github_auth.oauth_url(config.oauth.github_client_id),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
} else if (user.oauthProvider === 'discord') {
|
} else if (user.oauthProvider === 'discord') {
|
||||||
const resp = await fetch('https://discord.com/api/users/@me', {
|
const resp = await fetch('https://discord.com/api/users/@me', {
|
||||||
headers: {
|
headers: {
|
||||||
'Authorization': `Bearer ${user.oauthAccessToken}`,
|
Authorization: `Bearer ${user.oauthAccessToken}`,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
if (!resp.ok) {
|
if (!resp.ok) {
|
||||||
req.cleanCookie('user');
|
req.cleanCookie('user');
|
||||||
Logger.get('user').info(`User ${user.username} (${user.id}) logged out (oauth token expired)`);
|
Logger.get('user').info(`User ${user.username} (${user.id}) logged out (oauth token expired)`);
|
||||||
|
|
||||||
return res.json({ error: 'oauth token expired', redirect_uri: discord_auth.oauth_url(config.oauth.discord_client_id, `${config.core.https ? 'https' : 'http'}://${req.headers.host}`) });
|
return res.json({
|
||||||
|
error: 'oauth token expired',
|
||||||
|
redirect_uri: discord_auth.oauth_url(
|
||||||
|
config.oauth.discord_client_id,
|
||||||
|
`${config.core.https ? 'https' : 'http'}://${req.headers.host}`
|
||||||
|
),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -58,33 +67,39 @@ async function handler(req: NextApiReq, res: NextApiRes) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (req.body.avatar) await prisma.user.update({
|
if (req.body.avatar)
|
||||||
|
await prisma.user.update({
|
||||||
where: { id: user.id },
|
where: { id: user.id },
|
||||||
data: { avatar: req.body.avatar },
|
data: { avatar: req.body.avatar },
|
||||||
});
|
});
|
||||||
|
|
||||||
if (req.body.embedTitle) await prisma.user.update({
|
if (req.body.embedTitle)
|
||||||
|
await prisma.user.update({
|
||||||
where: { id: user.id },
|
where: { id: user.id },
|
||||||
data: { embedTitle: req.body.embedTitle },
|
data: { embedTitle: req.body.embedTitle },
|
||||||
});
|
});
|
||||||
|
|
||||||
if (req.body.embedColor) await prisma.user.update({
|
if (req.body.embedColor)
|
||||||
|
await prisma.user.update({
|
||||||
where: { id: user.id },
|
where: { id: user.id },
|
||||||
data: { embedColor: req.body.embedColor },
|
data: { embedColor: req.body.embedColor },
|
||||||
});
|
});
|
||||||
|
|
||||||
if (req.body.embedSiteName) await prisma.user.update({
|
if (req.body.embedSiteName)
|
||||||
|
await prisma.user.update({
|
||||||
where: { id: user.id },
|
where: { id: user.id },
|
||||||
data: { embedSiteName: req.body.embedSiteName },
|
data: { embedSiteName: req.body.embedSiteName },
|
||||||
});
|
});
|
||||||
|
|
||||||
if (req.body.systemTheme) await prisma.user.update({
|
if (req.body.systemTheme)
|
||||||
|
await prisma.user.update({
|
||||||
where: { id: user.id },
|
where: { id: user.id },
|
||||||
data: { systemTheme: req.body.systemTheme },
|
data: { systemTheme: req.body.systemTheme },
|
||||||
});
|
});
|
||||||
|
|
||||||
if (req.body.domains) {
|
if (req.body.domains) {
|
||||||
if (!req.body.domains) await prisma.user.update({
|
if (!req.body.domains)
|
||||||
|
await prisma.user.update({
|
||||||
where: { id: user.id },
|
where: { id: user.id },
|
||||||
data: { domains: [] },
|
data: { domains: [] },
|
||||||
});
|
});
|
||||||
|
|
|
@ -7,7 +7,7 @@ async function handler(req: NextApiReq, res: NextApiRes) {
|
||||||
|
|
||||||
const take = Number(req.query.take ?? 4);
|
const take = Number(req.query.take ?? 4);
|
||||||
|
|
||||||
if (take > 50) return res.error('take can\'t be more than 50');
|
if (take > 50) return res.error("take can't be more than 50");
|
||||||
|
|
||||||
let images = await prisma.image.findMany({
|
let images = await prisma.image.findMany({
|
||||||
take,
|
take,
|
||||||
|
@ -27,8 +27,9 @@ async function handler(req: NextApiReq, res: NextApiRes) {
|
||||||
});
|
});
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
images.map(image => image.url = `/r/${image.file}`);
|
images.map((image) => (image.url = `/r/${image.file}`));
|
||||||
if (req.query.filter && req.query.filter === 'media') images = images.filter(x => /^(video|audio|image)/.test(x.mimetype));
|
if (req.query.filter && req.query.filter === 'media')
|
||||||
|
images = images.filter((x) => /^(video|audio|image)/.test(x.mimetype));
|
||||||
|
|
||||||
return res.json(images);
|
return res.json(images);
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,7 +33,7 @@ async function handler(req: NextApiReq, res: NextApiRes) {
|
||||||
});
|
});
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
urls.map(url => url.url = `${config.urls.route}/${url.vanity ?? url.id}`);
|
urls.map((url) => (url.url = `${config.urls.route}/${url.vanity ?? url.id}`));
|
||||||
return res.json(urls);
|
return res.json(urls);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,17 +21,17 @@ async function handler(req: NextApiReq, res: NextApiRes) {
|
||||||
|
|
||||||
const user = await req.user();
|
const user = await req.user();
|
||||||
if (!user) return res.forbid('not logged in');
|
if (!user) return res.forbid('not logged in');
|
||||||
if (!user.administrator) return res.forbid('you aren\'t an administrator');
|
if (!user.administrator) return res.forbid("you aren't an administrator");
|
||||||
|
|
||||||
if (req.method === 'DELETE') {
|
if (req.method === 'DELETE') {
|
||||||
if (req.body.id === user.id) return res.forbid('you can\'t delete your own account');
|
if (req.body.id === user.id) return res.forbid("you can't delete your own account");
|
||||||
|
|
||||||
const deleteUser = await prisma.user.findFirst({
|
const deleteUser = await prisma.user.findFirst({
|
||||||
where: {
|
where: {
|
||||||
id: req.body.id,
|
id: req.body.id,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
if (!deleteUser) return res.forbid('user doesn\'t exist');
|
if (!deleteUser) return res.forbid("user doesn't exist");
|
||||||
|
|
||||||
if (req.body.delete_images) {
|
if (req.body.delete_images) {
|
||||||
const files = await prisma.image.findMany({
|
const files = await prisma.image.findMany({
|
||||||
|
@ -49,7 +49,9 @@ async function handler(req: NextApiReq, res: NextApiRes) {
|
||||||
userId: deleteUser.id,
|
userId: deleteUser.id,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
Logger.get('image').info(`User ${user.username} (${user.id}) deleted ${count} files of user ${deleteUser.username} (${deleteUser.id})`);
|
Logger.get('image').info(
|
||||||
|
`User ${user.username} (${user.id}) deleted ${count} files of user ${deleteUser.username} (${deleteUser.id})`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
await prisma.user.delete({
|
await prisma.user.delete({
|
||||||
|
|
|
@ -2,7 +2,6 @@ import { readFile } from 'fs/promises';
|
||||||
import config from 'lib/config';
|
import config from 'lib/config';
|
||||||
import { NextApiReq, NextApiRes, withZipline } from 'middleware/withZipline';
|
import { NextApiReq, NextApiRes, withZipline } from 'middleware/withZipline';
|
||||||
|
|
||||||
|
|
||||||
async function handler(req: NextApiReq, res: NextApiRes) {
|
async function handler(req: NextApiReq, res: NextApiRes) {
|
||||||
const user = await req.user();
|
const user = await req.user();
|
||||||
if (!user) return res.forbid('not logged in');
|
if (!user) return res.forbid('not logged in');
|
||||||
|
|
|
@ -29,14 +29,15 @@ export default function Login({ title, oauth_registration, oauth_providers: unpa
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const onSubmit = async values => {
|
const onSubmit = async (values) => {
|
||||||
const username = values.username.trim();
|
const username = values.username.trim();
|
||||||
const password = values.password.trim();
|
const password = values.password.trim();
|
||||||
|
|
||||||
if (username === '') return form.setFieldError('username', 'Username can\'t be nothing');
|
if (username === '') return form.setFieldError('username', "Username can't be nothing");
|
||||||
|
|
||||||
const res = await useFetch('/api/auth/login', 'POST', {
|
const res = await useFetch('/api/auth/login', 'POST', {
|
||||||
username, password,
|
username,
|
||||||
|
password,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (res.error) {
|
if (res.error) {
|
||||||
|
@ -47,7 +48,7 @@ export default function Login({ title, oauth_registration, oauth_providers: unpa
|
||||||
form.setFieldError('password', 'Invalid password');
|
form.setFieldError('password', 'Invalid password');
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
await router.push(router.query.url as string || '/dashboard');
|
await router.push((router.query.url as string) || '/dashboard');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -70,14 +71,18 @@ export default function Login({ title, oauth_registration, oauth_providers: unpa
|
||||||
<TextInput size='lg' id='username' label='Username' {...form.getInputProps('username')} />
|
<TextInput size='lg' id='username' label='Username' {...form.getInputProps('username')} />
|
||||||
<PasswordInput size='lg' id='password' label='Password' {...form.getInputProps('password')} />
|
<PasswordInput size='lg' id='password' label='Password' {...form.getInputProps('password')} />
|
||||||
|
|
||||||
<Button size='lg' type='submit' fullWidth mt={12}>Login</Button>
|
<Button size='lg' type='submit' fullWidth mt={12}>
|
||||||
|
Login
|
||||||
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
{oauth_registration && (
|
{oauth_registration && (
|
||||||
<>
|
<>
|
||||||
<Divider label='or' labelPosition='center' my={8} />
|
<Divider label='or' labelPosition='center' my={8} />
|
||||||
{oauth_providers.map(({ url, name, Icon }, i) => (
|
{oauth_providers.map(({ url, name, Icon }, i) => (
|
||||||
<Link key={i} href={url} passHref>
|
<Link key={i} href={url} passHref>
|
||||||
<Button size='lg' fullWidth leftIcon={<Icon />} component='a' my={8}>Login in with {name}</Button>
|
<Button size='lg' fullWidth leftIcon={<Icon />} component='a' my={8}>
|
||||||
|
Login in with {name}
|
||||||
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
))}
|
))}
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -23,9 +23,7 @@ export default function Logout() {
|
||||||
})();
|
})();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return <LoadingOverlay visible={true} />;
|
||||||
<LoadingOverlay visible={true} />
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Logout.title = 'Zipline - Logout';
|
Logout.title = 'Zipline - Logout';
|
|
@ -23,7 +23,7 @@ export default function Login({ title, oauth_registration, oauth_providers: unpa
|
||||||
if (!oauth_registration) {
|
if (!oauth_registration) {
|
||||||
router.push('/auth/login');
|
router.push('/auth/login');
|
||||||
return null;
|
return null;
|
||||||
};
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
(async () => {
|
(async () => {
|
||||||
|
@ -46,7 +46,9 @@ export default function Login({ title, oauth_registration, oauth_providers: unpa
|
||||||
</Link>
|
</Link>
|
||||||
{oauth_providers.map(({ url, name, Icon }, i) => (
|
{oauth_providers.map(({ url, name, Icon }, i) => (
|
||||||
<Link key={i} href={url} passHref>
|
<Link key={i} href={url} passHref>
|
||||||
<Button size='lg' fullWidth mt={12} leftIcon={<Icon />} component='a'>Sign in with {name}</Button>
|
<Button size='lg' fullWidth mt={12} leftIcon={<Icon />} component='a'>
|
||||||
|
Sign in with {name}
|
||||||
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -4,17 +4,15 @@ import { streamToString } from 'lib/utils/streams';
|
||||||
import { GetServerSideProps } from 'next';
|
import { GetServerSideProps } from 'next';
|
||||||
|
|
||||||
type CodeProps = {
|
type CodeProps = {
|
||||||
code: string,
|
code: string;
|
||||||
id: string,
|
id: string;
|
||||||
}
|
};
|
||||||
|
|
||||||
// Code component
|
// Code component
|
||||||
export default function Code(
|
export default function Code({ code, id }: CodeProps) {
|
||||||
{ code, id }: CodeProps
|
|
||||||
) {
|
|
||||||
return (
|
return (
|
||||||
<Prism
|
<Prism
|
||||||
sx={t => ({ height: '100vh', backgroundColor: t.colors.dark[8] })}
|
sx={(t) => ({ height: '100vh', backgroundColor: t.colors.dark[8] })}
|
||||||
withLineNumbers
|
withLineNumbers
|
||||||
language={exts[id.split('.').pop()]?.toLowerCase()}
|
language={exts[id.split('.').pop()]?.toLowerCase()}
|
||||||
>
|
>
|
||||||
|
@ -30,14 +28,12 @@ export const getServerSideProps: GetServerSideProps<CodeProps> = async (context)
|
||||||
const { default: datasource } = await import('lib/datasource');
|
const { default: datasource } = await import('lib/datasource');
|
||||||
|
|
||||||
const data = await datasource.get(context.params.id as string);
|
const data = await datasource.get(context.params.id as string);
|
||||||
if (!data) return {
|
if (!data)
|
||||||
|
return {
|
||||||
notFound: true,
|
notFound: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
context.res.setHeader(
|
context.res.setHeader('Cache-Control', 'public, max-age=2628000, stale-while-revalidate=86400');
|
||||||
'Cache-Control',
|
|
||||||
'public, max-age=2628000, stale-while-revalidate=86400'
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
|
|
|
@ -17,9 +17,7 @@ export default function FilesPage(props) {
|
||||||
<title>{props.title} - Files</title>
|
<title>{props.title} - Files</title>
|
||||||
</Head>
|
</Head>
|
||||||
|
|
||||||
<Layout
|
<Layout props={props}>
|
||||||
props={props}
|
|
||||||
>
|
|
||||||
<Files disableMediaPreview={props.disable_media_preview} />
|
<Files disableMediaPreview={props.disable_media_preview} />
|
||||||
</Layout>
|
</Layout>
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -16,9 +16,7 @@ export default function DashboardPage(props) {
|
||||||
<Head>
|
<Head>
|
||||||
<title>{props.title}</title>
|
<title>{props.title}</title>
|
||||||
</Head>
|
</Head>
|
||||||
<Layout
|
<Layout props={props}>
|
||||||
props={props}
|
|
||||||
>
|
|
||||||
<Dashboard disableMediaPreview={props.disable_media_preview} />
|
<Dashboard disableMediaPreview={props.disable_media_preview} />
|
||||||
</Layout>
|
</Layout>
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -14,7 +14,7 @@ export default function InvitesPage(props) {
|
||||||
router.push('/dashboard');
|
router.push('/dashboard');
|
||||||
}, []);
|
}, []);
|
||||||
return null;
|
return null;
|
||||||
};
|
}
|
||||||
|
|
||||||
const { loading } = useLogin();
|
const { loading } = useLogin();
|
||||||
|
|
||||||
|
@ -25,9 +25,7 @@ export default function InvitesPage(props) {
|
||||||
<Head>
|
<Head>
|
||||||
<title>{props.title} - Invites</title>
|
<title>{props.title} - Invites</title>
|
||||||
</Head>
|
</Head>
|
||||||
<Layout
|
<Layout props={props}>
|
||||||
props={props}
|
|
||||||
>
|
|
||||||
<Invites />
|
<Invites />
|
||||||
</Layout>
|
</Layout>
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -16,9 +16,7 @@ export default function ManagePage(props) {
|
||||||
<Head>
|
<Head>
|
||||||
<title>{props.title} - Manage User</title>
|
<title>{props.title} - Manage User</title>
|
||||||
</Head>
|
</Head>
|
||||||
<Layout
|
<Layout props={props}>
|
||||||
props={props}
|
|
||||||
>
|
|
||||||
<Manage />
|
<Manage />
|
||||||
</Layout>
|
</Layout>
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -16,9 +16,7 @@ export default function StatsPage(props) {
|
||||||
<Head>
|
<Head>
|
||||||
<title>{props.title} - Stats</title>
|
<title>{props.title} - Stats</title>
|
||||||
</Head>
|
</Head>
|
||||||
<Layout
|
<Layout props={props}>
|
||||||
props={props}
|
|
||||||
>
|
|
||||||
<Stats />
|
<Stats />
|
||||||
</Layout>
|
</Layout>
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -16,9 +16,7 @@ export default function UploadTextPage(props) {
|
||||||
<Head>
|
<Head>
|
||||||
<title>{props.title} - Upload Text</title>
|
<title>{props.title} - Upload Text</title>
|
||||||
</Head>
|
</Head>
|
||||||
<Layout
|
<Layout props={props}>
|
||||||
props={props}
|
|
||||||
>
|
|
||||||
<UploadText />
|
<UploadText />
|
||||||
</Layout>
|
</Layout>
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -16,9 +16,7 @@ export default function UploadPage(props) {
|
||||||
<Head>
|
<Head>
|
||||||
<title>{props.title} - Upload</title>
|
<title>{props.title} - Upload</title>
|
||||||
</Head>
|
</Head>
|
||||||
<Layout
|
<Layout props={props}>
|
||||||
props={props}
|
|
||||||
>
|
|
||||||
<Upload />
|
<Upload />
|
||||||
</Layout>
|
</Layout>
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -16,9 +16,7 @@ export default function UrlsPage(props) {
|
||||||
<Head>
|
<Head>
|
||||||
<title>{props.title} - URLs</title>
|
<title>{props.title} - URLs</title>
|
||||||
</Head>
|
</Head>
|
||||||
<Layout
|
<Layout props={props}>
|
||||||
props={props}
|
|
||||||
>
|
|
||||||
<Urls />
|
<Urls />
|
||||||
</Layout>
|
</Layout>
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -16,9 +16,7 @@ export default function UsersPage(props) {
|
||||||
<Head>
|
<Head>
|
||||||
<title>{props.title} - Users</title>
|
<title>{props.title} - Users</title>
|
||||||
</Head>
|
</Head>
|
||||||
<Layout
|
<Layout props={props}>
|
||||||
props={props}
|
|
||||||
>
|
|
||||||
<Users />
|
<Users />
|
||||||
</Layout>
|
</Layout>
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -51,7 +51,11 @@ export default function Invite({ code, title }) {
|
||||||
};
|
};
|
||||||
|
|
||||||
const createUser = async () => {
|
const createUser = async () => {
|
||||||
const res = await useFetch('/api/auth/create', 'POST', { code, username, password });
|
const res = await useFetch('/api/auth/create', 'POST', {
|
||||||
|
code,
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
});
|
||||||
if (res.error) {
|
if (res.error) {
|
||||||
showNotification({
|
showNotification({
|
||||||
title: 'Error while creating user',
|
title: 'Error while creating user',
|
||||||
|
@ -70,22 +74,24 @@ export default function Invite({ code, title }) {
|
||||||
setUser(null);
|
setUser(null);
|
||||||
await useFetch('/api/auth/logout');
|
await useFetch('/api/auth/logout');
|
||||||
await useFetch('/api/auth/login', 'POST', {
|
await useFetch('/api/auth/login', 'POST', {
|
||||||
username, password,
|
username,
|
||||||
|
password,
|
||||||
});
|
});
|
||||||
|
|
||||||
router.push('/dashboard');
|
router.push('/dashboard');
|
||||||
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Head>
|
<Head>
|
||||||
<title>{title} - Invite ({code})</title>
|
<title>
|
||||||
|
{title} - Invite ({code})
|
||||||
|
</title>
|
||||||
</Head>
|
</Head>
|
||||||
<Center sx={{ height: '100vh' }}>
|
<Center sx={{ height: '100vh' }}>
|
||||||
<Box
|
<Box
|
||||||
sx={t => ({
|
sx={(t) => ({
|
||||||
backgroundColor: t.colors.dark[6],
|
backgroundColor: t.colors.dark[6],
|
||||||
borderRadius: t.radius.sm,
|
borderRadius: t.radius.sm,
|
||||||
})}
|
})}
|
||||||
|
@ -101,14 +107,20 @@ export default function Invite({ code, title }) {
|
||||||
onBlur={() => checkUsername()}
|
onBlur={() => checkUsername()}
|
||||||
/>
|
/>
|
||||||
<Group position='center' mt='xl'>
|
<Group position='center' mt='xl'>
|
||||||
<Button disabled={usernameError !== '' || username == ''} onClick={nextStep}>Continue</Button>
|
<Button disabled={usernameError !== '' || username == ''} onClick={nextStep}>
|
||||||
|
Continue
|
||||||
|
</Button>
|
||||||
</Group>
|
</Group>
|
||||||
</Stepper.Step>
|
</Stepper.Step>
|
||||||
<Stepper.Step label='Choose a password' allowStepSelect={active > 1 && usernameError === ''}>
|
<Stepper.Step label='Choose a password' allowStepSelect={active > 1 && usernameError === ''}>
|
||||||
<PasswordStrength value={password} setValue={setPassword} setStrength={setStrength} />
|
<PasswordStrength value={password} setValue={setPassword} setStrength={setStrength} />
|
||||||
<Group position='center' mt='xl'>
|
<Group position='center' mt='xl'>
|
||||||
<Button variant='default' onClick={prevStep}>Back</Button>
|
<Button variant='default' onClick={prevStep}>
|
||||||
<Button disabled={strength !== 100} onClick={nextStep}>Continue</Button>
|
Back
|
||||||
|
</Button>
|
||||||
|
<Button disabled={strength !== 100} onClick={nextStep}>
|
||||||
|
Continue
|
||||||
|
</Button>
|
||||||
</Group>
|
</Group>
|
||||||
</Stepper.Step>
|
</Stepper.Step>
|
||||||
<Stepper.Step label='Verify your password' allowStepSelect={active > 2}>
|
<Stepper.Step label='Verify your password' allowStepSelect={active > 2}>
|
||||||
|
@ -120,13 +132,19 @@ export default function Invite({ code, title }) {
|
||||||
onBlur={() => checkPassword()}
|
onBlur={() => checkPassword()}
|
||||||
/>
|
/>
|
||||||
<Group position='center' mt='xl'>
|
<Group position='center' mt='xl'>
|
||||||
<Button variant='default' onClick={prevStep}>Back</Button>
|
<Button variant='default' onClick={prevStep}>
|
||||||
<Button disabled={verifyPasswordError !== '' || verifyPassword == ''} onClick={nextStep}>Continue</Button>
|
Back
|
||||||
|
</Button>
|
||||||
|
<Button disabled={verifyPasswordError !== '' || verifyPassword == ''} onClick={nextStep}>
|
||||||
|
Continue
|
||||||
|
</Button>
|
||||||
</Group>
|
</Group>
|
||||||
</Stepper.Step>
|
</Stepper.Step>
|
||||||
<Stepper.Completed>
|
<Stepper.Completed>
|
||||||
<Group position='center' mt='xl'>
|
<Group position='center' mt='xl'>
|
||||||
<Button variant='default' onClick={() => setActive(0)}>Go back</Button>
|
<Button variant='default' onClick={() => setActive(0)}>
|
||||||
|
Go back
|
||||||
|
</Button>
|
||||||
<Button onClick={() => createUser()}>Finish setup</Button>
|
<Button onClick={() => createUser()}>Finish setup</Button>
|
||||||
</Group>
|
</Group>
|
||||||
</Stepper.Completed>
|
</Stepper.Completed>
|
||||||
|
@ -137,8 +155,9 @@ export default function Invite({ code, title }) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getServerSideProps: GetServerSideProps = async context => {
|
export const getServerSideProps: GetServerSideProps = async (context) => {
|
||||||
if (!config.features.invites) return {
|
if (!config.features.invites)
|
||||||
|
return {
|
||||||
notFound: true,
|
notFound: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -23,13 +23,17 @@ async function start() {
|
||||||
// annoy user if they didnt change secret from default "changethis"
|
// annoy user if they didnt change secret from default "changethis"
|
||||||
if (config.core.secret === 'changethis') {
|
if (config.core.secret === 'changethis') {
|
||||||
logger.error('Secret is not set!');
|
logger.error('Secret is not set!');
|
||||||
logger.error('Running Zipline as is, without a randomized secret is not recommended and leaves your instance at risk!');
|
logger.error(
|
||||||
|
'Running Zipline as is, without a randomized secret is not recommended and leaves your instance at risk!'
|
||||||
|
);
|
||||||
logger.error('Please change your secret in the config file or environment variables.');
|
logger.error('Please change your secret in the config file or environment variables.');
|
||||||
logger.error('The config file is located at `config.toml`, or if using docker-compose you can change the variables in the `docker-compose.yml` file.');
|
logger.error(
|
||||||
|
'The config file is located at `config.toml`, or if using docker-compose you can change the variables in the `docker-compose.yml` file.'
|
||||||
|
);
|
||||||
logger.error('It is recomended to use a secret that is alphanumeric and randomized.');
|
logger.error('It is recomended to use a secret that is alphanumeric and randomized.');
|
||||||
logger.error('A way you can generate this is through a password manager you may have.');
|
logger.error('A way you can generate this is through a password manager you may have.');
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
};
|
}
|
||||||
|
|
||||||
process.env.DATABASE_URL = config.core.database_url;
|
process.env.DATABASE_URL = config.core.database_url;
|
||||||
await migrations();
|
await migrations();
|
||||||
|
@ -55,15 +59,15 @@ async function start() {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
router.on('GET', config.uploader.route === '/' ? '/:id(^[^\\.]+\\.[^\\.]+)' : `${config.uploader.route}/:id`, async (req, res, params) => {
|
router.on(
|
||||||
|
'GET',
|
||||||
|
config.uploader.route === '/' ? '/:id(^[^\\.]+\\.[^\\.]+)' : `${config.uploader.route}/:id`,
|
||||||
|
async (req, res, params) => {
|
||||||
if (params.id === '') return nextServer.render404(req, res as ServerResponse);
|
if (params.id === '') return nextServer.render404(req, res as ServerResponse);
|
||||||
|
|
||||||
const image = await prisma.image.findFirst({
|
const image = await prisma.image.findFirst({
|
||||||
where: {
|
where: {
|
||||||
OR: [
|
OR: [{ file: params.id }, { invisible: { invis: decodeURI(params.id) } }],
|
||||||
{ file: params.id },
|
|
||||||
{ invisible: { invis: decodeURI(params.id) } },
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -73,17 +77,15 @@ async function start() {
|
||||||
else if (image.embed) await handle(req, res);
|
else if (image.embed) await handle(req, res);
|
||||||
else await fileDb(req, res, nextServer, prisma, handle, image);
|
else await fileDb(req, res, nextServer, prisma, handle, image);
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
);
|
||||||
|
|
||||||
router.on('GET', '/r/:id', async (req, res, params) => {
|
router.on('GET', '/r/:id', async (req, res, params) => {
|
||||||
if (params.id === '') return nextServer.render404(req, res as ServerResponse);
|
if (params.id === '') return nextServer.render404(req, res as ServerResponse);
|
||||||
|
|
||||||
const image = await prisma.image.findFirst({
|
const image = await prisma.image.findFirst({
|
||||||
where: {
|
where: {
|
||||||
OR: [
|
OR: [{ file: params.id }, { invisible: { invis: decodeURI(params.id) } }],
|
||||||
{ file: params.id },
|
|
||||||
{ invisible: { invis: decodeURI(params.id) } },
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -127,12 +129,7 @@ async function start() {
|
||||||
}, config.core.invites_interval * 1000);
|
}, config.core.invites_interval * 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function rawFile(
|
async function rawFile(req: IncomingMessage, res: OutgoingMessage, nextServer: NextServer, id: string) {
|
||||||
req: IncomingMessage,
|
|
||||||
res: OutgoingMessage,
|
|
||||||
nextServer: NextServer,
|
|
||||||
id: string,
|
|
||||||
) {
|
|
||||||
const data = await datasource.get(id);
|
const data = await datasource.get(id);
|
||||||
if (!data) return nextServer.render404(req, res as ServerResponse);
|
if (!data) return nextServer.render404(req, res as ServerResponse);
|
||||||
const mimetype = await guess(extname(id));
|
const mimetype = await guess(extname(id));
|
||||||
|
@ -151,7 +148,7 @@ async function rawFileDb(
|
||||||
res: OutgoingMessage,
|
res: OutgoingMessage,
|
||||||
nextServer: NextServer,
|
nextServer: NextServer,
|
||||||
prisma: PrismaClient,
|
prisma: PrismaClient,
|
||||||
image: Image,
|
image: Image
|
||||||
) {
|
) {
|
||||||
if (image.expires_at && image.expires_at < new Date()) {
|
if (image.expires_at && image.expires_at < new Date()) {
|
||||||
Logger.get('server').info(`${image.file} expired`);
|
Logger.get('server').info(`${image.file} expired`);
|
||||||
|
@ -185,7 +182,7 @@ async function fileDb(
|
||||||
nextServer: NextServer,
|
nextServer: NextServer,
|
||||||
prisma: PrismaClient,
|
prisma: PrismaClient,
|
||||||
handle: RequestHandler,
|
handle: RequestHandler,
|
||||||
image: Image,
|
image: Image
|
||||||
) {
|
) {
|
||||||
if (image.expires_at && image.expires_at < new Date()) {
|
if (image.expires_at && image.expires_at < new Date()) {
|
||||||
await datasource.delete(image.file);
|
await datasource.delete(image.file);
|
||||||
|
|
|
@ -48,7 +48,6 @@ export function bytesToRead(bytes: number) {
|
||||||
return `${bytes.toFixed(1)} ${units[num]}`;
|
return `${bytes.toFixed(1)} ${units[num]}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export async function getStats(prisma: PrismaClient, datasource: Datasource) {
|
export async function getStats(prisma: PrismaClient, datasource: Datasource) {
|
||||||
const size = await datasource.fullSize();
|
const size = await datasource.fullSize();
|
||||||
const byUser = await prisma.image.groupBy({
|
const byUser = await prisma.image.groupBy({
|
||||||
|
@ -88,7 +87,11 @@ export async function getStats(prisma: PrismaClient, datasource: Datasource) {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const types_count = [];
|
const types_count = [];
|
||||||
for (let i = 0, L = typesCount.length; i !== L; ++i) types_count.push({ mimetype: typesCount[i].mimetype, count: typesCount[i]._count.mimetype });
|
for (let i = 0, L = typesCount.length; i !== L; ++i)
|
||||||
|
types_count.push({
|
||||||
|
mimetype: typesCount[i].mimetype,
|
||||||
|
count: typesCount[i]._count.mimetype,
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
size: bytesToRead(size),
|
size: bytesToRead(size),
|
||||||
|
@ -96,7 +99,7 @@ export async function getStats(prisma: PrismaClient, datasource: Datasource) {
|
||||||
count,
|
count,
|
||||||
count_by_user: count_by_user.sort((a, b) => b.count - a.count),
|
count_by_user: count_by_user.sort((a, b) => b.count - a.count),
|
||||||
count_users,
|
count_users,
|
||||||
views_count: (viewsCount[0]?._sum?.views ?? 0),
|
views_count: viewsCount[0]?._sum?.views ?? 0,
|
||||||
types_count: types_count.sort((a, b) => b.count - a.count),
|
types_count: types_count.sort((a, b) => b.count - a.count),
|
||||||
};
|
};
|
||||||
}
|
}
|
|
@ -1,11 +1,7 @@
|
||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "esnext",
|
"target": "esnext",
|
||||||
"lib": [
|
"lib": ["dom", "dom.iterable", "esnext"],
|
||||||
"dom",
|
|
||||||
"dom.iterable",
|
|
||||||
"esnext"
|
|
||||||
],
|
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"strict": false,
|
"strict": false,
|
||||||
|
@ -20,18 +16,10 @@
|
||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
"baseUrl": "src",
|
"baseUrl": "src",
|
||||||
"paths": {
|
"paths": {
|
||||||
"components/*": [
|
"components/*": ["components/*"],
|
||||||
"components/*"
|
"hooks/*": ["lib/hooks/*"],
|
||||||
],
|
"middleware/*": ["lib/middleware/*"],
|
||||||
"hooks/*": [
|
"lib/*": ["lib/*"]
|
||||||
"lib/hooks/*"
|
|
||||||
],
|
|
||||||
"middleware/*": [
|
|
||||||
"lib/middleware/*"
|
|
||||||
],
|
|
||||||
"lib/*": [
|
|
||||||
"lib/*"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"incremental": true
|
"incremental": true
|
||||||
},
|
},
|
||||||
|
@ -44,10 +32,5 @@
|
||||||
"**/**/*.tsx",
|
"**/**/*.tsx",
|
||||||
"prisma/seed.ts"
|
"prisma/seed.ts"
|
||||||
],
|
],
|
||||||
"exclude": [
|
"exclude": ["node_modules", "dist", ".yarn", ".next"]
|
||||||
"node_modules",
|
|
||||||
"dist",
|
|
||||||
".yarn",
|
|
||||||
".next",
|
|
||||||
]
|
|
||||||
}
|
}
|
22
yarn.lock
22
yarn.lock
|
@ -3880,6 +3880,17 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"eslint-config-prettier@npm:^8.5.0":
|
||||||
|
version: 8.5.0
|
||||||
|
resolution: "eslint-config-prettier@npm:8.5.0"
|
||||||
|
peerDependencies:
|
||||||
|
eslint: ">=7.0.0"
|
||||||
|
bin:
|
||||||
|
eslint-config-prettier: bin/cli.js
|
||||||
|
checksum: 0d0f5c32e7a0ad91249467ce71ca92394ccd343178277d318baf32063b79ea90216f4c81d1065d60f96366fdc60f151d4d68ae7811a58bd37228b84c2083f893
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"eslint-import-resolver-node@npm:^0.3.6":
|
"eslint-import-resolver-node@npm:^0.3.6":
|
||||||
version: 0.3.6
|
version: 0.3.6
|
||||||
resolution: "eslint-import-resolver-node@npm:0.3.6"
|
resolution: "eslint-import-resolver-node@npm:0.3.6"
|
||||||
|
@ -6982,6 +6993,15 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"prettier@npm:^2.7.1":
|
||||||
|
version: 2.7.1
|
||||||
|
resolution: "prettier@npm:2.7.1"
|
||||||
|
bin:
|
||||||
|
prettier: bin-prettier.js
|
||||||
|
checksum: 55a4409182260866ab31284d929b3cb961e5fdb91fe0d2e099dac92eaecec890f36e524b4c19e6ceae839c99c6d7195817579cdffc8e2c80da0cb794463a748b
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"prism-react-renderer@npm:^1.2.1":
|
"prism-react-renderer@npm:^1.2.1":
|
||||||
version: 1.3.5
|
version: 1.3.5
|
||||||
resolution: "prism-react-renderer@npm:1.3.5"
|
resolution: "prism-react-renderer@npm:1.3.5"
|
||||||
|
@ -8922,6 +8942,7 @@ __metadata:
|
||||||
esbuild: ^0.14.44
|
esbuild: ^0.14.44
|
||||||
eslint: ^7.32.0
|
eslint: ^7.32.0
|
||||||
eslint-config-next: 12.1.6
|
eslint-config-next: 12.1.6
|
||||||
|
eslint-config-prettier: ^8.5.0
|
||||||
eslint-plugin-prettier: ^4.2.1
|
eslint-plugin-prettier: ^4.2.1
|
||||||
fflate: ^0.7.3
|
fflate: ^0.7.3
|
||||||
find-my-way: ^6.3.0
|
find-my-way: ^6.3.0
|
||||||
|
@ -8930,6 +8951,7 @@ __metadata:
|
||||||
multer: ^1.4.5-lts.1
|
multer: ^1.4.5-lts.1
|
||||||
next: ^12.1.6
|
next: ^12.1.6
|
||||||
npm-run-all: ^4.1.5
|
npm-run-all: ^4.1.5
|
||||||
|
prettier: ^2.7.1
|
||||||
prisma: ^4.1.0
|
prisma: ^4.1.0
|
||||||
react: ^18.2.0
|
react: ^18.2.0
|
||||||
react-chartjs-2: ^4.3.1
|
react-chartjs-2: ^4.3.1
|
||||||
|
|
Loading…
Reference in a new issue