diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml index 5bed5f9..c9180e6 100644 --- a/.github/ISSUE_TEMPLATE/bug.yml +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -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. diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index e1e8e7d..d5e2306 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -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? diff --git a/package.json b/package.json index b67f80e..67b4b57 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/components/File.tsx b/src/components/File.tsx index 9da9cd8..949372d 100644 --- a/src/components/File.tsx +++ b/src/components/File.tsx @@ -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 ? ( diff --git a/src/components/Layout.tsx b/src/components/Layout.tsx index 788c205..3425497 100644 --- a/src/components/Layout.tsx +++ b/src/components/Layout.tsx @@ -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: , text: 'Upload', - link: '/dashboard/upload', + link: '/dashboard/upload/file', }, { icon: , text: 'Upload Text', - link: '/dashboard/text', + link: '/dashboard/upload/text', }, ]; @@ -150,7 +149,7 @@ const admin_items = [ icon: , text: 'Users', link: '/dashboard/users', - if: (props) => true, + if: () => true, }, { icon: , diff --git a/src/components/PasswordStrength.tsx b/src/components/PasswordStrength.tsx index 47097fe..5b0494d 100644 --- a/src/components/PasswordStrength.tsx +++ b/src/components/PasswordStrength.tsx @@ -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 }) { diff --git a/src/components/Theming.tsx b/src/components/Theming.tsx index 1141c7f..0565a11 100644 --- a/src/components/Theming.tsx +++ b/src/components/Theming.tsx @@ -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 }) { ({ @@ -92,6 +100,7 @@ export default function ZiplineTheming({ Component, pageProps, ...props }) { Popover: { defaultProps: { transition: 'pop', + shadow: 'lg', }, }, LoadingOverlay: { diff --git a/src/components/dropzone/Dropzone.tsx b/src/components/dropzone/Dropzone.tsx index 87d9688..482f7b1 100644 --- a/src/components/dropzone/Dropzone.tsx +++ b/src/components/dropzone/Dropzone.tsx @@ -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 }) { diff --git a/src/components/dropzone/DropzoneFile.tsx b/src/components/dropzone/DropzoneFile.tsx index 5c77091..0b38562 100644 --- a/src/components/dropzone/DropzoneFile.tsx +++ b/src/components/dropzone/DropzoneFile.tsx @@ -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 }) { diff --git a/src/components/pages/Dashboard/RecentFiles.tsx b/src/components/pages/Dashboard/RecentFiles.tsx index 73c5428..757dcf1 100644 --- a/src/components/pages/Dashboard/RecentFiles.tsx +++ b/src/components/pages/Dashboard/RecentFiles.tsx @@ -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'; diff --git a/src/components/pages/Dashboard/StatCards.tsx b/src/components/pages/Dashboard/StatCards.tsx index 8ceaf53..71885f3 100644 --- a/src/components/pages/Dashboard/StatCards.tsx +++ b/src/components/pages/Dashboard/StatCards.tsx @@ -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() { diff --git a/src/components/pages/Files/FilePagation.tsx b/src/components/pages/Files/FilePagation.tsx index 18d6338..662abb0 100644 --- a/src/components/pages/Files/FilePagation.tsx +++ b/src/components/pages/Files/FilePagation.tsx @@ -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'; diff --git a/src/components/pages/Invites.tsx b/src/components/pages/Invites.tsx index a799ac7..3065cfe 100644 --- a/src/components/pages/Invites.tsx +++ b/src/components/pages/Invites.tsx @@ -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']; diff --git a/src/components/pages/Manage/GeneratorModal.tsx b/src/components/pages/Manage/GeneratorModal.tsx index 670fda8..fbb3b96 100644 --- a/src/components/pages/Manage/GeneratorModal.tsx +++ b/src/components/pages/Manage/GeneratorModal.tsx @@ -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'; diff --git a/src/components/pages/Stats/Graphs.tsx b/src/components/pages/Stats/Graphs.tsx index dd08dd6..a7950c6 100644 --- a/src/components/pages/Stats/Graphs.tsx +++ b/src/components/pages/Stats/Graphs.tsx @@ -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'; diff --git a/src/components/pages/Stats/Types.tsx b/src/components/pages/Stats/Types.tsx index f670de7..564f013 100644 --- a/src/components/pages/Stats/Types.tsx +++ b/src/components/pages/Stats/Types.tsx @@ -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'; diff --git a/src/components/pages/Upload.tsx b/src/components/pages/Upload.tsx index 0ac83c0..6fdaf33 100644 --- a/src/components/pages/Upload.tsx +++ b/src/components/pages/Upload.tsx @@ -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'; diff --git a/src/components/pages/UploadText.tsx b/src/components/pages/UploadText.tsx index b895b91..72dcb78 100644 --- a/src/components/pages/UploadText.tsx +++ b/src/components/pages/UploadText.tsx @@ -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'; diff --git a/src/components/pages/Urls/index.tsx b/src/components/pages/Urls/index.tsx index 8852715..7339117 100644 --- a/src/components/pages/Urls/index.tsx +++ b/src/components/pages/Urls/index.tsx @@ -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); diff --git a/src/components/pages/Users/CreateUserModal.tsx b/src/components/pages/Users/CreateUserModal.tsx index 6879437..277cb42 100644 --- a/src/components/pages/Users/CreateUserModal.tsx +++ b/src/components/pages/Users/CreateUserModal.tsx @@ -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'; diff --git a/src/components/pages/Users/EditUserModal.tsx b/src/components/pages/Users/EditUserModal.tsx index fd660fa..b743600 100644 --- a/src/components/pages/Users/EditUserModal.tsx +++ b/src/components/pages/Users/EditUserModal.tsx @@ -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'; diff --git a/src/components/pages/Users/index.tsx b/src/components/pages/Users/index.tsx index 7d381a5..c5fe4e8 100644 --- a/src/components/pages/Users/index.tsx +++ b/src/components/pages/Users/index.tsx @@ -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, diff --git a/src/lib/config/Config.ts b/src/lib/config/Config.ts index d9357e3..5190ffb 100644 --- a/src/lib/config/Config.ts +++ b/src/lib/config/Config.ts @@ -101,6 +101,8 @@ export interface ConfigDiscordEmbed { export interface ConfigFeatures { invites: boolean; + invites_length: number; + oauth_registration: boolean; user_registration: boolean; } diff --git a/src/lib/config/readConfig.ts b/src/lib/config/readConfig.ts index afb4981..33e585d 100644 --- a/src/lib/config/readConfig.ts +++ b/src/lib/config/readConfig.ts @@ -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; diff --git a/src/lib/config/validateConfig.ts b/src/lib/config/validateConfig.ts index 59e91f5..822c71a 100644 --- a/src/lib/config/validateConfig.ts +++ b/src/lib/config/validateConfig.ts @@ -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); } diff --git a/src/lib/datasource.ts b/src/lib/datasource.ts index 1b2103b..7616a8e 100644 --- a/src/lib/datasource.ts +++ b/src/lib/datasource.ts @@ -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'); diff --git a/src/lib/discord.ts b/src/lib/discord.ts index 188450a..a41f868 100644 --- a/src/lib/discord.ts +++ b/src/lib/discord.ts @@ -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; diff --git a/src/lib/hooks/useLogin.ts b/src/lib/hooks/useLogin.ts index 23a885d..cbc5f26 100644 --- a/src/lib/hooks/useLogin.ts +++ b/src/lib/hooks/useLogin.ts @@ -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'; diff --git a/src/lib/logger.ts b/src/lib/logger.ts index f6dc25e..3e7f3d0 100644 --- a/src/lib/logger.ts +++ b/src/lib/logger.ts @@ -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'); } } } diff --git a/src/lib/middleware/withOAuth.ts b/src/lib/middleware/withOAuth.ts index 0f498e2..78f1ed3 100644 --- a/src/lib/middleware/withOAuth.ts +++ b/src/lib/middleware/withOAuth.ts @@ -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) => + ( + provider: 'discord' | 'github' | 'google', + oauth: (query: OAuthQuery, logger: Logger) => Promise + ) => 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'); }; diff --git a/src/lib/middleware/withZipline.ts b/src/lib/middleware/withZipline.ts index f11084e..8ba69c6 100644 --- a/src/lib/middleware/withZipline.ts +++ b/src/lib/middleware/withZipline.ts @@ -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, api_config: ZiplineApiConfig = { methods: ['GET'] } ) => (req: NextApiReq, res: NextApiRes) => { diff --git a/src/lib/util.ts b/src/lib/util.ts index 1194e1a..1369af1 100644 --- a/src/lib/util.ts +++ b/src/lib/util.ts @@ -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 { return await hash(s); diff --git a/src/pages/404.tsx b/src/pages/404.tsx index 7b4fbf4..8f9f8df 100644 --- a/src/pages/404.tsx +++ b/src/pages/404.tsx @@ -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'; diff --git a/src/pages/500.tsx b/src/pages/500.tsx index 5555be1..b33d830 100644 --- a/src/pages/500.tsx +++ b/src/pages/500.tsx @@ -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'; diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index 63c5f9b..9186944 100644 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -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 }) { diff --git a/src/pages/_document.tsx b/src/pages/_document.tsx index ecb99c9..598ee40 100644 --- a/src/pages/_document.tsx +++ b/src/pages/_document.tsx @@ -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 ( - + + + + +
diff --git a/src/pages/_error.tsx b/src/pages/_error.tsx index d577ad1..b7eeec4 100644 --- a/src/pages/_error.tsx +++ b/src/pages/_error.tsx @@ -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 ( <> - {statusCode} Error + Error ({statusCode}) 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 { diff --git a/src/pages/api/auth/login.ts b/src/pages/api/auth/login.ts index 77a23f4..e1e91fc 100644 --- a/src/pages/api/auth/login.ts +++ b/src/pages/api/auth/login.ts @@ -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 }); } diff --git a/src/pages/api/auth/logout.ts b/src/pages/api/auth/logout.ts index 70187df..a3697ed 100644 --- a/src/pages/api/auth/logout.ts +++ b/src/pages/api/auth/logout.ts @@ -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'); diff --git a/src/pages/api/auth/oauth/discord.ts b/src/pages/api/auth/oauth/discord.ts index 2218bcc..a05c51f 100644 --- a/src/pages/api/auth/oauth/discord.ts +++ b/src/pages/api/auth/oauth/discord.ts @@ -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 { +async function handler({ code, state, host }: OAuthQuery, logger: Logger): Promise { if (!config.features.oauth_registration) return { error_code: 403, @@ -13,7 +13,8 @@ async function handler({ code, state, host }: OAuthQuery): Promise 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' }; diff --git a/src/pages/api/auth/oauth/github.ts b/src/pages/api/auth/oauth/github.ts index e8df4f6..41f06d7 100644 --- a/src/pages/api/auth/oauth/github.ts +++ b/src/pages/api/auth/oauth/github.ts @@ -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 { +async function handler({ code, state }: OAuthQuery, logger: Logger): Promise { if (!config.features.oauth_registration) return { error_code: 403, @@ -13,7 +13,7 @@ async function handler({ code, state }: OAuthQuery): Promise { }; 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 { 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' }; diff --git a/src/pages/api/auth/oauth/google.ts b/src/pages/api/auth/oauth/google.ts index 05ef6be..442caa1 100644 --- a/src/pages/api/auth/oauth/google.ts +++ b/src/pages/api/auth/oauth/google.ts @@ -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 { +async function handler({ code, state, host }: OAuthQuery, logger: Logger): Promise { if (!config.features.oauth_registration) return { error_code: 403, @@ -13,7 +13,7 @@ async function handler({ code, state, host }: OAuthQuery): Promise 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' }; diff --git a/src/pages/api/auth/oauth/index.ts b/src/pages/api/auth/oauth/index.ts index 1c21bb7..4fd1cea 100644 --- a/src/pages/api/auth/oauth/index.ts +++ b/src/pages/api/auth/oauth/index.ts @@ -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') { diff --git a/src/pages/api/shorten.ts b/src/pages/api/shorten.ts index 1b8b022..a920ce5 100644 --- a/src/pages/api/shorten.ts +++ b/src/pages/api/shorten.ts @@ -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( diff --git a/src/pages/api/stats.ts b/src/pages/api/stats.ts index 4e17dbf..670fa27 100644 --- a/src/pages/api/stats.ts +++ b/src/pages/api/stats.ts @@ -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') { diff --git a/src/pages/api/upload.ts b/src/pages/api/upload.ts index 437dca5..eff7c69 100644 --- a/src/pages/api/upload.ts +++ b/src/pages/api/upload.ts @@ -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, diff --git a/src/pages/api/user/[id].ts b/src/pages/api/user/[id].ts index d79dc4d..932b1a9 100644 --- a/src/pages/api/user/[id].ts +++ b/src/pages/api/user/[id].ts @@ -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; diff --git a/src/pages/api/user/check.ts b/src/pages/api/user/check.ts new file mode 100644 index 0000000..e239d2f --- /dev/null +++ b/src/pages/api/user/check.ts @@ -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'], +}); diff --git a/src/pages/api/user/export.ts b/src/pages/api/user/export.ts index 660638d..691e23f 100644 --- a/src/pages/api/user/export.ts +++ b/src/pages/api/user/export.ts @@ -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 }); diff --git a/src/pages/api/user/files.ts b/src/pages/api/user/files.ts index 393dedf..99e7a1a 100644 --- a/src/pages/api/user/files.ts +++ b/src/pages/api/user/files.ts @@ -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); diff --git a/src/pages/api/user/index.ts b/src/pages/api/user/index.ts index 72bba44..908b14f 100644 --- a/src/pages/api/user/index.ts +++ b/src/pages/api/user/index.ts @@ -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 { diff --git a/src/pages/api/user/recent.ts b/src/pages/api/user/recent.ts index a190e34..f818f6d 100644 --- a/src/pages/api/user/recent.ts +++ b/src/pages/api/user/recent.ts @@ -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, diff --git a/src/pages/api/user/token.ts b/src/pages/api/user/token.ts index 501c9d1..08e2fe7 100644 --- a/src/pages/api/user/token.ts +++ b/src/pages/api/user/token.ts @@ -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, diff --git a/src/pages/api/user/urls.ts b/src/pages/api/user/urls.ts index 9ca286b..6537bae 100644 --- a/src/pages/api/user/urls.ts +++ b/src/pages/api/user/urls.ts @@ -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') { diff --git a/src/pages/api/users.ts b/src/pages/api/users.ts index 2fd6dd7..7478058 100644 --- a/src/pages/api/users.ts +++ b/src/pages/api/users.ts @@ -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); - } + return res.json(users); } export default withZipline(handler, { - methods: ['GET', 'POST', 'DELETE'], + methods: ['GET', 'POST'], + user: true, + administrator: true, }); diff --git a/src/pages/api/version.ts b/src/pages/api/version.ts index 0896ff6..d933e6a 100644 --- a/src/pages/api/version.ts +++ b/src/pages/api/version.ts @@ -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')); diff --git a/src/pages/auth/login.tsx b/src/pages/auth/login.tsx index 87ef0ac..64e9ca5 100644 --- a/src/pages/auth/login.tsx +++ b/src/pages/auth/login.tsx @@ -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'); diff --git a/src/pages/auth/logout.tsx b/src/pages/auth/logout.tsx index 8b9d42d..3a751c6 100644 --- a/src/pages/auth/logout.tsx +++ b/src/pages/auth/logout.tsx @@ -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 ; -} + const full_title = `${title} - Logout`; -Logout.title = 'Zipline - Logout'; + return ( + <> + + {full_title} + + + + + ); +} diff --git a/src/pages/auth/register.tsx b/src/pages/auth/register.tsx index 2830d85..161006b 100644 --- a/src/pages/auth/register.tsx +++ b/src/pages/auth/register.tsx @@ -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 ( <> - {title} + {full_title}
{ const { code } = context.query as { code: string }; - if (!config.features.invites && code) - return { - notFound: true, - }; - - if (!config.features.user_registration && !code) return { notFound: true }; + const { default: Logger } = await import('lib/logger'); + const logger = Logger.get('pages::register'); if (code) { + if (!config.features.invites) + return { + notFound: true, + }; + 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, diff --git a/src/pages/code/[id].tsx b/src/pages/code/[id].tsx index 843bc1c..e1a668a 100644 --- a/src/pages/code/[id].tsx +++ b/src/pages/code/[id].tsx @@ -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 }) { diff --git a/src/pages/dashboard/files.tsx b/src/pages/dashboard/files.tsx index 213c6d7..745a855 100644 --- a/src/pages/dashboard/files.tsx +++ b/src/pages/dashboard/files.tsx @@ -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'; diff --git a/src/pages/dashboard/index.tsx b/src/pages/dashboard/index.tsx index f631c75..47e43cb 100644 --- a/src/pages/dashboard/index.tsx +++ b/src/pages/dashboard/index.tsx @@ -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'; diff --git a/src/pages/dashboard/invites.tsx b/src/pages/dashboard/invites.tsx index f007dd5..297b0de 100644 --- a/src/pages/dashboard/invites.tsx +++ b/src/pages/dashboard/invites.tsx @@ -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) { diff --git a/src/pages/dashboard/manage.tsx b/src/pages/dashboard/manage.tsx index fdff0d0..531f646 100644 --- a/src/pages/dashboard/manage.tsx +++ b/src/pages/dashboard/manage.tsx @@ -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'; diff --git a/src/pages/dashboard/stats.tsx b/src/pages/dashboard/stats.tsx index 7c39b46..57fcad5 100644 --- a/src/pages/dashboard/stats.tsx +++ b/src/pages/dashboard/stats.tsx @@ -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'; diff --git a/src/pages/dashboard/upload.tsx b/src/pages/dashboard/upload/file.tsx similarity index 96% rename from src/pages/dashboard/upload.tsx rename to src/pages/dashboard/upload/file.tsx index 8b452db..e2f7f7c 100644 --- a/src/pages/dashboard/upload.tsx +++ b/src/pages/dashboard/upload/file.tsx @@ -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'; diff --git a/src/pages/dashboard/text.tsx b/src/pages/dashboard/upload/text.tsx similarity index 95% rename from src/pages/dashboard/text.tsx rename to src/pages/dashboard/upload/text.tsx index a3e2c2e..e68b96c 100644 --- a/src/pages/dashboard/text.tsx +++ b/src/pages/dashboard/upload/text.tsx @@ -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'; diff --git a/src/pages/dashboard/urls.tsx b/src/pages/dashboard/urls.tsx index 90e55a9..ccb5938 100644 --- a/src/pages/dashboard/urls.tsx +++ b/src/pages/dashboard/urls.tsx @@ -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'; diff --git a/src/pages/dashboard/users.tsx b/src/pages/dashboard/users.tsx index 74385ed..676b457 100644 --- a/src/pages/dashboard/users.tsx +++ b/src/pages/dashboard/users.tsx @@ -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'; diff --git a/src/pages/oauth_error.tsx b/src/pages/oauth_error.tsx new file mode 100644 index 0000000..359b59a --- /dev/null +++ b/src/pages/oauth_error.tsx @@ -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 ( + <> + + Authentication Error + + + + + Error while authenticating with {provider} + + {error} + + Redirecting to login in {remaining} second{remaining === 1 ? 's' : ''} + + + + + ); +} + +export const getServerSideProps: GetServerSideProps = async (ctx) => { + return { + props: { + error: ctx.query.error ?? 'Unknown', + provider: ctx.query.provider ?? 'Unknown', + }, + }; +}; diff --git a/src/pages/view/[id].tsx b/src/pages/view/[id].tsx index 44d2fb6..b035d37 100644 --- a/src/pages/view/[id].tsx +++ b/src/pages/view/[id].tsx @@ -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(); diff --git a/src/scripts/import-dir.ts b/src/scripts/import-dir.ts index 74eecac..d9501e6 100644 --- a/src/scripts/import-dir.ts +++ b/src/scripts/import-dir.ts @@ -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]; diff --git a/src/scripts/list-users.ts b/src/scripts/list-users.ts index 97bbd5d..603adb1 100644 --- a/src/scripts/list-users.ts +++ b/src/scripts/list-users.ts @@ -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() { diff --git a/src/scripts/set-user.ts b/src/scripts/set-user.ts index 338c867..8ec7104 100644 --- a/src/scripts/set-user.ts +++ b/src/scripts/set-user.ts @@ -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', diff --git a/src/server/index.ts b/src/server/index.ts index eef84ff..f7d3b0b 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -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( - '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( - '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.'); + logger + .error('Secret is not set!') + .error( + 'Running Zipline as is, without a randomized secret is not recommended and leaves your instance at risk!' + ) + .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.' + ) + .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); } diff --git a/src/server/util.ts b/src/server/util.ts index 9a761ac..c58be38 100644 --- a/src/server/util.ts +++ b/src/server/util.ts @@ -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); } }