diff --git a/prisma/schema.prisma b/prisma/schema.prisma index c0068a0..9f3c8fc 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -393,3 +393,14 @@ model AdminLoginLog { userAgent String? success Boolean @default(true) // false for failed login attempts } + +// Audit log for customer logins — tracks customer account access +model CustomerLoginLog { + id String @id @default(cuid()) + customerId String? + email String // captured for failed attempts where customer doesn't exist + loginAt DateTime @default(now()) + ipAddress String? + userAgent String? + success Boolean @default(true) // false for failed login attempts +} diff --git a/src/app/account/actions.ts b/src/app/account/actions.ts index a387611..c1786b1 100644 --- a/src/app/account/actions.ts +++ b/src/app/account/actions.ts @@ -62,8 +62,14 @@ export async function loginCustomer(formData: FormData) { const next = safeNext(formData.get('next')); const ip = getClientIp(headers()); + const userAgent = headers().get('user-agent') ?? undefined; + const { allowed } = await checkRateLimit(`login:customer:${ip}`, 5, 15 * 60 * 1000); if (!allowed) { + // Log rate limited attempt + await prisma.customerLoginLog.create({ + data: { success: false, email, ipAddress: ip, userAgent }, + }); redirect(`/account/login?error=rate_limited&next=${encodeURIComponent(next)}`); } @@ -71,9 +77,18 @@ export async function loginCustomer(formData: FormData) { const valid = customer ? await verifyPassword(password, customer.passwordHash) : false; if (!customer || !valid) { + // Log failed login attempt + await prisma.customerLoginLog.create({ + data: { success: false, email, ipAddress: ip, userAgent }, + }); redirect(`/account/login?error=invalid&next=${encodeURIComponent(next)}`); } + // Log successful login + await prisma.customerLoginLog.create({ + data: { success: true, customerId: customer.id, email, ipAddress: ip, userAgent }, + }); + await linkGuestOrders(customer.id, customer.email); await createSession(customer.id); redirect(next); diff --git a/src/app/admin/customer-login-logs/page.tsx b/src/app/admin/customer-login-logs/page.tsx new file mode 100644 index 0000000..75eae92 --- /dev/null +++ b/src/app/admin/customer-login-logs/page.tsx @@ -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
+ Record of customer account access attempts. Track login patterns and identify unusual activity. +
+ + {/* Stats */} +Successful Logins
+{successCount}
+Failed Attempts
+{failureCount}
+Last 30 Days
+{last30Days}
+Active Customers
+{uniqueCustomers}
+No customer login attempts recorded.
+ ) : ( +| Date & Time | +Status | +IP Address | +Browser / 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} + | +
+ Showing last 200 login attempts. Use this to monitor customer access patterns and detect suspicious activity. +
+