Files
Bruno Bernardino 7fac7febcf Public File Sharing (#72)
* Public File Sharing

This implements public file sharing (read-only) with and without passwords (#57).

It also fixes a problem with filenames including special characters like `#` not working properly (#71).

You can share a directory or a single file, by using the new share icon on the right of the directories/files, and click on it to manage an existing file share (setting a new password, or deleting the file share).

There is some other minor cleanup and other copy updates in the README.

Closes #57
Fixes #71

* Hide UI elements when sharing isn't allowed
2025-06-20 12:04:16 +01:00

108 lines
3.1 KiB
TypeScript

import { Handlers, PageProps } from 'fresh/server.ts';
import { FreshContextState } from '/lib/types.ts';
import { getFormDataField } from '/lib/form-utils.tsx';
import { AppConfig } from '/lib/config.ts';
import { FileShareModel } from '/lib/models/files.ts';
import { generateHash } from '/lib/utils/misc.ts';
import { PASSWORD_SALT } from '/lib/auth.ts';
import ShareVerifyForm from '/components/files/ShareVerifyForm.tsx';
interface Data {
error?: {
title: string;
message: string;
};
}
export const handler: Handlers<Data, FreshContextState> = {
async GET(request, context) {
const { fileShareId } = context.params;
if (!fileShareId) {
return new Response('Not Found', { status: 404 });
}
const isPublicFileSharingAllowed = await AppConfig.isPublicFileSharingAllowed();
if (!isPublicFileSharingAllowed) {
return new Response('Not Found', { status: 404 });
}
const fileShare = await FileShareModel.getById(fileShareId);
if (!fileShare) {
return new Response('Not Found', { status: 404 });
}
if (!fileShare.extra.hashed_password) {
return new Response('Redirect', { status: 303, headers: { 'Location': `/file-share/${fileShareId}` } });
}
return await context.render({});
},
async POST(request, context) {
const { fileShareId } = context.params;
if (!fileShareId) {
return new Response('Not Found', { status: 404 });
}
const isPublicFileSharingAllowed = await AppConfig.isPublicFileSharingAllowed();
if (!isPublicFileSharingAllowed) {
return new Response('Not Found', { status: 404 });
}
const fileShare = await FileShareModel.getById(fileShareId);
if (!fileShare) {
return new Response('Not Found', { status: 404 });
}
if (!fileShare.extra.hashed_password) {
return new Response('Redirect', { status: 303, headers: { 'Location': `/file-share/${fileShareId}` } });
}
try {
const formData = await request.formData();
const password = getFormDataField(formData, 'password');
if (!password) {
throw new Error('Password is required');
}
const hashedPassword = await generateHash(`${password}:${PASSWORD_SALT}`, 'SHA-256');
if (hashedPassword !== fileShare.extra.hashed_password) {
throw new Error('Invalid password');
}
const response = new Response('Redirect', { status: 303, headers: { 'Location': `/file-share/${fileShareId}` } });
return await FileShareModel.createSessionCookie(request, response, fileShareId, hashedPassword);
} catch (error) {
console.error('File share verification error:', error);
return await context.render({
error: {
title: 'Verification Failed',
message: (error as Error).message,
},
});
}
},
};
export default function ShareVerifyPage({ data }: PageProps<Data, FreshContextState>) {
return (
<main>
<section class='max-w-screen-md mx-auto flex flex-col items-center justify-center'>
<ShareVerifyForm
error={data.error}
/>
</section>
</main>
);
}