mirror of
https://github.com/diced/zipline.git
synced 2026-06-12 10:51:17 -07:00
refactor: use access tokens for file/url passwords
no longer using cookies, since they are buggy and weird with caching using a access token that "expires" in 10 minutes
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import DashboardFileType from '@/components/file/DashboardFileType';
|
||||
import TagPill from '@/components/pages/files/tags/TagPill';
|
||||
import { useSsrData } from '@/components/ZiplineSSRProvider';
|
||||
import { useTitle } from '@/lib/client/hooks/useTitle';
|
||||
import { File } from '@/lib/db/models/file';
|
||||
import { User } from '@/lib/db/models/user';
|
||||
@@ -24,7 +25,6 @@ import { IconDownload, IconExternalLink, IconInfoCircleFilled } from '@tabler/ic
|
||||
import * as sanitize from 'isomorphic-dompurify';
|
||||
import { useState } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useSsrData } from '../../../components/ZiplineSSRProvider';
|
||||
import { getFile } from '../../ssr-view/server';
|
||||
|
||||
type SsrData = {
|
||||
@@ -33,7 +33,7 @@ type SsrData = {
|
||||
code: boolean;
|
||||
user?: Partial<User>;
|
||||
host: string;
|
||||
pw?: string | null;
|
||||
token?: string | null;
|
||||
metrics?: Awaited<ReturnType<typeof parserMetrics>>;
|
||||
filesRoute?: string;
|
||||
};
|
||||
@@ -42,7 +42,7 @@ export default function ViewFileId() {
|
||||
const data = useSsrData<SsrData>();
|
||||
if (!data) return null;
|
||||
|
||||
const { file, password, code, user, host, metrics, filesRoute, pw } = data;
|
||||
const { file, password, code, user, host, metrics, filesRoute, token } = data;
|
||||
|
||||
const [passwordValue, setPassword] = useState<string>('');
|
||||
const [passwordError, setPasswordError] = useState<string>('');
|
||||
@@ -50,7 +50,7 @@ export default function ViewFileId() {
|
||||
|
||||
useTitle(file.originalName ?? file.name ?? 'View File');
|
||||
|
||||
return password && !pw ? (
|
||||
return password && !token ? (
|
||||
<Modal onClose={() => {}} opened={true} withCloseButton={false} centered title='Password required'>
|
||||
<form
|
||||
onSubmit={async (e) => {
|
||||
@@ -63,7 +63,8 @@ export default function ViewFileId() {
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
window.location.reload();
|
||||
const json = (await res.json()) as { token: string };
|
||||
window.location.replace(`/view/${file.name}?token=${encodeURIComponent(json.token)}`);
|
||||
} else {
|
||||
setPasswordError('Invalid password');
|
||||
}
|
||||
@@ -104,7 +105,7 @@ export default function ViewFileId() {
|
||||
size='md'
|
||||
variant='outline'
|
||||
component={Link}
|
||||
to={`/raw/${file.name}?download=true${pw ? `&pw=${encodeURIComponent(pw)}` : ''}`}
|
||||
to={`/raw/${file.name}?download=true${token ? `&token=${encodeURIComponent(token)}` : ''}`}
|
||||
target='_blank'
|
||||
>
|
||||
<IconDownload size='1rem' />
|
||||
@@ -143,7 +144,7 @@ export default function ViewFileId() {
|
||||
</Collapse>
|
||||
|
||||
<Center m='sm'>
|
||||
<DashboardFileType file={file as unknown as File} password={pw} show code={code} fullscreen />
|
||||
<DashboardFileType file={file as unknown as File} token={token} show code={code} fullscreen />
|
||||
</Center>
|
||||
</>
|
||||
) : (
|
||||
@@ -194,7 +195,7 @@ export default function ViewFileId() {
|
||||
size='md'
|
||||
variant='outline'
|
||||
component={Link}
|
||||
to={`/raw/${file.name}${pw ? `?pw=${encodeURIComponent(pw)}` : ''}`}
|
||||
to={`/raw/${file.name}${token ? `?token=${encodeURIComponent(token)}` : ''}`}
|
||||
target='_blank'
|
||||
>
|
||||
<IconExternalLink size='1rem' />
|
||||
@@ -205,7 +206,7 @@ export default function ViewFileId() {
|
||||
size='md'
|
||||
variant='outline'
|
||||
component={Link}
|
||||
to={`/raw/${file.name}?download=true${pw ? `&pw=${encodeURIComponent(pw)}` : ''}`}
|
||||
to={`/raw/${file.name}?download=true${token ? `&token=${encodeURIComponent(token)}` : ''}`}
|
||||
target='_blank'
|
||||
>
|
||||
<IconDownload size='1rem' />
|
||||
@@ -214,7 +215,7 @@ export default function ViewFileId() {
|
||||
</ActionIcon.Group>
|
||||
</Group>
|
||||
|
||||
<DashboardFileType allowZoom file={file as unknown as File} password={pw} show />
|
||||
<DashboardFileType allowZoom file={file as unknown as File} token={token} show />
|
||||
|
||||
{user?.view!.content && (
|
||||
<Typography>
|
||||
|
||||
@@ -6,10 +6,11 @@ export default function ViewUrlId() {
|
||||
const data = useSsrData<{
|
||||
url: { id: string; destination?: string };
|
||||
password?: boolean;
|
||||
token?: string | null;
|
||||
}>();
|
||||
if (!data) return null;
|
||||
|
||||
const { url, password } = data;
|
||||
const { url, password, token } = data;
|
||||
|
||||
const [passwordValue, setPassword] = useState<string>('');
|
||||
const [passwordError, setPasswordError] = useState<string>('');
|
||||
@@ -18,7 +19,7 @@ export default function ViewUrlId() {
|
||||
if (!password && url.destination) window.location.href = url.destination;
|
||||
}, []);
|
||||
|
||||
return password ? (
|
||||
return password && !token ? (
|
||||
<Modal onClose={() => {}} opened={true} withCloseButton={false} centered title='Password required'>
|
||||
<form
|
||||
onSubmit={async (e) => {
|
||||
@@ -31,7 +32,8 @@ export default function ViewUrlId() {
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
window.location.reload();
|
||||
const json = (await res.json()) as { token: string };
|
||||
window.location.replace(`/view/url/${url.id}?token=${encodeURIComponent(json.token)}`);
|
||||
} else {
|
||||
setPasswordError('Invalid password');
|
||||
}
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import { verifyAccessToken } from '@/lib/accessToken';
|
||||
import { config as zConfig } from '@/lib/config';
|
||||
import { Config } from '@/lib/config/validate';
|
||||
import { verifyPassword } from '@/lib/crypto';
|
||||
import { prisma } from '@/lib/db';
|
||||
import { getPasswordCookie } from '@/lib/passwordCookie';
|
||||
import { renderHtml } from '@/lib/ssr/renderHtml';
|
||||
import { ZiplineTheme } from '@/lib/theme';
|
||||
import * as cookie from 'cookie';
|
||||
import { FastifyRequest } from 'fastify';
|
||||
import { createRoutes } from './routes';
|
||||
|
||||
@@ -17,13 +15,11 @@ export async function render(
|
||||
}: {
|
||||
themes: ZiplineTheme[];
|
||||
defaultTheme: Config['website']['theme'];
|
||||
req: FastifyRequest;
|
||||
req: FastifyRequest<{ Params: { id: string }; Querystring: { token?: string } }>;
|
||||
},
|
||||
url: string,
|
||||
) {
|
||||
const routes = createRoutes(themes, defaultTheme);
|
||||
|
||||
const id = url.split('/').pop();
|
||||
const id = req.params?.id ?? null;
|
||||
if (!id) return { html: 'Not Found', meta: '', status: 404 };
|
||||
|
||||
const { config: libConfig, reloadSettings } = await import('@/lib/config');
|
||||
@@ -52,31 +48,27 @@ export async function render(
|
||||
return { html: 'Gone', meta: '', status: 410 };
|
||||
}
|
||||
|
||||
const cookies = cookie.parse(req.headers.cookie || '');
|
||||
const pw = getPasswordCookie(cookies, 'url', urlEntry.id);
|
||||
const token = req.query.token;
|
||||
const valid = token && urlEntry.password ? verifyAccessToken(token, 'url', urlEntry.id) : false;
|
||||
const hasPassword = !!urlEntry.password;
|
||||
|
||||
const data = {
|
||||
url: { ...urlEntry },
|
||||
password: hasPassword,
|
||||
token: valid ? token : null,
|
||||
};
|
||||
|
||||
delete (data.url as any).password;
|
||||
|
||||
const routes = createRoutes(themes, defaultTheme);
|
||||
|
||||
if (hasPassword) {
|
||||
delete (data.url as any).password;
|
||||
if (pw) {
|
||||
const verified = await verifyPassword(pw, urlEntry.password!);
|
||||
if (!verified) {
|
||||
delete (data.url as any).destination;
|
||||
return renderHtml(routes, { url, data, status: 403 });
|
||||
}
|
||||
} else {
|
||||
if (!valid) {
|
||||
delete (data.url as any).destination;
|
||||
return renderHtml(routes, { url, data, status: 403 });
|
||||
}
|
||||
}
|
||||
|
||||
delete (data.url as any).password;
|
||||
|
||||
await prisma.url.update({
|
||||
where: { id: urlEntry.id },
|
||||
data: { views: { increment: 1 } },
|
||||
|
||||
@@ -5,21 +5,19 @@ import '@mantine/dropzone/styles.css';
|
||||
import '@mantine/notifications/styles.css';
|
||||
import 'mantine-datatable/styles.css';
|
||||
|
||||
import { verifyAccessToken } from '@/lib/accessToken';
|
||||
import { isCode } from '@/lib/code';
|
||||
import { config as zConfig } from '@/lib/config';
|
||||
import type { Config } from '@/lib/config/validate';
|
||||
import { verifyPassword } from '@/lib/crypto';
|
||||
import { prisma } from '@/lib/db';
|
||||
import { File, fileSelect } from '@/lib/db/models/file';
|
||||
import { User, userSelect } from '@/lib/db/models/user';
|
||||
import { parseString } from '@/lib/parser';
|
||||
import { parserMetrics } from '@/lib/parser/metrics';
|
||||
import { getPasswordCookie } from '@/lib/passwordCookie';
|
||||
import { createZiplineSsr } from '@/lib/ssr/createZiplineSsr';
|
||||
import { stripHtml } from '@/lib/stripHtml';
|
||||
import type { ZiplineTheme } from '@/lib/theme';
|
||||
import { readThemes } from '@/lib/theme/file';
|
||||
import * as cookie from 'cookie';
|
||||
import { FastifyRequest } from 'fastify';
|
||||
import { renderToString } from 'react-dom/server';
|
||||
import { createStaticHandler, createStaticRouter, StaticRouterProvider } from 'react-router-dom';
|
||||
@@ -45,11 +43,11 @@ export async function render(
|
||||
}: {
|
||||
themes: ZiplineTheme[];
|
||||
defaultTheme: Config['website']['theme'];
|
||||
req: FastifyRequest;
|
||||
req: FastifyRequest<{ Params: { id: string }; Querystring: { token?: string } }>;
|
||||
},
|
||||
url: string,
|
||||
) {
|
||||
const id = url.split('/').pop();
|
||||
const id = req.params?.id ?? null;
|
||||
if (!id) return { html: 'Not Found', meta: '', status: 404 };
|
||||
|
||||
const { config: libConfig, reloadSettings } = await import('@/lib/config');
|
||||
@@ -95,17 +93,15 @@ export async function render(
|
||||
const metrics = await parserMetrics(user.id);
|
||||
const config = { website: { theme: zConfig.website.theme } };
|
||||
|
||||
const cookies = cookie.parse(req.headers.cookie || '');
|
||||
const pw = getPasswordCookie(cookies, 'file', file.id);
|
||||
const token = req.query.token;
|
||||
const valid = token && file.password ? verifyAccessToken(token, 'file', file.id) : false;
|
||||
const hasPassword = !!file.password;
|
||||
|
||||
delete (file as any).password;
|
||||
|
||||
if (hasPassword) {
|
||||
if (pw) {
|
||||
const verified = await verifyPassword(pw, file.password!);
|
||||
if (!verified) return { html: 'Forbidden', meta: '', status: 403 };
|
||||
delete (file as any).password;
|
||||
} else {
|
||||
delete (file as any).password;
|
||||
console.log('File is password protected');
|
||||
if (!valid) {
|
||||
const data = {
|
||||
file: { id: file.id, name: file.name, type: file.type },
|
||||
password: true,
|
||||
@@ -142,7 +138,7 @@ export async function render(
|
||||
const data = {
|
||||
file,
|
||||
password: hasPassword,
|
||||
pw: pw || null,
|
||||
token: valid ? token : null,
|
||||
code,
|
||||
user,
|
||||
host,
|
||||
|
||||
@@ -39,7 +39,7 @@ export function Placeholder({ text, Icon, ...props }: { text: string; Icon: Icon
|
||||
export default function DashboardFileType({
|
||||
file,
|
||||
show,
|
||||
password,
|
||||
token,
|
||||
code,
|
||||
allowZoom,
|
||||
fullscreen,
|
||||
@@ -47,14 +47,14 @@ export default function DashboardFileType({
|
||||
}: {
|
||||
file: DbFile | File;
|
||||
show?: boolean;
|
||||
password?: string | null;
|
||||
token?: string | null;
|
||||
code?: boolean;
|
||||
allowZoom?: boolean;
|
||||
fullscreen?: boolean;
|
||||
scrollParent?: HTMLElement | null;
|
||||
}) {
|
||||
const disableMediaPreview = useSettingsStore((state) => state.settings.disableMediaPreview);
|
||||
const { fileUrl, thumbnailUrl, viewUrl } = useFileUrls({ file, password });
|
||||
const { fileUrl, thumbnailUrl, viewUrl } = useFileUrls({ file, token });
|
||||
const db = isDbFile(file) ? file : null;
|
||||
|
||||
const extension = file.name.split('.').pop() || '';
|
||||
|
||||
@@ -2,15 +2,17 @@ import { useUserStore } from '@/lib/client/store/user';
|
||||
import type { File as DbFile } from '@/lib/db/models/file';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
export function appendPassword(url: string, password?: string | null) {
|
||||
return `${url}${password ? `?pw=${encodeURIComponent(password)}` : ''}`;
|
||||
function appendToken(url: string, token?: string | null) {
|
||||
if (!token) return url;
|
||||
|
||||
return `${url}${token ? `?token=${encodeURIComponent(token)}` : ''}`;
|
||||
}
|
||||
|
||||
export function isDbFile(file: DbFile | File): file is DbFile {
|
||||
return typeof globalThis.File !== 'undefined' ? !(file instanceof globalThis.File) : 'thumbnail' in file;
|
||||
}
|
||||
|
||||
export default function useFileUrls({ file, password }: { file: DbFile | File; password?: string | null }): {
|
||||
export default function useFileUrls({ file, token }: { file: DbFile | File; token?: string | null }): {
|
||||
fileUrl: string;
|
||||
thumbnailUrl: string | null;
|
||||
viewUrl: string | null;
|
||||
@@ -20,17 +22,15 @@ export default function useFileUrls({ file, password }: { file: DbFile | File; p
|
||||
const blobUrl = useMemo(() => (isDbFile(file) ? null : URL.createObjectURL(file as File)), [file]);
|
||||
|
||||
return useMemo(() => {
|
||||
if (!isDbFile(file)) {
|
||||
return { fileUrl: blobUrl ?? '', thumbnailUrl: null, viewUrl: null };
|
||||
}
|
||||
if (!isDbFile(file)) return { fileUrl: blobUrl ?? '', thumbnailUrl: null, viewUrl: null };
|
||||
|
||||
const thumb = file.thumbnail?.path;
|
||||
const thumbnailUrl = thumb ? (user ? `/api/user/files/${thumb}/raw` : `/raw/${thumb}`) : null;
|
||||
|
||||
return {
|
||||
fileUrl: appendPassword(user ? `/api/user/files/${file.id}/raw` : `/raw/${file.name}`, password),
|
||||
viewUrl: appendPassword(`/view/${file.name}`, password),
|
||||
fileUrl: appendToken(user ? `/api/user/files/${file.id}/raw` : `/raw/${file.name}`, token),
|
||||
viewUrl: appendToken(`/view/${file.name}`, token),
|
||||
thumbnailUrl,
|
||||
};
|
||||
}, [blobUrl, file, password, user]);
|
||||
}, [token, blobUrl, file, user]);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import { config } from '@/lib/config';
|
||||
import { decrypt, encrypt } from '@/lib/crypto';
|
||||
|
||||
type AccessTokenPayload = {
|
||||
type: string;
|
||||
id: string;
|
||||
expiry: number;
|
||||
};
|
||||
|
||||
export function createAccessToken({ type, id }: { type: string; id: string }): string {
|
||||
const payload: AccessTokenPayload = {
|
||||
type: type,
|
||||
id,
|
||||
expiry: Date.now() + 5 * 60_000, // 5 minutes
|
||||
};
|
||||
|
||||
return encrypt(JSON.stringify(payload), config.core.secret);
|
||||
}
|
||||
|
||||
export function verifyAccessToken(token: string | null | undefined, type: string, id: string): boolean {
|
||||
if (!token) return false;
|
||||
|
||||
try {
|
||||
const raw = decrypt(token, config.core.secret);
|
||||
const payload = JSON.parse(raw) as Partial<AccessTokenPayload>;
|
||||
if (!payload || typeof payload !== 'object') return false;
|
||||
|
||||
if (payload.type !== type) return false;
|
||||
if (payload.id !== id) return false;
|
||||
if (typeof payload.expiry !== 'number') return false;
|
||||
if (payload.expiry < Date.now()) return false;
|
||||
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -90,6 +90,7 @@ export const API_ERRORS = {
|
||||
3015: 'Not super admin',
|
||||
3016: 'OAuth registration is disabled',
|
||||
3017: 'OAuth login is not allowed for this account',
|
||||
3018: 'Invalid access token provided.',
|
||||
|
||||
// 4xxx, not founds
|
||||
4000: 'File not found',
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
import { config } from '@/lib/config';
|
||||
import { decrypt, encrypt } from '@/lib/crypto';
|
||||
import { FastifyReply } from 'fastify';
|
||||
|
||||
export function setPasswordCookie(res: FastifyReply, kind: 'file' | 'url', id: string, password: string) {
|
||||
res.cookie(`${kind}_pw_${id}`, encrypt(password, config.core.secret), {
|
||||
sameSite: 'lax',
|
||||
expires: new Date(Date.now() + 15 * 60_000),
|
||||
httpOnly: true,
|
||||
secure: config.core.returnHttpsUrls,
|
||||
path: '/',
|
||||
});
|
||||
}
|
||||
|
||||
export function getPasswordCookie(
|
||||
cookies: Record<string, string | undefined>,
|
||||
kind: 'file' | 'url',
|
||||
id: string,
|
||||
) {
|
||||
const cookie = cookies[`${kind}_pw_${id}`];
|
||||
if (!cookie) return null;
|
||||
try {
|
||||
return decrypt(cookie, config.core.secret);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
import { ApiError } from '@/lib/api/errors';
|
||||
import { createAccessToken } from '@/lib/accessToken';
|
||||
import { verifyPassword } from '@/lib/crypto';
|
||||
import { prisma } from '@/lib/db';
|
||||
import { log } from '@/lib/logger';
|
||||
import { secondlyRatelimit } from '@/lib/ratelimits';
|
||||
import { zStringTrimmed } from '@/lib/validation';
|
||||
import { setPasswordCookie } from '@/lib/passwordCookie';
|
||||
import typedPlugin from '@/server/typedPlugin';
|
||||
import z from 'zod';
|
||||
|
||||
@@ -21,7 +21,8 @@ export default typedPlugin(
|
||||
PATH,
|
||||
{
|
||||
schema: {
|
||||
description: 'Verify the password for a password-protected file by ID or name.',
|
||||
description:
|
||||
'Verify the password for a password-protected file by ID or name and receive an access token if the password is correct',
|
||||
body: z.object({
|
||||
password: zStringTrimmed,
|
||||
}),
|
||||
@@ -31,6 +32,7 @@ export default typedPlugin(
|
||||
response: {
|
||||
200: z.object({
|
||||
success: z.boolean(),
|
||||
token: z.string(),
|
||||
}),
|
||||
},
|
||||
},
|
||||
@@ -60,11 +62,12 @@ export default typedPlugin(
|
||||
|
||||
throw new ApiError(3005);
|
||||
}
|
||||
logger.info(`${file.name} was accessed with the correct password`, { ua: req.headers['user-agent'] });
|
||||
logger.info(`${file.name} was accessed with the correct password, a new access token was created`, {
|
||||
ua: req.headers['user-agent'],
|
||||
});
|
||||
|
||||
setPasswordCookie(res, 'file', file.id, req.body.password);
|
||||
|
||||
return res.send({ success: true });
|
||||
const token = createAccessToken({ type: 'file', id: file.id });
|
||||
return res.send({ success: true, token });
|
||||
},
|
||||
);
|
||||
},
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { verifyAccessToken } from '@/lib/accessToken';
|
||||
import { ApiError } from '@/lib/api/errors';
|
||||
import { parseRange } from '@/lib/api/range';
|
||||
import { config } from '@/lib/config';
|
||||
import { verifyPassword } from '@/lib/crypto';
|
||||
import { datasource } from '@/lib/datasource';
|
||||
import { prisma } from '@/lib/db';
|
||||
import { sanitizeFilename } from '@/lib/fs';
|
||||
@@ -27,7 +27,7 @@ export default typedPlugin(
|
||||
id: z.string(),
|
||||
}),
|
||||
querystring: z.object({
|
||||
pw: z.string().optional(),
|
||||
token: z.string().optional(),
|
||||
download: zQsBoolean.optional(),
|
||||
}),
|
||||
tags: ['auth'],
|
||||
@@ -35,7 +35,7 @@ export default typedPlugin(
|
||||
preHandler: [userMiddleware],
|
||||
},
|
||||
async (req, res) => {
|
||||
const { pw, download } = req.query;
|
||||
const { token, download } = req.query;
|
||||
|
||||
const id = sanitizeFilename(req.params.id);
|
||||
if (!id) throw new ApiError(9002);
|
||||
@@ -114,9 +114,8 @@ export default typedPlugin(
|
||||
}
|
||||
|
||||
if (file?.password) {
|
||||
if (!pw) throw new ApiError(3004);
|
||||
const verified = await verifyPassword(pw, file.password!);
|
||||
if (!verified) throw new ApiError(3005);
|
||||
const valid = verifyAccessToken(token, 'file', file.id);
|
||||
if (!valid) throw new ApiError(3018);
|
||||
}
|
||||
|
||||
const size = file?.size || (await datasource.size(file?.name ?? id));
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import { ApiError } from '@/lib/api/errors';
|
||||
import { createAccessToken } from '@/lib/accessToken';
|
||||
import { verifyPassword } from '@/lib/crypto';
|
||||
import { prisma } from '@/lib/db';
|
||||
import { log } from '@/lib/logger';
|
||||
import { secondlyRatelimit } from '@/lib/ratelimits';
|
||||
import { zStringTrimmed } from '@/lib/validation';
|
||||
import { setPasswordCookie } from '@/lib/passwordCookie';
|
||||
import typedPlugin from '@/server/typedPlugin';
|
||||
import z from 'zod';
|
||||
|
||||
export type ApiUserUrlsIdPasswordResponse = {
|
||||
success: boolean;
|
||||
token: string;
|
||||
};
|
||||
|
||||
const logger = log('api').c('user').c('urls').c('[id]').c('password');
|
||||
@@ -28,6 +29,12 @@ export default typedPlugin(
|
||||
body: z.object({
|
||||
password: zStringTrimmed,
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
success: z.boolean(),
|
||||
token: z.string(),
|
||||
}),
|
||||
},
|
||||
},
|
||||
...secondlyRatelimit(2),
|
||||
},
|
||||
@@ -59,9 +66,8 @@ export default typedPlugin(
|
||||
ua: req.headers['user-agent'],
|
||||
});
|
||||
|
||||
setPasswordCookie(res, 'url', url.id, req.body.password);
|
||||
|
||||
return res.send({ success: true });
|
||||
const token = createAccessToken({ type: 'url', id: url.id });
|
||||
return res.send({ success: true, token });
|
||||
},
|
||||
);
|
||||
},
|
||||
|
||||
@@ -7,7 +7,7 @@ type Params = {
|
||||
};
|
||||
|
||||
type Query = {
|
||||
pw?: string;
|
||||
token?: string;
|
||||
download?: string;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { verifyAccessToken } from '@/lib/accessToken';
|
||||
import { ApiError } from '@/lib/api/errors';
|
||||
import { parseRange } from '@/lib/api/range';
|
||||
import { config } from '@/lib/config';
|
||||
import { verifyPassword } from '@/lib/crypto';
|
||||
import { datasource } from '@/lib/datasource';
|
||||
import { prisma } from '@/lib/db';
|
||||
import { log } from '@/lib/logger';
|
||||
@@ -18,7 +18,7 @@ type Params = {
|
||||
};
|
||||
|
||||
type Querystring = {
|
||||
pw?: string;
|
||||
token?: string;
|
||||
download?: string;
|
||||
};
|
||||
|
||||
@@ -32,7 +32,7 @@ export const rawFileHandler = async (
|
||||
res: FastifyReply,
|
||||
) => {
|
||||
const { id } = req.params;
|
||||
const { pw, download } = req.query;
|
||||
const { token, download } = req.query;
|
||||
|
||||
if (id.startsWith('.thumbnail')) {
|
||||
const thumbnail = await prisma.thumbnail.findFirst({
|
||||
@@ -80,10 +80,8 @@ export const rawFileHandler = async (
|
||||
}
|
||||
|
||||
if (file?.password) {
|
||||
if (!pw) throw new ApiError(3004);
|
||||
const verified = await verifyPassword(pw, file.password!);
|
||||
|
||||
if (!verified) throw new ApiError(3005);
|
||||
const valid = verifyAccessToken(token, 'file', file.id);
|
||||
if (!valid) throw new ApiError(3018);
|
||||
}
|
||||
|
||||
const size = file?.size || (await datasource.size(file?.name ?? id));
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { verifyAccessToken } from '@/lib/accessToken';
|
||||
import { config } from '@/lib/config';
|
||||
import { verifyPassword } from '@/lib/crypto';
|
||||
import { prisma } from '@/lib/db';
|
||||
import { log } from '@/lib/logger';
|
||||
import { FastifyReply, FastifyRequest } from 'fastify';
|
||||
@@ -9,7 +9,7 @@ type Params = {
|
||||
};
|
||||
|
||||
type Query = {
|
||||
pw?: string;
|
||||
token?: string;
|
||||
};
|
||||
|
||||
const logger = log('server').c('urls');
|
||||
@@ -19,7 +19,7 @@ export async function urlsRoute(
|
||||
res: FastifyReply,
|
||||
) {
|
||||
const { id } = req.params;
|
||||
const { pw } = req.query;
|
||||
const { token } = req.query;
|
||||
|
||||
const url = await prisma.url.findFirst({
|
||||
where: {
|
||||
@@ -48,10 +48,8 @@ export async function urlsRoute(
|
||||
}
|
||||
|
||||
if (url.password) {
|
||||
if (!pw) return res.redirect(`/view/url/${url.id}`);
|
||||
const verified = await verifyPassword(pw as string, url.password);
|
||||
|
||||
if (!verified) return res.redirect(`/view/url/${url.id}`);
|
||||
const valid = verifyAccessToken(token, 'url', url.id);
|
||||
if (!valid) return res.redirect(`/view/url/${url.id}`);
|
||||
}
|
||||
|
||||
await prisma.url.update({
|
||||
|
||||
Reference in New Issue
Block a user