Add customer login activity logging

- Add CustomerLoginLog model to track customer login attempts
- Record successful and failed customer logins
- Capture email, IP address, and user agent for each attempt
- Create /admin/customer-login-logs page to view activity
- Show login stats (successes, failures, active customers)
- Display browser/device and IP for each login attempt
- Add "Customer login activity" link to admin menu
- Monitor customer access patterns and detect suspicious activity
- Last 30 days active customer count

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
Andymick
2026-07-16 21:43:09 +01:00
co-authored by Claude Haiku 4.5
parent ea6f9d2e25
commit e69759081c
4 changed files with 168 additions and 1 deletions
+140
View File
@@ -0,0 +1,140 @@
import { prisma } from '@/lib/prisma';
import { isAdminAuthenticated } from '@/lib/adminAuth';
import AdminNav from '@/components/AdminNav';
import Link from 'next/link';
export default async function CustomerLoginLogsPage() {
const isAdmin = await isAdminAuthenticated();
if (!isAdmin) {
return <div className="px-6 py-12 text-center">Unauthorized</div>;
}
const logs = await prisma.customerLoginLog.findMany({
orderBy: { loginAt: 'desc' },
take: 200,
});
const successCount = logs.filter((l) => l.success).length;
const failureCount = logs.filter((l) => !l.success).length;
const last30Days = logs.filter(
(l) => l.loginAt > new Date(Date.now() - 30 * 24 * 60 * 60 * 1000)
).length;
// Count unique customers who logged in last 30 days
const last30DaysLogs = logs.filter(
(l) => l.loginAt > new Date(Date.now() - 30 * 24 * 60 * 60 * 1000) && l.success && l.customerId
);
const uniqueCustomers = new Set(last30DaysLogs.map((l) => l.customerId)).size;
return (
<div className="mx-auto max-w-4xl px-6 py-12">
<AdminNav />
<div className="flex flex-wrap items-baseline justify-between gap-3">
<h1 className="font-display text-3xl">Customer Login Activity</h1>
<Link href="/admin" className="text-sm text-clay hover:text-clay-dark">
Back to admin
</Link>
</div>
<p className="mt-4 text-sm text-muted">
Record of customer account access attempts. Track login patterns and identify unusual activity.
</p>
{/* Stats */}
<div className="mt-6 grid gap-3 sm:grid-cols-4">
<div className="border border-line bg-surface p-4">
<p className="tag-label">Successful Logins</p>
<p className="mt-1 font-mono text-xl">{successCount}</p>
</div>
<div className="border border-line bg-surface p-4">
<p className="tag-label">Failed Attempts</p>
<p className="mt-1 font-mono text-xl">{failureCount}</p>
</div>
<div className="border border-line bg-surface p-4">
<p className="tag-label">Last 30 Days</p>
<p className="mt-1 font-mono text-xl">{last30Days}</p>
</div>
<div className="border border-line bg-surface p-4">
<p className="tag-label">Active Customers</p>
<p className="mt-1 font-mono text-xl">{uniqueCustomers}</p>
</div>
</div>
{/* Logs */}
{logs.length === 0 ? (
<p className="mt-8 text-sm text-muted">No customer login attempts recorded.</p>
) : (
<div className="mt-8 overflow-x-auto">
<table className="w-full border-collapse text-sm">
<thead>
<tr className="border-b border-line">
<th className="text-left py-2 px-3 font-medium">Date & Time</th>
<th className="text-left py-2 px-3 font-medium">Email</th>
<th className="text-left py-2 px-3 font-medium">Status</th>
<th className="text-left py-2 px-3 font-medium">IP Address</th>
<th className="text-left py-2 px-3 font-medium">Browser / Device</th>
</tr>
</thead>
<tbody>
{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 (
<tr key={log.id} className="border-b border-line hover:bg-surface">
<td className="py-2 px-3 font-mono text-xs">
{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',
})}
</td>
<td className="py-2 px-3 text-xs">
<span className="break-all">{log.email}</span>
</td>
<td className="py-2 px-3">
<span
className={`tag-label rounded-full px-3 py-1 text-xs ${
log.success
? 'bg-green-100 text-green-700'
: 'bg-splash-pink/10 text-splash-pink'
}`}
>
{log.success ? 'Success' : 'Failed'}
</span>
</td>
<td className="py-2 px-3 font-mono text-xs">{log.ipAddress || '—'}</td>
<td className="py-2 px-3 text-xs text-muted">
{browser} / {device}
</td>
</tr>
);
})}
</tbody>
</table>
</div>
)}
<p className="mt-8 text-xs text-muted">
Showing last 200 login attempts. Use this to monitor customer access patterns and detect suspicious activity.
</p>
</div>
);
}