From 948c694ed63d0ca8a42d343d2ed3c5faa6a83c5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rados=C5=82aw=20Gierwia=C5=82o?= Date: Wed, 3 Dec 2025 20:27:51 +0100 Subject: [PATCH] feat(system): implement 404 page with activity logging and change profile route format Backend Changes: - Added public API endpoint /api/public/log-404 (no auth required) - Created backend/src/routes/public.js for public endpoints - Added ACTIONS.SYSTEM_404 and CATEGORIES.system to activity log service - Registered public routes in app.js Frontend Changes: - Created NotFoundPage.jsx with standalone layout (no auth required) - Added publicAPI.log404() to log 404 access attempts - Logs both authenticated and anonymous users - Changed profile route from /@:username to /u/:username - Made profile route public (removed ProtectedRoute wrapper) - Updated all profile links from /@${username} to /u/${username} in: - ChatMessage.jsx - DashboardMatchCard.jsx - MatchRequestCards.jsx - MatchCard.jsx - UserListItem.jsx - MatchChatPage.jsx - PublicProfilePage.jsx Fixes: - React Router doesn't support @ in path segments - 404 page now accessible to non-authenticated users without redirect - Profile route no longer catches all unmatched routes --- backend/src/app.js | 3 + backend/src/routes/public.js | 49 +++++++++++ backend/src/services/activityLog.js | 4 + frontend/src/App.jsx | 16 ++-- frontend/src/components/chat/ChatMessage.jsx | 2 +- .../dashboard/DashboardMatchCard.jsx | 4 +- .../dashboard/MatchRequestCards.jsx | 8 +- frontend/src/components/matches/MatchCard.jsx | 4 +- .../src/components/users/UserListItem.jsx | 2 +- frontend/src/pages/MatchChatPage.jsx | 4 +- frontend/src/pages/NotFoundPage.jsx | 83 +++++++++++++++++++ frontend/src/pages/PublicProfilePage.jsx | 4 +- frontend/src/services/api.js | 12 +++ 13 files changed, 172 insertions(+), 23 deletions(-) create mode 100644 backend/src/routes/public.js create mode 100644 frontend/src/pages/NotFoundPage.jsx diff --git a/backend/src/app.js b/backend/src/app.js index 5476f75..1cd4160 100644 --- a/backend/src/app.js +++ b/backend/src/app.js @@ -131,6 +131,9 @@ app.get('/api/debug/ip', (req, res) => { // Apply rate limiting to all API routes app.use('/api/', apiLimiter); +// Public routes (no authentication required) +app.use('/api/public', require('./routes/public')); + // API routes app.use('/api/auth', require('./routes/auth')); app.use('/api/users', require('./routes/users')); diff --git a/backend/src/routes/public.js b/backend/src/routes/public.js new file mode 100644 index 0000000..dfde63f --- /dev/null +++ b/backend/src/routes/public.js @@ -0,0 +1,49 @@ +const express = require('express'); +const { ACTIONS, log: activityLog } = require('../services/activityLog'); +const { getClientIP } = require('../utils/request'); + +const router = express.Router(); + +/** + * POST /api/public/log-404 + * Log 404 page access (no authentication required) + * Logs both authenticated and unauthenticated users + */ +router.post('/log-404', async (req, res) => { + try { + const { path, search } = req.body; + + // Get user info if authenticated (optional) + const userId = req.user?.id || null; + const username = req.user?.username || 'anonymous'; + + // Construct full path + const fullPath = search ? `${path}${search}` : path; + + // Log to activity logs + activityLog({ + userId, + username, + ipAddress: getClientIP(req), + action: ACTIONS.SYSTEM_404, + resource: fullPath, + method: 'GET', + path: fullPath, + metadata: { + requestedPath: path, + queryString: search || null, + userAgent: req.headers['user-agent'] || null, + referer: req.headers.referer || null, + }, + success: true, + }); + + res.json({ success: true, logged: true }); + } catch (error) { + console.error('Error logging 404:', error); + // Don't fail the request if logging fails + res.json({ success: true, logged: false }); + } +}); + +module.exports = router; diff --git a/backend/src/services/activityLog.js b/backend/src/services/activityLog.js index f181e14..689ceab 100644 --- a/backend/src/services/activityLog.js +++ b/backend/src/services/activityLog.js @@ -34,6 +34,9 @@ const ACTIONS = { CHAT_MESSAGE: 'chat.message', CHAT_JOIN_ROOM: 'chat.join_room', CHAT_LEAVE_ROOM: 'chat.leave_room', + + // System actions + SYSTEM_404: 'system.404', }; // Category mapping from action @@ -43,6 +46,7 @@ const CATEGORIES = { 'match': 'match', 'admin': 'admin', 'chat': 'chat', + 'system': 'system', }; /** diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index e7dbd9d..09396dd 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -19,6 +19,7 @@ import HistoryPage from './pages/HistoryPage'; import ProfilePage from './pages/ProfilePage'; import PublicProfilePage from './pages/PublicProfilePage'; import ActivityLogsPage from './pages/admin/ActivityLogsPage'; +import NotFoundPage from './pages/NotFoundPage'; import VerificationBanner from './components/common/VerificationBanner'; import InstallPWA from './components/pwa/InstallPWA'; @@ -210,18 +211,15 @@ function App() { } /> - {/* Public Profile - must be before home route */} - - - - } - /> {/* Home Page */} } /> + + {/* Public Profile - /u/username format (no auth required) */} + } /> + + {/* 404 Not Found - Catch all unmatched routes */} + } /> diff --git a/frontend/src/components/chat/ChatMessage.jsx b/frontend/src/components/chat/ChatMessage.jsx index a6b3207..81df727 100644 --- a/frontend/src/components/chat/ChatMessage.jsx +++ b/frontend/src/components/chat/ChatMessage.jsx @@ -119,7 +119,7 @@ const ChatMessage = ({ message, user, participant, isOwn, formatTime }) => { )} {user.username} diff --git a/frontend/src/components/dashboard/DashboardMatchCard.jsx b/frontend/src/components/dashboard/DashboardMatchCard.jsx index 0ee00ea..e99b16a 100644 --- a/frontend/src/components/dashboard/DashboardMatchCard.jsx +++ b/frontend/src/components/dashboard/DashboardMatchCard.jsx @@ -57,7 +57,7 @@ const DashboardMatchCard = ({ match }) => {
{/* Avatar */} - + {partner.username} {
- +

{partner.firstName && partner.lastName ? `${partner.firstName} ${partner.lastName}` diff --git a/frontend/src/components/dashboard/MatchRequestCards.jsx b/frontend/src/components/dashboard/MatchRequestCards.jsx index f920db2..60768bc 100644 --- a/frontend/src/components/dashboard/MatchRequestCards.jsx +++ b/frontend/src/components/dashboard/MatchRequestCards.jsx @@ -11,7 +11,7 @@ export const IncomingRequestCard = ({ request, onAccept, onReject, processing }) return (
- + {requester.username}
- +

{requester.firstName && requester.lastName ? `${requester.firstName} ${requester.lastName}` @@ -69,7 +69,7 @@ export const OutgoingRequestCard = ({ request, onCancel, processing }) => { return (
- + {recipient.username} {
- +

{recipient.firstName && recipient.lastName ? `${recipient.firstName} ${recipient.lastName}` diff --git a/frontend/src/components/matches/MatchCard.jsx b/frontend/src/components/matches/MatchCard.jsx index a5b4c53..053f5fd 100644 --- a/frontend/src/components/matches/MatchCard.jsx +++ b/frontend/src/components/matches/MatchCard.jsx @@ -16,7 +16,7 @@ const MatchCard = ({ match, onAccept, onReject, onOpenChat, processing }) => {
- + {match.partner.username} { />
- +

{match.partner.firstName && match.partner.lastName ? `${match.partner.firstName} ${match.partner.lastName}` diff --git a/frontend/src/components/users/UserListItem.jsx b/frontend/src/components/users/UserListItem.jsx index 025c343..c8c21c6 100644 --- a/frontend/src/components/users/UserListItem.jsx +++ b/frontend/src/components/users/UserListItem.jsx @@ -46,7 +46,7 @@ const UserListItem = ({ const usernameContent = linkToProfile ? ( {user.username} diff --git a/frontend/src/pages/MatchChatPage.jsx b/frontend/src/pages/MatchChatPage.jsx index 92bd4fb..4f11c89 100644 --- a/frontend/src/pages/MatchChatPage.jsx +++ b/frontend/src/pages/MatchChatPage.jsx @@ -219,7 +219,7 @@ const MatchChatPage = () => {
- + { />
- +

{partner.firstName && partner.lastName ? `${partner.firstName} ${partner.lastName}` diff --git a/frontend/src/pages/NotFoundPage.jsx b/frontend/src/pages/NotFoundPage.jsx new file mode 100644 index 0000000..d6564e9 --- /dev/null +++ b/frontend/src/pages/NotFoundPage.jsx @@ -0,0 +1,83 @@ +import { useEffect } from 'react'; +import { Link, useLocation } from 'react-router-dom'; +import { Home, ArrowLeft } from 'lucide-react'; +import { publicAPI } from '../services/api'; + +export default function NotFoundPage() { + const location = useLocation(); + + useEffect(() => { + // Log 404 access to activity logs (works for both logged in and non-logged in users) + const log404 = async () => { + try { + await publicAPI.log404(location.pathname, location.search); + } catch (err) { + // Silently fail - logging should never break user experience + console.debug('Failed to log 404:', err); + } + }; + + log404(); + }, [location.pathname, location.search]); + + return ( +
+ {/* Simple header */} +
+
+ + + spotlight.cam + +
+
+ +
+
+ {/* 404 Icon */} +
+
404
+

+ Page Not Found +

+

+ The page you're looking for doesn't exist or has been moved. +

+
+ + {/* Current Path */} +
+

Requested URL:

+ + {location.pathname} + {location.search} + +
+ + {/* Action Buttons */} +
+ + + + Go Home + +
+ + {/* Help Text */} +
+

If you believe this is an error, please contact support.

+
+
+
+
+ ); +} diff --git a/frontend/src/pages/PublicProfilePage.jsx b/frontend/src/pages/PublicProfilePage.jsx index b269eba..0862b1a 100644 --- a/frontend/src/pages/PublicProfilePage.jsx +++ b/frontend/src/pages/PublicProfilePage.jsx @@ -267,7 +267,7 @@ const PublicProfilePage = () => {
{/* Rater Info */} - + {
{rating.rater.firstName && rating.rater.lastName diff --git a/frontend/src/services/api.js b/frontend/src/services/api.js index 2ed5c62..f634eec 100644 --- a/frontend/src/services/api.js +++ b/frontend/src/services/api.js @@ -495,4 +495,16 @@ export const adminAPI = { }, }; +// Public API (no authentication required) +export const publicAPI = { + async log404(path, search) { + const data = await fetchAPI('/public/log-404', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ path, search }), + }); + return data; + }, +}; + export { ApiError };