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