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 = `${safeName}`; @@ -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 + ? '' + : `
+ + ${nav} +
`; + return ` @@ -19,10 +26,7 @@ export function layout(title: string, body: string, opts: { authed?: boolean; hi -
- ${hideLogo ? '' : ''} - ${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;