diff --git a/src/constants.ts b/src/constants.ts index bea0d34..943ab53 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -4,7 +4,5 @@ export const SESSION_TTL_DAYS = 30; export const SESSION_TTL_SECONDS = SESSION_TTL_DAYS * 24 * 60 * 60; export const SESSION_RENEW_THRESHOLD_SECONDS = 60 * 60; -// Phase 1: keep cookie name as 'token' so existing tests stay green. -// Phase 2 flips this to 'nanodrop_session'. -export const SESSION_COOKIE_NAME = 'token'; +export const SESSION_COOKIE_NAME = 'nanodrop_session'; export const LOGOUT_PATHS = new Set(['/logout', '/api/v1/auth/logout']); diff --git a/src/server.ts b/src/server.ts index cbc0ce3..67e2864 100644 --- a/src/server.ts +++ b/src/server.ts @@ -9,6 +9,7 @@ import { join } from 'path'; import { fileURLToPath } from 'url'; import type Database from 'better-sqlite3'; import type { Config } from './config.ts'; +import { SESSION_COOKIE_NAME } from './constants.ts'; import { createLogger } from './middleware/logging.ts'; import { createLockoutService } from './services/lockout.ts'; import { authApiRoutes } from './routes/api/v1/auth.ts'; @@ -30,7 +31,7 @@ export function createServer({ config, db }: ServerDeps) { app.register(fastifyCookie); app.register(fastifyJwt, { secret: config.jwtSecret, - cookie: { cookieName: 'token', signed: false }, + cookie: { cookieName: SESSION_COOKIE_NAME, signed: false }, }); app.register(fastifyFormbody); app.register(fastifyMultipart, { limits: { fileSize: config.maxFileSize } }); diff --git a/tests/helpers/setup.ts b/tests/helpers/setup.ts index a105ee6..2c466b1 100644 --- a/tests/helpers/setup.ts +++ b/tests/helpers/setup.ts @@ -3,6 +3,7 @@ import { tmpdir } from 'os'; import { join } from 'path'; import { initDb } from '../../src/db/schema.ts'; import { createServer } from '../../src/server.ts'; +import { SESSION_COOKIE_NAME } from '../../src/constants.ts'; import type { Config } from '../../src/config.ts'; import type Database from 'better-sqlite3'; import type { FastifyInstance } from 'fastify'; @@ -15,7 +16,7 @@ export async function loginAs(app: FastifyInstance, username: string, password: body: JSON.stringify({ username, password }), }); const cookie = res.headers['set-cookie'] as string; - return cookie.split(';')[0].replace('token=', ''); + return cookie.split(';')[0].replace(`${SESSION_COOKIE_NAME}=`, ''); } interface MultipartFile { diff --git a/tests/integration/auth-api.test.ts b/tests/integration/auth-api.test.ts index b8636d1..b26ff25 100644 --- a/tests/integration/auth-api.test.ts +++ b/tests/integration/auth-api.test.ts @@ -26,7 +26,8 @@ describe('POST /api/v1/auth/login', () => { }); expect(res.statusCode).toBe(200); expect(res.json()).toEqual({ ok: true }); - expect(res.headers['set-cookie']).toMatch(/token=/); + expect(res.headers['set-cookie']).toMatch(/nanodrop_session=/); + expect(res.headers['set-cookie']).toMatch(/Max-Age=2592000/); }); it('returns 401 on wrong password', async () => { @@ -76,7 +77,7 @@ describe('POST /api/v1/auth/logout', () => { body: JSON.stringify({ username: 'alice', password: 'secret' }), }); const cookie = res.headers['set-cookie'] as string; - token = cookie.split(';')[0].replace('token=', ''); + token = cookie.split(';')[0].replace('nanodrop_session=', ''); }); afterEach(async () => { @@ -88,10 +89,10 @@ describe('POST /api/v1/auth/logout', () => { const res = await ctx.app.inject({ method: 'POST', url: '/api/v1/auth/logout', - cookies: { token }, + cookies: { nanodrop_session: token }, }); expect(res.statusCode).toBe(200); - expect(res.headers['set-cookie']).toMatch(/token=;/); + expect(res.headers['set-cookie']).toMatch(/nanodrop_session=;/); }); it('returns 401 without cookie', async () => { diff --git a/tests/integration/auth-rate-limit.test.ts b/tests/integration/auth-rate-limit.test.ts index bfb898b..091cd15 100644 --- a/tests/integration/auth-rate-limit.test.ts +++ b/tests/integration/auth-rate-limit.test.ts @@ -56,14 +56,14 @@ describe('per-IP rate limit on login routes', () => { body: JSON.stringify({ username: 'alice', password: 'correct-pw' }), }); expect(loginRes.statusCode).toBe(200); - const cookie = (loginRes.headers['set-cookie'] as string).split(';')[0].replace('token=', ''); + const cookie = (loginRes.headers['set-cookie'] as string).split(';')[0].replace('nanodrop_session=', ''); // Now hit /upload (GET) repeatedly past the login-route limit threshold for (let i = 0; i < 6; i++) { const r = await ctx.app.inject({ method: 'GET', url: '/upload', - cookies: { token: cookie }, + cookies: { nanodrop_session: cookie }, }); expect(r.statusCode).toBe(200); } diff --git a/tests/integration/files-api.test.ts b/tests/integration/files-api.test.ts index 52b8893..3be5882 100644 --- a/tests/integration/files-api.test.ts +++ b/tests/integration/files-api.test.ts @@ -23,7 +23,7 @@ describe('GET /api/v1/files', () => { const res = await ctx.app.inject({ method: 'GET', url: '/api/v1/files', - cookies: { token }, + cookies: { nanodrop_session: token }, }); expect(res.statusCode).toBe(200); expect(res.json().files).toEqual([]); @@ -55,7 +55,7 @@ describe('POST /api/v1/files', () => { const res = await ctx.app.inject({ method: 'POST', url: '/api/v1/files', - cookies: { token }, + cookies: { nanodrop_session: token }, ...buildMultipart({ file: { filename: 'test.txt', contentType: 'text/plain', data: Buffer.from('hello') } }), }); expect(res.statusCode).toBe(201); @@ -84,7 +84,7 @@ describe('DELETE /api/v1/files/:id', () => { const uploadRes = await ctx.app.inject({ method: 'POST', url: '/api/v1/files', - cookies: { token }, + cookies: { nanodrop_session: token }, ...buildMultipart({ file: { filename: 'f.txt', contentType: 'text/plain', data: Buffer.from('data') } }), }); fileId = uploadRes.json().file.id; @@ -99,7 +99,7 @@ describe('DELETE /api/v1/files/:id', () => { const res = await ctx.app.inject({ method: 'DELETE', url: `/api/v1/files/${fileId}`, - cookies: { token }, + cookies: { nanodrop_session: token }, }); expect(res.statusCode).toBe(200); }); @@ -108,7 +108,7 @@ describe('DELETE /api/v1/files/:id', () => { const res = await ctx.app.inject({ method: 'DELETE', url: '/api/v1/files/doesnotexist', - cookies: { token }, + cookies: { nanodrop_session: token }, }); expect(res.statusCode).toBe(404); }); diff --git a/tests/integration/pages.test.ts b/tests/integration/pages.test.ts index 20402c8..d5ccb60 100644 --- a/tests/integration/pages.test.ts +++ b/tests/integration/pages.test.ts @@ -25,7 +25,7 @@ describe('GET /', () => { it('redirects to /upload when authenticated', async () => { const token = await loginAs(ctx.app, 'alice', 'secret'); - const res = await ctx.app.inject({ method: 'GET', url: '/', cookies: { token } }); + const res = await ctx.app.inject({ method: 'GET', url: '/', cookies: { nanodrop_session: token } }); expect(res.statusCode).toBe(302); expect(res.headers['location']).toBe('/upload'); }); @@ -46,7 +46,7 @@ describe('POST /login (page)', () => { }); expect(res.statusCode).toBe(302); expect(res.headers['location']).toBe('/upload'); - expect(res.headers['set-cookie']).toMatch(/token=/); + expect(res.headers['set-cookie']).toMatch(/nanodrop_session=/); }); it('shows login page with error on invalid credentials', async () => { @@ -72,7 +72,7 @@ describe('GET /upload + POST /upload', () => { afterEach(async () => { await ctx.app.close(); ctx.cleanup(); }); it('shows upload form', async () => { - const res = await ctx.app.inject({ method: 'GET', url: '/upload', cookies: { token } }); + const res = await ctx.app.inject({ method: 'GET', url: '/upload', cookies: { nanodrop_session: token } }); expect(res.statusCode).toBe(200); expect(res.body).toContain('Upload'); }); @@ -87,7 +87,7 @@ describe('GET /upload + POST /upload', () => { const res = await ctx.app.inject({ method: 'POST', url: '/upload', - cookies: { token }, + cookies: { nanodrop_session: token }, ...buildMultipart({ file: { filename: 'doc.txt', contentType: 'text/plain', data: Buffer.from('content') } }), }); expect(res.statusCode).toBe(200); @@ -106,7 +106,7 @@ describe('GET /f/:id and GET /f/:id/raw', () => { const uploadRes = await ctx.app.inject({ method: 'POST', url: '/upload', - cookies: { token }, + cookies: { nanodrop_session: token }, ...buildMultipart({ file: { filename: 'hello.txt', contentType: 'text/plain', data: Buffer.from('hello!') } }), }); // Extract file id from response body @@ -188,7 +188,7 @@ describe('GET /f/:id — image inline', () => { const uploadRes = await ctx.app.inject({ method: 'POST', url: '/upload', - cookies: { token }, + cookies: { nanodrop_session: token }, ...buildMultipart({ file: { filename: 'photo.png', contentType: 'image/png', data: Buffer.from('fakepng') } }), }); const match = uploadRes.body.match(/\/f\/([^/"]+)/); @@ -213,14 +213,14 @@ describe('GET /files — copy link', () => { await ctx.app.inject({ method: 'POST', url: '/upload', - cookies: { token }, + cookies: { nanodrop_session: token }, ...buildMultipart({ file: { filename: 'test.txt', contentType: 'text/plain', data: Buffer.from('hi') } }), }); }); afterEach(async () => { await ctx.app.close(); ctx.cleanup(); }); it('shows Copy link button for each file', async () => { - const res = await ctx.app.inject({ method: 'GET', url: '/files', cookies: { token } }); + const res = await ctx.app.inject({ method: 'GET', url: '/files', cookies: { nanodrop_session: token } }); expect(res.statusCode).toBe(200); expect(res.body).toContain('Copy link'); expect(res.body).toContain('class="table-wrap"'); @@ -245,7 +245,7 @@ describe('GET /f/:id — owner-aware header', () => { const uploadRes = await ctx.app.inject({ method: 'POST', url: '/upload', - cookies: { token: aliceToken }, + cookies: { nanodrop_session: aliceToken }, ...buildMultipart({ file: { filename: 'owned.txt', contentType: 'text/plain', data: Buffer.from('data') } }), }); const match = uploadRes.body.match(/\/f\/([^/"]+)/); @@ -254,19 +254,19 @@ describe('GET /f/:id — owner-aware header', () => { 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 } }); + const res = await ctx.app.inject({ method: 'GET', url: `/f/${fileId}`, cookies: { nanodrop_session: 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 } }); + const res = await ctx.app.inject({ method: 'GET', url: `/f/${fileId}`, cookies: { nanodrop_session: 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 } }); + const res = await ctx.app.inject({ method: 'GET', url: `/f/${fileId}`, cookies: { nanodrop_session: bobToken } }); expect(res.statusCode).toBe(200); expect(res.body).not.toContain(' { const uploadRes = await ctx.app.inject({ method: 'POST', url: '/upload', - cookies: { token }, + cookies: { nanodrop_session: token }, ...buildMultipart({ file: { filename: 'del.txt', contentType: 'text/plain', data: Buffer.from('bye') } }), }); const match = uploadRes.body.match(/\/f\/([^/"]+)/); @@ -301,7 +301,7 @@ describe('POST /files/:id/delete', () => { const res = await ctx.app.inject({ method: 'POST', url: `/files/${fileId}/delete`, - cookies: { token }, + cookies: { nanodrop_session: token }, }); expect(res.statusCode).toBe(302); expect(res.headers['location']).toBe('/files');