diff --git a/public/style.css b/public/style.css
index 5ced5f4..7ce63c9 100644
--- a/public/style.css
+++ b/public/style.css
@@ -170,8 +170,7 @@ input[type="file"] {
input[type="file"]::-webkit-file-upload-button {
font-family: var(--font);
font-size: 11px;
- letter-spacing: 0.06em;
- text-transform: uppercase;
+ letter-spacing: 0.02em;
background: var(--black);
color: var(--white);
border: none;
@@ -182,8 +181,7 @@ input[type="file"]::-webkit-file-upload-button {
input[type="file"]::file-selector-button {
font-family: var(--font);
font-size: 11px;
- letter-spacing: 0.06em;
- text-transform: uppercase;
+ letter-spacing: 0.02em;
background: var(--black);
color: var(--white);
border: none;
@@ -200,8 +198,7 @@ button[type="submit"],
font-family: var(--font);
font-size: 11px;
font-weight: 600;
- letter-spacing: 0.08em;
- text-transform: uppercase;
+ letter-spacing: 0.02em;
background: var(--black);
color: var(--white);
border: var(--border);
@@ -237,8 +234,7 @@ a.btn {
padding: 0.6rem 1.25rem;
font-size: 11px;
font-weight: 600;
- letter-spacing: 0.08em;
- text-transform: uppercase;
+ letter-spacing: 0.02em;
text-decoration: none;
background: var(--black);
color: var(--white);
@@ -285,8 +281,7 @@ a.btn:hover {
font-family: var(--font);
font-size: 11px;
font-weight: 600;
- letter-spacing: 0.08em;
- text-transform: uppercase;
+ letter-spacing: 0.02em;
background: var(--white);
color: var(--black);
border: var(--border);
diff --git a/src/routes/pages.ts b/src/routes/pages.ts
index c14f363..ac1a1b9 100644
--- a/src/routes/pages.ts
+++ b/src/routes/pages.ts
@@ -119,9 +119,16 @@ export const pageRoutes: FastifyPluginAsync<{ deps: Deps }> = async (app, { deps
reply.redirect('/files');
});
- // GET /f/:id — public file view
+ // GET /f/:id — public file view (owner-aware)
app.get<{ Params: { id: string } }>('/f/:id', async (request, reply) => {
const { id } = request.params;
+
+ let userId: number | null = null;
+ try {
+ await request.jwtVerify();
+ userId = (request.user as JwtPayload).sub;
+ } catch { /* not logged in — fine */ }
+
const file = getFileById(db, id);
if (!file) {
@@ -129,7 +136,8 @@ export const pageRoutes: FastifyPluginAsync<{ deps: Deps }> = async (app, { deps
return reply.status(404).type('text/html').send(notFoundPage());
}
- reply.type('text/html').send(fileViewPage(file));
+ const isOwner = userId !== null && userId === file.user_id;
+ reply.type('text/html').send(fileViewPage(file, isOwner));
});
// GET /f/:id/raw — serve raw file
diff --git a/src/views/file-view.ts b/src/views/file-view.ts
index 4ff28ae..628ce86 100644
--- a/src/views/file-view.ts
+++ b/src/views/file-view.ts
@@ -1,7 +1,7 @@
import { layout, escHtml } from './layout.ts';
import type { FileRow } from '../db/files.ts';
-export function fileViewPage(file: FileRow): string {
+export function fileViewPage(file: FileRow, isOwner: boolean): string {
const rawUrl = escHtml(`/f/${file.id}/raw`);
const safeName = escHtml(file.original_name);
const actions = `
@@ -10,6 +10,12 @@ export function fileViewPage(file: FileRow): string {
Open
`;
+ const deleteForm = isOwner
+ ? `
`
+ : '';
+
let media = '';
if (file.mime_type.startsWith('image/')) {
media = `
`;
@@ -19,11 +25,14 @@ export function fileViewPage(file: FileRow): string {
media = ``;
}
+ const layoutOpts = isOwner ? { authed: true } : { hideHeader: true };
+
return layout(file.original_name, `
${safeName}
${media}
${actions}
+ ${deleteForm}
- `, { hideLogo: true });
+ `, layoutOpts);
}
diff --git a/src/views/layout.ts b/src/views/layout.ts
index c4312ba..a519c48 100644
--- a/src/views/layout.ts
+++ b/src/views/layout.ts
@@ -1,5 +1,5 @@
-export function layout(title: string, body: string, opts: { authed?: boolean; hideLogo?: boolean } = {}): string {
- const { authed = false, hideLogo = false } = opts;
+export function layout(title: string, body: string, opts: { authed?: boolean; hideHeader?: boolean } = {}): string {
+ const { authed = false, hideHeader = false } = opts;
const nav = authed
? ``
: '';
+ const header = hideHeader
+ ? ''
+ : ` `;
+
return `
@@ -19,10 +26,7 @@ export function layout(title: string, body: string, opts: { authed?: boolean; hi
-
- ${hideLogo ? '' : 'Nanodrop'}
- ${nav}
-
+${header}
${body}
diff --git a/tests/integration/pages.test.ts b/tests/integration/pages.test.ts
index e395053..322d60b 100644
--- a/tests/integration/pages.test.ts
+++ b/tests/integration/pages.test.ts
@@ -181,6 +181,57 @@ describe('GET /files — copy link', () => {
});
});
+describe('GET /f/:id — owner-aware header', () => {
+ let ctx: TestContext;
+ let aliceToken: string;
+ let bobToken: string;
+ let fileId: string;
+
+ beforeEach(async () => {
+ ctx = createTestApp();
+ const hash = await hashPassword('secret');
+ createUser(ctx.db, { username: 'alice', passwordHash: hash });
+ createUser(ctx.db, { username: 'bob', passwordHash: hash });
+
+ aliceToken = await loginAs(ctx.app, 'alice', 'secret');
+ bobToken = await loginAs(ctx.app, 'bob', 'secret');
+
+ const uploadRes = await ctx.app.inject({
+ method: 'POST',
+ url: '/upload',
+ cookies: { token: aliceToken },
+ ...buildMultipart({ file: { filename: 'owned.txt', contentType: 'text/plain', data: Buffer.from('data') } }),
+ });
+ const match = uploadRes.body.match(/\/f\/([^/"]+)/);
+ fileId = match?.[1] ?? '';
+ });
+ afterEach(async () => { await ctx.app.close(); ctx.cleanup(); });
+
+ it('shows nav when owner views their file', async () => {
+ const res = await ctx.app.inject({ method: 'GET', url: `/f/${fileId}`, cookies: { token: aliceToken } });
+ expect(res.statusCode).toBe(200);
+ expect(res.body).toContain('My Files');
+ });
+
+ it('shows delete button when owner views', async () => {
+ const res = await ctx.app.inject({ method: 'GET', url: `/f/${fileId}`, cookies: { token: aliceToken } });
+ expect(res.statusCode).toBe(200);
+ expect(res.body).toContain('delete');
+ });
+
+ it('no header when non-owner views', async () => {
+ const res = await ctx.app.inject({ method: 'GET', url: `/f/${fileId}`, cookies: { token: bobToken } });
+ expect(res.statusCode).toBe(200);
+ expect(res.body).not.toContain(' {
+ const res = await ctx.app.inject({ method: 'GET', url: `/f/${fileId}` });
+ expect(res.statusCode).toBe(200);
+ expect(res.body).not.toContain(' {
let ctx: TestContext;
let token: string;