mirror of
https://github.com/diced/zipline.git
synced 2025-12-10 23:00:46 -08:00
Compare commits
36 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
48cfa41405 | ||
|
|
9c26d64420 | ||
|
|
f3638f3d6d | ||
|
|
8e59158769 | ||
|
|
317c7365f8 | ||
|
|
974e9f7fa2 | ||
|
|
4330bdcc4c | ||
|
|
7f9de82804 | ||
|
|
70050afb5f | ||
|
|
1f00dd51f9 | ||
|
|
5e37d89b18 | ||
|
|
08d3bfb36d | ||
|
|
56f07cb5ec | ||
|
|
658cc61df0 | ||
|
|
d3be545548 | ||
|
|
c8625c1e13 | ||
|
|
511f17e1a5 | ||
|
|
5b88b59724 | ||
|
|
1816e13879 | ||
|
|
1a837c02d2 | ||
|
|
f3634eff48 | ||
|
|
23ef407dd3 | ||
|
|
f40803f515 | ||
|
|
6b97d30a69 | ||
|
|
bd8d4e33fd | ||
|
|
70d48dd8c3 | ||
|
|
2e0a5f1d9c | ||
|
|
0ab814fc11 | ||
|
|
265760fb9c | ||
|
|
76ff3817af | ||
|
|
0dfe3fdcd1 | ||
|
|
5a522e0375 | ||
|
|
b15390f26c | ||
|
|
6fef197620 | ||
|
|
1d0bb2fa4f | ||
|
|
abb5bb5f25 |
2
LICENSE
2
LICENSE
@@ -1,6 +1,6 @@
|
|||||||
MIT License
|
MIT License
|
||||||
|
|
||||||
Copyright (c) 2023 dicedtomato
|
Copyright (c) 2024 dicedtomato
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
|||||||
@@ -121,7 +121,7 @@ This section requires [Flameshot](https://www.flameshot.org/), [jq](https://sted
|
|||||||
|
|
||||||
If using wayland you will need to have [wl-clipboard](https://github.com/bugaevc/wl-clipboard) installed, for the `wl-copy` command.
|
If using wayland you will need to have [wl-clipboard](https://github.com/bugaevc/wl-clipboard) installed, for the `wl-copy` command.
|
||||||
|
|
||||||
If you are not using GNOME/KDE/Qtile/Sway, and are using something like a wlroots-based compositor (ex. [Hyprland](https://github.com/hyprwm/Hyprland/), [River](https://github.com/riverwm/river), etc), you will need to set the `XDG_CURRENT_DESKTOP` environment variable to `sway`, which will just override it for this script. Adding `export XDG_CURRENT_DESKTOP=sway` to the start of the script will work.
|
If you are not using GNOME/KDE/Qtile/Sway, and are using something like a wlroots-based or wlroots-compatible compositor (ex. [Hyprland](https://github.com/hyprwm/Hyprland/), [River](https://github.com/riverwm/river), etc), you will need to set the `XDG_CURRENT_DESKTOP` environment variable to `sway`, which will just override it for this script. Adding `export XDG_CURRENT_DESKTOP=sway` to the start of the script will work.
|
||||||
|
|
||||||
After this, replace the `xsel -ib` with `wl-copy` in the script.
|
After this, replace the `xsel -ib` with `wl-copy` in the script.
|
||||||
|
|
||||||
@@ -141,7 +141,7 @@ To upload files using flameshot we will use a script. Replace $TOKEN and $HOST w
|
|||||||
DATE=$(date '+%h_%Y_%d_%I_%m_%S.png');
|
DATE=$(date '+%h_%Y_%d_%I_%m_%S.png');
|
||||||
flameshot gui -r > ~/Pictures/$DATE;
|
flameshot gui -r > ~/Pictures/$DATE;
|
||||||
|
|
||||||
curl -H "Content-Type: multipart/form-data" -H "authorization: $TOKEN" -F file=@$1 $HOST/api/upload | jq -r 'files[0].url' | xsel -ib
|
curl -H "Content-Type: multipart/form-data" -H "authorization: $TOKEN" -F file=@$1 $HOST/api/upload | jq -r '.files[0]' | xsel -ib
|
||||||
```
|
```
|
||||||
|
|
||||||
# Contributing
|
# Contributing
|
||||||
@@ -169,4 +169,4 @@ Create a pull request on GitHub. If your PR does not pass the action checks, the
|
|||||||
|
|
||||||
# Documentation
|
# Documentation
|
||||||
|
|
||||||
Documentation source code is located in [diced/zipline-docs](https://github.com/diced/zipline-docs), and can be accessed [here](https://zipl.vercel.app).
|
Documentation source code is located in [diced/zipline-docs](https://github.com/diced/zipline-docs), and can be accessed [here](https://zipl.vercel.app).
|
||||||
|
|||||||
@@ -42,6 +42,9 @@
|
|||||||
["afm", ["application/octet-stream"]],
|
["afm", ["application/octet-stream"]],
|
||||||
["afp", ["application/vnd.ibm.modcap"]],
|
["afp", ["application/vnd.ibm.modcap"]],
|
||||||
["ahead", ["application/vnd.ahead.space"]],
|
["ahead", ["application/vnd.ahead.space"]],
|
||||||
|
["ahk", ["text/autohotkey"]],
|
||||||
|
["ahk1", ["text/autohotkey"]],
|
||||||
|
["ahk2", ["text/autohotkey"]],
|
||||||
["ai", ["application/postscript"]],
|
["ai", ["application/postscript"]],
|
||||||
["aif", ["audio/aiff"]],
|
["aif", ["audio/aiff"]],
|
||||||
["aifc", ["audio/aiff"]],
|
["aifc", ["audio/aiff"]],
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "zipline",
|
"name": "zipline",
|
||||||
"version": "3.7.7",
|
"version": "3.7.10",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "npm-run-all build:server dev:run",
|
"dev": "npm-run-all build:server dev:run",
|
||||||
|
|||||||
14
prisma/migrations/20240912180249_exports/migration.sql
Normal file
14
prisma/migrations/20240912180249_exports/migration.sql
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "Export" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
"complete" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
"path" TEXT NOT NULL,
|
||||||
|
"userId" INTEGER NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "Export_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "Export" ADD CONSTRAINT "Export_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
@@ -27,6 +27,20 @@ model User {
|
|||||||
Invite Invite[]
|
Invite Invite[]
|
||||||
Folder Folder[]
|
Folder Folder[]
|
||||||
IncompleteFile IncompleteFile[]
|
IncompleteFile IncompleteFile[]
|
||||||
|
Exports Export[]
|
||||||
|
}
|
||||||
|
|
||||||
|
model Export {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
complete Boolean @default(false)
|
||||||
|
|
||||||
|
path String
|
||||||
|
|
||||||
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
|
userId Int
|
||||||
}
|
}
|
||||||
|
|
||||||
model Folder {
|
model Folder {
|
||||||
|
|||||||
@@ -72,6 +72,9 @@ export default function File({
|
|||||||
},
|
},
|
||||||
transition: 'filter 0.2s ease-in-out',
|
transition: 'filter 0.2s ease-in-out',
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
}}
|
}}
|
||||||
shadow='md'
|
shadow='md'
|
||||||
onClick={() => setOpen(true)}
|
onClick={() => setOpen(true)}
|
||||||
|
|||||||
@@ -356,7 +356,7 @@ export default function Layout({ children, props }) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
variant='subtle'
|
variant='subtle'
|
||||||
color='gray'
|
color={theme.colorScheme === 'dark' ? 'dark' : 'gray'}
|
||||||
compact
|
compact
|
||||||
size='xl'
|
size='xl'
|
||||||
p='sm'
|
p='sm'
|
||||||
|
|||||||
@@ -4,6 +4,10 @@ import { useEffect } from 'react';
|
|||||||
import ayu_dark from 'lib/themes/ayu_dark';
|
import ayu_dark from 'lib/themes/ayu_dark';
|
||||||
import ayu_light from 'lib/themes/ayu_light';
|
import ayu_light from 'lib/themes/ayu_light';
|
||||||
import ayu_mirage from 'lib/themes/ayu_mirage';
|
import ayu_mirage from 'lib/themes/ayu_mirage';
|
||||||
|
import catppuccin_mocha from 'lib/themes/catppuccin_mocha';
|
||||||
|
import catppuccin_macchiato from 'lib/themes/catppuccin_macchiato';
|
||||||
|
import catppuccin_frappe from 'lib/themes/catppuccin_frappe';
|
||||||
|
import catppuccin_latte from 'lib/themes/catppuccin_latte';
|
||||||
import dark from 'lib/themes/dark';
|
import dark from 'lib/themes/dark';
|
||||||
import dark_blue from 'lib/themes/dark_blue';
|
import dark_blue from 'lib/themes/dark_blue';
|
||||||
import dracula from 'lib/themes/dracula';
|
import dracula from 'lib/themes/dracula';
|
||||||
@@ -32,6 +36,10 @@ export const themes = {
|
|||||||
ayu_dark,
|
ayu_dark,
|
||||||
ayu_mirage,
|
ayu_mirage,
|
||||||
ayu_light,
|
ayu_light,
|
||||||
|
catppuccin_mocha,
|
||||||
|
catppuccin_macchiato,
|
||||||
|
catppuccin_frappe,
|
||||||
|
catppuccin_latte,
|
||||||
nord,
|
nord,
|
||||||
dracula,
|
dracula,
|
||||||
matcha_dark_azul,
|
matcha_dark_azul,
|
||||||
@@ -46,6 +54,10 @@ export const friendlyThemeName = {
|
|||||||
ayu_dark: 'Ayu Dark',
|
ayu_dark: 'Ayu Dark',
|
||||||
ayu_mirage: 'Ayu Mirage',
|
ayu_mirage: 'Ayu Mirage',
|
||||||
ayu_light: 'Ayu Light',
|
ayu_light: 'Ayu Light',
|
||||||
|
catppuccin_mocha: 'Catppuccin Mocha',
|
||||||
|
catppuccin_macchiato: 'Catppuccin Macchiato',
|
||||||
|
catppuccin_frappe: 'Catppuccin Frappé',
|
||||||
|
catppuccin_latte: 'Catppuccin Latte',
|
||||||
nord: 'Nord',
|
nord: 'Nord',
|
||||||
dracula: 'Dracula',
|
dracula: 'Dracula',
|
||||||
matcha_dark_azul: 'Matcha Dark Azul',
|
matcha_dark_azul: 'Matcha Dark Azul',
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ function VideoThumbnailPlaceholder({ file, mediaPreview, ...props }) {
|
|||||||
return (
|
return (
|
||||||
<Box sx={{ position: 'relative' }}>
|
<Box sx={{ position: 'relative' }}>
|
||||||
<Image
|
<Image
|
||||||
src={file.thumbnail}
|
src={typeof file.thumbnail === 'string' ? file.thumbnail : `/r/${file.thumbnail.name}`}
|
||||||
sx={{
|
sx={{
|
||||||
width: '100%',
|
width: '100%',
|
||||||
height: 'auto',
|
height: 'auto',
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ import {
|
|||||||
IconUserExclamation,
|
IconUserExclamation,
|
||||||
IconUserMinus,
|
IconUserMinus,
|
||||||
IconUserX,
|
IconUserX,
|
||||||
|
IconX,
|
||||||
} from '@tabler/icons-react';
|
} from '@tabler/icons-react';
|
||||||
import AnchorNext from 'components/AnchorNext';
|
import AnchorNext from 'components/AnchorNext';
|
||||||
import { FlameshotIcon, ShareXIcon } from 'components/icons';
|
import { FlameshotIcon, ShareXIcon } from 'components/icons';
|
||||||
@@ -264,7 +265,7 @@ export default function Manage({ oauth_registration, oauth_providers: raw_oauth_
|
|||||||
setExports(
|
setExports(
|
||||||
res.exports
|
res.exports
|
||||||
?.map((s) => ({
|
?.map((s) => ({
|
||||||
date: new Date(Number(s.name.split('_')[3].slice(0, -4))),
|
date: new Date(s.createdAt),
|
||||||
size: s.size,
|
size: s.size,
|
||||||
full: s.name,
|
full: s.name,
|
||||||
}))
|
}))
|
||||||
@@ -272,6 +273,26 @@ export default function Manage({ oauth_registration, oauth_providers: raw_oauth_
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const deleteExport = async (name) => {
|
||||||
|
const res = await useFetch('/api/user/export?name=' + name, 'DELETE');
|
||||||
|
if (res.error) {
|
||||||
|
showNotification({
|
||||||
|
title: 'Error deleting export',
|
||||||
|
message: res.error,
|
||||||
|
color: 'red',
|
||||||
|
icon: <IconX size='1rem' />,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
showNotification({
|
||||||
|
message: 'Deleted export',
|
||||||
|
color: 'green',
|
||||||
|
icon: <IconFileZip size='1rem' />,
|
||||||
|
});
|
||||||
|
|
||||||
|
await getExports();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleDelete = async () => {
|
const handleDelete = async () => {
|
||||||
const res = await useFetch('/api/user/files', 'DELETE', {
|
const res = await useFetch('/api/user/files', 'DELETE', {
|
||||||
all: true,
|
all: true,
|
||||||
@@ -580,6 +601,7 @@ export default function Manage({ oauth_registration, oauth_providers: raw_oauth_
|
|||||||
{ id: 'name', name: 'Name' },
|
{ id: 'name', name: 'Name' },
|
||||||
{ id: 'date', name: 'Date' },
|
{ id: 'date', name: 'Date' },
|
||||||
{ id: 'size', name: 'Size' },
|
{ id: 'size', name: 'Size' },
|
||||||
|
{ id: 'actions', name: '' },
|
||||||
]}
|
]}
|
||||||
rows={
|
rows={
|
||||||
exports
|
exports
|
||||||
@@ -591,6 +613,11 @@ export default function Manage({ oauth_registration, oauth_providers: raw_oauth_
|
|||||||
),
|
),
|
||||||
date: x.date.toLocaleString(),
|
date: x.date.toLocaleString(),
|
||||||
size: bytesToHuman(x.size),
|
size: bytesToHuman(x.size),
|
||||||
|
actions: (
|
||||||
|
<ActionIcon onClick={() => deleteExport(x.full)}>
|
||||||
|
<IconTrash size='1rem' />
|
||||||
|
</ActionIcon>
|
||||||
|
),
|
||||||
}))
|
}))
|
||||||
: []
|
: []
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -364,7 +364,8 @@ export default function File({ chunks: chunks_config }) {
|
|||||||
<Button
|
<Button
|
||||||
leftIcon={<IconFileUpload size='1rem' />}
|
leftIcon={<IconFileUpload size='1rem' />}
|
||||||
onClick={handleUpload}
|
onClick={handleUpload}
|
||||||
disabled={files.length === 0 ? true : false}
|
loading={loading}
|
||||||
|
disabled={files.length === 0 || loading}
|
||||||
>
|
>
|
||||||
Upload
|
Upload
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ export default function Text() {
|
|||||||
|
|
||||||
const [value, setValue] = useState('');
|
const [value, setValue] = useState('');
|
||||||
const [lang, setLang] = useState('txt');
|
const [lang, setLang] = useState('txt');
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
const [options, setOpened, OptionsModal] = useUploadOptions();
|
const [options, setOpened, OptionsModal] = useUploadOptions();
|
||||||
|
|
||||||
@@ -29,6 +30,9 @@ export default function Text() {
|
|||||||
const shouldRenderTex = lang === 'tex';
|
const shouldRenderTex = lang === 'tex';
|
||||||
|
|
||||||
const handleUpload = async () => {
|
const handleUpload = async () => {
|
||||||
|
if (value.trim().length === 0) return;
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
const file = new File([value], 'text.' + lang);
|
const file = new File([value], 'text.' + lang);
|
||||||
|
|
||||||
const expiresAt = options.expires === 'never' ? null : expireReadToDate(options.expires);
|
const expiresAt = options.expires === 'never' ? null : expireReadToDate(options.expires);
|
||||||
@@ -53,6 +57,16 @@ export default function Text() {
|
|||||||
message: '',
|
message: '',
|
||||||
});
|
});
|
||||||
showFilesModal(clipboard, modals, json.files);
|
showFilesModal(clipboard, modals, json.files);
|
||||||
|
setLoading(false);
|
||||||
|
setValue('');
|
||||||
|
} else {
|
||||||
|
updateNotification({
|
||||||
|
id: 'upload-text',
|
||||||
|
title: 'Upload Failed',
|
||||||
|
message: json.error,
|
||||||
|
color: 'red',
|
||||||
|
});
|
||||||
|
setLoading(false);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -136,7 +150,8 @@ export default function Text() {
|
|||||||
<Button
|
<Button
|
||||||
leftIcon={<IconFileUpload size='1rem' />}
|
leftIcon={<IconFileUpload size='1rem' />}
|
||||||
onClick={handleUpload}
|
onClick={handleUpload}
|
||||||
disabled={value.trim().length === 0 ? true : false}
|
disabled={value.trim().length === 0 || loading}
|
||||||
|
loading={loading}
|
||||||
>
|
>
|
||||||
Upload
|
Upload
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ export function CreateUserModal({ open, setOpen, updateUsers }) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
const res = await useFetch('/api/auth/create', 'POST', data);
|
const res = await useFetch('/api/auth/register', 'POST', data);
|
||||||
if (res.error) {
|
if (res.error) {
|
||||||
showNotification({
|
showNotification({
|
||||||
title: 'Failed to create user',
|
title: 'Failed to create user',
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { Readable } from 'stream';
|
|||||||
export abstract class Datasource {
|
export abstract class Datasource {
|
||||||
public name: string;
|
public name: string;
|
||||||
|
|
||||||
public abstract save(file: string, data: Buffer): Promise<void>;
|
public abstract save(file: string, data: Buffer, options?: { type: string }): Promise<void>;
|
||||||
public abstract delete(file: string): Promise<void>;
|
public abstract delete(file: string): Promise<void>;
|
||||||
public abstract clear(): Promise<void>;
|
public abstract clear(): Promise<void>;
|
||||||
public abstract size(file: string): Promise<number>;
|
public abstract size(file: string): Promise<number>;
|
||||||
|
|||||||
@@ -20,8 +20,13 @@ export class S3 extends Datasource {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async save(file: string, data: Buffer): Promise<void> {
|
public async save(file: string, data: Buffer, options?: { type: string }): Promise<void> {
|
||||||
await this.s3.putObject(this.config.bucket, file, data);
|
await this.s3.putObject(
|
||||||
|
this.config.bucket,
|
||||||
|
file,
|
||||||
|
data,
|
||||||
|
options ? { 'Content-Type': options.type } : undefined,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async delete(file: string): Promise<void> {
|
public async delete(file: string): Promise<void> {
|
||||||
|
|||||||
39
src/lib/themes/catppuccin_frappe.ts
Normal file
39
src/lib/themes/catppuccin_frappe.ts
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
// https://github.com/SeaswimmerTheFsh
|
||||||
|
// https://catppuccin.com/palette
|
||||||
|
|
||||||
|
import createTheme from '.';
|
||||||
|
|
||||||
|
export default createTheme({
|
||||||
|
colorScheme: 'dark',
|
||||||
|
primaryColor: 'blue',
|
||||||
|
other: {
|
||||||
|
AppShell_backgroundColor: '#232634',
|
||||||
|
hover: '#414559',
|
||||||
|
},
|
||||||
|
colors: {
|
||||||
|
dark: [
|
||||||
|
'#c6d0f5',
|
||||||
|
'#949cbb',
|
||||||
|
'#838ba7',
|
||||||
|
'#737994',
|
||||||
|
'#626880',
|
||||||
|
'#51576d',
|
||||||
|
'#414559',
|
||||||
|
'#303446',
|
||||||
|
'#292c3c',
|
||||||
|
'#232634',
|
||||||
|
],
|
||||||
|
blue: [
|
||||||
|
'#FFFFFF',
|
||||||
|
'#b8caf4',
|
||||||
|
'#a2baf1',
|
||||||
|
'#7599ea',
|
||||||
|
'#5f89e7',
|
||||||
|
'#8c99ee',
|
||||||
|
'#8ca1ee',
|
||||||
|
'#8cb2ee',
|
||||||
|
'#8cbaee',
|
||||||
|
'#8caaee',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
39
src/lib/themes/catppuccin_latte.ts
Normal file
39
src/lib/themes/catppuccin_latte.ts
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
// https://github.com/SeaswimmerTheFsh
|
||||||
|
// https://catppuccin.com/palette
|
||||||
|
|
||||||
|
import createTheme from '.';
|
||||||
|
|
||||||
|
export default createTheme({
|
||||||
|
colorScheme: 'dark',
|
||||||
|
primaryColor: 'blue',
|
||||||
|
other: {
|
||||||
|
AppShell_backgroundColor: '#dce0e8',
|
||||||
|
hover: '#ccd0da',
|
||||||
|
},
|
||||||
|
colors: {
|
||||||
|
dark: [
|
||||||
|
'#4c4f69',
|
||||||
|
'#8c8fa1',
|
||||||
|
'#8c8fa1',
|
||||||
|
'#9ca0b0',
|
||||||
|
'#acb0be',
|
||||||
|
'#bcc0cc',
|
||||||
|
'#ccd0da',
|
||||||
|
'#eff1f5',
|
||||||
|
'#e6e9ef',
|
||||||
|
'#dce0e8',
|
||||||
|
],
|
||||||
|
blue: [
|
||||||
|
'#FFFFFF',
|
||||||
|
'#3676f6',
|
||||||
|
'#0a57ee',
|
||||||
|
'#094ed6',
|
||||||
|
'#1d42f5',
|
||||||
|
'#1d54f5',
|
||||||
|
'#1d65f5',
|
||||||
|
'#1d77f5',
|
||||||
|
'#1d89f5',
|
||||||
|
'#1e66f5',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
39
src/lib/themes/catppuccin_macchiato.ts
Normal file
39
src/lib/themes/catppuccin_macchiato.ts
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
// https://github.com/SeaswimmerTheFsh
|
||||||
|
// https://catppuccin.com/palette
|
||||||
|
|
||||||
|
import createTheme from '.';
|
||||||
|
|
||||||
|
export default createTheme({
|
||||||
|
colorScheme: 'dark',
|
||||||
|
primaryColor: 'blue',
|
||||||
|
other: {
|
||||||
|
AppShell_backgroundColor: '#181926',
|
||||||
|
hover: '#363a4f',
|
||||||
|
},
|
||||||
|
colors: {
|
||||||
|
dark: [
|
||||||
|
'#cad3f5',
|
||||||
|
'#8087a2',
|
||||||
|
'#8087a2',
|
||||||
|
'#6e738d',
|
||||||
|
'#5b6078',
|
||||||
|
'#494d64',
|
||||||
|
'#363a4f',
|
||||||
|
'#24273a',
|
||||||
|
'#1e2030',
|
||||||
|
'#181926',
|
||||||
|
],
|
||||||
|
blue: [
|
||||||
|
'#FFFFFF',
|
||||||
|
'#a1bdf6',
|
||||||
|
'#729cf1',
|
||||||
|
'#5b8cef',
|
||||||
|
'#899bf4',
|
||||||
|
'#89a4f4',
|
||||||
|
'#89acf4',
|
||||||
|
'#89b5f4',
|
||||||
|
'#89bef4',
|
||||||
|
'#8aadf4',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
39
src/lib/themes/catppuccin_mocha.ts
Normal file
39
src/lib/themes/catppuccin_mocha.ts
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
// https://github.com/SeaswimmerTheFsh
|
||||||
|
// https://catppuccin.com/palette
|
||||||
|
|
||||||
|
import createTheme from '.';
|
||||||
|
|
||||||
|
export default createTheme({
|
||||||
|
colorScheme: 'dark',
|
||||||
|
primaryColor: 'blue',
|
||||||
|
other: {
|
||||||
|
AppShell_backgroundColor: '#11111b',
|
||||||
|
hover: '#313244',
|
||||||
|
},
|
||||||
|
colors: {
|
||||||
|
dark: [
|
||||||
|
'#cdd6f4',
|
||||||
|
'#9399b2',
|
||||||
|
'#7f849c',
|
||||||
|
'#6c7086',
|
||||||
|
'#585b70',
|
||||||
|
'#45475a',
|
||||||
|
'#313244',
|
||||||
|
'#1e1e2e',
|
||||||
|
'#181825',
|
||||||
|
'#11111b',
|
||||||
|
],
|
||||||
|
blue: [
|
||||||
|
'#FFFFFF',
|
||||||
|
'#b9d3fc',
|
||||||
|
'#a1c3fb',
|
||||||
|
'#70a4f8',
|
||||||
|
'#5894f7',
|
||||||
|
'#89a1fa',
|
||||||
|
'#89aafa',
|
||||||
|
'#89b4fa',
|
||||||
|
'#89bdfa',
|
||||||
|
'#89c6fa',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -24,16 +24,16 @@ export function humanToBytes(value: string): number {
|
|||||||
return bytes;
|
return bytes;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function bytesToHuman(value: number): string {
|
export function bytesToHuman(value: number | bigint): string {
|
||||||
if (isNaN(value)) return '0.0 B';
|
if (typeof value !== 'bigint' && isNaN(value)) return '0.0 B';
|
||||||
if (value === Infinity) return '0.0 B';
|
if (value === Infinity) return '0.0 B';
|
||||||
const units = ['B', 'kB', 'MB', 'GB', 'TB', 'PB'];
|
const units = ['B', 'kB', 'MB', 'GB', 'TB', 'PB']; // if people upload stuff bigger than a petabyte then idk
|
||||||
let num = 0;
|
let num = 0;
|
||||||
|
|
||||||
while (value > 1024) {
|
while (value > 1024) {
|
||||||
value /= 1024;
|
value = Number(value) / 1024;
|
||||||
++num;
|
++num;
|
||||||
}
|
}
|
||||||
|
|
||||||
return `${value.toFixed(1)} ${units[num]}`;
|
return `${Number(value).toFixed(1)} ${units[num] || ''}`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,22 +51,22 @@ export function humanTime(string: StringValue | string): Date {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function parseExpiry(header: string): Date | null {
|
export function parseExpiry(header: string): Date {
|
||||||
if (!header) return null;
|
if (!header) throw new Error('no expiry provided');
|
||||||
header = header.toLowerCase();
|
header = header.toLowerCase();
|
||||||
|
|
||||||
if (header.startsWith('date=')) {
|
if (header.startsWith('date=')) {
|
||||||
const date = new Date(header.substring(5));
|
const date = new Date(header.substring(5));
|
||||||
|
|
||||||
if (!date.getTime()) return null;
|
if (!date.getTime()) throw new Error('invalid date');
|
||||||
if (date.getTime() < Date.now()) return null;
|
if (date.getTime() < Date.now()) throw new Error('expiry must be in the future');
|
||||||
return date;
|
return date;
|
||||||
}
|
}
|
||||||
|
|
||||||
const human = humanTime(header);
|
const human = humanTime(header);
|
||||||
|
|
||||||
if (!human) return null;
|
if (!human) throw new Error('failed to parse human time');
|
||||||
if (human.getTime() < Date.now()) return null;
|
if (human.getTime() < Date.now()) throw new Error('expiry must be in the future');
|
||||||
|
|
||||||
return human;
|
return human;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ export async function removeGPSData(image: File): Promise<void> {
|
|||||||
|
|
||||||
logger.debug(`reading file to upload to datasource: ${file} -> ${image.name}`);
|
logger.debug(`reading file to upload to datasource: ${file} -> ${image.name}`);
|
||||||
const buffer = await readFile(file);
|
const buffer = await readFile(file);
|
||||||
await datasource.save(image.name, buffer);
|
await datasource.save(image.name, buffer, { type: image.mimetype });
|
||||||
|
|
||||||
logger.debug(`removing temp file: ${file}`);
|
logger.debug(`removing temp file: ${file}`);
|
||||||
await rm(file);
|
await rm(file);
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import type { File, User, Url } from '@prisma/client';
|
import type { File, User, Url } from '@prisma/client';
|
||||||
import { bytesToHuman } from './bytes';
|
import { bytesToHuman } from './bytes';
|
||||||
|
import Logger from 'lib/logger';
|
||||||
|
|
||||||
export type ParseValue = {
|
export type ParseValue = {
|
||||||
file?: File;
|
file?: File;
|
||||||
@@ -10,6 +11,8 @@ export type ParseValue = {
|
|||||||
raw_link?: string;
|
raw_link?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const logger = Logger.get('parser');
|
||||||
|
|
||||||
export function parseString(str: string, value: ParseValue) {
|
export function parseString(str: string, value: ParseValue) {
|
||||||
if (!str) return null;
|
if (!str) return null;
|
||||||
str = str
|
str = str
|
||||||
@@ -17,7 +20,7 @@ export function parseString(str: string, value: ParseValue) {
|
|||||||
.replace(/\{raw_link\}/gi, value.raw_link)
|
.replace(/\{raw_link\}/gi, value.raw_link)
|
||||||
.replace(/\\n/g, '\n');
|
.replace(/\\n/g, '\n');
|
||||||
|
|
||||||
const re = /\{(?<type>file|url|user)\.(?<prop>\w+)(::(?<mod>\w+))?\}/gi;
|
const re = /\{(?<type>file|url|user)\.(?<prop>\w+)(::(?<mod>\w+))?(::(?<mod_tzlocale>\S+))?\}/gi;
|
||||||
let matches: RegExpMatchArray;
|
let matches: RegExpMatchArray;
|
||||||
|
|
||||||
while ((matches = re.exec(str))) {
|
while ((matches = re.exec(str))) {
|
||||||
@@ -54,7 +57,12 @@ export function parseString(str: string, value: ParseValue) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (matches.groups.mod) {
|
if (matches.groups.mod) {
|
||||||
str = replaceCharsFromString(str, modifier(matches.groups.mod, v), matches.index, re.lastIndex);
|
str = replaceCharsFromString(
|
||||||
|
str,
|
||||||
|
modifier(matches.groups.mod, v, matches.groups.mod_tzlocale ?? undefined),
|
||||||
|
matches.index,
|
||||||
|
re.lastIndex,
|
||||||
|
);
|
||||||
re.lastIndex = matches.index;
|
re.lastIndex = matches.index;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -66,17 +74,42 @@ export function parseString(str: string, value: ParseValue) {
|
|||||||
return str;
|
return str;
|
||||||
}
|
}
|
||||||
|
|
||||||
function modifier(mod: string, value: unknown): string {
|
function modifier(mod: string, value: unknown, tzlocale?: string): string {
|
||||||
mod = mod.toLowerCase();
|
mod = mod.toLowerCase();
|
||||||
|
|
||||||
if (value instanceof Date) {
|
if (value instanceof Date) {
|
||||||
|
const args = [undefined, undefined];
|
||||||
|
|
||||||
|
if (tzlocale) {
|
||||||
|
const [locale, tz] = tzlocale.split(/\s?,\s?/).map((v) => v.trim());
|
||||||
|
|
||||||
|
if (locale) {
|
||||||
|
try {
|
||||||
|
Intl.DateTimeFormat.supportedLocalesOf(locale);
|
||||||
|
args[0] = locale;
|
||||||
|
} catch (e) {
|
||||||
|
args[0] = undefined;
|
||||||
|
logger.error(`invalid locale provided ${locale}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tz) {
|
||||||
|
const intlTz = Intl.supportedValuesOf('timeZone').find((v) => v.toLowerCase() === tz.toLowerCase());
|
||||||
|
if (intlTz) args[1] = { timeZone: intlTz };
|
||||||
|
else {
|
||||||
|
args[1] = undefined;
|
||||||
|
logger.error(`invalid timezone provided ${tz}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
switch (mod) {
|
switch (mod) {
|
||||||
case 'locale':
|
case 'locale':
|
||||||
return value.toLocaleString();
|
return value.toLocaleString(...args);
|
||||||
case 'time':
|
case 'time':
|
||||||
return value.toLocaleTimeString();
|
return value.toLocaleTimeString(...args);
|
||||||
case 'date':
|
case 'date':
|
||||||
return value.toLocaleDateString();
|
return value.toLocaleDateString(...args);
|
||||||
case 'unix':
|
case 'unix':
|
||||||
return Math.floor(value.getTime() / 1000).toString();
|
return Math.floor(value.getTime() / 1000).toString();
|
||||||
case 'iso':
|
case 'iso':
|
||||||
@@ -95,6 +128,10 @@ function modifier(mod: string, value: unknown): string {
|
|||||||
return value.getMinutes().toString();
|
return value.getMinutes().toString();
|
||||||
case 'second':
|
case 'second':
|
||||||
return value.getSeconds().toString();
|
return value.getSeconds().toString();
|
||||||
|
case 'ampm':
|
||||||
|
return value.getHours() < 12 ? 'am' : 'pm';
|
||||||
|
case 'AMPM':
|
||||||
|
return value.getHours() < 12 ? 'AM' : 'PM';
|
||||||
default:
|
default:
|
||||||
return '{unknown_date_modifier}';
|
return '{unknown_date_modifier}';
|
||||||
}
|
}
|
||||||
@@ -117,7 +154,7 @@ function modifier(mod: string, value: unknown): string {
|
|||||||
default:
|
default:
|
||||||
return '{unknown_str_modifier}';
|
return '{unknown_str_modifier}';
|
||||||
}
|
}
|
||||||
} else if (typeof value === 'number') {
|
} else if (typeof value === 'number' || typeof value === 'bigint') {
|
||||||
switch (mod) {
|
switch (mod) {
|
||||||
case 'comma':
|
case 'comma':
|
||||||
return value.toLocaleString();
|
return value.toLocaleString();
|
||||||
|
|||||||
@@ -1,132 +0,0 @@
|
|||||||
import { readFile } from 'fs/promises';
|
|
||||||
import config from 'lib/config';
|
|
||||||
import Logger from 'lib/logger';
|
|
||||||
import { NextApiReq, NextApiRes, withZipline } from 'lib/middleware/withZipline';
|
|
||||||
import { guess } from 'lib/mimes';
|
|
||||||
import prisma from 'lib/prisma';
|
|
||||||
import { createToken, hashPassword } from 'lib/util';
|
|
||||||
import { jsonUserReplacer } from 'lib/utils/client';
|
|
||||||
import { extname } from 'path';
|
|
||||||
|
|
||||||
const logger = Logger.get('user');
|
|
||||||
|
|
||||||
async function handler(req: NextApiReq, res: NextApiRes) {
|
|
||||||
// handle invites
|
|
||||||
if (req.body.code) {
|
|
||||||
if (!config.features.invites) return res.badRequest('invites are disabled');
|
|
||||||
|
|
||||||
const { code, username, password } = req.body as {
|
|
||||||
code?: string;
|
|
||||||
username: string;
|
|
||||||
password: string;
|
|
||||||
};
|
|
||||||
const invite = await prisma.invite.findUnique({
|
|
||||||
where: { code: code ?? '' },
|
|
||||||
});
|
|
||||||
if (!invite && code) return res.badRequest('invalid invite code');
|
|
||||||
|
|
||||||
const user = await prisma.user.findFirst({
|
|
||||||
where: { username },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (user) return res.badRequest('username already exists');
|
|
||||||
const hashed = await hashPassword(password);
|
|
||||||
|
|
||||||
let avatar;
|
|
||||||
if (config.features.default_avatar) {
|
|
||||||
logger.debug(`using default avatar ${config.features.default_avatar}`);
|
|
||||||
|
|
||||||
const buf = await readFile(config.features.default_avatar);
|
|
||||||
const mimetype = await guess(extname(config.features.default_avatar));
|
|
||||||
logger.debug(`guessed mimetype ${mimetype} for ${config.features.default_avatar}`);
|
|
||||||
|
|
||||||
avatar = `data:${mimetype};base64,${buf.toString('base64')}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const newUser = await prisma.user.create({
|
|
||||||
data: {
|
|
||||||
password: hashed,
|
|
||||||
username,
|
|
||||||
token: createToken(),
|
|
||||||
administrator: false,
|
|
||||||
avatar,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (code) {
|
|
||||||
await prisma.invite.update({
|
|
||||||
where: {
|
|
||||||
code,
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
used: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.debug(`created user via invite ${code} ${JSON.stringify(newUser, jsonUserReplacer)}`);
|
|
||||||
|
|
||||||
logger.info(
|
|
||||||
`Created user ${newUser.username} (${newUser.id}) ${
|
|
||||||
code ? `from invite code ${code}` : 'via registration'
|
|
||||||
}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
return res.json({ success: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
const user = await req.user();
|
|
||||||
if (!user) return res.unauthorized('not logged in');
|
|
||||||
if (!user.administrator) return res.forbidden('you arent an administrator');
|
|
||||||
|
|
||||||
const { username, password, administrator } = req.body as {
|
|
||||||
username: string;
|
|
||||||
password: string;
|
|
||||||
administrator: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!username) return res.badRequest('no username');
|
|
||||||
if (!password) return res.badRequest('no password');
|
|
||||||
|
|
||||||
const existing = await prisma.user.findFirst({
|
|
||||||
where: {
|
|
||||||
username,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
if (existing) return res.badRequest('user exists');
|
|
||||||
|
|
||||||
const hashed = await hashPassword(password);
|
|
||||||
|
|
||||||
let avatar;
|
|
||||||
if (config.features.default_avatar) {
|
|
||||||
logger.debug(`using default avatar ${config.features.default_avatar}`);
|
|
||||||
|
|
||||||
const buf = await readFile(config.features.default_avatar);
|
|
||||||
const mimetype = await guess(extname(config.features.default_avatar));
|
|
||||||
logger.debug(`guessed mimetype ${mimetype} for ${config.features.default_avatar}`);
|
|
||||||
|
|
||||||
avatar = `data:${mimetype};base64,${buf.toString('base64')}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const newUser = await prisma.user.create({
|
|
||||||
data: {
|
|
||||||
password: hashed,
|
|
||||||
username,
|
|
||||||
token: createToken(),
|
|
||||||
administrator,
|
|
||||||
avatar,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
logger.debug(`created user ${JSON.stringify(newUser, jsonUserReplacer)}`);
|
|
||||||
|
|
||||||
delete newUser.password;
|
|
||||||
|
|
||||||
logger.info(`Created user ${newUser.username} (${newUser.id})`);
|
|
||||||
|
|
||||||
return res.json(newUser);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default withZipline(handler, {
|
|
||||||
methods: ['POST'],
|
|
||||||
});
|
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { PrismaClientKnownRequestError } from '@prisma/client/runtime/library';
|
||||||
import config from 'lib/config';
|
import config from 'lib/config';
|
||||||
import Logger from 'lib/logger';
|
import Logger from 'lib/logger';
|
||||||
import { NextApiReq, NextApiRes, UserExtended, withZipline } from 'lib/middleware/withZipline';
|
import { NextApiReq, NextApiRes, UserExtended, withZipline } from 'lib/middleware/withZipline';
|
||||||
@@ -16,8 +17,12 @@ async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) {
|
|||||||
count: number;
|
count: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
const expiry = parseExpiry(expiresAt);
|
let expiry: Date;
|
||||||
if (!expiry) return res.badRequest('invalid date');
|
try {
|
||||||
|
expiry = parseExpiry(expiresAt);
|
||||||
|
} catch (error) {
|
||||||
|
return res.badRequest(error.message);
|
||||||
|
}
|
||||||
const counts = count ? count : 1;
|
const counts = count ? count : 1;
|
||||||
|
|
||||||
if (counts > 1) {
|
if (counts > 1) {
|
||||||
@@ -60,19 +65,22 @@ async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) {
|
|||||||
const { code } = req.query as { code: string };
|
const { code } = req.query as { code: string };
|
||||||
if (!code) return res.badRequest('no code');
|
if (!code) return res.badRequest('no code');
|
||||||
|
|
||||||
const invite = await prisma.invite.delete({
|
try {
|
||||||
where: {
|
const invite = await prisma.invite.delete({
|
||||||
code,
|
where: {
|
||||||
},
|
code,
|
||||||
});
|
},
|
||||||
|
});
|
||||||
|
|
||||||
if (!invite) return res.notFound('invite not found');
|
logger.debug(`deleted invite ${JSON.stringify(invite)}`);
|
||||||
|
|
||||||
logger.debug(`deleted invite ${JSON.stringify(invite)}`);
|
logger.info(`${user.username} (${user.id}) deleted invite ${invite.code}`);
|
||||||
|
|
||||||
logger.info(`${user.username} (${user.id}) deleted invite ${invite.code}`);
|
return res.json(invite);
|
||||||
|
} catch (error) {
|
||||||
return res.json(invite);
|
if (error instanceof PrismaClientKnownRequestError) return res.notFound('invite not found');
|
||||||
|
else throw error;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
const invites = await prisma.invite.findMany({
|
const invites = await prisma.invite.findMany({
|
||||||
orderBy: {
|
orderBy: {
|
||||||
|
|||||||
@@ -14,8 +14,8 @@ async function handler(req: NextApiReq, res: NextApiRes) {
|
|||||||
code?: string;
|
code?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const users = await prisma.user.findMany();
|
const users = await prisma.user.count();
|
||||||
if (users.length === 0) {
|
if (users === 0) {
|
||||||
logger.debug('no users found... creating default user...');
|
logger.debug('no users found... creating default user...');
|
||||||
await prisma.user.create({
|
await prisma.user.create({
|
||||||
data: {
|
data: {
|
||||||
|
|||||||
@@ -11,23 +11,49 @@ import { extname } from 'path';
|
|||||||
const logger = Logger.get('user');
|
const logger = Logger.get('user');
|
||||||
|
|
||||||
async function handler(req: NextApiReq, res: NextApiRes) {
|
async function handler(req: NextApiReq, res: NextApiRes) {
|
||||||
if (!config.features.user_registration) return res.badRequest('user registration is disabled');
|
const user = await req.user();
|
||||||
|
let badRequest,
|
||||||
|
usedInvite = false;
|
||||||
|
|
||||||
const { username, password, administrator } = req.body as {
|
if (!config.features.user_registration && !config.features.invites && !user?.administrator)
|
||||||
|
return res.badRequest('This endpoint is unavailable due to current configurations');
|
||||||
|
else if (!!user && !user?.administrator) return res.badRequest('Already logged in');
|
||||||
|
|
||||||
|
const { username, password, administrator, code } = req.body as {
|
||||||
username: string;
|
username: string;
|
||||||
password: string;
|
password: string;
|
||||||
administrator: boolean;
|
administrator: boolean;
|
||||||
|
code?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!username) return res.badRequest('no username');
|
if (!username) badRequest = true;
|
||||||
if (!password) return res.badRequest('no password');
|
if (!password) badRequest = true;
|
||||||
|
|
||||||
const existing = await prisma.user.findFirst({
|
const existing = await prisma.user.findFirst({
|
||||||
where: {
|
where: {
|
||||||
username,
|
username,
|
||||||
},
|
},
|
||||||
|
select: {
|
||||||
|
username: true,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
if (existing) return res.badRequest('user exists');
|
|
||||||
|
if (existing) badRequest = true;
|
||||||
|
|
||||||
|
if (badRequest) return res.badRequest('Bad Username/Password');
|
||||||
|
|
||||||
|
if (code) {
|
||||||
|
if (config.features.invites) {
|
||||||
|
const invite = await prisma.invite.findUnique({
|
||||||
|
where: {
|
||||||
|
code,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!invite || invite?.used) return res.badRequest('Bad invite');
|
||||||
|
usedInvite = true;
|
||||||
|
} else return res.badRequest('Bad Username/Password');
|
||||||
|
} else if (config.features.invites && !user?.administrator) return res.badRequest('Bad invite');
|
||||||
|
|
||||||
const hashed = await hashPassword(password);
|
const hashed = await hashPassword(password);
|
||||||
|
|
||||||
@@ -47,12 +73,20 @@ async function handler(req: NextApiReq, res: NextApiRes) {
|
|||||||
password: hashed,
|
password: hashed,
|
||||||
username,
|
username,
|
||||||
token: createToken(),
|
token: createToken(),
|
||||||
administrator,
|
administrator: user?.superAdmin ? administrator : false,
|
||||||
avatar,
|
avatar,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
logger.debug(`registered user ${JSON.stringify(newUser, jsonUserReplacer)}`);
|
if (usedInvite)
|
||||||
|
await prisma.invite.update({
|
||||||
|
where: { code },
|
||||||
|
data: { used: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.debug(
|
||||||
|
`registered user${usedInvite ? ' via invite ' + code : ''} ${JSON.stringify(newUser, jsonUserReplacer)}`,
|
||||||
|
);
|
||||||
|
|
||||||
delete newUser.password;
|
delete newUser.password;
|
||||||
|
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ async function handler(req: NextApiReq, res: NextApiRes) {
|
|||||||
|
|
||||||
logger.debug(`shortened ${JSON.stringify(url)}`);
|
logger.debug(`shortened ${JSON.stringify(url)}`);
|
||||||
|
|
||||||
logger.info(`User ${user.username} (${user.id}) shortenned a url ${url.destination} (${url.id})`);
|
logger.info(`User ${user.username} (${user.id}) shortened a url ${url.destination} (${url.id})`);
|
||||||
|
|
||||||
let domain;
|
let domain;
|
||||||
if (req.headers['override-domain']) {
|
if (req.headers['override-domain']) {
|
||||||
|
|||||||
@@ -30,6 +30,45 @@ async function handler(req: NextApiReq, res: NextApiRes) {
|
|||||||
|
|
||||||
if (!user) return res.forbidden('authorization incorrect');
|
if (!user) return res.forbidden('authorization incorrect');
|
||||||
|
|
||||||
|
if (user.ratelimit && !req.headers['content-range']) {
|
||||||
|
const remaining = user.ratelimit.getTime() - Date.now();
|
||||||
|
logger.debug(`${user.id} encountered ratelimit, ${remaining}ms remaining`);
|
||||||
|
if (remaining <= 0) {
|
||||||
|
await prisma.user.update({
|
||||||
|
where: {
|
||||||
|
id: user.id,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
ratelimit: null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return res.ratelimited(remaining);
|
||||||
|
}
|
||||||
|
} else if (!user.ratelimit && !req.headers['content-range']) {
|
||||||
|
if (user.administrator && zconfig.ratelimit.admin > 0) {
|
||||||
|
await prisma.user.update({
|
||||||
|
where: {
|
||||||
|
id: user.id,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
ratelimit: new Date(Date.now() + zconfig.ratelimit.admin * 1000),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else if (!user.administrator && zconfig.ratelimit.user > 0) {
|
||||||
|
if (user.administrator && zconfig.ratelimit.user > 0) {
|
||||||
|
await prisma.user.update({
|
||||||
|
where: {
|
||||||
|
id: user.id,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
ratelimit: new Date(Date.now() + zconfig.ratelimit.user * 1000),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
await new Promise((resolve, reject) => {
|
await new Promise((resolve, reject) => {
|
||||||
uploader.array('file')(req as never, res as never, (result: unknown) => {
|
uploader.array('file')(req as never, res as never, (result: unknown) => {
|
||||||
if (result instanceof Error) reject(result.message);
|
if (result instanceof Error) reject(result.message);
|
||||||
@@ -42,6 +81,7 @@ async function handler(req: NextApiReq, res: NextApiRes) {
|
|||||||
expiresAt?: Date;
|
expiresAt?: Date;
|
||||||
removed_gps?: boolean;
|
removed_gps?: boolean;
|
||||||
assumed_mimetype?: string | boolean;
|
assumed_mimetype?: string | boolean;
|
||||||
|
folder?: number;
|
||||||
} = {
|
} = {
|
||||||
files: [],
|
files: [],
|
||||||
};
|
};
|
||||||
@@ -49,16 +89,20 @@ async function handler(req: NextApiReq, res: NextApiRes) {
|
|||||||
let expiry: Date;
|
let expiry: Date;
|
||||||
|
|
||||||
if (expiresAt) {
|
if (expiresAt) {
|
||||||
expiry = parseExpiry(expiresAt);
|
try {
|
||||||
if (!expiry) return res.badRequest('invalid date');
|
expiry = parseExpiry(expiresAt);
|
||||||
else {
|
|
||||||
response.expiresAt = expiry;
|
response.expiresAt = expiry;
|
||||||
|
} catch (error) {
|
||||||
|
return res.badRequest(error.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (zconfig.uploader.default_expiration) {
|
if (zconfig.uploader.default_expiration) {
|
||||||
expiry = parseExpiry(zconfig.uploader.default_expiration);
|
try {
|
||||||
if (!expiry) return res.badRequest('invalid date (UPLOADER_DEFAULT_EXPIRATION)');
|
expiry = parseExpiry(zconfig.uploader.default_expiration);
|
||||||
|
} catch (error) {
|
||||||
|
return res.badRequest(`${error.message} (UPLOADER_DEFAULT_EXPIRATION)`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const rawFormat = ((req.headers['format'] as string) || zconfig.uploader.default_format).toLowerCase();
|
const rawFormat = ((req.headers['format'] as string) || zconfig.uploader.default_format).toLowerCase();
|
||||||
@@ -78,6 +122,20 @@ async function handler(req: NextApiReq, res: NextApiRes) {
|
|||||||
if (isNaN(fileMaxViews)) return res.badRequest('invalid max views (invalid number)');
|
if (isNaN(fileMaxViews)) return res.badRequest('invalid max views (invalid number)');
|
||||||
if (fileMaxViews < 0) return res.badRequest('invalid max views (max views < 0)');
|
if (fileMaxViews < 0) return res.badRequest('invalid max views (max views < 0)');
|
||||||
|
|
||||||
|
const folderToAdd = req.headers['x-zipline-folder'] ? Number(req.headers['x-zipline-folder']) : null;
|
||||||
|
if (folderToAdd) {
|
||||||
|
if (isNaN(folderToAdd)) return res.badRequest('invalid folder id (invalid number)');
|
||||||
|
const folder = await prisma.folder.findFirst({
|
||||||
|
where: {
|
||||||
|
id: folderToAdd,
|
||||||
|
userId: user.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (!folder) return res.badRequest('invalid folder id (no folder found)');
|
||||||
|
|
||||||
|
response.folder = folder.id;
|
||||||
|
}
|
||||||
|
|
||||||
// handle partial uploads before ratelimits
|
// handle partial uploads before ratelimits
|
||||||
if (req.headers['content-range'] && zconfig.chunks.enabled) {
|
if (req.headers['content-range'] && zconfig.chunks.enabled) {
|
||||||
if (format === 'name') {
|
if (format === 'name') {
|
||||||
@@ -128,6 +186,9 @@ async function handler(req: NextApiReq, res: NextApiRes) {
|
|||||||
mimetype: req.headers.uploadtext ? 'text/plain' : mimetype,
|
mimetype: req.headers.uploadtext ? 'text/plain' : mimetype,
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
originalName: req.headers['original-name'] ? filename ?? null : null,
|
originalName: req.headers['original-name'] ? filename ?? null : null,
|
||||||
|
...(folderToAdd && {
|
||||||
|
folderId: folderToAdd,
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -175,23 +236,6 @@ async function handler(req: NextApiReq, res: NextApiRes) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (user.ratelimit) {
|
|
||||||
const remaining = user.ratelimit.getTime() - Date.now();
|
|
||||||
logger.debug(`${user.id} encountered ratelimit, ${remaining}ms remaining`);
|
|
||||||
if (remaining <= 0) {
|
|
||||||
await prisma.user.update({
|
|
||||||
where: {
|
|
||||||
id: user.id,
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
ratelimit: null,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
return res.ratelimited(remaining);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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');
|
||||||
|
|
||||||
@@ -262,6 +306,9 @@ async function handler(req: NextApiReq, res: NextApiRes) {
|
|||||||
maxViews: fileMaxViews,
|
maxViews: fileMaxViews,
|
||||||
originalName: req.headers['original-name'] ? decodedName ?? null : null,
|
originalName: req.headers['original-name'] ? decodedName ?? null : null,
|
||||||
size: file.size,
|
size: file.size,
|
||||||
|
...(folderToAdd && {
|
||||||
|
folderId: folderToAdd,
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -270,12 +317,12 @@ 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(fileUpload.name, buffer);
|
await datasource.save(fileUpload.name, buffer, { type: 'image/jpeg' });
|
||||||
logger.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(fileUpload.name, file.buffer);
|
await datasource.save(fileUpload.name, file.buffer, { type: file.mimetype });
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info(`User ${user.username} (${user.id}) uploaded ${fileUpload.name} (${fileUpload.id})`);
|
logger.info(`User ${user.username} (${user.id}) uploaded ${fileUpload.name} (${fileUpload.id})`);
|
||||||
@@ -315,28 +362,6 @@ async function handler(req: NextApiReq, res: NextApiRes) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (user.administrator && zconfig.ratelimit.admin > 0) {
|
|
||||||
await prisma.user.update({
|
|
||||||
where: {
|
|
||||||
id: user.id,
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
ratelimit: new Date(Date.now() + zconfig.ratelimit.admin * 1000),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
} else if (!user.administrator && zconfig.ratelimit.user > 0) {
|
|
||||||
if (user.administrator && zconfig.ratelimit.user > 0) {
|
|
||||||
await prisma.user.update({
|
|
||||||
where: {
|
|
||||||
id: user.id,
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
ratelimit: new Date(Date.now() + zconfig.ratelimit.user * 1000),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (req.headers['no-json']) {
|
if (req.headers['no-json']) {
|
||||||
res.setHeader('Content-Type', 'text/plain');
|
res.setHeader('Content-Type', 'text/plain');
|
||||||
return res.end(response.files.join(','));
|
return res.end(response.files.join(','));
|
||||||
|
|||||||
@@ -21,6 +21,9 @@ async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) {
|
|||||||
include: {
|
include: {
|
||||||
thumbnail: true,
|
thumbnail: true,
|
||||||
},
|
},
|
||||||
|
orderBy: {
|
||||||
|
createdAt: 'desc',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
Folder: true,
|
Folder: true,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Zip, ZipPassThrough } from 'fflate';
|
import { Zip, ZipPassThrough } from 'fflate';
|
||||||
import { createReadStream, createWriteStream } from 'fs';
|
import { createReadStream, createWriteStream } from 'fs';
|
||||||
import { readdir, stat } from 'fs/promises';
|
import { rm, stat } from 'fs/promises';
|
||||||
import datasource from 'lib/datasource';
|
import datasource from 'lib/datasource';
|
||||||
import Logger from 'lib/logger';
|
import Logger from 'lib/logger';
|
||||||
import prisma from 'lib/prisma';
|
import prisma from 'lib/prisma';
|
||||||
@@ -23,6 +23,13 @@ async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) {
|
|||||||
const export_name = `zipline_export_${user.id}_${Date.now()}.zip`;
|
const export_name = `zipline_export_${user.id}_${Date.now()}.zip`;
|
||||||
const path = join(config.core.temp_directory, export_name);
|
const path = join(config.core.temp_directory, export_name);
|
||||||
|
|
||||||
|
const exportDb = await prisma.export.create({
|
||||||
|
data: {
|
||||||
|
path: export_name,
|
||||||
|
userId: user.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
logger.debug(`creating write stream at ${path}`);
|
logger.debug(`creating write stream at ${path}`);
|
||||||
const write_stream = createWriteStream(path);
|
const write_stream = createWriteStream(path);
|
||||||
|
|
||||||
@@ -79,11 +86,27 @@ async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) {
|
|||||||
logger.info(
|
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}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
await prisma.export.update({
|
||||||
|
where: {
|
||||||
|
id: exportDb.id,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
complete: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
write_stream.close();
|
write_stream.close();
|
||||||
logger.debug(`error while writing to zip: ${err}`);
|
logger.error(
|
||||||
logger.error(`Export for ${user.username} (${user.id}) has failed\n${err}`);
|
`Export for ${user.username} (${user.id}) has failed and has been removed from the database\n${err}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
await prisma.export.delete({
|
||||||
|
where: {
|
||||||
|
id: exportDb.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -114,27 +137,62 @@ async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) {
|
|||||||
res.json({
|
res.json({
|
||||||
url: '/api/user/export?name=' + export_name,
|
url: '/api/user/export?name=' + export_name,
|
||||||
});
|
});
|
||||||
} else {
|
} else if (req.method === 'DELETE') {
|
||||||
const export_name = req.query.name as string;
|
const name = req.query.name as string;
|
||||||
if (export_name) {
|
if (!name) return res.badRequest('no name provided');
|
||||||
const parts = export_name.split('_');
|
|
||||||
if (Number(parts[2]) !== user.id) return res.unauthorized('cannot access export owned by another user');
|
|
||||||
|
|
||||||
const stream = createReadStream(join(config.core.temp_directory, export_name));
|
const exportDb = await prisma.export.findFirst({
|
||||||
|
where: {
|
||||||
|
userId: user.id,
|
||||||
|
path: name,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!exportDb) return res.notFound('export not found');
|
||||||
|
|
||||||
|
await prisma.export.delete({
|
||||||
|
where: {
|
||||||
|
id: exportDb.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await rm(join(config.core.temp_directory, exportDb.path));
|
||||||
|
} catch (e) {
|
||||||
|
logger
|
||||||
|
.error(`export file ${exportDb.path} has been removed from the database`)
|
||||||
|
.error(`but failed to remove the file from the filesystem: ${e}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const exportsDb = await prisma.export.findMany({
|
||||||
|
where: {
|
||||||
|
userId: user.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const name = req.query.name as string;
|
||||||
|
if (name) {
|
||||||
|
const exportDb = exportsDb.find((e) => e.path === name);
|
||||||
|
if (!exportDb) return res.notFound('export not found');
|
||||||
|
|
||||||
|
const stream = createReadStream(join(config.core.temp_directory, exportDb.path));
|
||||||
|
|
||||||
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="${exportDb.path}"`);
|
||||||
stream.pipe(res);
|
stream.pipe(res);
|
||||||
} else {
|
} else {
|
||||||
const files = await readdir(config.core.temp_directory);
|
|
||||||
const exp = files.filter((f) => f.startsWith('zipline_export_'));
|
|
||||||
const exports = [];
|
const exports = [];
|
||||||
for (let i = 0; i !== exp.length; ++i) {
|
|
||||||
const name = exp[i];
|
|
||||||
const stats = await stat(join(config.core.temp_directory, name));
|
|
||||||
|
|
||||||
if (Number(exp[i].split('_')[2]) !== user.id) continue;
|
for (let i = 0; i !== exportsDb.length; ++i) {
|
||||||
exports.push({ name, size: stats.size });
|
const exportDb = exportsDb[i];
|
||||||
|
if (!exportDb.complete) continue;
|
||||||
|
|
||||||
|
const stats = await stat(join(config.core.temp_directory, exportDb.path));
|
||||||
|
exports.push({ name: exportDb.path, size: stats.size, createdAt: exportDb.createdAt });
|
||||||
}
|
}
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
@@ -145,6 +203,6 @@ async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default withZipline(handler, {
|
export default withZipline(handler, {
|
||||||
methods: ['GET', 'POST'],
|
methods: ['GET', 'POST', 'DELETE'],
|
||||||
user: true,
|
user: true,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -16,7 +16,13 @@ async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) {
|
|||||||
id: idParsed,
|
id: idParsed,
|
||||||
},
|
},
|
||||||
select: {
|
select: {
|
||||||
files: !!req.query.files,
|
files: req.query.files
|
||||||
|
? {
|
||||||
|
include: {
|
||||||
|
thumbnail: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: false,
|
||||||
id: true,
|
id: true,
|
||||||
name: true,
|
name: true,
|
||||||
userId: true,
|
userId: true,
|
||||||
@@ -70,7 +76,13 @@ async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
select: {
|
select: {
|
||||||
files: !!req.query.files,
|
files: req.query.files
|
||||||
|
? {
|
||||||
|
include: {
|
||||||
|
thumbnail: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: false,
|
||||||
id: true,
|
id: true,
|
||||||
name: true,
|
name: true,
|
||||||
userId: true,
|
userId: true,
|
||||||
@@ -111,7 +123,13 @@ async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) {
|
|||||||
public: !!publicFolder,
|
public: !!publicFolder,
|
||||||
},
|
},
|
||||||
select: {
|
select: {
|
||||||
files: !!req.query.files,
|
files: req.query.files
|
||||||
|
? {
|
||||||
|
include: {
|
||||||
|
thumbnail: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: false,
|
||||||
id: true,
|
id: true,
|
||||||
name: true,
|
name: true,
|
||||||
userId: true,
|
userId: true,
|
||||||
@@ -200,7 +218,13 @@ async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
select: {
|
select: {
|
||||||
files: !!req.query.files,
|
files: req.query.files
|
||||||
|
? {
|
||||||
|
include: {
|
||||||
|
thumbnail: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: false,
|
||||||
id: true,
|
id: true,
|
||||||
name: true,
|
name: true,
|
||||||
userId: true,
|
userId: true,
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { PrismaClientKnownRequestError } from '@prisma/client/runtime/library';
|
||||||
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 prisma from 'lib/prisma';
|
||||||
@@ -8,15 +9,22 @@ async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) {
|
|||||||
if (req.method === 'DELETE') {
|
if (req.method === 'DELETE') {
|
||||||
if (!req.body.id) return res.badRequest('no url id');
|
if (!req.body.id) return res.badRequest('no url id');
|
||||||
|
|
||||||
const url = await prisma.url.delete({
|
try {
|
||||||
where: {
|
const url = await prisma.url.delete({
|
||||||
id: req.body.id,
|
where: {
|
||||||
},
|
id: req.body.id,
|
||||||
});
|
},
|
||||||
|
});
|
||||||
|
|
||||||
Logger.get('url').info(`User ${user.username} (${user.id}) deleted a url ${url.destination} (${url.id})`);
|
Logger.get('url').info(
|
||||||
|
`User ${user.username} (${user.id}) deleted a url ${url.destination} (${url.id})`,
|
||||||
|
);
|
||||||
|
|
||||||
return res.json(url);
|
return res.json(url);
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof PrismaClientKnownRequestError) return res.notFound('url not found');
|
||||||
|
else throw err;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
const urls = await prisma.url.findMany({
|
const urls = await prisma.url.findMany({
|
||||||
where: {
|
where: {
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ export default function Register({ code = undefined, title, user_registration })
|
|||||||
};
|
};
|
||||||
|
|
||||||
const createUser = async () => {
|
const createUser = async () => {
|
||||||
const res = await useFetch(`/api/auth/${user_registration ? 'register' : 'create'}`, 'POST', {
|
const res = await useFetch('/api/auth/register', 'POST', {
|
||||||
code: user_registration ? null : code,
|
code: user_registration ? null : code,
|
||||||
username,
|
username,
|
||||||
password,
|
password,
|
||||||
|
|||||||
@@ -115,6 +115,17 @@ export const getServerSideProps: GetServerSideProps = async (context) => {
|
|||||||
renderType = null;
|
renderType = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await prisma.file.update({
|
||||||
|
where: {
|
||||||
|
id: file.id,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
views: {
|
||||||
|
increment: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
code: await streamToString(data),
|
code: await streamToString(data),
|
||||||
|
|||||||
@@ -85,6 +85,12 @@ export const getServerSideProps: GetServerSideProps<Props> = async (context) =>
|
|||||||
createdAt: true,
|
createdAt: true,
|
||||||
password: true,
|
password: true,
|
||||||
size: true,
|
size: true,
|
||||||
|
thumbnail: {
|
||||||
|
select: {
|
||||||
|
name: true,
|
||||||
|
id: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
user: {
|
user: {
|
||||||
@@ -106,6 +112,9 @@ export const getServerSideProps: GetServerSideProps<Props> = async (context) =>
|
|||||||
folder.files[j].name,
|
folder.files[j].name,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
folder.files[j].size = Number(folder.files[j].size);
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
if (folder.files[j].password) folder.files[j].password = true;
|
if (folder.files[j].password) folder.files[j].password = true;
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import Link from 'next/link';
|
|||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import zconfig from 'lib/config';
|
import zconfig from 'lib/config';
|
||||||
|
import { log } from 'server/util';
|
||||||
|
|
||||||
export default function EmbeddedFile({
|
export default function EmbeddedFile({
|
||||||
file,
|
file,
|
||||||
@@ -63,11 +64,15 @@ export default function EmbeddedFile({
|
|||||||
|
|
||||||
const img = new Image();
|
const img = new Image();
|
||||||
img.addEventListener('load', function () {
|
img.addEventListener('load', function () {
|
||||||
if (this.naturalWidth > innerWidth)
|
// my best attempt of recreating https://searchfox.org/mozilla-central/source/dom/html/ImageDocument.cpp#271-276
|
||||||
imageEl.width = Math.floor(
|
// and it actually works
|
||||||
this.naturalWidth * Math.min(innerHeight / this.naturalHeight, innerWidth / this.naturalWidth),
|
|
||||||
);
|
const ratio = Math.min(innerHeight / this.naturalHeight, innerWidth / this.naturalWidth);
|
||||||
else imageEl.width = this.naturalWidth;
|
const newWidth = Math.max(1, Math.floor(ratio * this.naturalWidth));
|
||||||
|
const newHeight = Math.max(1, Math.floor(ratio * this.naturalHeight));
|
||||||
|
|
||||||
|
imageEl.width = newWidth;
|
||||||
|
imageEl.height = newHeight;
|
||||||
});
|
});
|
||||||
|
|
||||||
img.src = url || dataURL('/r');
|
img.src = url || dataURL('/r');
|
||||||
@@ -80,11 +85,19 @@ export default function EmbeddedFile({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (pass) {
|
if (pass) {
|
||||||
setOpened(true);
|
setOpened(true);
|
||||||
} else {
|
|
||||||
updateImage();
|
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!file?.mimetype?.startsWith('image')) return;
|
||||||
|
|
||||||
|
updateImage();
|
||||||
|
window.addEventListener('resize', () => updateImage());
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('resize', () => updateImage());
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Head>
|
<Head>
|
||||||
@@ -124,8 +137,6 @@ export default function EmbeddedFile({
|
|||||||
<meta name='twitter:card' content='player' />
|
<meta name='twitter:card' content='player' />
|
||||||
<meta name='twitter:player' content={`${host}/r/${file.name}`} />
|
<meta name='twitter:player' content={`${host}/r/${file.name}`} />
|
||||||
<meta name='twitter:player:stream' content={`${host}/r/${file.name}`} />
|
<meta name='twitter:player:stream' content={`${host}/r/${file.name}`} />
|
||||||
<meta name='twitter:player:width' content='720' />
|
|
||||||
<meta name='twitter:player:height' content='480' />
|
|
||||||
<meta name='twitter:player:stream:content_type' content={file.mimetype} />
|
<meta name='twitter:player:stream:content_type' content={file.mimetype} />
|
||||||
<meta name='twitter:title' content={file.name} />
|
<meta name='twitter:title' content={file.name} />
|
||||||
|
|
||||||
@@ -142,8 +153,6 @@ export default function EmbeddedFile({
|
|||||||
<meta property='og:video:url' content={`${host}/r/${file.name}`} />
|
<meta property='og:video:url' content={`${host}/r/${file.name}`} />
|
||||||
<meta property='og:video:secure_url' content={`${host}/r/${file.name}`} />
|
<meta property='og:video:secure_url' content={`${host}/r/${file.name}`} />
|
||||||
<meta property='og:video:type' content={file.mimetype} />
|
<meta property='og:video:type' content={file.mimetype} />
|
||||||
<meta property='og:video:width' content='720' />
|
|
||||||
<meta property='og:video:height' content='480' />
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{file.mimetype.startsWith('audio') && (
|
{file.mimetype.startsWith('audio') && (
|
||||||
@@ -200,7 +209,17 @@ export default function EmbeddedFile({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{file.mimetype.startsWith('video') && (
|
{file.mimetype.startsWith('video') && (
|
||||||
<video src={dataURL('/r')} controls autoPlay muted id='video_content' />
|
<video
|
||||||
|
style={{
|
||||||
|
maxHeight: '100vh',
|
||||||
|
maxWidth: '100vw',
|
||||||
|
}}
|
||||||
|
src={dataURL('/r')}
|
||||||
|
controls
|
||||||
|
autoPlay
|
||||||
|
muted
|
||||||
|
id='video_content'
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{file.mimetype.startsWith('audio') && (
|
{file.mimetype.startsWith('audio') && (
|
||||||
@@ -233,6 +252,17 @@ export const getServerSideProps: GetServerSideProps = async (context) => {
|
|||||||
let host = context.req.headers.host;
|
let host = context.req.headers.host;
|
||||||
if (!file) return { notFound: true };
|
if (!file) return { notFound: true };
|
||||||
|
|
||||||
|
const logger = log('view');
|
||||||
|
|
||||||
|
if (file.maxViews && file.views >= file.maxViews) {
|
||||||
|
await datasource.delete(file.name);
|
||||||
|
await prisma.file.delete({ where: { id: file.id } });
|
||||||
|
|
||||||
|
logger.child('file').info(`File ${file.name} has been deleted due to max views (${file.maxViews})`);
|
||||||
|
|
||||||
|
return { notFound: true };
|
||||||
|
}
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
file.size = Number(file.size);
|
file.size = Number(file.size);
|
||||||
|
|
||||||
@@ -258,6 +288,7 @@ export const getServerSideProps: GetServerSideProps = async (context) => {
|
|||||||
delete user.password;
|
delete user.password;
|
||||||
delete user.totpSecret;
|
delete user.totpSecret;
|
||||||
delete user.token;
|
delete user.token;
|
||||||
|
delete user.ratelimit;
|
||||||
|
|
||||||
// @ts-ignore workaround because next wont allow date
|
// @ts-ignore workaround because next wont allow date
|
||||||
file.createdAt = file.createdAt.toString();
|
file.createdAt = file.createdAt.toString();
|
||||||
@@ -306,6 +337,17 @@ export const getServerSideProps: GetServerSideProps = async (context) => {
|
|||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
if (file.password) file.password = true;
|
if (file.password) file.password = true;
|
||||||
|
|
||||||
|
await prisma.file.update({
|
||||||
|
where: {
|
||||||
|
id: file.id,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
views: {
|
||||||
|
increment: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
file,
|
file,
|
||||||
|
|||||||
@@ -29,12 +29,12 @@ async function main() {
|
|||||||
const mime = await guess(files[i].split('.').pop());
|
const mime = await guess(files[i].split('.').pop());
|
||||||
const { size } = statSync(join(directory, files[i]));
|
const { size } = statSync(join(directory, files[i]));
|
||||||
|
|
||||||
data.push({
|
data[i] = {
|
||||||
name: files[i],
|
name: files[i],
|
||||||
mimetype: mime,
|
mimetype: mime,
|
||||||
userId,
|
userId,
|
||||||
size,
|
size,
|
||||||
});
|
};
|
||||||
|
|
||||||
console.log(`Imported ${files[i]} (${bytesToHuman(size)}) (${mime} mimetype) to user ${userId}`);
|
console.log(`Imported ${files[i]} (${bytesToHuman(size)}) (${mime} mimetype) to user ${userId}`);
|
||||||
}
|
}
|
||||||
@@ -54,7 +54,9 @@ async function main() {
|
|||||||
console.log(`Copying files to ${config.datasource.type} storage..`);
|
console.log(`Copying files to ${config.datasource.type} storage..`);
|
||||||
for (let i = 0; i !== files.length; ++i) {
|
for (let i = 0; i !== files.length; ++i) {
|
||||||
const file = files[i];
|
const file = files[i];
|
||||||
await datasource.save(file, await readFile(join(directory, file)));
|
await datasource.save(file, await readFile(join(directory, file)), {
|
||||||
|
type: data[i]?.mimetype ?? 'application/octet-stream',
|
||||||
|
});
|
||||||
}
|
}
|
||||||
console.log(`Finished copying files to ${config.datasource.type} storage.`);
|
console.log(`Finished copying files to ${config.datasource.type} storage.`);
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,11 @@ function dbFileDecorator(fastify: FastifyInstance, _, done) {
|
|||||||
this.header('Content-Length', size);
|
this.header('Content-Length', size);
|
||||||
this.header('Content-Type', download ? 'application/octet-stream' : file.mimetype);
|
this.header('Content-Type', download ? 'application/octet-stream' : file.mimetype);
|
||||||
this.header('Content-Disposition', `inline; filename="${encodeURI(file.originalName || file.name)}"`);
|
this.header('Content-Disposition', `inline; filename="${encodeURI(file.originalName || file.name)}"`);
|
||||||
|
if (file.mimetype.startsWith('video/') || file.mimetype.startsWith('audio/')) {
|
||||||
|
this.header('Accept-Ranges', 'bytes');
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Range
|
||||||
|
this.header('Content-Range', `bytes 0-${size - 1}/${size}`);
|
||||||
|
}
|
||||||
|
|
||||||
return this.send(data);
|
return this.send(data);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -115,7 +115,7 @@ async function start() {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
await datasource.save(thumb.name, thumbnail);
|
await datasource.save(thumb.name, thumbnail, { type: 'image/jpeg' });
|
||||||
|
|
||||||
logger.info(`thumbnail saved - ${thumb.name}`);
|
logger.info(`thumbnail saved - ${thumb.name}`);
|
||||||
logger.debug(`thumbnail ${JSON.stringify(thumb)}`);
|
logger.debug(`thumbnail ${JSON.stringify(thumb)}`);
|
||||||
|
|||||||
@@ -121,7 +121,9 @@ async function start() {
|
|||||||
await fd.close();
|
await fd.close();
|
||||||
} else {
|
} else {
|
||||||
logger.debug('writing file to datasource');
|
logger.debug('writing file to datasource');
|
||||||
await datasource.save(file.filename, Buffer.from(fd as Uint8Array));
|
await datasource.save(file.filename, Buffer.from(fd as Uint8Array), {
|
||||||
|
type: file.mimetype ?? 'application/octet-stream',
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const final = await prisma.incompleteFile.update({
|
const final = await prisma.incompleteFile.update({
|
||||||
|
|||||||
@@ -3063,9 +3063,9 @@ __metadata:
|
|||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"caniuse-lite@npm:^1.0.30001406":
|
"caniuse-lite@npm:^1.0.30001406":
|
||||||
version: 1.0.30001561
|
version: 1.0.30001642
|
||||||
resolution: "caniuse-lite@npm:1.0.30001561"
|
resolution: "caniuse-lite@npm:1.0.30001642"
|
||||||
checksum: 949829fe037e23346595614e01d362130245920503a12677f2506ce68e1240360113d6383febed41e8aa38cd0f5fd9c69c21b0af65a71c0246d560db489f1373
|
checksum: 23f823ec115306eaf9299521328bb6ad0c4ce65254c375b14fd497ceda759ee8ee5b8763b7b622cb36b6b5fb53c6cb8569785fba842fe289be7dc3fcf008eb4f
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user