feat: comprehensive security and architecture improvements

- Add Zod validation schemas with strong password requirements (12+ chars, complexity)
- Implement rate limiting for authentication endpoints (registration, password reset)
- Remove duplicate MetricCard component, consolidate to ui/metric-card.tsx
- Update README.md to use pnpm commands consistently
- Enhance authentication security with 12-round bcrypt hashing
- Add comprehensive input validation for all API endpoints
- Fix security vulnerabilities in user registration and password reset flows

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-06-28 01:52:53 +02:00
parent 192f9497b4
commit 7f48a085bf
68 changed files with 8045 additions and 4542 deletions

View File

@ -1,222 +1,237 @@
// Unit tests for transcript fetcher
import { describe, it, expect, beforeEach, vi } from 'vitest';
import fetch from 'node-fetch';
import {
fetchTranscriptContent,
isValidTranscriptUrl,
extractSessionIdFromTranscript
} from '../../lib/transcriptFetcher';
import { describe, it, expect, beforeEach, vi } from "vitest";
import fetch from "node-fetch";
import {
fetchTranscriptContent,
isValidTranscriptUrl,
extractSessionIdFromTranscript,
} from "../../lib/transcriptFetcher";
// Mock node-fetch
const mockFetch = fetch as any;
describe('Transcript Fetcher', () => {
describe("Transcript Fetcher", () => {
beforeEach(() => {
vi.clearAllMocks();
});
describe('fetchTranscriptContent', () => {
it('should successfully fetch transcript content', async () => {
describe("fetchTranscriptContent", () => {
it("should successfully fetch transcript content", async () => {
const mockResponse = {
ok: true,
text: vi.fn().mockResolvedValue('Session transcript content'),
};
mockFetch.mockResolvedValue(mockResponse as any);
const result = await fetchTranscriptContent('https://example.com/transcript');
expect(result.success).toBe(true);
expect(result.content).toBe('Session transcript content');
expect(result.error).toBeUndefined();
expect(mockFetch).toHaveBeenCalledWith('https://example.com/transcript', {
method: 'GET',
headers: {
'User-Agent': 'LiveDash-Transcript-Fetcher/1.0',
},
signal: expect.any(AbortSignal),
});
});
it('should handle authentication with username and password', async () => {
const mockResponse = {
ok: true,
text: vi.fn().mockResolvedValue('Authenticated transcript'),
text: vi.fn().mockResolvedValue("Session transcript content"),
};
mockFetch.mockResolvedValue(mockResponse as any);
const result = await fetchTranscriptContent(
'https://example.com/transcript',
'user123',
'pass456'
"https://example.com/transcript"
);
expect(result.success).toBe(true);
expect(result.content).toBe('Authenticated transcript');
const expectedAuth = 'Basic ' + Buffer.from('user123:pass456').toString('base64');
expect(mockFetch).toHaveBeenCalledWith('https://example.com/transcript', {
method: 'GET',
expect(result.content).toBe("Session transcript content");
expect(result.error).toBeUndefined();
expect(mockFetch).toHaveBeenCalledWith("https://example.com/transcript", {
method: "GET",
headers: {
'User-Agent': 'LiveDash-Transcript-Fetcher/1.0',
'Authorization': expectedAuth,
"User-Agent": "LiveDash-Transcript-Fetcher/1.0",
},
signal: expect.any(AbortSignal),
});
});
it('should handle HTTP errors', async () => {
it("should handle authentication with username and password", async () => {
const mockResponse = {
ok: true,
text: vi.fn().mockResolvedValue("Authenticated transcript"),
};
mockFetch.mockResolvedValue(mockResponse as any);
const result = await fetchTranscriptContent(
"https://example.com/transcript",
"user123",
"pass456"
);
expect(result.success).toBe(true);
expect(result.content).toBe("Authenticated transcript");
const expectedAuth =
"Basic " + Buffer.from("user123:pass456").toString("base64");
expect(mockFetch).toHaveBeenCalledWith("https://example.com/transcript", {
method: "GET",
headers: {
"User-Agent": "LiveDash-Transcript-Fetcher/1.0",
Authorization: expectedAuth,
},
signal: expect.any(AbortSignal),
});
});
it("should handle HTTP errors", async () => {
const mockResponse = {
ok: false,
status: 404,
statusText: 'Not Found',
statusText: "Not Found",
};
mockFetch.mockResolvedValue(mockResponse as any);
const result = await fetchTranscriptContent('https://example.com/transcript');
const result = await fetchTranscriptContent(
"https://example.com/transcript"
);
expect(result.success).toBe(false);
expect(result.error).toBe('HTTP 404: Not Found');
expect(result.error).toBe("HTTP 404: Not Found");
expect(result.content).toBeUndefined();
});
it('should handle empty transcript content', async () => {
it("should handle empty transcript content", async () => {
const mockResponse = {
ok: true,
text: vi.fn().mockResolvedValue(' '),
text: vi.fn().mockResolvedValue(" "),
};
mockFetch.mockResolvedValue(mockResponse as any);
const result = await fetchTranscriptContent('https://example.com/transcript');
const result = await fetchTranscriptContent(
"https://example.com/transcript"
);
expect(result.success).toBe(false);
expect(result.error).toBe('Empty transcript content');
expect(result.error).toBe("Empty transcript content");
expect(result.content).toBeUndefined();
});
it('should handle network errors', async () => {
mockFetch.mockRejectedValue(new Error('ENOTFOUND example.com'));
it("should handle network errors", async () => {
mockFetch.mockRejectedValue(new Error("ENOTFOUND example.com"));
const result = await fetchTranscriptContent('https://example.com/transcript');
const result = await fetchTranscriptContent(
"https://example.com/transcript"
);
expect(result.success).toBe(false);
expect(result.error).toBe('Domain not found');
expect(result.error).toBe("Domain not found");
expect(result.content).toBeUndefined();
});
it('should handle connection refused errors', async () => {
mockFetch.mockRejectedValue(new Error('ECONNREFUSED'));
it("should handle connection refused errors", async () => {
mockFetch.mockRejectedValue(new Error("ECONNREFUSED"));
const result = await fetchTranscriptContent('https://example.com/transcript');
const result = await fetchTranscriptContent(
"https://example.com/transcript"
);
expect(result.success).toBe(false);
expect(result.error).toBe('Connection refused');
expect(result.error).toBe("Connection refused");
expect(result.content).toBeUndefined();
});
it('should handle timeout errors', async () => {
mockFetch.mockRejectedValue(new Error('Request timeout'));
it("should handle timeout errors", async () => {
mockFetch.mockRejectedValue(new Error("Request timeout"));
const result = await fetchTranscriptContent('https://example.com/transcript');
const result = await fetchTranscriptContent(
"https://example.com/transcript"
);
expect(result.success).toBe(false);
expect(result.error).toBe('Request timeout');
expect(result.error).toBe("Request timeout");
expect(result.content).toBeUndefined();
});
it('should handle empty URL', async () => {
const result = await fetchTranscriptContent('');
it("should handle empty URL", async () => {
const result = await fetchTranscriptContent("");
expect(result.success).toBe(false);
expect(result.error).toBe('No transcript URL provided');
expect(result.error).toBe("No transcript URL provided");
expect(result.content).toBeUndefined();
expect(mockFetch).not.toHaveBeenCalled();
});
it('should trim whitespace from content', async () => {
it("should trim whitespace from content", async () => {
const mockResponse = {
ok: true,
text: vi.fn().mockResolvedValue(' \n Session content \n '),
text: vi.fn().mockResolvedValue(" \n Session content \n "),
};
mockFetch.mockResolvedValue(mockResponse as any);
const result = await fetchTranscriptContent('https://example.com/transcript');
const result = await fetchTranscriptContent(
"https://example.com/transcript"
);
expect(result.success).toBe(true);
expect(result.content).toBe('Session content');
expect(result.content).toBe("Session content");
});
});
describe('isValidTranscriptUrl', () => {
it('should validate HTTP URLs', () => {
expect(isValidTranscriptUrl('http://example.com/transcript')).toBe(true);
describe("isValidTranscriptUrl", () => {
it("should validate HTTP URLs", () => {
expect(isValidTranscriptUrl("http://example.com/transcript")).toBe(true);
});
it('should validate HTTPS URLs', () => {
expect(isValidTranscriptUrl('https://example.com/transcript')).toBe(true);
it("should validate HTTPS URLs", () => {
expect(isValidTranscriptUrl("https://example.com/transcript")).toBe(true);
});
it('should reject invalid URLs', () => {
expect(isValidTranscriptUrl('not-a-url')).toBe(false);
expect(isValidTranscriptUrl('ftp://example.com')).toBe(false);
expect(isValidTranscriptUrl('')).toBe(false);
it("should reject invalid URLs", () => {
expect(isValidTranscriptUrl("not-a-url")).toBe(false);
expect(isValidTranscriptUrl("ftp://example.com")).toBe(false);
expect(isValidTranscriptUrl("")).toBe(false);
expect(isValidTranscriptUrl(null as any)).toBe(false);
expect(isValidTranscriptUrl(undefined as any)).toBe(false);
});
it('should handle malformed URLs', () => {
expect(isValidTranscriptUrl('http://')).toBe(false);
expect(isValidTranscriptUrl('https://')).toBe(false);
expect(isValidTranscriptUrl('://example.com')).toBe(false);
it("should handle malformed URLs", () => {
expect(isValidTranscriptUrl("http://")).toBe(false);
expect(isValidTranscriptUrl("https://")).toBe(false);
expect(isValidTranscriptUrl("://example.com")).toBe(false);
});
});
describe('extractSessionIdFromTranscript', () => {
it('should extract session ID from session_id pattern', () => {
const content = 'session_id: abc123def456\nOther content...';
describe("extractSessionIdFromTranscript", () => {
it("should extract session ID from session_id pattern", () => {
const content = "session_id: abc123def456\nOther content...";
const result = extractSessionIdFromTranscript(content);
expect(result).toBe('abc123def456');
expect(result).toBe("abc123def456");
});
it('should extract session ID from sessionId pattern', () => {
const content = 'sessionId: xyz789\nTranscript data...';
it("should extract session ID from sessionId pattern", () => {
const content = "sessionId: xyz789\nTranscript data...";
const result = extractSessionIdFromTranscript(content);
expect(result).toBe('xyz789');
expect(result).toBe("xyz789");
});
it('should extract session ID from id pattern', () => {
const content = 'id: session-12345678\nChat log...';
it("should extract session ID from id pattern", () => {
const content = "id: session-12345678\nChat log...";
const result = extractSessionIdFromTranscript(content);
expect(result).toBe('session-12345678');
expect(result).toBe("session-12345678");
});
it('should extract session ID from first line', () => {
const content = 'abc123def456\nUser: Hello\nBot: Hi there';
it("should extract session ID from first line", () => {
const content = "abc123def456\nUser: Hello\nBot: Hi there";
const result = extractSessionIdFromTranscript(content);
expect(result).toBe('abc123def456');
expect(result).toBe("abc123def456");
});
it('should return null for content without session ID', () => {
const content = 'User: Hello\nBot: Hi there\nUser: How are you?';
it("should return null for content without session ID", () => {
const content = "User: Hello\nBot: Hi there\nUser: How are you?";
const result = extractSessionIdFromTranscript(content);
expect(result).toBe(null);
});
it('should return null for empty content', () => {
expect(extractSessionIdFromTranscript('')).toBe(null);
it("should return null for empty content", () => {
expect(extractSessionIdFromTranscript("")).toBe(null);
expect(extractSessionIdFromTranscript(null as any)).toBe(null);
expect(extractSessionIdFromTranscript(undefined as any)).toBe(null);
});
it('should handle case-insensitive patterns', () => {
const content = 'SESSION_ID: ABC123\nContent...';
it("should handle case-insensitive patterns", () => {
const content = "SESSION_ID: ABC123\nContent...";
const result = extractSessionIdFromTranscript(content);
expect(result).toBe('ABC123');
expect(result).toBe("ABC123");
});
it('should extract the first matching pattern', () => {
const content = 'session_id: first123\nid: second456\nMore content...';
it("should extract the first matching pattern", () => {
const content = "session_id: first123\nid: second456\nMore content...";
const result = extractSessionIdFromTranscript(content);
expect(result).toBe('first123');
expect(result).toBe("first123");
});
});
});