Add auth service, storage service, types, and logging middleware

- Auth service: hashPassword/verifyPassword via bcrypt
- Storage service: saveFile, getFilePath, deleteStoredFile with ENOENT handling
- Types: JwtPayload interface
- Logging middleware: createLogger writing AUTH_FAILURE, AUTH_SUCCESS, FILE_NOT_FOUND
- 27 tests passing

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-03 15:49:22 -08:00
parent b6aa6211a9
commit 157d1e8230
6 changed files with 178 additions and 0 deletions

View File

@@ -0,0 +1,60 @@
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { mkdtempSync, rmSync, readFileSync, existsSync } from 'fs';
import { tmpdir } from 'os';
import { join } from 'path';
import { createLogger } from '../../src/middleware/logging.ts';
describe('middleware/logging', () => {
let logDir: string;
let logFile: string;
beforeEach(() => {
logDir = mkdtempSync(join(tmpdir(), 'nanodrop-log-test-'));
logFile = join(logDir, 'test.log');
});
afterEach(() => {
rmSync(logDir, { recursive: true, force: true });
});
it('writes AUTH_FAILURE log entry', async () => {
const logger = createLogger(logFile);
await logger.authFailure({ ip: '1.2.3.4', userAgent: 'TestAgent/1.0', username: 'admin' });
const content = readFileSync(logFile, 'utf-8');
expect(content).toMatch(/AUTH_FAILURE/);
expect(content).toMatch(/ip=1\.2\.3\.4/);
expect(content).toMatch(/username="admin"/);
expect(content).toMatch(/user-agent="TestAgent\/1\.0"/);
});
it('writes AUTH_SUCCESS log entry', async () => {
const logger = createLogger(logFile);
await logger.authSuccess({ ip: '1.2.3.4', userAgent: 'TestAgent/1.0', username: 'bob' });
const content = readFileSync(logFile, 'utf-8');
expect(content).toMatch(/AUTH_SUCCESS/);
expect(content).toMatch(/username="bob"/);
});
it('writes FILE_NOT_FOUND log entry', async () => {
const logger = createLogger(logFile);
await logger.fileNotFound({ ip: '5.6.7.8', userAgent: 'curl/7.0', fileId: 'abc123' });
const content = readFileSync(logFile, 'utf-8');
expect(content).toMatch(/FILE_NOT_FOUND/);
expect(content).toMatch(/file_id="abc123"/);
});
it('creates log file if it does not exist', async () => {
expect(existsSync(logFile)).toBe(false);
const logger = createLogger(logFile);
await logger.authSuccess({ ip: '1.1.1.1', userAgent: 'x', username: 'u' });
expect(existsSync(logFile)).toBe(true);
});
it('appends multiple entries', async () => {
const logger = createLogger(logFile);
await logger.authSuccess({ ip: '1.1.1.1', userAgent: 'a', username: 'u1' });
await logger.authFailure({ ip: '2.2.2.2', userAgent: 'b', username: 'u2' });
const lines = readFileSync(logFile, 'utf-8').trim().split('\n');
expect(lines).toHaveLength(2);
});
});

View File

@@ -0,0 +1,42 @@
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { mkdtempSync, rmSync, existsSync, readFileSync } from 'fs';
import { tmpdir } from 'os';
import { join } from 'path';
import { saveFile, deleteStoredFile, getFilePath } from '../../src/services/storage.ts';
describe('services/storage', () => {
let uploadDir: string;
beforeEach(() => {
uploadDir = mkdtempSync(join(tmpdir(), 'nanodrop-test-'));
});
afterEach(() => {
rmSync(uploadDir, { recursive: true, force: true });
});
it('saves a file buffer and returns stored name', async () => {
const content = Buffer.from('hello world');
const storedName = await saveFile(uploadDir, 'file1.txt', content);
expect(storedName).toBe('file1.txt');
expect(existsSync(join(uploadDir, 'file1.txt'))).toBe(true);
expect(readFileSync(join(uploadDir, 'file1.txt'))).toEqual(content);
});
it('returns full path for a stored file', () => {
const path = getFilePath(uploadDir, 'file1.txt');
expect(path).toBe(join(uploadDir, 'file1.txt'));
});
it('deletes a stored file', async () => {
const content = Buffer.from('to delete');
await saveFile(uploadDir, 'todelete.txt', content);
expect(existsSync(join(uploadDir, 'todelete.txt'))).toBe(true);
await deleteStoredFile(uploadDir, 'todelete.txt');
expect(existsSync(join(uploadDir, 'todelete.txt'))).toBe(false);
});
it('does not throw when deleting non-existent file', async () => {
await expect(deleteStoredFile(uploadDir, 'ghost.txt')).resolves.not.toThrow();
});
});