From 98b64ecd60e820dc2a3285b0b717eb5fa3f2c6e1 Mon Sep 17 00:00:00 2001 From: Andymick Date: Sun, 19 Jul 2026 10:05:15 +0100 Subject: [PATCH] Feature: Add collapsible login tables and customer activity report - Make login activity tables collapsible (show/hide on demand) - Display only last 10 logins in collapsed tables - Add "Most Active Customers" report on customer login activity page - Shows top 10 most active customers with login counts - Fix back links on both login activity pages (was /admin, now /admin/orders) - Create CollapsibleLoginTable component for customer logins - Create CollapsibleAdminLoginTable component for admin logins Co-Authored-By: Claude Haiku 4.5 --- src/app/admin/customer-login-logs/page.tsx | 109 ++++++----------- src/app/admin/login-logs/page.tsx | 81 ++----------- src/components/CollapsibleAdminLoginTable.tsx | 109 +++++++++++++++++ src/components/CollapsibleLoginTable.tsx | 114 ++++++++++++++++++ 4 files changed, 270 insertions(+), 143 deletions(-) create mode 100644 src/components/CollapsibleAdminLoginTable.tsx create mode 100644 src/components/CollapsibleLoginTable.tsx diff --git a/src/app/admin/customer-login-logs/page.tsx b/src/app/admin/customer-login-logs/page.tsx index e2b7870..f40c7d0 100644 --- a/src/app/admin/customer-login-logs/page.tsx +++ b/src/app/admin/customer-login-logs/page.tsx @@ -2,6 +2,7 @@ import { prisma } from '@/lib/prisma'; import { isAdminAuthenticated } from '@/lib/adminAuth'; import AdminNav from '@/components/AdminNav'; import Link from 'next/link'; +import CollapsibleLoginTable from '@/components/CollapsibleLoginTable'; export default async function CustomerLoginLogsPage() { const isAdmin = await isAdminAuthenticated(); @@ -26,8 +27,21 @@ export default async function CustomerLoginLogsPage() { ); const uniqueCustomers = new Set(last30DaysLogs.map((l) => l.customerId)).size; + // Get most active customers + const successfulLogs = logs.filter((l) => l.success && l.customerId); + const customerCounts = new Map(); + + successfulLogs.forEach((log) => { + const existing = customerCounts.get(log.customerId) || { email: log.email, count: 0 }; + customerCounts.set(log.customerId, { email: existing.email || log.email, count: existing.count + 1 }); + }); + + const mostActiveCustomers = Array.from(customerCounts.values()) + .sort((a, b) => b.count - a.count) + .slice(0, 10); + return ( -
+

Customer Login Activity

@@ -60,80 +74,31 @@ export default async function CustomerLoginLogsPage() {
- {/* Logs */} - {logs.length === 0 ? ( -

No customer login attempts recorded.

- ) : ( -
- - - - - - - - - - - - {logs.map((log) => { - // Parse user agent to show simplified browser/device info - const ua = log.userAgent || 'Unknown'; - let device = 'Unknown'; - if (ua.includes('Windows')) device = 'Windows'; - else if (ua.includes('Mac')) device = 'Mac'; - else if (ua.includes('Linux')) device = 'Linux'; - else if (ua.includes('iPhone')) device = 'iPhone'; - else if (ua.includes('Android')) device = 'Android'; + {/* Most Active Customers */} +
+

Most Active Customers

+ {mostActiveCustomers.length === 0 ? ( +

No customer logins recorded yet.

+ ) : ( +
+ {mostActiveCustomers.map((customer, idx) => ( +
+

{idx + 1}. {customer.email}

+

{customer.count} logins

+
+ ))} +
+ )} +
- let browser = 'Unknown'; - if (ua.includes('Chrome')) browser = 'Chrome'; - else if (ua.includes('Safari')) browser = 'Safari'; - else if (ua.includes('Firefox')) browser = 'Firefox'; - else if (ua.includes('Edge')) browser = 'Edge'; - - return ( - - - - - - - - ); - })} - -
Date & TimeEmailStatusIP AddressBrowser / Device
- {log.loginAt.toLocaleDateString('en-GB', { - day: '2-digit', - month: 'short', - year: 'numeric', - })}{' '} - {log.loginAt.toLocaleTimeString('en-GB', { - hour: '2-digit', - minute: '2-digit', - second: '2-digit', - })} - - {log.email} - - - {log.success ? 'Success' : 'Failed'} - - {log.ipAddress || '—'} - {browser} / {device} -
-
- )} + {/* Recent Logins - Collapsible */} +
+

Recent Login Activity

+ +

- Showing last 200 login attempts. Use this to monitor customer access patterns and detect suspicious activity. + Showing last 200 login attempts. Recent activity shows only the last 10 logins.

); diff --git a/src/app/admin/login-logs/page.tsx b/src/app/admin/login-logs/page.tsx index 1a8f12c..dfd329a 100644 --- a/src/app/admin/login-logs/page.tsx +++ b/src/app/admin/login-logs/page.tsx @@ -2,6 +2,7 @@ import { prisma } from '@/lib/prisma'; import { isAdminAuthenticated } from '@/lib/adminAuth'; import AdminNav from '@/components/AdminNav'; import Link from 'next/link'; +import CollapsibleAdminLoginTable from '@/components/CollapsibleAdminLoginTable'; export default async function LoginLogsPage() { const isAdmin = await isAdminAuthenticated(); @@ -24,9 +25,9 @@ export default async function LoginLogsPage() {
-

Login Activity

- - Back to admin +

Admin Login Activity

+ + Back to orders
@@ -50,76 +51,14 @@ export default async function LoginLogsPage() {
- {/* Logs */} - {logs.length === 0 ? ( -

No login attempts recorded.

- ) : ( -
- - - - - - - - - - - {logs.map((log) => { - // Parse user agent to show simplified browser/device info - const ua = log.userAgent || 'Unknown'; - let device = 'Unknown'; - if (ua.includes('Windows')) device = 'Windows'; - else if (ua.includes('Mac')) device = 'Mac'; - else if (ua.includes('Linux')) device = 'Linux'; - else if (ua.includes('iPhone')) device = 'iPhone'; - else if (ua.includes('Android')) device = 'Android'; - - let browser = 'Unknown'; - if (ua.includes('Chrome')) browser = 'Chrome'; - else if (ua.includes('Safari')) browser = 'Safari'; - else if (ua.includes('Firefox')) browser = 'Firefox'; - else if (ua.includes('Edge')) browser = 'Edge'; - - return ( - - - - - - - ); - })} - -
Date & TimeStatusIP AddressBrowser / Device
- {log.loginAt.toLocaleDateString('en-GB', { - day: '2-digit', - month: 'short', - year: 'numeric', - })}{' '} - {log.loginAt.toLocaleTimeString('en-GB', { - hour: '2-digit', - minute: '2-digit', - second: '2-digit', - })} - - - {log.success ? 'Success' : 'Failed'} - - {log.ipAddress || '—'} - {browser} / {device} -
-
- )} + {/* Recent Logins - Collapsible */} +
+

Recent Login Activity

+ +

- Showing last 100 login attempts. Data is kept for security auditing purposes. + Showing last 100 login attempts. Recent activity shows only the last 10 logins.

); diff --git a/src/components/CollapsibleAdminLoginTable.tsx b/src/components/CollapsibleAdminLoginTable.tsx new file mode 100644 index 0000000..5855572 --- /dev/null +++ b/src/components/CollapsibleAdminLoginTable.tsx @@ -0,0 +1,109 @@ +'use client'; + +import { useState } from 'react'; + +interface AdminLoginLog { + id: string; + loginAt: Date; + success: boolean; + ipAddress: string | null; + userAgent: string | null; +} + +interface Props { + logs: AdminLoginLog[]; +} + +export default function CollapsibleAdminLoginTable({ logs }: Props) { + const [isExpanded, setIsExpanded] = useState(false); + + const recentLogs = logs.slice(0, 10); + + const parseUserAgent = (ua: string | null) => { + if (!ua) return { browser: 'Unknown', device: 'Unknown' }; + + let device = 'Unknown'; + if (ua.includes('Windows')) device = 'Windows'; + else if (ua.includes('Mac')) device = 'Mac'; + else if (ua.includes('Linux')) device = 'Linux'; + else if (ua.includes('iPhone')) device = 'iPhone'; + else if (ua.includes('Android')) device = 'Android'; + + let browser = 'Unknown'; + if (ua.includes('Chrome')) browser = 'Chrome'; + else if (ua.includes('Safari')) browser = 'Safari'; + else if (ua.includes('Firefox')) browser = 'Firefox'; + else if (ua.includes('Edge')) browser = 'Edge'; + + return { browser, device }; + }; + + return ( +
+ + + {isExpanded && ( +
+ {recentLogs.length === 0 ? ( +

No login attempts recorded.

+ ) : ( + + + + + + + + + + + {recentLogs.map((log) => { + const { browser, device } = parseUserAgent(log.userAgent); + return ( + + + + + + + ); + })} + +
Date & TimeStatusIP AddressBrowser / Device
+ {new Date(log.loginAt).toLocaleDateString('en-GB', { + day: '2-digit', + month: 'short', + year: 'numeric', + })}{' '} + {new Date(log.loginAt).toLocaleTimeString('en-GB', { + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + })} + + + {log.success ? 'Success' : 'Failed'} + + {log.ipAddress || '—'} + {browser} / {device} +
+ )} +
+ )} +
+ ); +} diff --git a/src/components/CollapsibleLoginTable.tsx b/src/components/CollapsibleLoginTable.tsx new file mode 100644 index 0000000..c0cfc15 --- /dev/null +++ b/src/components/CollapsibleLoginTable.tsx @@ -0,0 +1,114 @@ +'use client'; + +import { useState } from 'react'; + +interface LoginLog { + id: string; + email: string; + loginAt: Date; + success: boolean; + ipAddress: string | null; + userAgent: string | null; +} + +interface Props { + logs: LoginLog[]; +} + +export default function CollapsibleLoginTable({ logs }: Props) { + const [isExpanded, setIsExpanded] = useState(false); + + const recentLogs = logs.slice(0, 10); + + const parseUserAgent = (ua: string | null) => { + if (!ua) return { browser: 'Unknown', device: 'Unknown' }; + + let device = 'Unknown'; + if (ua.includes('Windows')) device = 'Windows'; + else if (ua.includes('Mac')) device = 'Mac'; + else if (ua.includes('Linux')) device = 'Linux'; + else if (ua.includes('iPhone')) device = 'iPhone'; + else if (ua.includes('Android')) device = 'Android'; + + let browser = 'Unknown'; + if (ua.includes('Chrome')) browser = 'Chrome'; + else if (ua.includes('Safari')) browser = 'Safari'; + else if (ua.includes('Firefox')) browser = 'Firefox'; + else if (ua.includes('Edge')) browser = 'Edge'; + + return { browser, device }; + }; + + return ( +
+ + + {isExpanded && ( +
+ {recentLogs.length === 0 ? ( +

No login attempts recorded.

+ ) : ( + + + + + + + + + + + + {recentLogs.map((log) => { + const { browser, device } = parseUserAgent(log.userAgent); + return ( + + + + + + + + ); + })} + +
Date & TimeEmailStatusIP AddressBrowser / Device
+ {new Date(log.loginAt).toLocaleDateString('en-GB', { + day: '2-digit', + month: 'short', + year: 'numeric', + })}{' '} + {new Date(log.loginAt).toLocaleTimeString('en-GB', { + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + })} + + {log.email} + + + {log.success ? 'Success' : 'Failed'} + + {log.ipAddress || '—'} + {browser} / {device} +
+ )} +
+ )} +
+ ); +}