feat: bunch of stuff

This commit is contained in:
diced 2022-11-13 19:34:38 -08:00
parent bb7367615d
commit f67d1d41cb
No known key found for this signature in database
GPG key ID: 370BD1BA142842D1
79 changed files with 708 additions and 466 deletions

View file

@ -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.

View file

@ -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?

View file

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

View file

@ -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 ? (

View file

@ -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} />,

View file

@ -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 }) {

View file

@ -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: {

View file

@ -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 }) {

View file

@ -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 }) {

View 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';

View file

@ -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() {

View file

@ -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';

View file

@ -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'];

View file

@ -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';

View file

@ -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';

View file

@ -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';

View file

@ -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';

View file

@ -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';

View file

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

View file

@ -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';

View file

@ -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';

View file

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

View file

@ -101,6 +101,8 @@ export interface ConfigDiscordEmbed {
export interface ConfigFeatures {
invites: boolean;
invites_length: number;
oauth_registration: boolean;
user_registration: boolean;
}

View file

@ -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;

View file

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

View file

@ -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');

View file

@ -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;

View file

@ -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';

View file

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

View file

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

View file

@ -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) => {

View file

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

View file

@ -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';

View file

@ -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';

View file

@ -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 }) {

View file

@ -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 />

View file

@ -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...';

View file

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

View file

@ -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) {

View file

@ -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 {

View file

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

View file

@ -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');

View file

@ -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' };

View file

@ -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' };

View file

@ -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' };

View file

@ -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') {

View file

@ -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(

View file

@ -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') {

View file

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

View file

@ -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;

View 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'],
});

View file

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

View file

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

View file

@ -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 {

View file

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

View file

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

View file

@ -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') {

View file

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

View file

@ -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'));

View file

@ -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');

View file

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

View file

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

View file

@ -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 }) {

View file

@ -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';

View file

@ -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';

View file

@ -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) {

View file

@ -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';

View file

@ -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';

View file

@ -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';

View file

@ -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';

View file

@ -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';

View file

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

View file

@ -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();

View file

@ -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];

View file

@ -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() {

View file

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

View file

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

View file

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