feat: bunch of stuff
This commit is contained in:
parent
bb7367615d
commit
f67d1d41cb
79 changed files with 708 additions and 466 deletions
17
.github/ISSUE_TEMPLATE/bug.yml
vendored
17
.github/ISSUE_TEMPLATE/bug.yml
vendored
|
@ -17,8 +17,9 @@ body:
|
|||
label: Version
|
||||
description: What version of Zipline are you using?
|
||||
options:
|
||||
- upstream
|
||||
- latest
|
||||
- upstream (ghcr.io/diced/zipline:trunk)
|
||||
- latest (ghcr.io/diced/zipline:latest)
|
||||
- other (provide version in additional info)
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
|
@ -28,14 +29,15 @@ body:
|
|||
multiple: true
|
||||
options:
|
||||
- Firefox
|
||||
- Chrome
|
||||
- Chromium-based (Chrome, Edge, Brave, Opera, mobile chrome/chromium based, etc)
|
||||
- Safari
|
||||
- Microsoft Edge
|
||||
- Firefox Mobile
|
||||
- Safari Mobile
|
||||
- type: textarea
|
||||
id: zipline-logs
|
||||
attributes:
|
||||
label: Zipline Logs
|
||||
description: Please copy and paste any relevant log output.
|
||||
description: Please copy and paste any relevant log output. Not seeing anything interesting? Try adding the `DEBUG=true` environment variable to see more logs, make sure to review the output and remove any sensitive information as it can be VERY verbose at times.
|
||||
render: shell
|
||||
- type: textarea
|
||||
id: browser-logs
|
||||
|
@ -43,3 +45,8 @@ body:
|
|||
label: Browser Logs
|
||||
description: Please copy and paste any relevant log output.
|
||||
render: shell
|
||||
- type: textarea
|
||||
id: additional-info
|
||||
attributes:
|
||||
label: Additional Info
|
||||
description: Anything else that could be used to narrow down the issue, like your config.
|
||||
|
|
3
.github/ISSUE_TEMPLATE/config.yml
vendored
3
.github/ISSUE_TEMPLATE/config.yml
vendored
|
@ -3,3 +3,6 @@ contact_links:
|
|||
- name: Zipline Discord
|
||||
url: https://discord.gg/EAhCRfGxCF
|
||||
about: Ask for help with anything related to Zipline!
|
||||
- name: Zipline Docs
|
||||
url: https://zipline.diced.tech
|
||||
about: Maybe take a look a the docs?
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
"license": "MIT",
|
||||
"scripts": {
|
||||
"dev": "npm-run-all build:server dev:run",
|
||||
"dev:run": "cross-env REACT_EDITOR=code NODE_ENV=development RECOIL_DUPLICATE_ATOM_KEY_CHECKING_ENABLED=false node --enable-source-maps dist/server",
|
||||
"dev:run": "cross-env DEBUG=true REACT_EDITOR=code NODE_ENV=development RECOIL_DUPLICATE_ATOM_KEY_CHECKING_ENABLED=false node --enable-source-maps dist/server",
|
||||
"build": "npm-run-all build:server build:schema build:next",
|
||||
"build-ci": "cross-env ZIPLINE_DOCKER_BUILD=1 npm-run-all build:server build:schema build:next",
|
||||
"build:server": "node esbuild.config.js",
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import { Button, Card, Group, LoadingOverlay, Modal, Stack, Text, Title, Tooltip } from '@mantine/core';
|
||||
import { useClipboard } from '@mantine/hooks';
|
||||
import { showNotification } from '@mantine/notifications';
|
||||
import { relativeTime } from 'lib/utils/client';
|
||||
import { useFileDelete, useFileFavorite } from 'lib/queries/files';
|
||||
import { relativeTime } from 'lib/utils/client';
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
CalendarIcon,
|
||||
|
@ -11,15 +11,15 @@ import {
|
|||
CrossIcon,
|
||||
DeleteIcon,
|
||||
ExternalLinkIcon,
|
||||
EyeIcon,
|
||||
FileIcon,
|
||||
HashIcon,
|
||||
ImageIcon,
|
||||
StarIcon,
|
||||
EyeIcon,
|
||||
} from './icons';
|
||||
import Link from './Link';
|
||||
import MutedText from './MutedText';
|
||||
import Type from './Type';
|
||||
import Link from './Link';
|
||||
|
||||
export function FileMeta({ Icon, title, subtitle, ...other }) {
|
||||
return other.tooltip ? (
|
||||
|
|
|
@ -1,11 +1,14 @@
|
|||
import {
|
||||
AppShell,
|
||||
Badge,
|
||||
Box,
|
||||
Burger,
|
||||
Button,
|
||||
Divider,
|
||||
Group,
|
||||
Header,
|
||||
Image,
|
||||
MediaQuery,
|
||||
Menu,
|
||||
Navbar,
|
||||
NavLink,
|
||||
Paper,
|
||||
|
@ -15,13 +18,9 @@ import {
|
|||
Stack,
|
||||
Text,
|
||||
Title,
|
||||
Tooltip,
|
||||
UnstyledButton,
|
||||
useMantineTheme,
|
||||
Group,
|
||||
Image,
|
||||
Tooltip,
|
||||
Badge,
|
||||
Menu,
|
||||
} from '@mantine/core';
|
||||
import { useClipboard } from '@mantine/hooks';
|
||||
import { useModals } from '@mantine/modals';
|
||||
|
@ -30,18 +29,21 @@ import useFetch from 'hooks/useFetch';
|
|||
import { useVersion } from 'lib/queries/version';
|
||||
import { userSelector } from 'lib/recoil/user';
|
||||
import { capitalize } from 'lib/utils/client';
|
||||
import { useRecoilState } from 'recoil';
|
||||
import Link from 'next/link';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useState } from 'react';
|
||||
import { useRecoilState } from 'recoil';
|
||||
import {
|
||||
ExternalLinkIcon,
|
||||
ActivityIcon,
|
||||
CheckIcon,
|
||||
CopyIcon,
|
||||
CrossIcon,
|
||||
DeleteIcon,
|
||||
DiscordIcon,
|
||||
ExternalLinkIcon,
|
||||
FileIcon,
|
||||
GitHubIcon,
|
||||
GoogleIcon,
|
||||
HomeIcon,
|
||||
LinkIcon,
|
||||
LogoutIcon,
|
||||
|
@ -51,9 +53,6 @@ import {
|
|||
TypeIcon,
|
||||
UploadIcon,
|
||||
UserIcon,
|
||||
DiscordIcon,
|
||||
GitHubIcon,
|
||||
GoogleIcon,
|
||||
} from './icons';
|
||||
import { friendlyThemeName, themes } from './Theming';
|
||||
|
||||
|
@ -136,12 +135,12 @@ const items = [
|
|||
{
|
||||
icon: <UploadIcon size={18} />,
|
||||
text: 'Upload',
|
||||
link: '/dashboard/upload',
|
||||
link: '/dashboard/upload/file',
|
||||
},
|
||||
{
|
||||
icon: <TypeIcon size={18} />,
|
||||
text: 'Upload Text',
|
||||
link: '/dashboard/text',
|
||||
link: '/dashboard/upload/text',
|
||||
},
|
||||
];
|
||||
|
||||
|
@ -150,7 +149,7 @@ const admin_items = [
|
|||
icon: <UserIcon size={18} />,
|
||||
text: 'Users',
|
||||
link: '/dashboard/users',
|
||||
if: (props) => true,
|
||||
if: () => true,
|
||||
},
|
||||
{
|
||||
icon: <TagIcon size={18} />,
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
// https://mantine.dev/core/password-input/
|
||||
|
||||
import { Box, PasswordInput, Popover, Progress, Text } from '@mantine/core';
|
||||
import { useState } from 'react';
|
||||
import { PasswordInput, Progress, Text, Popover, Box } from '@mantine/core';
|
||||
import { CheckIcon, CrossIcon } from './icons';
|
||||
|
||||
function PasswordRequirement({ meets, label }: { meets: boolean; label: string }) {
|
||||
|
|
|
@ -12,12 +12,12 @@ import matcha_dark_azul from 'lib/themes/matcha_dark_azul';
|
|||
import nord from 'lib/themes/nord';
|
||||
import qogir_dark from 'lib/themes/qogir_dark';
|
||||
|
||||
import { MantineProvider, MantineThemeOverride } from '@mantine/core';
|
||||
import { createEmotionCache, MantineProvider, MantineThemeOverride } from '@mantine/core';
|
||||
import { useColorScheme } from '@mantine/hooks';
|
||||
import { ModalsProvider } from '@mantine/modals';
|
||||
import { NotificationsProvider } from '@mantine/notifications';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { userSelector } from 'lib/recoil/user';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
export const themes = {
|
||||
system: (colorScheme: 'dark' | 'light') => (colorScheme === 'dark' ? dark_blue : light_blue),
|
||||
|
@ -47,6 +47,8 @@ export const friendlyThemeName = {
|
|||
qogir_dark: 'Qogir Dark',
|
||||
};
|
||||
|
||||
const cache = createEmotionCache({ key: 'zipline' });
|
||||
|
||||
export default function ZiplineTheming({ Component, pageProps, ...props }) {
|
||||
const user = useRecoilValue(userSelector);
|
||||
const colorScheme = useColorScheme();
|
||||
|
@ -65,8 +67,14 @@ export default function ZiplineTheming({ Component, pageProps, ...props }) {
|
|||
<MantineProvider
|
||||
withGlobalStyles
|
||||
withNormalizeCSS
|
||||
emotionCache={cache}
|
||||
theme={{
|
||||
...theme,
|
||||
fontFamily: 'Ubuntu, sans-serif',
|
||||
fontFamilyMonospace: 'Ubuntu Mono, monospace',
|
||||
headings: {
|
||||
fontFamily: 'Ubuntu, sans-serif',
|
||||
},
|
||||
components: {
|
||||
AppShell: {
|
||||
styles: (t) => ({
|
||||
|
@ -92,6 +100,7 @@ export default function ZiplineTheming({ Component, pageProps, ...props }) {
|
|||
Popover: {
|
||||
defaultProps: {
|
||||
transition: 'pop',
|
||||
shadow: 'lg',
|
||||
},
|
||||
},
|
||||
LoadingOverlay: {
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import React from 'react';
|
||||
import { Dropzone as MantineDropzone } from '@mantine/dropzone';
|
||||
import { Group, Text, useMantineTheme } from '@mantine/core';
|
||||
import { Dropzone as MantineDropzone } from '@mantine/dropzone';
|
||||
import { ImageIcon } from 'components/icons';
|
||||
|
||||
export default function Dropzone({ loading, onDrop, children }) {
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import React from 'react';
|
||||
import { Table, Tooltip, Badge, HoverCard, Text, useMantineTheme, Group } from '@mantine/core';
|
||||
import { Badge, Group, HoverCard, Table, useMantineTheme } from '@mantine/core';
|
||||
import Type from 'components/Type';
|
||||
|
||||
export function FilePreview({ file }: { file: File }) {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { Box, Card as MantineCard, Center, Group, SimpleGrid, Skeleton, Title } from '@mantine/core';
|
||||
import { Card as MantineCard, Center, Group, SimpleGrid, Skeleton, Title } from '@mantine/core';
|
||||
import { randomId } from '@mantine/hooks';
|
||||
import File from 'components/File';
|
||||
import MutedText from 'components/MutedText';
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import { SimpleGrid } from '@mantine/core';
|
||||
import { FileIcon } from 'components/icons';
|
||||
import StatCard from 'components/StatCard';
|
||||
import { percentChange } from 'lib/utils/client';
|
||||
import { useStats } from 'lib/queries/stats';
|
||||
import { percentChange } from 'lib/utils/client';
|
||||
import { Database, Eye, Users } from 'react-feather';
|
||||
|
||||
export function StatCards() {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { Box, Center, Checkbox, Group, Pagination, SimpleGrid, Skeleton, Text, Title } from '@mantine/core';
|
||||
import { Box, Center, Checkbox, Group, Pagination, SimpleGrid, Skeleton, Title } from '@mantine/core';
|
||||
import File from 'components/File';
|
||||
import { FileIcon } from 'components/icons';
|
||||
import MutedText from 'components/MutedText';
|
||||
|
|
|
@ -13,16 +13,16 @@ import {
|
|||
Title,
|
||||
Tooltip,
|
||||
} from '@mantine/core';
|
||||
import { useClipboard } from '@mantine/hooks';
|
||||
import { useForm } from '@mantine/form';
|
||||
import { useClipboard } from '@mantine/hooks';
|
||||
import { useModals } from '@mantine/modals';
|
||||
import { showNotification } from '@mantine/notifications';
|
||||
import { CopyIcon, CrossIcon, DeleteIcon, PlusIcon, TagIcon } from 'components/icons';
|
||||
import MutedText from 'components/MutedText';
|
||||
import useFetch from 'hooks/useFetch';
|
||||
import { expireText, relativeTime } from 'lib/utils/client';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { expireText, relativeTime } from 'lib/utils/client';
|
||||
|
||||
const expires = ['30m', '1h', '6h', '12h', '1d', '3d', '5d', '7d', 'never'];
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { Modal, Select, NumberInput, Group, Checkbox, Button, Title, Text } from '@mantine/core';
|
||||
import { Button, Checkbox, Group, Modal, NumberInput, Select, Text, Title } from '@mantine/core';
|
||||
import { useForm } from '@mantine/form';
|
||||
import { DownloadIcon } from 'components/icons';
|
||||
|
||||
|
|
|
@ -13,8 +13,8 @@ import {
|
|||
} from 'chart.js';
|
||||
import ChartDataLabels from 'chartjs-plugin-datalabels';
|
||||
import ColorHash from 'color-hash';
|
||||
import { bytesToHuman } from 'lib/utils/bytes';
|
||||
import { useStats } from 'lib/queries/stats';
|
||||
import { bytesToHuman } from 'lib/utils/bytes';
|
||||
import { useMemo } from 'react';
|
||||
import { Chart, Pie } from 'react-chartjs-2';
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { LoadingOverlay, Card, Box } from '@mantine/core';
|
||||
import { Box, Card, LoadingOverlay } from '@mantine/core';
|
||||
import { SmallTable } from 'components/SmallTable';
|
||||
import { useStats } from 'lib/queries/stats';
|
||||
|
||||
|
|
|
@ -2,12 +2,12 @@ import {
|
|||
Button,
|
||||
Collapse,
|
||||
Group,
|
||||
NumberInput,
|
||||
PasswordInput,
|
||||
Progress,
|
||||
Select,
|
||||
Title,
|
||||
PasswordInput,
|
||||
Tooltip,
|
||||
NumberInput,
|
||||
} from '@mantine/core';
|
||||
import { randomId, useClipboard } from '@mantine/hooks';
|
||||
import { showNotification, updateNotification } from '@mantine/notifications';
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
import { Button, Group, NumberInput, PasswordInput, Select, Tabs, Title, Tooltip } from '@mantine/core';
|
||||
import { Prism } from '@mantine/prism';
|
||||
import { Language } from 'prism-react-renderer';
|
||||
import { showNotification, updateNotification } from '@mantine/notifications';
|
||||
import { Prism } from '@mantine/prism';
|
||||
import CodeInput from 'components/CodeInput';
|
||||
import { ClockIcon, ImageIcon, TypeIcon, UploadIcon } from 'components/icons';
|
||||
import Link from 'components/Link';
|
||||
import exts from 'lib/exts';
|
||||
import { userSelector } from 'lib/recoil/user';
|
||||
import { Language } from 'prism-react-renderer';
|
||||
import { useState } from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
|
|
|
@ -1,25 +1,25 @@
|
|||
import {
|
||||
ActionIcon,
|
||||
Button,
|
||||
Card,
|
||||
Center,
|
||||
Group,
|
||||
Modal,
|
||||
NumberInput,
|
||||
SimpleGrid,
|
||||
Skeleton,
|
||||
TextInput,
|
||||
Title,
|
||||
Card,
|
||||
Center,
|
||||
NumberInput,
|
||||
} from '@mantine/core';
|
||||
import { useForm } from '@mantine/form';
|
||||
import { showNotification } from '@mantine/notifications';
|
||||
import { CrossIcon, LinkIcon, PlusIcon } from 'components/icons';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useURLs } from 'lib/queries/url';
|
||||
import URLCard from './URLCard';
|
||||
import MutedText from 'components/MutedText';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { useURLs } from 'lib/queries/url';
|
||||
import { userSelector } from 'lib/recoil/user';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import URLCard from './URLCard';
|
||||
|
||||
export default function Urls() {
|
||||
const user = useRecoilValue(userSelector);
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { Modal, TextInput, Switch, Group, Button, Title } from '@mantine/core';
|
||||
import { Button, Group, Modal, Switch, TextInput, Title } from '@mantine/core';
|
||||
import { useForm } from '@mantine/form';
|
||||
import { showNotification } from '@mantine/notifications';
|
||||
import { DeleteIcon, PlusIcon } from 'components/icons';
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { Modal, TextInput, Switch, Group, Button, Title } from '@mantine/core';
|
||||
import { Button, Group, Modal, Switch, TextInput, Title } from '@mantine/core';
|
||||
import { useForm } from '@mantine/form';
|
||||
import { showNotification } from '@mantine/notifications';
|
||||
import { DeleteIcon, PlusIcon } from 'components/icons';
|
||||
|
|
|
@ -21,11 +21,11 @@ export default function Users() {
|
|||
const [editOpen, setEditOpen] = useState(false);
|
||||
const [selectedUser, setSelectedUser] = useState(null);
|
||||
|
||||
const handleDelete = async (user, delete_images) => {
|
||||
const res = await useFetch('/api/users', 'DELETE', {
|
||||
id: user.id,
|
||||
delete_images,
|
||||
const handleDelete = async (user, delete_files) => {
|
||||
const res = await useFetch(`/api/user/${user.id}`, 'DELETE', {
|
||||
delete_files,
|
||||
});
|
||||
|
||||
if (res.error) {
|
||||
showNotification({
|
||||
title: 'Failed to delete user',
|
||||
|
@ -52,7 +52,7 @@ export default function Users() {
|
|||
labels: { confirm: 'Yes', cancel: 'No' },
|
||||
onConfirm: () => {
|
||||
modals.openConfirmModal({
|
||||
title: `Delete ${user.username}'s images?`,
|
||||
title: `Delete ${user.username}'s files?`,
|
||||
labels: { confirm: 'Yes', cancel: 'No' },
|
||||
centered: true,
|
||||
overlayBlur: 3,
|
||||
|
|
|
@ -101,6 +101,8 @@ export interface ConfigDiscordEmbed {
|
|||
|
||||
export interface ConfigFeatures {
|
||||
invites: boolean;
|
||||
invites_length: number;
|
||||
|
||||
oauth_registration: boolean;
|
||||
user_registration: boolean;
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { parse } from 'dotenv';
|
||||
import { expand } from 'dotenv-expand';
|
||||
import { existsSync, readFileSync } from 'fs';
|
||||
import Logger from '../logger';
|
||||
import { humanToBytes } from '../utils/bytes';
|
||||
|
||||
export type ValueType = 'string' | 'number' | 'boolean' | 'array' | 'json-array' | 'human-to-byte';
|
||||
|
@ -36,6 +37,9 @@ function map(env: string, type: ValueType, path: string) {
|
|||
}
|
||||
|
||||
export default function readConfig() {
|
||||
const logger = Logger.get('config');
|
||||
|
||||
logger.debug('attemping to read .env.local/.env or environment variables');
|
||||
if (existsSync('.env.local')) {
|
||||
const contents = readFileSync('.env.local');
|
||||
|
||||
|
@ -132,6 +136,8 @@ export default function readConfig() {
|
|||
map('OAUTH_GOOGLE_CLIENT_SECRET', 'string', 'oauth.google_client_secret'),
|
||||
|
||||
map('FEATURES_INVITES', 'boolean', 'features.invites'),
|
||||
map('FEATURES_INVITES_LENGTH', 'number', 'features.invites_length'),
|
||||
|
||||
map('FEATURES_OAUTH_REGISTRATION', 'boolean', 'features.oauth_registration'),
|
||||
map('FEATURES_USER_REGISTRATION', 'boolean', 'features.user_registration'),
|
||||
|
||||
|
@ -154,6 +160,10 @@ export default function readConfig() {
|
|||
break;
|
||||
case 'number':
|
||||
parsed = Number(value);
|
||||
if (isNaN(parsed)) {
|
||||
parsed = undefined;
|
||||
logger.debug(`Failed to parse number ${map.env}=${value}`);
|
||||
}
|
||||
break;
|
||||
case 'boolean':
|
||||
parsed = value === 'true';
|
||||
|
@ -162,11 +172,13 @@ export default function readConfig() {
|
|||
try {
|
||||
parsed = JSON.parse(value);
|
||||
} catch (e) {
|
||||
parsed = [];
|
||||
logger.debug(`Failed to parse JSON array ${map.env}=${value}`);
|
||||
}
|
||||
break;
|
||||
case 'human-to-byte':
|
||||
parsed = humanToBytes(value) ?? undefined;
|
||||
if (!parsed) logger.debug(`Unable to parse ${map.env}=${value}`);
|
||||
|
||||
break;
|
||||
default:
|
||||
parsed = value;
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { Config } from 'lib/config/Config';
|
||||
import { s } from '@sapphire/shapeshift';
|
||||
import { Config } from 'lib/config/Config';
|
||||
import { inspect } from 'util';
|
||||
import Logger from '../logger';
|
||||
import { humanToBytes } from '../utils/bytes';
|
||||
|
@ -168,10 +168,11 @@ const validator = s.object({
|
|||
features: s
|
||||
.object({
|
||||
invites: s.boolean.default(false),
|
||||
invites_length: s.number.default(6),
|
||||
oauth_registration: s.boolean.default(false),
|
||||
user_registration: s.boolean.default(false),
|
||||
})
|
||||
.default({ invites: false, oauth_registration: false, user_registration: false }),
|
||||
.default({ invites: false, invites_length: 6, oauth_registration: false, user_registration: false }),
|
||||
chunks: s
|
||||
.object({
|
||||
max_size: s.number.default(humanToBytes('90MB')),
|
||||
|
@ -184,8 +185,12 @@ const validator = s.object({
|
|||
});
|
||||
|
||||
export default function validate(config): Config {
|
||||
const logger = Logger.get('config');
|
||||
|
||||
try {
|
||||
logger.debug(`Attemping to validate ${JSON.stringify(config)}`);
|
||||
const validated = validator.parse(config);
|
||||
logger.debug(`Recieved config: ${JSON.stringify(validated)}`);
|
||||
switch (validated.datasource.type) {
|
||||
case 's3': {
|
||||
const errors = [];
|
||||
|
@ -221,8 +226,9 @@ export default function validate(config): Config {
|
|||
|
||||
e.stack = '';
|
||||
|
||||
Logger.get('config').error('Config is invalid, see below:');
|
||||
Logger.get('config').error(inspect(e, { depth: Infinity, colors: true }));
|
||||
Logger.get('config')
|
||||
.error('Config is invalid, see below:')
|
||||
.error(inspect(e, { depth: Infinity, colors: true }));
|
||||
|
||||
process.exit(1);
|
||||
}
|
||||
|
|
|
@ -1,20 +1,22 @@
|
|||
import config from './config';
|
||||
import { Swift, Local, S3, Datasource } from './datasources';
|
||||
import { Datasource, Local, S3, Swift } from './datasources';
|
||||
import Logger from './logger';
|
||||
|
||||
const logger = Logger.get('datasource');
|
||||
|
||||
if (!global.datasource) {
|
||||
switch (config.datasource.type) {
|
||||
case 's3':
|
||||
global.datasource = new S3(config.datasource.s3);
|
||||
Logger.get('datasource').info(`using S3(${config.datasource.s3.bucket}) datasource`);
|
||||
logger.info(`using S3(${config.datasource.s3.bucket}) datasource`);
|
||||
break;
|
||||
case 'local':
|
||||
global.datasource = new Local(config.datasource.local.directory);
|
||||
Logger.get('datasource').info(`using Local(${config.datasource.local.directory}) datasource`);
|
||||
logger.info(`using Local(${config.datasource.local.directory}) datasource`);
|
||||
break;
|
||||
case 'swift':
|
||||
global.datasource = new Swift(config.datasource.swift);
|
||||
Logger.get('datasource').info(`using Swift(${config.datasource.swift.container}) datasource`);
|
||||
logger.info(`using Swift(${config.datasource.swift.container}) datasource`);
|
||||
break;
|
||||
default:
|
||||
throw new Error('Invalid datasource type');
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
import { Image, Url, User } from '@prisma/client';
|
||||
import { ConfigDiscordContent } from 'lib/config/Config';
|
||||
import config from 'lib/config';
|
||||
import { ConfigDiscordContent } from 'lib/config/Config';
|
||||
import Logger from './logger';
|
||||
|
||||
// [user, image, url, route (ex. https://example.com/r/something.png)]
|
||||
export type Args = [User, Image?, Url?, string?];
|
||||
|
||||
const logger = Logger.get('discord');
|
||||
|
||||
function parse(str: string, args: Args) {
|
||||
if (!str) return null;
|
||||
|
||||
|
@ -63,7 +65,7 @@ export async function sendUpload(user: User, image: Image, host: string) {
|
|||
const parsed = parseContent(config.discord.upload, [user, image, null, host]);
|
||||
const isImage = image.mimetype.startsWith('image/');
|
||||
|
||||
const body = {
|
||||
const body = JSON.stringify({
|
||||
username: config.discord.username,
|
||||
avatar_url: config.discord.avatar_url,
|
||||
content: parsed.content ?? null,
|
||||
|
@ -95,11 +97,12 @@ export async function sendUpload(user: User, image: Image, host: string) {
|
|||
},
|
||||
]
|
||||
: null,
|
||||
};
|
||||
});
|
||||
|
||||
logger.debug('attempting to send shorten notification to discord', body);
|
||||
const res = await fetch(config.discord.url, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(body),
|
||||
body,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
|
@ -107,10 +110,9 @@ export async function sendUpload(user: User, image: Image, host: string) {
|
|||
|
||||
if (!res.ok) {
|
||||
const text = await res.text();
|
||||
Logger.get('discord').error(
|
||||
`Failed to send upload notification to discord: ${res.status} ${res.statusText}`
|
||||
);
|
||||
Logger.get('discord').error(`Received response: ${text}`);
|
||||
logger
|
||||
.error(`Failed to send shorten notification to discord: ${res.status}`)
|
||||
.error(`Received response:\n${text}`);
|
||||
}
|
||||
|
||||
return;
|
||||
|
@ -121,7 +123,7 @@ export async function sendShorten(user: User, url: Url, host: string) {
|
|||
|
||||
const parsed = parseContent(config.discord.shorten, [user, null, url, host]);
|
||||
|
||||
const body = {
|
||||
const body = JSON.stringify({
|
||||
username: config.discord.username,
|
||||
avatar_url: config.discord.avatar_url,
|
||||
content: parsed.content ?? null,
|
||||
|
@ -141,20 +143,22 @@ export async function sendShorten(user: User, url: Url, host: string) {
|
|||
},
|
||||
]
|
||||
: null,
|
||||
};
|
||||
});
|
||||
|
||||
logger.debug('attempting to send shorten notification to discord', body);
|
||||
const res = await fetch(config.discord.url, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(body),
|
||||
body,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
Logger.get('discord').error(
|
||||
`Failed to send url shorten notification to discord: ${res.status} ${res.statusText}`
|
||||
);
|
||||
const text = await res.text();
|
||||
logger
|
||||
.error(`Failed to send shorten notification to discord: ${res.status}`)
|
||||
.error(`Received response:\n${text}`);
|
||||
}
|
||||
|
||||
return;
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { userSelector } from 'lib/recoil/user';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { userSelector } from 'lib/recoil/user';
|
||||
import { useRecoilState } from 'recoil';
|
||||
import useFetch from './useFetch';
|
||||
|
||||
|
|
|
@ -1,18 +1,19 @@
|
|||
import { blueBright, cyan, red, yellow } from 'colorette';
|
||||
import dayjs from 'dayjs';
|
||||
import { blueBright, red, cyan } from 'colorette';
|
||||
|
||||
export enum LoggerLevel {
|
||||
ERROR,
|
||||
INFO,
|
||||
DEBUG,
|
||||
}
|
||||
|
||||
export default class Logger {
|
||||
public name: string;
|
||||
|
||||
static get(clas: any) {
|
||||
if (typeof clas !== 'function') if (typeof clas !== 'string') throw new Error('not string/function');
|
||||
static get(klass: any) {
|
||||
if (typeof klass !== 'function') if (typeof klass !== 'string') throw new Error('not string/function');
|
||||
|
||||
const name = clas.name ?? clas;
|
||||
const name = klass.name ?? klass;
|
||||
|
||||
return new Logger(name);
|
||||
}
|
||||
|
@ -21,19 +22,31 @@ export default class Logger {
|
|||
this.name = name;
|
||||
}
|
||||
|
||||
info(...args: any[]) {
|
||||
console.log(this.formatMessage(LoggerLevel.INFO, this.name, args.join(' ')));
|
||||
info(...args: any[]): this {
|
||||
process.stdout.write(this.formatMessage(LoggerLevel.INFO, this.name, args.join(' ')));
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
error(...args: any[]) {
|
||||
console.log(
|
||||
error(...args: any[]): this {
|
||||
process.stdout.write(
|
||||
this.formatMessage(LoggerLevel.ERROR, this.name, args.map((error) => error.stack ?? error).join(' '))
|
||||
);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
debug(...args: any[]): this {
|
||||
if (!process.env.DEBUG) return;
|
||||
|
||||
process.stdout.write(this.formatMessage(LoggerLevel.DEBUG, this.name, args.join(' ')));
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
formatMessage(level: LoggerLevel, name: string, message: string) {
|
||||
const time = dayjs().format('YYYY-MM-DD hh:mm:ss,SSS A');
|
||||
return `${time} ${this.formatLevel(level)} [${blueBright(name)}] ${message}`;
|
||||
return `${time} ${this.formatLevel(level)} [${blueBright(name)}] ${message}\n`;
|
||||
}
|
||||
|
||||
formatLevel(level: LoggerLevel) {
|
||||
|
@ -42,6 +55,8 @@ export default class Logger {
|
|||
return cyan('info ');
|
||||
case LoggerLevel.ERROR:
|
||||
return red('error');
|
||||
case LoggerLevel.DEBUG:
|
||||
return yellow('debug');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import { createToken } from 'lib/util';
|
||||
import Logger from 'lib/logger';
|
||||
import { NextApiReq, NextApiRes } from './withZipline';
|
||||
import prisma from 'lib/prisma';
|
||||
import { OauthProviders } from '@prisma/client';
|
||||
import Logger from 'lib/logger';
|
||||
import prisma from 'lib/prisma';
|
||||
import { createToken } from 'lib/util';
|
||||
import { NextApiReq, NextApiRes } from './withZipline';
|
||||
|
||||
export interface OAuthQuery {
|
||||
state?: string;
|
||||
|
@ -22,21 +22,32 @@ export interface OAuthResponse {
|
|||
}
|
||||
|
||||
export const withOAuth =
|
||||
(provider: 'discord' | 'github' | 'google', oauth: (query: OAuthQuery) => Promise<OAuthResponse>) =>
|
||||
(
|
||||
provider: 'discord' | 'github' | 'google',
|
||||
oauth: (query: OAuthQuery, logger: Logger) => Promise<OAuthResponse>
|
||||
) =>
|
||||
async (req: NextApiReq, res: NextApiRes) => {
|
||||
const logger = Logger.get(`oauth::${provider}`);
|
||||
|
||||
function oauthError(error: string) {
|
||||
return res.redirect(`/oauth_error?error=${error}&provider=${provider}`);
|
||||
}
|
||||
|
||||
req.query.host = req.headers.host;
|
||||
|
||||
const oauth_resp = await oauth(req.query as unknown as OAuthQuery);
|
||||
const oauth_resp = await oauth(req.query as unknown as OAuthQuery, logger);
|
||||
|
||||
if (oauth_resp.error) {
|
||||
return res.json({ error: oauth_resp.error }, oauth_resp.error_code || 500);
|
||||
logger.debug(`Failed to authenticate with ${provider}: ${JSON.stringify(oauth_resp)})`);
|
||||
|
||||
return oauthError(oauth_resp.error);
|
||||
}
|
||||
|
||||
if (oauth_resp.redirect) {
|
||||
return res.redirect(oauth_resp.redirect);
|
||||
}
|
||||
|
||||
const { code, state } = req.query as { code: string; state?: string };
|
||||
const { state } = req.query as { state?: string };
|
||||
|
||||
const existing = await prisma.user.findFirst({
|
||||
where: {
|
||||
|
@ -55,13 +66,15 @@ export const withOAuth =
|
|||
const user = await req.user();
|
||||
|
||||
const existingOauth = existing?.oauth?.find((o) => o.provider === provider.toUpperCase());
|
||||
const existingUserOauth = user?.oauth?.find((o) => o.provider === provider.toUpperCase());
|
||||
const userOauth = user?.oauth?.find((o) => o.provider === provider.toUpperCase());
|
||||
|
||||
if (state === 'link') {
|
||||
if (!user) return res.error('not logged in, unable to link account');
|
||||
if (!user) return oauthError('You are not logged in, unable to link account.');
|
||||
|
||||
if (user.oauth && user.oauth.find((o) => o.provider === provider.toUpperCase()))
|
||||
return res.error(`account already linked with ${provider}`);
|
||||
return oauthError(`This account was already linked with ${provider}!`);
|
||||
|
||||
logger.debug(`attempting to link ${provider} account to ${user.username}`);
|
||||
await prisma.user.update({
|
||||
where: {
|
||||
id: user.id,
|
||||
|
@ -80,13 +93,14 @@ export const withOAuth =
|
|||
});
|
||||
|
||||
res.setUserCookie(user.id);
|
||||
Logger.get('user').info(`User ${user.username} (${user.id}) linked account via oauth(${provider})`);
|
||||
logger.info(`User ${user.username} (${user.id}) linked account via oauth(${provider})`);
|
||||
|
||||
return res.redirect('/');
|
||||
} else if (user && existingUserOauth) {
|
||||
} else if (user && userOauth) {
|
||||
logger.debug(`attempting to refresh ${provider} account for ${user.username}`);
|
||||
await prisma.oAuth.update({
|
||||
where: {
|
||||
id: existingUserOauth!.id,
|
||||
id: userOauth!.id,
|
||||
},
|
||||
data: {
|
||||
token: oauth_resp.access_token,
|
||||
|
@ -96,7 +110,7 @@ export const withOAuth =
|
|||
});
|
||||
|
||||
res.setUserCookie(user.id);
|
||||
Logger.get('user').info(`User ${user.username} (${user.id}) logged in via oauth(${provider})`);
|
||||
logger.info(`User ${user.username} (${user.id}) logged in via oauth(${provider})`);
|
||||
|
||||
return res.redirect('/dashboard');
|
||||
} else if (existing && existingOauth) {
|
||||
|
@ -116,9 +130,10 @@ export const withOAuth =
|
|||
|
||||
return res.redirect('/dashboard');
|
||||
} else if (existing) {
|
||||
return res.badRequest('username is already taken');
|
||||
return oauthError(`Username "${oauth_resp.username}" is already taken, unable to create account.`);
|
||||
}
|
||||
|
||||
logger.debug('creating new user via oauth');
|
||||
const nuser = await prisma.user.create({
|
||||
data: {
|
||||
username: oauth_resp.username,
|
||||
|
@ -134,10 +149,12 @@ export const withOAuth =
|
|||
avatar: oauth_resp.avatar,
|
||||
},
|
||||
});
|
||||
Logger.get('user').info(`Created user ${nuser.username} via oauth(${provider})`);
|
||||
|
||||
logger.debug(`created user ${JSON.stringify(nuser)} via oauth(${provider})`);
|
||||
logger.info(`Created user ${nuser.username} via oauth(${provider})`);
|
||||
|
||||
res.setUserCookie(nuser.id);
|
||||
Logger.get('user').info(`User ${nuser.username} (${nuser.id}) logged in via oauth(${provider})`);
|
||||
logger.info(`User ${nuser.username} (${nuser.id}) logged in via oauth(${provider})`);
|
||||
|
||||
return res.redirect('/dashboard');
|
||||
};
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import type { CookieSerializeOptions } from 'cookie';
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
|
||||
import { OAuth, User } from '@prisma/client';
|
||||
import { serialize } from 'cookie';
|
||||
import { sign64, unsign64 } from 'lib/utils/crypto';
|
||||
import { HTTPMethod } from 'find-my-way';
|
||||
import config from 'lib/config';
|
||||
import prisma from 'lib/prisma';
|
||||
import { OAuth, User } from '@prisma/client';
|
||||
import { HTTPMethod } from 'find-my-way';
|
||||
import { sign64, unsign64 } from 'lib/utils/crypto';
|
||||
|
||||
export interface NextApiFile {
|
||||
fieldname: string;
|
||||
|
@ -54,7 +54,7 @@ export type ZiplineApiConfig = {
|
|||
|
||||
export const withZipline =
|
||||
(
|
||||
handler: (req: NextApiRequest, res: NextApiResponse, user?: UserExtended) => unknown,
|
||||
handler: (req: NextApiRequest, res: NextApiResponse, user?: UserExtended) => Promise<unknown>,
|
||||
api_config: ZiplineApiConfig = { methods: ['GET'] }
|
||||
) =>
|
||||
(req: NextApiReq, res: NextApiRes) => {
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import { randomBytes } from 'crypto';
|
||||
import { hash, verify } from 'argon2';
|
||||
import { readdir, stat } from 'fs/promises';
|
||||
import { join } from 'path';
|
||||
import prisma from 'lib/prisma';
|
||||
import { InvisibleImage, InvisibleUrl } from '@prisma/client';
|
||||
import { hash, verify } from 'argon2';
|
||||
import { randomBytes } from 'crypto';
|
||||
import { readdir, stat } from 'fs/promises';
|
||||
import prisma from 'lib/prisma';
|
||||
import { join } from 'path';
|
||||
|
||||
export async function hashPassword(s: string): Promise<string> {
|
||||
return await hash(s);
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import React from 'react';
|
||||
import { Button, Stack, Title } from '@mantine/core';
|
||||
import Link from 'components/Link';
|
||||
import MutedText from 'components/MutedText';
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import React from 'react';
|
||||
import { Button, Stack, Title, Tooltip } from '@mantine/core';
|
||||
import Link from 'components/Link';
|
||||
import MutedText from 'components/MutedText';
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
import React from 'react';
|
||||
import Head from 'next/head';
|
||||
import ZiplineTheming from 'components/Theming';
|
||||
import { QueryClientProvider } from '@tanstack/react-query';
|
||||
import ZiplineTheming from 'components/Theming';
|
||||
import queryClient from 'lib/queries/client';
|
||||
import Head from 'next/head';
|
||||
import { RecoilRoot } from 'recoil';
|
||||
|
||||
export default function MyApp({ Component, pageProps }) {
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import React from 'react';
|
||||
import Document, { Html, Head, Main, NextScript } from 'next/document';
|
||||
import { createGetInitialProps } from '@mantine/next';
|
||||
import Document, { Head, Html, Main, NextScript } from 'next/document';
|
||||
|
||||
const getInitialProps = createGetInitialProps();
|
||||
|
||||
|
@ -10,7 +9,14 @@ class MyDocument extends Document {
|
|||
render() {
|
||||
return (
|
||||
<Html lang='en'>
|
||||
<Head />
|
||||
<Head>
|
||||
<link rel='preconnect' href='https://fonts.googleapis.com' />
|
||||
<link rel='preconnect' href='https://fonts.gstatic.com' crossOrigin='' />
|
||||
<link
|
||||
href='https://fonts.googleapis.com/css2?family=Ubuntu+Mono:wght@400;700&family=Ubuntu:wght@400;500;700&display=swap'
|
||||
rel='stylesheet'
|
||||
/>
|
||||
</Head>
|
||||
<body>
|
||||
<Main />
|
||||
<NextScript />
|
||||
|
|
|
@ -1,14 +1,13 @@
|
|||
import React from 'react';
|
||||
import { Button, Stack, Title } from '@mantine/core';
|
||||
import Link from 'components/Link';
|
||||
import MutedText from 'components/MutedText';
|
||||
import Head from 'next/head';
|
||||
|
||||
export default function Error({ statusCode }) {
|
||||
export default function Error({ statusCode, oauthError }) {
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>{statusCode} Error</title>
|
||||
<title>Error ({statusCode})</title>
|
||||
</Head>
|
||||
|
||||
<Stack
|
||||
|
@ -35,5 +34,3 @@ export function getInitialProps({ res, err }) {
|
|||
const statusCode = res ? res.statusCode : err ? err.statusCode : 404;
|
||||
return { pageProps: { statusCode } };
|
||||
}
|
||||
|
||||
Error.title = 'Zipline - Something went wrong...';
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
import prisma from 'lib/prisma';
|
||||
import { NextApiReq, NextApiRes, withZipline } from 'lib/middleware/withZipline';
|
||||
import { createToken, hashPassword } from 'lib/util';
|
||||
import Logger from 'lib/logger';
|
||||
import config from 'lib/config';
|
||||
import Logger from 'lib/logger';
|
||||
import { NextApiReq, NextApiRes, withZipline } from 'lib/middleware/withZipline';
|
||||
import prisma from 'lib/prisma';
|
||||
import { createToken, hashPassword } from 'lib/util';
|
||||
|
||||
const logger = Logger.get('user');
|
||||
|
||||
async function handler(req: NextApiReq, res: NextApiRes) {
|
||||
// handle invites
|
||||
if (req.method === 'POST' && req.body) {
|
||||
if (req.body.code) {
|
||||
if (!config.features.invites && req.body.code) return res.badRequest('invites are disabled');
|
||||
if (!config.features.user_registration && !req.body.code)
|
||||
return res.badRequest('user registration is disabled');
|
||||
|
@ -47,7 +49,9 @@ async function handler(req: NextApiReq, res: NextApiRes) {
|
|||
});
|
||||
}
|
||||
|
||||
Logger.get('user').info(
|
||||
logger.debug(`created user via invite ${code} ${JSON.stringify(newUser)}`);
|
||||
|
||||
logger.info(
|
||||
`Created user ${newUser.username} (${newUser.id}) ${
|
||||
code ? `from invite code ${code}` : 'via registration'
|
||||
}`
|
||||
|
@ -60,8 +64,6 @@ async function handler(req: NextApiReq, res: NextApiRes) {
|
|||
if (!user) return res.unauthorized('not logged in');
|
||||
if (!user.administrator) return res.forbidden('you arent an administrator');
|
||||
|
||||
if (req.method !== 'POST') return res.status(405).end();
|
||||
|
||||
const { username, password, administrator } = req.body as {
|
||||
username: string;
|
||||
password: string;
|
||||
|
@ -89,9 +91,11 @@ async function handler(req: NextApiReq, res: NextApiRes) {
|
|||
},
|
||||
});
|
||||
|
||||
logger.debug(`created user ${JSON.stringify(newUser)}`);
|
||||
|
||||
delete newUser.password;
|
||||
|
||||
Logger.get('user').info(`Created user ${newUser.username} (${newUser.id})`);
|
||||
logger.info(`Created user ${newUser.username} (${newUser.id})`);
|
||||
|
||||
return res.json(newUser);
|
||||
}
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import prisma from 'lib/prisma';
|
||||
import { NextApiReq, NextApiRes, withZipline } from 'middleware/withZipline';
|
||||
import { checkPassword } from 'lib/util';
|
||||
import datasource from 'lib/datasource';
|
||||
import { guess } from 'lib/mimes';
|
||||
import prisma from 'lib/prisma';
|
||||
import { checkPassword } from 'lib/util';
|
||||
import { NextApiReq, NextApiRes, withZipline } from 'middleware/withZipline';
|
||||
import { extname } from 'path';
|
||||
|
||||
async function handler(req: NextApiReq, res: NextApiRes) {
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
import prisma from 'lib/prisma';
|
||||
import { NextApiReq, NextApiRes, UserExtended, withZipline } from 'lib/middleware/withZipline';
|
||||
import { randomChars } from 'lib/util';
|
||||
import Logger from 'lib/logger';
|
||||
import config from 'lib/config';
|
||||
import Logger from 'lib/logger';
|
||||
import { NextApiReq, NextApiRes, UserExtended, withZipline } from 'lib/middleware/withZipline';
|
||||
import prisma from 'lib/prisma';
|
||||
import { randomChars } from 'lib/util';
|
||||
|
||||
const logger = Logger.get('invite');
|
||||
|
||||
async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) {
|
||||
if (!config.features.invites) return res.badRequest('invites are disabled');
|
||||
|
@ -24,7 +26,7 @@ async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) {
|
|||
const data = [];
|
||||
for (let i = 0; i !== counts; ++i) {
|
||||
data.push({
|
||||
code: randomChars(8),
|
||||
code: randomChars(config.features.invites_length),
|
||||
createdById: user.id,
|
||||
expires_at: expiry,
|
||||
});
|
||||
|
@ -32,7 +34,9 @@ async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) {
|
|||
|
||||
await prisma.invite.createMany({ data });
|
||||
|
||||
Logger.get('invite').info(
|
||||
logger.debug(`created invites ${JSON.stringify(data)}`);
|
||||
|
||||
logger.info(
|
||||
`${user.username} (${user.id}) created ${data.length} invites with codes ${data
|
||||
.map((invite) => invite.code)
|
||||
.join(', ')}`
|
||||
|
@ -40,17 +44,17 @@ async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) {
|
|||
|
||||
return res.json(data);
|
||||
} else {
|
||||
const code = randomChars(6);
|
||||
|
||||
const invite = await prisma.invite.create({
|
||||
data: {
|
||||
code,
|
||||
code: randomChars(config.features.invites_length),
|
||||
createdById: user.id,
|
||||
expires_at: expiry,
|
||||
},
|
||||
});
|
||||
|
||||
Logger.get('invite').info(`${user.username} (${user.id}) created invite ${invite.code}`);
|
||||
logger.debug(`created invite ${JSON.stringify(invite)}`);
|
||||
|
||||
logger.info(`${user.username} (${user.id}) created invite ${invite.code}`);
|
||||
|
||||
return res.json(invite);
|
||||
}
|
||||
|
@ -63,7 +67,9 @@ async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) {
|
|||
},
|
||||
});
|
||||
|
||||
Logger.get('invite').info(`${user.username} (${user.id}) deleted invite ${invite.code}`);
|
||||
logger.debug(`deleted invite ${JSON.stringify(invite)}`);
|
||||
|
||||
logger.info(`${user.username} (${user.id}) deleted invite ${invite.code}`);
|
||||
|
||||
return res.json(invite);
|
||||
} else {
|
||||
|
|
|
@ -4,6 +4,8 @@ import { checkPassword, createToken, hashPassword } from 'lib/util';
|
|||
import Logger from 'lib/logger';
|
||||
|
||||
async function handler(req: NextApiReq, res: NextApiRes) {
|
||||
const logger = Logger.get('login');
|
||||
|
||||
const { username, password } = req.body as {
|
||||
username: string;
|
||||
password: string;
|
||||
|
@ -11,7 +13,7 @@ async function handler(req: NextApiReq, res: NextApiRes) {
|
|||
|
||||
const users = await prisma.user.findMany();
|
||||
if (users.length === 0) {
|
||||
Logger.get('database').info('no users found... creating default user...');
|
||||
logger.debug('no users found... creating default user...');
|
||||
await prisma.user.create({
|
||||
data: {
|
||||
username: 'administrator',
|
||||
|
@ -20,7 +22,7 @@ async function handler(req: NextApiReq, res: NextApiRes) {
|
|||
administrator: true,
|
||||
},
|
||||
});
|
||||
Logger.get('database').info('created default user:\nUsername: "administrator"\nPassword: "password"');
|
||||
logger.info('created default user:\nUsername: "administrator"\nPassword: "password"');
|
||||
}
|
||||
|
||||
const user = await prisma.user.findFirst({
|
||||
|
@ -35,7 +37,7 @@ async function handler(req: NextApiReq, res: NextApiRes) {
|
|||
if (!valid) return res.unauthorized('Wrong password');
|
||||
|
||||
res.setUserCookie(user.id);
|
||||
Logger.get('user').info(`User ${user.username} (${user.id}) logged in`);
|
||||
logger.info(`User ${user.username} (${user.id}) logged in`);
|
||||
|
||||
return res.json({ success: true });
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { NextApiReq, NextApiRes, UserExtended, withZipline } from 'middleware/withZipline';
|
||||
import Logger from 'lib/logger';
|
||||
import { NextApiReq, NextApiRes, UserExtended, withZipline } from 'middleware/withZipline';
|
||||
|
||||
async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) {
|
||||
req.cleanCookie('user');
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import { withZipline } from 'lib/middleware/withZipline';
|
||||
import { getBase64URLFromURL, notNull } from 'lib/util';
|
||||
import Logger from 'lib/logger';
|
||||
import config from 'lib/config';
|
||||
import Logger from 'lib/logger';
|
||||
import { OAuthQuery, OAuthResponse, withOAuth } from 'lib/middleware/withOAuth';
|
||||
import { withZipline } from 'lib/middleware/withZipline';
|
||||
import { discord_auth } from 'lib/oauth';
|
||||
import { withOAuth, OAuthResponse, OAuthQuery } from 'lib/middleware/withOAuth';
|
||||
import { getBase64URLFromURL, notNull } from 'lib/util';
|
||||
|
||||
async function handler({ code, state, host }: OAuthQuery): Promise<OAuthResponse> {
|
||||
async function handler({ code, state, host }: OAuthQuery, logger: Logger): Promise<OAuthResponse> {
|
||||
if (!config.features.oauth_registration)
|
||||
return {
|
||||
error_code: 403,
|
||||
|
@ -13,7 +13,8 @@ async function handler({ code, state, host }: OAuthQuery): Promise<OAuthResponse
|
|||
};
|
||||
|
||||
if (!notNull(config.oauth.discord_client_id, config.oauth.discord_client_secret)) {
|
||||
Logger.get('oauth').error('Discord OAuth is not configured');
|
||||
logger.error('Discord OAuth is not configured');
|
||||
|
||||
return {
|
||||
error_code: 401,
|
||||
error: 'Discord OAuth is not configured',
|
||||
|
@ -29,23 +30,31 @@ async function handler({ code, state, host }: OAuthQuery): Promise<OAuthResponse
|
|||
),
|
||||
};
|
||||
|
||||
const resp = await fetch('https://discord.com/api/oauth2/token', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
body: new URLSearchParams({
|
||||
const body = new URLSearchParams({
|
||||
client_id: config.oauth.discord_client_id,
|
||||
client_secret: config.oauth.discord_client_secret,
|
||||
code,
|
||||
grant_type: 'authorization_code',
|
||||
redirect_uri: `${config.core.https ? 'https' : 'http'}://${host}/api/auth/oauth/discord`,
|
||||
scope: 'identify',
|
||||
}),
|
||||
});
|
||||
if (!resp.ok) return { error: 'invalid request' };
|
||||
|
||||
const json = await resp.json();
|
||||
const resp = await fetch('https://discord.com/api/oauth2/token', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
body,
|
||||
});
|
||||
|
||||
const text = await resp.text();
|
||||
logger.debug(`oauth https://discord.com/api/oauth2/token -> body(${body}) resp(${text})`);
|
||||
|
||||
if (!resp.ok) {
|
||||
return { error: 'invalid request' };
|
||||
}
|
||||
|
||||
const json = JSON.parse(text);
|
||||
|
||||
if (!json.access_token) return { error: 'no access_token in response' };
|
||||
if (!json.refresh_token) return { error: 'no refresh_token in response' };
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import { withZipline } from 'lib/middleware/withZipline';
|
||||
import { getBase64URLFromURL, notNull } from 'lib/util';
|
||||
import Logger from 'lib/logger';
|
||||
import config from 'lib/config';
|
||||
import Logger from 'lib/logger';
|
||||
import { OAuthQuery, OAuthResponse, withOAuth } from 'lib/middleware/withOAuth';
|
||||
import { withZipline } from 'lib/middleware/withZipline';
|
||||
import { github_auth } from 'lib/oauth';
|
||||
import { withOAuth, OAuthResponse, OAuthQuery } from 'lib/middleware/withOAuth';
|
||||
import { getBase64URLFromURL, notNull } from 'lib/util';
|
||||
|
||||
async function handler({ code, state }: OAuthQuery): Promise<OAuthResponse> {
|
||||
async function handler({ code, state }: OAuthQuery, logger: Logger): Promise<OAuthResponse> {
|
||||
if (!config.features.oauth_registration)
|
||||
return {
|
||||
error_code: 403,
|
||||
|
@ -13,7 +13,7 @@ async function handler({ code, state }: OAuthQuery): Promise<OAuthResponse> {
|
|||
};
|
||||
|
||||
if (!notNull(config.oauth.github_client_id, config.oauth.github_client_secret)) {
|
||||
Logger.get('oauth').error('GitHub OAuth is not configured');
|
||||
logger.error('GitHub OAuth is not configured');
|
||||
return {
|
||||
error_code: 401,
|
||||
error: 'GitHub OAuth is not configured',
|
||||
|
@ -25,22 +25,27 @@ async function handler({ code, state }: OAuthQuery): Promise<OAuthResponse> {
|
|||
redirect: github_auth.oauth_url(config.oauth.github_client_id, state),
|
||||
};
|
||||
|
||||
const body = JSON.stringify({
|
||||
client_id: config.oauth.github_client_id,
|
||||
client_secret: config.oauth.github_client_secret,
|
||||
code,
|
||||
});
|
||||
|
||||
const resp = await fetch('https://github.com/login/oauth/access_token', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
client_id: config.oauth.github_client_id,
|
||||
client_secret: config.oauth.github_client_secret,
|
||||
code,
|
||||
}),
|
||||
body,
|
||||
});
|
||||
|
||||
const text = await resp.text();
|
||||
logger.debug(`oauth https://github.com/login/oauth/access_token -> body(${body}) resp(${text})`);
|
||||
|
||||
if (!resp.ok) return { error: 'invalid request' };
|
||||
|
||||
const json = await resp.json();
|
||||
const json = JSON.parse(text);
|
||||
|
||||
if (!json.access_token) return { error: 'no access_token in response' };
|
||||
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import { withZipline } from 'lib/middleware/withZipline';
|
||||
import { getBase64URLFromURL, notNull } from 'lib/util';
|
||||
import Logger from 'lib/logger';
|
||||
import config from 'lib/config';
|
||||
import Logger from 'lib/logger';
|
||||
import { OAuthQuery, OAuthResponse, withOAuth } from 'lib/middleware/withOAuth';
|
||||
import { withZipline } from 'lib/middleware/withZipline';
|
||||
import { google_auth } from 'lib/oauth';
|
||||
import { withOAuth, OAuthResponse, OAuthQuery } from 'lib/middleware/withOAuth';
|
||||
import { getBase64URLFromURL, notNull } from 'lib/util';
|
||||
|
||||
async function handler({ code, state, host }: OAuthQuery): Promise<OAuthResponse> {
|
||||
async function handler({ code, state, host }: OAuthQuery, logger: Logger): Promise<OAuthResponse> {
|
||||
if (!config.features.oauth_registration)
|
||||
return {
|
||||
error_code: 403,
|
||||
|
@ -13,7 +13,7 @@ async function handler({ code, state, host }: OAuthQuery): Promise<OAuthResponse
|
|||
};
|
||||
|
||||
if (!notNull(config.oauth.google_client_id, config.oauth.google_client_secret)) {
|
||||
Logger.get('oauth').error('Google OAuth is not configured');
|
||||
logger.error('Google OAuth is not configured');
|
||||
return {
|
||||
error_code: 401,
|
||||
error: 'Google OAuth is not configured',
|
||||
|
@ -29,23 +29,28 @@ async function handler({ code, state, host }: OAuthQuery): Promise<OAuthResponse
|
|||
),
|
||||
};
|
||||
|
||||
const resp = await fetch('https://oauth2.googleapis.com/token', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
body: new URLSearchParams({
|
||||
const body = new URLSearchParams({
|
||||
code,
|
||||
client_id: config.oauth.google_client_id,
|
||||
client_secret: config.oauth.google_client_secret,
|
||||
redirect_uri: `${config.core.https ? 'https' : 'http'}://${host}/api/auth/oauth/google`,
|
||||
grant_type: 'authorization_code',
|
||||
}),
|
||||
});
|
||||
|
||||
const resp = await fetch('https://oauth2.googleapis.com/token', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
body,
|
||||
});
|
||||
|
||||
const text = await resp.text();
|
||||
logger.debug(`oauth https://oauth2.googleapis.com/token -> body(${body}) resp(${text})`);
|
||||
|
||||
if (!resp.ok) return { error: 'invalid request' };
|
||||
|
||||
const json = await resp.json();
|
||||
const json = JSON.parse(text);
|
||||
|
||||
if (!json.access_token) return { error: 'no access_token in response' };
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import prisma from 'lib/prisma';
|
||||
import { NextApiReq, NextApiRes, UserExtended, withZipline } from 'lib/middleware/withZipline';
|
||||
import { OauthProviders } from '@prisma/client';
|
||||
import { NextApiReq, NextApiRes, UserExtended, withZipline } from 'lib/middleware/withZipline';
|
||||
import prisma from 'lib/prisma';
|
||||
|
||||
async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) {
|
||||
if (req.method === 'DELETE') {
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
import prisma from 'lib/prisma';
|
||||
import zconfig from 'lib/config';
|
||||
import { NextApiReq, NextApiRes, withZipline } from 'lib/middleware/withZipline';
|
||||
import { createInvisURL, randomChars } from 'lib/util';
|
||||
import Logger from 'lib/logger';
|
||||
import config from 'lib/config';
|
||||
import { default as config, default as zconfig } from 'lib/config';
|
||||
import { sendShorten } from 'lib/discord';
|
||||
import Logger from 'lib/logger';
|
||||
import { NextApiReq, NextApiRes, withZipline } from 'lib/middleware/withZipline';
|
||||
import prisma from 'lib/prisma';
|
||||
import { createInvisURL, randomChars } from 'lib/util';
|
||||
|
||||
const logger = Logger.get('shorten');
|
||||
|
||||
async function handler(req: NextApiReq, res: NextApiRes) {
|
||||
if (!req.headers.authorization) return res.badRequest('no authorization');
|
||||
|
@ -49,9 +50,9 @@ async function handler(req: NextApiReq, res: NextApiRes) {
|
|||
|
||||
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.debug(`shortened ${JSON.stringify(url)}`);
|
||||
|
||||
logger.info(`User ${user.username} (${user.id}) shortenned a url ${url.destination} (${url.id})`);
|
||||
|
||||
if (config.discord?.shorten) {
|
||||
await sendShorten(
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import { NextApiReq, NextApiRes, UserExtended, withZipline } from 'middleware/withZipline';
|
||||
import prisma from 'lib/prisma';
|
||||
import config from 'lib/config';
|
||||
import { Stats } from '@prisma/client';
|
||||
import { getStats } from 'server/util';
|
||||
import config from 'lib/config';
|
||||
import datasource from 'lib/datasource';
|
||||
import prisma from 'lib/prisma';
|
||||
import { NextApiReq, NextApiRes, UserExtended, withZipline } from 'middleware/withZipline';
|
||||
import { getStats } from 'server/util';
|
||||
|
||||
async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) {
|
||||
if (req.method === 'POST') {
|
||||
|
|
|
@ -16,6 +16,7 @@ import { join } from 'path';
|
|||
import sharp from 'sharp';
|
||||
|
||||
const uploader = multer();
|
||||
const logger = Logger.get('upload');
|
||||
|
||||
async function handler(req: NextApiReq, res: NextApiRes) {
|
||||
if (!req.headers.authorization) return res.forbidden('no authorization');
|
||||
|
@ -76,7 +77,20 @@ async function handler(req: NextApiReq, res: NextApiRes) {
|
|||
const identifier = req.headers['x-zipline-partial-identifier'];
|
||||
const lastchunk = req.headers['x-zipline-partial-lastchunk'] === 'true';
|
||||
|
||||
logger.debug(
|
||||
`recieved partial upload ${JSON.stringify({
|
||||
filename,
|
||||
mimetype,
|
||||
identifier,
|
||||
lastchunk,
|
||||
start,
|
||||
end,
|
||||
total,
|
||||
})}`
|
||||
);
|
||||
|
||||
const tempFile = join(tmpdir(), `zipline_partial_${identifier}_${start}_${end}`);
|
||||
logger.debug(`writing partial to disk ${tempFile}`);
|
||||
await writeFile(tempFile, req.files[0].buffer);
|
||||
|
||||
if (lastchunk) {
|
||||
|
@ -149,9 +163,7 @@ async function handler(req: NextApiReq, res: NextApiRes) {
|
|||
|
||||
await datasource.save(file.file, Buffer.from(chunks));
|
||||
|
||||
Logger.get('file').info(
|
||||
`User ${user.username} (${user.id}) uploaded ${file.file} (${file.id}) (chunked)`
|
||||
);
|
||||
logger.info(`User ${user.username} (${user.id}) uploaded ${file.file} (${file.id}) (chunked)`);
|
||||
if (user.domains.length) {
|
||||
const domain = user.domains[Math.floor(Math.random() * user.domains.length)];
|
||||
response.files.push(
|
||||
|
@ -187,6 +199,7 @@ async function handler(req: NextApiReq, res: NextApiRes) {
|
|||
|
||||
if (user.ratelimit) {
|
||||
const remaining = user.ratelimit.getTime() - Date.now();
|
||||
logger.debug(`${user.id} encountered ratelimit, ${remaining}ms remaining`);
|
||||
if (remaining <= 0) {
|
||||
await prisma.user.update({
|
||||
where: {
|
||||
|
@ -204,6 +217,18 @@ async function handler(req: NextApiReq, res: NextApiRes) {
|
|||
if (!req.files) return res.badRequest('no files');
|
||||
if (req.files && req.files.length === 0) return res.badRequest('no files');
|
||||
|
||||
logger.debug(
|
||||
`recieved upload (len=${req.files.length}) ${JSON.stringify(
|
||||
req.files.map((x) => ({
|
||||
fieldname: x.fieldname,
|
||||
originalname: x.originalname,
|
||||
mimetype: x.mimetype,
|
||||
size: x.size,
|
||||
encoding: x.encoding,
|
||||
}))
|
||||
)}`
|
||||
);
|
||||
|
||||
for (let i = 0; i !== req.files.length; ++i) {
|
||||
const file = req.files[i];
|
||||
if (file.size > zconfig.uploader[user.administrator ? 'admin_limit' : 'user_limit'])
|
||||
|
@ -258,14 +283,14 @@ async function handler(req: NextApiReq, res: NextApiRes) {
|
|||
if (compressionUsed) {
|
||||
const buffer = await sharp(file.buffer).jpeg({ quality: imageCompressionPercent }).toBuffer();
|
||||
await datasource.save(image.file, buffer);
|
||||
Logger.get('file').info(
|
||||
logger.info(
|
||||
`User ${user.username} (${user.id}) compressed image from ${file.buffer.length} -> ${buffer.length} bytes`
|
||||
);
|
||||
} else {
|
||||
await datasource.save(image.file, file.buffer);
|
||||
}
|
||||
|
||||
Logger.get('file').info(`User ${user.username} (${user.id}) uploaded ${image.file} (${image.id})`);
|
||||
logger.info(`User ${user.username} (${user.id}) uploaded ${image.file} (${image.id})`);
|
||||
if (user.domains.length) {
|
||||
const domain = user.domains[Math.floor(Math.random() * user.domains.length)];
|
||||
response.files.push(
|
||||
|
@ -281,6 +306,8 @@ async function handler(req: NextApiReq, res: NextApiRes) {
|
|||
);
|
||||
}
|
||||
|
||||
logger.debug(`sent response: ${JSON.stringify(response)}`);
|
||||
|
||||
if (zconfig.discord?.upload) {
|
||||
await sendUpload(
|
||||
user,
|
||||
|
|
|
@ -1,7 +1,10 @@
|
|||
import datasource from 'lib/datasource';
|
||||
import Logger from 'lib/logger';
|
||||
import prisma from 'lib/prisma';
|
||||
import { hashPassword } from 'lib/util';
|
||||
import { NextApiReq, NextApiRes, UserExtended, withZipline } from 'middleware/withZipline';
|
||||
import Logger from 'lib/logger';
|
||||
|
||||
const logger = Logger.get('user');
|
||||
|
||||
async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) {
|
||||
const { id } = req.query as { id: string };
|
||||
|
@ -15,10 +18,43 @@ async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) {
|
|||
if (!target) return res.notFound('user not found');
|
||||
|
||||
if (req.method === 'DELETE') {
|
||||
if (target.id === user.id) return res.badRequest("you can't delete your own account");
|
||||
if (target.administrator && !user.superAdmin) return res.forbidden('cannot delete administrator');
|
||||
|
||||
const newTarget = await prisma.user.delete({
|
||||
where: { id: target.id },
|
||||
});
|
||||
if (newTarget.administrator && !user.superAdmin) return res.forbidden('cannot delete administrator');
|
||||
|
||||
logger.debug(`deleted user ${JSON.stringify(newTarget)}`);
|
||||
|
||||
if (req.body.delete_files) {
|
||||
logger.debug(`attempting to delete ${newTarget.id}'s files`);
|
||||
|
||||
const files = await prisma.image.findMany({
|
||||
where: {
|
||||
userId: newTarget.id,
|
||||
},
|
||||
});
|
||||
|
||||
for (let i = 0; i !== files.length; ++i) {
|
||||
try {
|
||||
await datasource.delete(files[i].file);
|
||||
} catch {
|
||||
logger.debug(`failed to find file ${files[i].file} to delete`);
|
||||
}
|
||||
}
|
||||
|
||||
const { count } = await prisma.image.deleteMany({
|
||||
where: {
|
||||
userId: newTarget.id,
|
||||
},
|
||||
});
|
||||
Logger.get('users').info(
|
||||
`User ${user.username} (${user.id}) deleted ${count} files of user ${newTarget.username} (${newTarget.id})`
|
||||
);
|
||||
}
|
||||
|
||||
logger.info(`User ${user.username} (${user.id}) deleted user ${newTarget.username} (${newTarget.id})`);
|
||||
|
||||
delete newTarget.password;
|
||||
|
||||
|
@ -26,6 +62,8 @@ async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) {
|
|||
} else if (req.method === 'PATCH') {
|
||||
if (target.administrator && !user.superAdmin) return res.forbidden('cannot modify administrator');
|
||||
|
||||
logger.debug(`attempting to update user ${id} with ${JSON.stringify(req.body)}`);
|
||||
|
||||
if (req.body.password) {
|
||||
const hashed = await hashPassword(req.body.password);
|
||||
await prisma.user.update({
|
||||
|
@ -119,27 +157,15 @@ async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) {
|
|||
where: {
|
||||
id: target.id,
|
||||
},
|
||||
select: {
|
||||
administrator: true,
|
||||
embedColor: true,
|
||||
embedTitle: true,
|
||||
embedSiteName: true,
|
||||
id: true,
|
||||
images: false,
|
||||
password: false,
|
||||
systemTheme: true,
|
||||
token: true,
|
||||
username: true,
|
||||
domains: true,
|
||||
avatar: true,
|
||||
oauth: true,
|
||||
},
|
||||
});
|
||||
|
||||
Logger.get('user').info(
|
||||
logger.debug(`updated user ${id} with ${JSON.stringify(newUser)}`);
|
||||
|
||||
logger.info(
|
||||
`User ${user.username} (${user.id}) updated ${target.username} (${newUser.username}) (${newUser.id})`
|
||||
);
|
||||
|
||||
delete newUser.password;
|
||||
return res.json(newUser);
|
||||
} else {
|
||||
delete target.password;
|
||||
|
|
28
src/pages/api/user/check.ts
Normal file
28
src/pages/api/user/check.ts
Normal file
|
@ -0,0 +1,28 @@
|
|||
import config from 'lib/config';
|
||||
import prisma from 'lib/prisma';
|
||||
import { NextApiReq, NextApiRes, withZipline } from 'middleware/withZipline';
|
||||
|
||||
async function handler(req: NextApiReq, res: NextApiRes) {
|
||||
if (!config.features.invites || !config.features.user_registration)
|
||||
return res.forbidden('user/invites are disabled');
|
||||
|
||||
if (!req.body?.code) return res.badRequest('no code');
|
||||
if (!req.body?.username) return res.badRequest('no username');
|
||||
|
||||
const { code, username } = req.body as { code: string; username: string };
|
||||
const invite = await prisma.invite.findUnique({
|
||||
where: { code },
|
||||
});
|
||||
if (!invite) return res.badRequest('invalid invite code');
|
||||
|
||||
const user = await prisma.user.findFirst({
|
||||
where: { username },
|
||||
});
|
||||
|
||||
if (user) return res.badRequest('username already exists');
|
||||
return res.json({ success: true });
|
||||
}
|
||||
|
||||
export default withZipline(handler, {
|
||||
methods: ['POST'],
|
||||
});
|
|
@ -1,11 +1,14 @@
|
|||
import { NextApiReq, NextApiRes, UserExtended, withZipline } from 'middleware/withZipline';
|
||||
import prisma from 'lib/prisma';
|
||||
import Logger from 'lib/logger';
|
||||
import { Zip, ZipPassThrough } from 'fflate';
|
||||
import datasource from 'lib/datasource';
|
||||
import { readdir, stat } from 'fs/promises';
|
||||
import { createReadStream, createWriteStream } from 'fs';
|
||||
import { readdir, stat } from 'fs/promises';
|
||||
import datasource from 'lib/datasource';
|
||||
import Logger from 'lib/logger';
|
||||
import prisma from 'lib/prisma';
|
||||
import { NextApiReq, NextApiRes, UserExtended, withZipline } from 'middleware/withZipline';
|
||||
import { tmpdir } from 'os';
|
||||
import { join } from 'path';
|
||||
|
||||
const logger = Logger.get('user::export');
|
||||
|
||||
async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) {
|
||||
if (req.method === 'POST') {
|
||||
|
@ -19,7 +22,10 @@ async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) {
|
|||
|
||||
const zip = new Zip();
|
||||
const export_name = `zipline_export_${user.id}_${Date.now()}.zip`;
|
||||
const write_stream = createWriteStream(tmpdir() + `/${export_name}`);
|
||||
const path = join(tmpdir(), export_name);
|
||||
|
||||
logger.debug(`creating write stream at ${path}`);
|
||||
const write_stream = createWriteStream(path);
|
||||
|
||||
// i found this on some stack overflow thing, forgot the url
|
||||
const onBackpressure = (stream, outputStream, cb) => {
|
||||
|
@ -70,21 +76,22 @@ async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) {
|
|||
write_stream.write(data);
|
||||
if (final) {
|
||||
write_stream.close();
|
||||
Logger.get('user').info(
|
||||
logger.debug(`finished writing zip to ${path} at ${data.length} bytes written`);
|
||||
logger.info(
|
||||
`Export for ${user.username} (${user.id}) has completed and is available at ${export_name}`
|
||||
);
|
||||
}
|
||||
} else {
|
||||
write_stream.close();
|
||||
|
||||
Logger.get('user').error(`Export for ${user.username} (${user.id}) has failed\n${err}`);
|
||||
logger.debug(`error while writing to zip: ${err}`);
|
||||
logger.error(`Export for ${user.username} (${user.id}) has failed\n${err}`);
|
||||
}
|
||||
};
|
||||
|
||||
Logger.get('user').info(`Export for ${user.username} (${user.id}) has started`);
|
||||
logger.info(`Export for ${user.username} (${user.id}) has started`);
|
||||
for (let i = 0; i !== files.length; ++i) {
|
||||
const file = files[i];
|
||||
// try {
|
||||
|
||||
const stream = await datasource.get(file.file);
|
||||
if (stream) {
|
||||
const def = new ZipPassThrough(file.file);
|
||||
|
@ -98,10 +105,9 @@ async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) {
|
|||
});
|
||||
stream.on('data', (c) => def.push(c));
|
||||
stream.on('end', () => def.push(new Uint8Array(0), true));
|
||||
} else {
|
||||
logger.debug(`couldn't find stream for ${file.file}`);
|
||||
}
|
||||
// } catch (e) {
|
||||
|
||||
// }
|
||||
}
|
||||
|
||||
zip.end();
|
||||
|
@ -115,7 +121,7 @@ async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) {
|
|||
const parts = export_name.split('_');
|
||||
if (Number(parts[2]) !== user.id) return res.unauthorized('cannot access export owned by another user');
|
||||
|
||||
const stream = createReadStream(tmpdir() + `/${export_name}`);
|
||||
const stream = createReadStream(join(tmpdir(), export_name));
|
||||
|
||||
res.setHeader('Content-Type', 'application/zip');
|
||||
res.setHeader('Content-Disposition', `attachment; filename="${export_name}"`);
|
||||
|
@ -126,7 +132,7 @@ async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) {
|
|||
const exports = [];
|
||||
for (let i = 0; i !== exp.length; ++i) {
|
||||
const name = exp[i];
|
||||
const stats = await stat(tmpdir() + `/${name}`);
|
||||
const stats = await stat(join(tmpdir(), name));
|
||||
|
||||
if (Number(exp[i].split('_')[2]) !== user.id) continue;
|
||||
exports.push({ name, size: stats.size });
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
import { NextApiReq, NextApiRes, UserExtended, withZipline } from 'middleware/withZipline';
|
||||
import config from 'lib/config';
|
||||
import datasource from 'lib/datasource';
|
||||
import Logger from 'lib/logger';
|
||||
import prisma from 'lib/prisma';
|
||||
import { chunk } from 'lib/util';
|
||||
import Logger from 'lib/logger';
|
||||
import datasource from 'lib/datasource';
|
||||
import config from 'lib/config';
|
||||
import { NextApiReq, NextApiRes, UserExtended, withZipline } from 'middleware/withZipline';
|
||||
|
||||
const logger = Logger.get('files');
|
||||
|
||||
async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) {
|
||||
if (req.method === 'DELETE') {
|
||||
|
@ -23,7 +25,7 @@ async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) {
|
|||
userId: user.id,
|
||||
},
|
||||
});
|
||||
Logger.get('users').info(`User ${user.username} (${user.id}) deleted ${count} files.`);
|
||||
logger.info(`User ${user.username} (${user.id}) deleted ${count} files.`);
|
||||
|
||||
return res.json({ count });
|
||||
} else {
|
||||
|
@ -37,9 +39,7 @@ async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) {
|
|||
|
||||
await datasource.delete(image.file);
|
||||
|
||||
Logger.get('users').info(
|
||||
`User ${user.username} (${user.id}) deleted an image ${image.file} (${image.id})`
|
||||
);
|
||||
logger.info(`User ${user.username} (${user.id}) deleted an image ${image.file} (${image.id})`);
|
||||
|
||||
delete image.password;
|
||||
return res.json(image);
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
import config from 'lib/config';
|
||||
import Logger from 'lib/logger';
|
||||
import { discord_auth, github_auth, google_auth } from 'lib/oauth';
|
||||
import prisma from 'lib/prisma';
|
||||
import { hashPassword } from 'lib/util';
|
||||
import { NextApiReq, NextApiRes, UserExtended, withZipline } from 'middleware/withZipline';
|
||||
import Logger from 'lib/logger';
|
||||
import config from 'lib/config';
|
||||
import { discord_auth, github_auth, google_auth } from 'lib/oauth';
|
||||
|
||||
const logger = Logger.get('user');
|
||||
|
||||
async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) {
|
||||
if (user.oauth) {
|
||||
|
@ -11,6 +13,8 @@ async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) {
|
|||
if (user.oauth.find((o) => o.provider === 'GITHUB')) {
|
||||
const resp = await github_auth.oauth_user(user.oauth.find((o) => o.provider === 'GITHUB').token);
|
||||
if (!resp) {
|
||||
logger.debug(`oauth expired for ${JSON.stringify(user)}`);
|
||||
|
||||
return res.json({
|
||||
error: 'oauth token expired',
|
||||
redirect_uri: github_auth.oauth_url(config.oauth.github_client_id),
|
||||
|
@ -24,7 +28,9 @@ async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) {
|
|||
});
|
||||
if (!resp.ok) {
|
||||
const provider = user.oauth.find((o) => o.provider === 'DISCORD');
|
||||
if (!provider.refresh)
|
||||
if (!provider.refresh) {
|
||||
logger.debug(`couldn't find a refresh token for ${JSON.stringify(user)}`);
|
||||
|
||||
return res.json({
|
||||
error: 'oauth token expired',
|
||||
redirect_uri: discord_auth.oauth_url(
|
||||
|
@ -32,6 +38,7 @@ async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) {
|
|||
`${config.core.https ? 'https' : 'http'}://${req.headers.host}`
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
const resp2 = await fetch('https://discord.com/api/oauth2/token', {
|
||||
method: 'POST',
|
||||
|
@ -45,7 +52,9 @@ async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) {
|
|||
refresh_token: provider.refresh,
|
||||
}),
|
||||
});
|
||||
if (!resp2.ok)
|
||||
if (!resp2.ok) {
|
||||
logger.debug(`oauth expired for ${JSON.stringify(user)}`);
|
||||
|
||||
return res.json({
|
||||
error: 'oauth token expired',
|
||||
redirect_uri: discord_auth.oauth_url(
|
||||
|
@ -53,7 +62,7 @@ async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) {
|
|||
`${config.core.https ? 'https' : 'http'}://${req.headers.host}`
|
||||
),
|
||||
});
|
||||
|
||||
}
|
||||
const json = await resp2.json();
|
||||
|
||||
await prisma.oAuth.update({
|
||||
|
@ -74,7 +83,9 @@ async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) {
|
|||
);
|
||||
if (!resp.ok) {
|
||||
const provider = user.oauth.find((o) => o.provider === 'GOOGLE');
|
||||
if (!provider.refresh)
|
||||
if (!provider.refresh) {
|
||||
logger.debug(`couldn't find a refresh token for ${JSON.stringify(user)}`);
|
||||
|
||||
return res.json({
|
||||
error: 'oauth token expired',
|
||||
redirect_uri: google_auth.oauth_url(
|
||||
|
@ -82,7 +93,7 @@ async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) {
|
|||
`${config.core.https ? 'https' : 'http'}://${req.headers.host}`
|
||||
),
|
||||
});
|
||||
|
||||
}
|
||||
const resp2 = await fetch('https://oauth2.googleapis.com/token', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
|
@ -95,7 +106,9 @@ async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) {
|
|||
refresh_token: provider.refresh,
|
||||
}),
|
||||
});
|
||||
if (!resp2.ok)
|
||||
if (!resp2.ok) {
|
||||
logger.debug(`oauth expired for ${JSON.stringify(user)}`);
|
||||
|
||||
return res.json({
|
||||
error: 'oauth token expired',
|
||||
redirect_uri: google_auth.oauth_url(
|
||||
|
@ -103,6 +116,7 @@ async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) {
|
|||
`${config.core.https ? 'https' : 'http'}://${req.headers.host}`
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
const json = await resp2.json();
|
||||
|
||||
|
@ -120,6 +134,8 @@ async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) {
|
|||
}
|
||||
|
||||
if (req.method === 'PATCH') {
|
||||
logger.debug(`attempting to update user ${JSON.stringify(user)}`);
|
||||
|
||||
if (req.body.password) {
|
||||
const hashed = await hashPassword(req.body.password);
|
||||
await prisma.user.update({
|
||||
|
@ -220,7 +236,9 @@ async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) {
|
|||
},
|
||||
});
|
||||
|
||||
Logger.get('user').info(`User ${user.username} (${newUser.username}) (${newUser.id}) was updated`);
|
||||
logger.debug(`updated user ${JSON.stringify(newUser)}`);
|
||||
|
||||
logger.info(`User ${user.username} (${newUser.username}) (${newUser.id}) was updated`);
|
||||
|
||||
return res.json(newUser);
|
||||
} else {
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import { NextApiReq, NextApiRes, UserExtended, withZipline } from 'middleware/withZipline';
|
||||
import prisma from 'lib/prisma';
|
||||
import config from 'lib/config';
|
||||
import prisma from 'lib/prisma';
|
||||
import { NextApiReq, NextApiRes, UserExtended, withZipline } from 'middleware/withZipline';
|
||||
|
||||
async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) {
|
||||
const take = Number(req.query.take ?? 4);
|
||||
|
||||
if (take > 50) return res.badRequest("take can't be more than 50");
|
||||
if (take >= 50) return res.badRequest("take can't be more than 50");
|
||||
|
||||
let images = await prisma.image.findMany({
|
||||
take,
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import { NextApiReq, NextApiRes, UserExtended, withZipline } from 'middleware/withZipline';
|
||||
import Logger from 'lib/logger';
|
||||
import prisma from 'lib/prisma';
|
||||
import { createToken } from 'lib/util';
|
||||
import Logger from 'lib/logger';
|
||||
import { NextApiReq, NextApiRes, UserExtended, withZipline } from 'middleware/withZipline';
|
||||
|
||||
async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) {
|
||||
async function handler(_: NextApiReq, res: NextApiRes, user: UserExtended) {
|
||||
const updated = await prisma.user.update({
|
||||
where: {
|
||||
id: user.id,
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { NextApiReq, NextApiRes, UserExtended, withZipline } from 'middleware/withZipline';
|
||||
import prisma from 'lib/prisma';
|
||||
import config from 'lib/config';
|
||||
import Logger from 'lib/logger';
|
||||
import prisma from 'lib/prisma';
|
||||
import { NextApiReq, NextApiRes, UserExtended, withZipline } from 'middleware/withZipline';
|
||||
|
||||
async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) {
|
||||
if (req.method === 'DELETE') {
|
||||
|
|
|
@ -1,87 +1,15 @@
|
|||
import { NextApiReq, NextApiRes, withZipline } from 'middleware/withZipline';
|
||||
import prisma from 'lib/prisma';
|
||||
import Logger from 'lib/logger';
|
||||
import datasource from 'lib/datasource';
|
||||
import { NextApiReq, NextApiRes, withZipline } from 'middleware/withZipline';
|
||||
|
||||
async function handler(req: NextApiReq, res: NextApiRes) {
|
||||
if (req.method === 'POST' && req.body && req.body.code) {
|
||||
const { code, username } = req.body as { code: string; username: string };
|
||||
const invite = await prisma.invite.findUnique({
|
||||
where: { code },
|
||||
});
|
||||
if (!invite) return res.badRequest('invalid invite code');
|
||||
async function handler(_: NextApiReq, res: NextApiRes) {
|
||||
const users = await prisma.user.findMany();
|
||||
for (let i = 0; i !== users.length; ++i) delete users[i].password;
|
||||
|
||||
const user = await prisma.user.findFirst({
|
||||
where: { username },
|
||||
});
|
||||
|
||||
if (user) return res.badRequest('username already exists');
|
||||
return res.json({ success: true });
|
||||
}
|
||||
|
||||
const user = await req.user();
|
||||
if (!user) return res.unauthorized('not logged in');
|
||||
if (!user.administrator) return res.forbidden('not an administrator');
|
||||
|
||||
if (req.method === 'DELETE') {
|
||||
if (req.body.id === user.id) return res.badRequest("you can't delete your own account");
|
||||
|
||||
const deleteUser = await prisma.user.findFirst({
|
||||
where: {
|
||||
id: req.body.id,
|
||||
},
|
||||
});
|
||||
if (!deleteUser) return res.notFound("user doesn't exist");
|
||||
|
||||
if (req.body.delete_images) {
|
||||
const files = await prisma.image.findMany({
|
||||
where: {
|
||||
userId: deleteUser.id,
|
||||
},
|
||||
});
|
||||
|
||||
for (let i = 0; i !== files.length; ++i) {
|
||||
try {
|
||||
await datasource.delete(files[i].file);
|
||||
} catch {}
|
||||
}
|
||||
|
||||
const { count } = await prisma.image.deleteMany({
|
||||
where: {
|
||||
userId: deleteUser.id,
|
||||
},
|
||||
});
|
||||
Logger.get('users').info(
|
||||
`User ${user.username} (${user.id}) deleted ${count} files of user ${deleteUser.username} (${deleteUser.id})`
|
||||
);
|
||||
}
|
||||
|
||||
await prisma.user.delete({
|
||||
where: {
|
||||
id: deleteUser.id,
|
||||
},
|
||||
});
|
||||
|
||||
delete deleteUser.password;
|
||||
return res.json(deleteUser);
|
||||
} else {
|
||||
const users = await prisma.user.findMany({
|
||||
select: {
|
||||
username: true,
|
||||
id: true,
|
||||
administrator: true,
|
||||
superAdmin: true,
|
||||
token: true,
|
||||
embedColor: true,
|
||||
embedTitle: true,
|
||||
systemTheme: true,
|
||||
avatar: true,
|
||||
},
|
||||
});
|
||||
return res.json(users);
|
||||
}
|
||||
}
|
||||
|
||||
export default withZipline(handler, {
|
||||
methods: ['GET', 'POST', 'DELETE'],
|
||||
methods: ['GET', 'POST'],
|
||||
user: true,
|
||||
administrator: true,
|
||||
});
|
||||
|
|
|
@ -2,7 +2,7 @@ import { readFile } from 'fs/promises';
|
|||
import config from 'lib/config';
|
||||
import { NextApiReq, NextApiRes, withZipline } from 'middleware/withZipline';
|
||||
|
||||
async function handler(req: NextApiReq, res: NextApiRes) {
|
||||
async function handler(_: NextApiReq, res: NextApiRes) {
|
||||
if (!config.website.show_version) return res.forbidden('version hidden');
|
||||
|
||||
const pkg = JSON.parse(await readFile('package.json', 'utf8'));
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import { Button, Center, TextInput, Title, PasswordInput, Divider, Group } from '@mantine/core';
|
||||
import { Button, Center, Divider, PasswordInput, TextInput, Title } from '@mantine/core';
|
||||
import { useForm } from '@mantine/form';
|
||||
import Link from 'next/link';
|
||||
import { DiscordIcon, GitHubIcon, GoogleIcon } from 'components/icons';
|
||||
import useFetch from 'hooks/useFetch';
|
||||
import Head from 'next/head';
|
||||
import Link from 'next/link';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useEffect } from 'react';
|
||||
import Head from 'next/head';
|
||||
import { GitHubIcon, DiscordIcon, GoogleIcon } from 'components/icons';
|
||||
export { getServerSideProps } from 'middleware/getServerSideProps';
|
||||
|
||||
export default function Login({ title, user_registration, oauth_registration, oauth_providers: unparsed }) {
|
||||
|
@ -42,7 +42,7 @@ export default function Login({ title, user_registration, oauth_registration, oa
|
|||
});
|
||||
|
||||
if (res.error) {
|
||||
if (res.error.startsWith('403')) {
|
||||
if (res.code === 403) {
|
||||
form.setFieldError('password', 'Invalid password');
|
||||
} else {
|
||||
form.setFieldError('username', 'Invalid username');
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
import React, { useEffect } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
import { LoadingOverlay } from '@mantine/core';
|
||||
import { useSetRecoilState } from 'recoil';
|
||||
import { userSelector } from 'lib/recoil/user';
|
||||
import Head from 'next/head';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useEffect } from 'react';
|
||||
import { useSetRecoilState } from 'recoil';
|
||||
export { getServerSideProps } from 'middleware/getServerSideProps';
|
||||
|
||||
export default function Logout() {
|
||||
export default function Logout({ title }) {
|
||||
const setUser = useSetRecoilState(userSelector);
|
||||
const router = useRouter();
|
||||
|
||||
|
@ -23,7 +25,15 @@ export default function Logout() {
|
|||
})();
|
||||
}, []);
|
||||
|
||||
return <LoadingOverlay visible={true} />;
|
||||
}
|
||||
const full_title = `${title} - Logout`;
|
||||
|
||||
Logout.title = 'Zipline - Logout';
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>{full_title}</title>
|
||||
</Head>
|
||||
|
||||
<LoadingOverlay visible={true} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,17 +1,17 @@
|
|||
import { GetServerSideProps } from 'next';
|
||||
import prisma from 'lib/prisma';
|
||||
import { useState } from 'react';
|
||||
import { Box, Button, Card, Center, Group, PasswordInput, Stepper, TextInput } from '@mantine/core';
|
||||
import useFetch from 'hooks/useFetch';
|
||||
import PasswordStrength from 'components/PasswordStrength';
|
||||
import { Box, Button, Center, Group, PasswordInput, Stepper, TextInput } from '@mantine/core';
|
||||
import { showNotification } from '@mantine/notifications';
|
||||
import { CrossIcon, UserIcon } from 'components/icons';
|
||||
import { useRouter } from 'next/router';
|
||||
import Head from 'next/head';
|
||||
import PasswordStrength from 'components/PasswordStrength';
|
||||
import useFetch from 'hooks/useFetch';
|
||||
import config from 'lib/config';
|
||||
import { useSetRecoilState } from 'recoil';
|
||||
import prisma from 'lib/prisma';
|
||||
import { userSelector } from 'lib/recoil/user';
|
||||
import { randomChars } from 'lib/util';
|
||||
import { GetServerSideProps } from 'next';
|
||||
import Head from 'next/head';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useState } from 'react';
|
||||
import { useSetRecoilState } from 'recoil';
|
||||
|
||||
export default function Register({ code, title, user_registration }) {
|
||||
const [active, setActive] = useState(0);
|
||||
|
@ -33,7 +33,7 @@ export default function Register({ code, title, user_registration }) {
|
|||
|
||||
setUsernameError('');
|
||||
|
||||
const res = await useFetch('/api/users', 'POST', { code, username });
|
||||
const res = await useFetch('/api/user/check', 'POST', { code, username });
|
||||
if (res.error) {
|
||||
setUsernameError('A user with that username already exists');
|
||||
} else {
|
||||
|
@ -46,9 +46,8 @@ export default function Register({ code, title, user_registration }) {
|
|||
setPassword(password.trim());
|
||||
setVerifyPassword(verifyPassword.trim());
|
||||
|
||||
if (password.trim() !== verifyPassword.trim()) {
|
||||
setVerifyPasswordError('Passwords do not match');
|
||||
}
|
||||
if (password !== verifyPassword) setVerifyPasswordError('Passwords do not match');
|
||||
else setVerifyPasswordError('');
|
||||
};
|
||||
|
||||
const createUser = async () => {
|
||||
|
@ -57,6 +56,7 @@ export default function Register({ code, title, user_registration }) {
|
|||
username,
|
||||
password,
|
||||
});
|
||||
|
||||
if (res.error) {
|
||||
showNotification({
|
||||
title: 'Error while creating user',
|
||||
|
@ -87,7 +87,7 @@ export default function Register({ code, title, user_registration }) {
|
|||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>{title}</title>
|
||||
<title>{full_title}</title>
|
||||
</Head>
|
||||
<Center sx={{ height: '100vh' }}>
|
||||
<Box
|
||||
|
@ -158,24 +158,31 @@ export default function Register({ code, title, user_registration }) {
|
|||
export const getServerSideProps: GetServerSideProps = async (context) => {
|
||||
const { code } = context.query as { code: string };
|
||||
|
||||
if (!config.features.invites && code)
|
||||
const { default: Logger } = await import('lib/logger');
|
||||
const logger = Logger.get('pages::register');
|
||||
|
||||
if (code) {
|
||||
if (!config.features.invites)
|
||||
return {
|
||||
notFound: true,
|
||||
};
|
||||
|
||||
if (!config.features.user_registration && !code) return { notFound: true };
|
||||
|
||||
if (code) {
|
||||
const invite = await prisma.invite.findUnique({
|
||||
where: {
|
||||
code,
|
||||
},
|
||||
});
|
||||
|
||||
logger.debug(`request to access ${JSON.stringify(invite)}`);
|
||||
|
||||
if (!invite) return { notFound: true };
|
||||
if (invite.used) return { notFound: true };
|
||||
|
||||
if (invite.expires_at && invite.expires_at < new Date()) return { notFound: true };
|
||||
if (invite.expires_at && invite.expires_at < new Date()) {
|
||||
logger.debug(`restricting access to ${JSON.stringify(invite)} as it has expired`);
|
||||
|
||||
return { notFound: true };
|
||||
}
|
||||
|
||||
return {
|
||||
props: {
|
||||
|
@ -184,13 +191,21 @@ export const getServerSideProps: GetServerSideProps = async (context) => {
|
|||
},
|
||||
};
|
||||
} else {
|
||||
if (!config.features.user_registration)
|
||||
return {
|
||||
notFound: true,
|
||||
};
|
||||
|
||||
const code = randomChars(4);
|
||||
await prisma.invite.create({
|
||||
const temp = await prisma.invite.create({
|
||||
data: {
|
||||
code,
|
||||
createdById: 1,
|
||||
},
|
||||
});
|
||||
|
||||
logger.debug(`request to access user registration, creating temporary invite ${JSON.stringify(temp)}`);
|
||||
|
||||
return {
|
||||
props: {
|
||||
title: config.website.title,
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import { Prism } from '@mantine/prism';
|
||||
import prisma from 'lib/prisma';
|
||||
import config from 'lib/config';
|
||||
import exts from 'lib/exts';
|
||||
import prisma from 'lib/prisma';
|
||||
import { checkPassword } from 'lib/util';
|
||||
import { streamToString } from 'lib/utils/streams';
|
||||
import { GetServerSideProps } from 'next';
|
||||
import { checkPassword } from 'lib/util';
|
||||
import config from 'lib/config';
|
||||
import Head from 'next/head';
|
||||
|
||||
export default function Code({ code, id, title }) {
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
import React from 'react';
|
||||
import useLogin from 'hooks/useLogin';
|
||||
import { LoadingOverlay } from '@mantine/core';
|
||||
import Layout from 'components/Layout';
|
||||
import Files from 'components/pages/Files';
|
||||
import { LoadingOverlay } from '@mantine/core';
|
||||
import useLogin from 'hooks/useLogin';
|
||||
import Head from 'next/head';
|
||||
export { getServerSideProps } from 'middleware/getServerSideProps';
|
||||
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
import React from 'react';
|
||||
import useLogin from 'hooks/useLogin';
|
||||
import { LoadingOverlay } from '@mantine/core';
|
||||
import Layout from 'components/Layout';
|
||||
import Dashboard from 'components/pages/Dashboard';
|
||||
import { LoadingOverlay } from '@mantine/core';
|
||||
import useLogin from 'hooks/useLogin';
|
||||
import Head from 'next/head';
|
||||
export { getServerSideProps } from 'middleware/getServerSideProps';
|
||||
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import React, { useEffect } from 'react';
|
||||
import useLogin from 'hooks/useLogin';
|
||||
import { LoadingOverlay } from '@mantine/core';
|
||||
import Layout from 'components/Layout';
|
||||
import Invites from 'components/pages/Invites';
|
||||
import { LoadingOverlay } from '@mantine/core';
|
||||
import useLogin from 'hooks/useLogin';
|
||||
import Head from 'next/head';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useEffect } from 'react';
|
||||
export { getServerSideProps } from 'middleware/getServerSideProps';
|
||||
|
||||
export default function InvitesPage(props) {
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
import React from 'react';
|
||||
import useLogin from 'hooks/useLogin';
|
||||
import { LoadingOverlay } from '@mantine/core';
|
||||
import Layout from 'components/Layout';
|
||||
import Manage from 'components/pages/Manage';
|
||||
import { LoadingOverlay } from '@mantine/core';
|
||||
import useLogin from 'hooks/useLogin';
|
||||
import Head from 'next/head';
|
||||
export { getServerSideProps } from 'middleware/getServerSideProps';
|
||||
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
import React from 'react';
|
||||
import useLogin from 'hooks/useLogin';
|
||||
import { LoadingOverlay } from '@mantine/core';
|
||||
import Layout from 'components/Layout';
|
||||
import Stats from 'components/pages/Stats';
|
||||
import { LoadingOverlay } from '@mantine/core';
|
||||
import useLogin from 'hooks/useLogin';
|
||||
import Head from 'next/head';
|
||||
export { getServerSideProps } from 'middleware/getServerSideProps';
|
||||
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
import React from 'react';
|
||||
import useLogin from 'hooks/useLogin';
|
||||
import { LoadingOverlay } from '@mantine/core';
|
||||
import Layout from 'components/Layout';
|
||||
import Upload from 'components/pages/Upload';
|
||||
import { LoadingOverlay } from '@mantine/core';
|
||||
import useLogin from 'hooks/useLogin';
|
||||
import Head from 'next/head';
|
||||
export { getServerSideProps } from 'middleware/getServerSideProps';
|
||||
|
|
@ -1,8 +1,7 @@
|
|||
import React from 'react';
|
||||
import useLogin from 'hooks/useLogin';
|
||||
import { LoadingOverlay } from '@mantine/core';
|
||||
import Layout from 'components/Layout';
|
||||
import UploadText from 'components/pages/UploadText';
|
||||
import { LoadingOverlay } from '@mantine/core';
|
||||
import useLogin from 'hooks/useLogin';
|
||||
import Head from 'next/head';
|
||||
export { getServerSideProps } from 'middleware/getServerSideProps';
|
||||
|
|
@ -1,8 +1,7 @@
|
|||
import React from 'react';
|
||||
import useLogin from 'hooks/useLogin';
|
||||
import { LoadingOverlay } from '@mantine/core';
|
||||
import Layout from 'components/Layout';
|
||||
import Urls from 'components/pages/Urls';
|
||||
import { LoadingOverlay } from '@mantine/core';
|
||||
import useLogin from 'hooks/useLogin';
|
||||
import Head from 'next/head';
|
||||
export { getServerSideProps } from 'middleware/getServerSideProps';
|
||||
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
import React from 'react';
|
||||
import useLogin from 'hooks/useLogin';
|
||||
import { LoadingOverlay } from '@mantine/core';
|
||||
import Layout from 'components/Layout';
|
||||
import Users from 'components/pages/Users';
|
||||
import { LoadingOverlay } from '@mantine/core';
|
||||
import useLogin from 'hooks/useLogin';
|
||||
import Head from 'next/head';
|
||||
export { getServerSideProps } from 'middleware/getServerSideProps';
|
||||
|
||||
|
|
63
src/pages/oauth_error.tsx
Normal file
63
src/pages/oauth_error.tsx
Normal file
|
@ -0,0 +1,63 @@
|
|||
import { Button, Stack, Title } from '@mantine/core';
|
||||
import Link from 'components/Link';
|
||||
import MutedText from 'components/MutedText';
|
||||
import { GetServerSideProps } from 'next';
|
||||
import Head from 'next/head';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
export default function OauthError({ error, provider }) {
|
||||
const [remaining, setRemaining] = useState(10);
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
setRemaining((remaining) => remaining - 1);
|
||||
}, 1000);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
if (remaining === 0) {
|
||||
router.push('/auth/login');
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>Authentication Error</title>
|
||||
</Head>
|
||||
|
||||
<Stack
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
minHeight: '100vh',
|
||||
justifyContent: 'center',
|
||||
position: 'relative',
|
||||
}}
|
||||
spacing='sm'
|
||||
>
|
||||
<Title sx={{ fontSize: 50, fontWeight: 900, lineHeight: 0.8 }}>
|
||||
Error while authenticating with {provider}
|
||||
</Title>
|
||||
<MutedText sx={{ fontSize: 40, fontWeight: 500 }}>{error}</MutedText>
|
||||
<MutedText>
|
||||
Redirecting to login in {remaining} second{remaining === 1 ? 's' : ''}
|
||||
</MutedText>
|
||||
<Button component={Link} href='/dashboard'>
|
||||
Head to the Dashboard
|
||||
</Button>
|
||||
</Stack>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export const getServerSideProps: GetServerSideProps = async (ctx) => {
|
||||
return {
|
||||
props: {
|
||||
error: ctx.query.error ?? 'Unknown',
|
||||
provider: ctx.query.provider ?? 'Unknown',
|
||||
},
|
||||
};
|
||||
};
|
|
@ -1,14 +1,13 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import Head from 'next/head';
|
||||
import { GetServerSideProps } from 'next';
|
||||
import { Box, Button, Modal, PasswordInput } from '@mantine/core';
|
||||
import config from 'lib/config';
|
||||
import exts from 'lib/exts';
|
||||
import prisma from 'lib/prisma';
|
||||
import { parse } from 'lib/utils/client';
|
||||
import exts from 'lib/exts';
|
||||
import { GetServerSideProps } from 'next';
|
||||
import Head from 'next/head';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
export default function EmbeddedImage({ image, user, pass, prismRender }) {
|
||||
export default function EmbeddedFile({ image, user, pass, prismRender }) {
|
||||
const dataURL = (route: string) => `${route}/${image.file}`;
|
||||
|
||||
const router = useRouter();
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import datasource from '../lib/datasource';
|
||||
import { readdir, readFile } from 'fs/promises';
|
||||
import config from '../lib/config';
|
||||
import { migrations } from '../server/util';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import { guess } from '../lib/mimes';
|
||||
import { readdir, readFile } from 'fs/promises';
|
||||
import { join } from 'path';
|
||||
import config from '../lib/config';
|
||||
import datasource from '../lib/datasource';
|
||||
import { guess } from '../lib/mimes';
|
||||
import { migrations } from '../server/util';
|
||||
|
||||
async function main() {
|
||||
const directory = process.argv[2];
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import config from '../lib/config';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import config from '../lib/config';
|
||||
import { migrations } from '../server/util';
|
||||
|
||||
async function main() {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import config from '../lib/config';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import { migrations } from '../server/util';
|
||||
import { hash } from 'argon2';
|
||||
import config from '../lib/config';
|
||||
import { migrations } from '../server/util';
|
||||
|
||||
const SUPPORTED_FIELDS = [
|
||||
'username',
|
||||
|
|
|
@ -1,18 +1,17 @@
|
|||
import { Image, PrismaClient } from '@prisma/client';
|
||||
import Router from 'find-my-way';
|
||||
import { mkdir } from 'fs/promises';
|
||||
import { createServer, IncomingMessage, OutgoingMessage, ServerResponse } from 'http';
|
||||
import next from 'next';
|
||||
import { NextServer, RequestHandler } from 'next/dist/server/next';
|
||||
import { Image, PrismaClient } from '@prisma/client';
|
||||
import { createServer, IncomingMessage, OutgoingMessage, ServerResponse } from 'http';
|
||||
import { extname } from 'path';
|
||||
import { mkdir } from 'fs/promises';
|
||||
import { getStats, log, migrations, redirect } from './util';
|
||||
import Logger from '../lib/logger';
|
||||
import { guess } from '../lib/mimes';
|
||||
import exts from '../lib/exts';
|
||||
import { version } from '../../package.json';
|
||||
import config from '../lib/config';
|
||||
import datasource from '../lib/datasource';
|
||||
import { NextUrlWithParsedQuery } from 'next/dist/server/request-meta';
|
||||
import exts from '../lib/exts';
|
||||
import Logger from '../lib/logger';
|
||||
import { guess } from '../lib/mimes';
|
||||
import { getStats, log, migrations, redirect } from './util';
|
||||
|
||||
const dev = process.env.NODE_ENV === 'development';
|
||||
const logger = Logger.get('server');
|
||||
|
@ -20,18 +19,22 @@ const logger = Logger.get('server');
|
|||
start();
|
||||
|
||||
async function start() {
|
||||
logger.debug('Starting server');
|
||||
|
||||
// annoy user if they didnt change secret from default "changethis"
|
||||
if (config.core.secret === 'changethis') {
|
||||
logger.error('Secret is not set!');
|
||||
logger.error(
|
||||
logger
|
||||
.error('Secret is not set!')
|
||||
.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(
|
||||
)
|
||||
.error('Please change your secret in the config file or environment variables.')
|
||||
.error(
|
||||
'The config file is located at `.env.local`, 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('A way you can generate this is through a password manager you may have.');
|
||||
)
|
||||
.error('It is recomended to use a secret that is alphanumeric and randomized.')
|
||||
.error('A way you can generate this is through a password manager you may have.');
|
||||
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
|
@ -50,6 +53,8 @@ async function start() {
|
|||
});
|
||||
|
||||
if (admin) {
|
||||
logger.debug('setting main administrator user to a superAdmin');
|
||||
|
||||
await prisma.user.update({
|
||||
where: {
|
||||
id: admin.id,
|
||||
|
@ -105,6 +110,8 @@ async function start() {
|
|||
},
|
||||
});
|
||||
|
||||
Logger.get('url').debug(`url deleted due to max views ${JSON.stringify(nUrl)}`);
|
||||
|
||||
return nextServer.render404(req, res as ServerResponse);
|
||||
}
|
||||
|
||||
|
@ -185,13 +192,13 @@ async function start() {
|
|||
stats(prisma);
|
||||
|
||||
setInterval(async () => {
|
||||
await prisma.invite.deleteMany({
|
||||
const { count } = await prisma.invite.deleteMany({
|
||||
where: {
|
||||
used: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (config.core.logger) logger.info('invites cleaned');
|
||||
logger.debug(`deleted ${count} used invites`);
|
||||
}, config.core.invites_interval * 1000);
|
||||
}
|
||||
|
||||
|
@ -269,6 +276,8 @@ async function stats(prisma: PrismaClient) {
|
|||
},
|
||||
});
|
||||
|
||||
logger.debug(`stats updated ${JSON.stringify(stats)}`);
|
||||
|
||||
setInterval(async () => {
|
||||
const stats = await getStats(prisma, datasource);
|
||||
await prisma.stats.create({
|
||||
|
@ -276,6 +285,7 @@ async function stats(prisma: PrismaClient) {
|
|||
data: stats,
|
||||
},
|
||||
});
|
||||
if (config.core.logger) logger.info('stats updated');
|
||||
|
||||
logger.debug(`stats updated ${JSON.stringify(stats)}`);
|
||||
}, config.core.stats_interval * 1000);
|
||||
}
|
||||
|
|
|
@ -1,14 +1,19 @@
|
|||
import { PrismaClient } from '@prisma/client';
|
||||
import { Migrate } from '@prisma/migrate/dist/Migrate';
|
||||
import { ensureDatabaseExists } from '@prisma/migrate/dist/utils/ensureDatabaseExists';
|
||||
import { ServerResponse } from 'http';
|
||||
import { Datasource } from '../lib/datasources';
|
||||
import Logger from '../lib/logger';
|
||||
import { bytesToHuman } from '../lib/utils/bytes';
|
||||
import { Datasource } from '../lib/datasources';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import { ServerResponse } from 'http';
|
||||
|
||||
export async function migrations() {
|
||||
const logger = Logger.get('database::migrations');
|
||||
|
||||
try {
|
||||
logger.debug('establishing database connection');
|
||||
const migrate = new Migrate('./prisma/schema.prisma');
|
||||
|
||||
logger.debug('ensuring database exists, if not creating database - may error if no permissions');
|
||||
await ensureDatabaseExists('apply', true, './prisma/schema.prisma');
|
||||
|
||||
const diagnose = await migrate.diagnoseMigrationHistory({
|
||||
|
@ -16,19 +21,28 @@ export async function migrations() {
|
|||
});
|
||||
|
||||
if (diagnose.history?.diagnostic === 'databaseIsBehind') {
|
||||
logger.debug('database is behind, attempting to migrate');
|
||||
try {
|
||||
Logger.get('database').info('migrating database');
|
||||
logger.debug('migrating database');
|
||||
await migrate.applyMigrations();
|
||||
} finally {
|
||||
migrate.stop();
|
||||
Logger.get('database').info('finished migrating database');
|
||||
logger.info('finished migrating database');
|
||||
}
|
||||
} else {
|
||||
logger.debug('exiting migrations engine - database is up to date');
|
||||
migrate.stop();
|
||||
}
|
||||
} catch (error) {
|
||||
Logger.get('database').error('Failed to migrate database... exiting...');
|
||||
Logger.get('database').error(error);
|
||||
if (error.message.startsWith('P1001')) {
|
||||
logger.error(
|
||||
`Unable to connect to database \`${process.env.DATABASE_URL}\`, check your database connection`
|
||||
);
|
||||
} else {
|
||||
logger.error('Failed to migrate database... exiting...');
|
||||
logger.error(error);
|
||||
}
|
||||
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue