Skip to content

Commit eeeff91

Browse files
Raushan kumarclaude
andcommitted
Add high priority production improvements
## Security - Add Firestore security rules for user data protection - Fix XSS vulnerability in Notes.jsx (use DOMPurify) - Add input validation utilities with sanitization ## User Experience - Add 404 NotFound component for unknown routes - Add reusable Skeleton loading components ## Testing - Add validation utility tests - Add AddTodo component tests - Add NotFound component tests - Add @testing-library/user-event dependency ## Input Validation - Validate todo text (required, max length) - Validate notes (title/content length limits) - Validate URLs, emails, numbers, dates - Validate expenses and IP addresses Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 99122cf commit eeeff91

10 files changed

Lines changed: 949 additions & 11 deletions

File tree

firestore.rules

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
rules_version = '2';
2+
3+
service cloud.firestore {
4+
match /databases/{database}/documents {
5+
// Helper functions
6+
function isAuthenticated() {
7+
return request.auth != null;
8+
}
9+
10+
function isOwner(userId) {
11+
return isAuthenticated() && request.auth.uid == userId;
12+
}
13+
14+
function isValidString(field, maxLength) {
15+
return field is string && field.size() <= maxLength;
16+
}
17+
18+
function isValidTimestamp(field) {
19+
return field is timestamp || field is string;
20+
}
21+
22+
// Users collection - each user can only access their own data
23+
match /users/{userId} {
24+
// Allow read/write only to the owner
25+
allow read, write: if isOwner(userId);
26+
27+
// User data subcollection (todos, attendance, notes, etc.)
28+
match /data/{document} {
29+
allow read: if isOwner(userId);
30+
31+
// Validate writes based on document type
32+
allow write: if isOwner(userId) && validateDocument(document);
33+
}
34+
}
35+
36+
// Document validation function
37+
function validateDocument(docType) {
38+
return docType in ['todos', 'attendance', 'notes', 'officeConfig',
39+
'gamification', 'leaves', 'trips', 'expenses',
40+
'salary', 'links', 'meetings', 'pomodoro'];
41+
}
42+
43+
// Deny all other access by default
44+
match /{document=**} {
45+
allow read, write: if false;
46+
}
47+
}
48+
}

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
"@eslint/js": "^9.39.1",
2424
"@testing-library/jest-dom": "^6.6.3",
2525
"@testing-library/react": "^16.3.0",
26+
"@testing-library/user-event": "^14.6.1",
2627
"@types/react": "^19.2.5",
2728
"@types/react-dom": "^19.2.3",
2829
"@vitejs/plugin-react": "^5.1.1",

src/App.jsx

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@ import { GamificationProvider } from './contexts/GamificationContext';
55
import { ErrorBoundary } from './components/ErrorBoundary';
66
import { SkipLink } from './components/SkipLink';
77
import { OfflineIndicator } from './components/OfflineIndicator';
8+
import { NotFound } from './components/NotFound';
89
import { useTodos } from './hooks/useFirestore';
910
import { formatDateKey, formatDisplayDate, generateId } from './utils/dateUtils';
11+
import { validateTodo } from './utils/validation';
1012

1113
// Navigation components (always needed)
1214
import { Sidebar } from './components/Navigation/Sidebar';
@@ -66,7 +68,13 @@ function MainContent() {
6668
const isToday = formatDateKey(today) === dateKey;
6769

6870
const addTodo = (text) => {
69-
const newTodo = { id: generateId(), text, completed: false };
71+
// Validate todo text
72+
const validation = validateTodo(text);
73+
if (!validation.valid) {
74+
console.warn('Invalid todo:', validation.error);
75+
return;
76+
}
77+
const newTodo = { id: generateId(), text: validation.value, completed: false };
7078
setTodos({ ...todos, [dateKey]: [...currentTodos, newTodo] });
7179
};
7280

@@ -247,7 +255,8 @@ function MainContent() {
247255
return <Notes />;
248256

249257
default:
250-
return <Dashboard onNavigate={navigate} />;
258+
// Show 404 page for unknown views
259+
return <NotFound onNavigate={navigate} />;
251260
}
252261
};
253262

src/components/AddTodo.test.jsx

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import { describe, it, expect, vi } from 'vitest';
2+
import { render, screen, fireEvent } from '@testing-library/react';
3+
import userEvent from '@testing-library/user-event';
4+
import { AddTodo } from './AddTodo';
5+
6+
describe('AddTodo', () => {
7+
it('should render input and button', () => {
8+
render(<AddTodo onAdd={vi.fn()} />);
9+
10+
expect(screen.getByPlaceholderText('Add a new task...')).toBeInTheDocument();
11+
expect(screen.getByRole('button', { name: /add task/i })).toBeInTheDocument();
12+
});
13+
14+
it('should disable button when input is empty', () => {
15+
render(<AddTodo onAdd={vi.fn()} />);
16+
17+
const button = screen.getByRole('button', { name: /add task/i });
18+
expect(button).toBeDisabled();
19+
});
20+
21+
it('should enable button when input has text', async () => {
22+
const user = userEvent.setup();
23+
render(<AddTodo onAdd={vi.fn()} />);
24+
25+
const input = screen.getByPlaceholderText('Add a new task...');
26+
await user.type(input, 'New task');
27+
28+
const button = screen.getByRole('button', { name: /add task: new task/i });
29+
expect(button).not.toBeDisabled();
30+
});
31+
32+
it('should call onAdd with trimmed text on submit', async () => {
33+
const user = userEvent.setup();
34+
const onAdd = vi.fn();
35+
render(<AddTodo onAdd={onAdd} />);
36+
37+
const input = screen.getByPlaceholderText('Add a new task...');
38+
await user.type(input, ' Buy groceries ');
39+
await user.keyboard('{Enter}');
40+
41+
expect(onAdd).toHaveBeenCalledWith('Buy groceries');
42+
});
43+
44+
it('should clear input after submission', async () => {
45+
const user = userEvent.setup();
46+
render(<AddTodo onAdd={vi.fn()} />);
47+
48+
const input = screen.getByPlaceholderText('Add a new task...');
49+
await user.type(input, 'New task');
50+
await user.keyboard('{Enter}');
51+
52+
expect(input).toHaveValue('');
53+
});
54+
55+
it('should not submit empty input', async () => {
56+
const user = userEvent.setup();
57+
const onAdd = vi.fn();
58+
render(<AddTodo onAdd={onAdd} />);
59+
60+
const input = screen.getByPlaceholderText('Add a new task...');
61+
await user.type(input, ' ');
62+
await user.keyboard('{Enter}');
63+
64+
expect(onAdd).not.toHaveBeenCalled();
65+
});
66+
67+
it('should show keyboard hint when focused with text', async () => {
68+
const user = userEvent.setup();
69+
render(<AddTodo onAdd={vi.fn()} />);
70+
71+
const input = screen.getByPlaceholderText('Add a new task...');
72+
await user.type(input, 'Task');
73+
74+
expect(screen.getByText(/press/i)).toBeInTheDocument();
75+
expect(screen.getByText('Enter')).toBeInTheDocument();
76+
});
77+
78+
it('should have proper accessibility attributes', () => {
79+
render(<AddTodo onAdd={vi.fn()} />);
80+
81+
const form = screen.getByRole('search', { name: /add new task/i });
82+
expect(form).toBeInTheDocument();
83+
84+
const input = screen.getByLabelText(/add a new task/i);
85+
expect(input).toBeInTheDocument();
86+
});
87+
});

src/components/NotFound.jsx

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
/**
2+
* 404 Not Found component for unknown views/routes
3+
*/
4+
export function NotFound({ onNavigate }) {
5+
return (
6+
<div className="min-h-[60vh] flex items-center justify-center">
7+
<div className="text-center max-w-md mx-auto px-4">
8+
{/* 404 Illustration */}
9+
<div className="relative mb-8">
10+
<div className="text-[120px] font-bold text-white/10 leading-none select-none">
11+
404
12+
</div>
13+
<div className="absolute inset-0 flex items-center justify-center">
14+
<div className="w-24 h-24 rounded-3xl bg-white/20 backdrop-blur-sm flex items-center justify-center shadow-xl">
15+
<svg
16+
className="w-12 h-12 text-white"
17+
fill="none"
18+
stroke="currentColor"
19+
viewBox="0 0 24 24"
20+
>
21+
<path
22+
strokeLinecap="round"
23+
strokeLinejoin="round"
24+
strokeWidth={1.5}
25+
d="M9.172 16.172a4 4 0 015.656 0M9 10h.01M15 10h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
26+
/>
27+
</svg>
28+
</div>
29+
</div>
30+
</div>
31+
32+
{/* Message */}
33+
<h1 className="text-2xl font-bold text-white mb-3">
34+
Page Not Found
35+
</h1>
36+
<p className="text-white/60 mb-8">
37+
Oops! The page you're looking for doesn't exist or has been moved.
38+
Let's get you back on track.
39+
</p>
40+
41+
{/* Actions */}
42+
<div className="flex flex-col sm:flex-row gap-3 justify-center">
43+
<button
44+
onClick={() => onNavigate('dashboard')}
45+
className="px-6 py-3 rounded-xl font-medium bg-white text-slate-800 shadow-lg hover:shadow-xl hover:-translate-y-0.5 transition-all"
46+
>
47+
Go to Dashboard
48+
</button>
49+
<button
50+
onClick={() => onNavigate('tasks')}
51+
className="px-6 py-3 rounded-xl font-medium bg-white/20 text-white hover:bg-white/30 transition-all"
52+
>
53+
View Tasks
54+
</button>
55+
</div>
56+
57+
{/* Quick Links */}
58+
<div className="mt-12 pt-8 border-t border-white/10">
59+
<p className="text-white/40 text-sm mb-4">Quick Links</p>
60+
<div className="flex flex-wrap justify-center gap-2">
61+
{[
62+
{ id: 'attendance', label: 'Attendance' },
63+
{ id: 'focus', label: 'Focus Timer' },
64+
{ id: 'notes', label: 'Notes' },
65+
{ id: 'expenses', label: 'Expenses' },
66+
].map((link) => (
67+
<button
68+
key={link.id}
69+
onClick={() => onNavigate(link.id)}
70+
className="px-3 py-1.5 rounded-lg text-sm text-white/60 hover:text-white hover:bg-white/10 transition-colors"
71+
>
72+
{link.label}
73+
</button>
74+
))}
75+
</div>
76+
</div>
77+
</div>
78+
</div>
79+
);
80+
}

src/components/NotFound.test.jsx

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { describe, it, expect, vi } from 'vitest';
2+
import { render, screen } from '@testing-library/react';
3+
import userEvent from '@testing-library/user-event';
4+
import { NotFound } from './NotFound';
5+
6+
describe('NotFound', () => {
7+
it('should render 404 message', () => {
8+
render(<NotFound onNavigate={vi.fn()} />);
9+
10+
expect(screen.getByText('404')).toBeInTheDocument();
11+
expect(screen.getByText('Page Not Found')).toBeInTheDocument();
12+
});
13+
14+
it('should have navigation buttons', () => {
15+
render(<NotFound onNavigate={vi.fn()} />);
16+
17+
expect(screen.getByRole('button', { name: /go to dashboard/i })).toBeInTheDocument();
18+
expect(screen.getByRole('button', { name: /view tasks/i })).toBeInTheDocument();
19+
});
20+
21+
it('should call onNavigate with dashboard when clicking Go to Dashboard', async () => {
22+
const user = userEvent.setup();
23+
const onNavigate = vi.fn();
24+
render(<NotFound onNavigate={onNavigate} />);
25+
26+
await user.click(screen.getByRole('button', { name: /go to dashboard/i }));
27+
28+
expect(onNavigate).toHaveBeenCalledWith('dashboard');
29+
});
30+
31+
it('should call onNavigate with tasks when clicking View Tasks', async () => {
32+
const user = userEvent.setup();
33+
const onNavigate = vi.fn();
34+
render(<NotFound onNavigate={onNavigate} />);
35+
36+
await user.click(screen.getByRole('button', { name: /view tasks/i }));
37+
38+
expect(onNavigate).toHaveBeenCalledWith('tasks');
39+
});
40+
41+
it('should have quick links section', () => {
42+
render(<NotFound onNavigate={vi.fn()} />);
43+
44+
expect(screen.getByText('Quick Links')).toBeInTheDocument();
45+
expect(screen.getByRole('button', { name: 'Attendance' })).toBeInTheDocument();
46+
expect(screen.getByRole('button', { name: 'Focus Timer' })).toBeInTheDocument();
47+
expect(screen.getByRole('button', { name: 'Notes' })).toBeInTheDocument();
48+
expect(screen.getByRole('button', { name: 'Expenses' })).toBeInTheDocument();
49+
});
50+
51+
it('should navigate to correct view when clicking quick links', async () => {
52+
const user = userEvent.setup();
53+
const onNavigate = vi.fn();
54+
render(<NotFound onNavigate={onNavigate} />);
55+
56+
await user.click(screen.getByRole('button', { name: 'Notes' }));
57+
expect(onNavigate).toHaveBeenCalledWith('notes');
58+
59+
await user.click(screen.getByRole('button', { name: 'Expenses' }));
60+
expect(onNavigate).toHaveBeenCalledWith('expenses');
61+
});
62+
});

0 commit comments

Comments
 (0)