Files
HealthLog/src/lib/auth/audit.ts
T
Marc-André Bombeck bdac5b2c0d feat(admin): resolve the sign-in carrier online + give it its own column
The network operator (carrier) used to resolve only from the optional
offline GeoLite2-ASN database. On a host without it the carrier stayed
empty even though the location resolved fine online.

Mine the operator from the same online provider response the location
lookup already fetches — ip-api's top-level `isp`/`org`/`as`, ipwho.is's
nested `connection.{isp,org,asn}` — so the carrier is populated out of the
box. The offline ASN database stays authoritative when configured.

A single `lookupIpGeo` now returns location + ASN + carrier in one pass;
the audit write and the backfill both go through it. The backfill also
revisits rows that already had a location but no carrier, so the column
fills in across existing history, not just for new sign-ins.

In the admin sign-in overview the carrier moves out of a chip stacked
under the login method into its own column, next to the location.
2026-06-29 23:51:40 +02:00

56 lines
1.7 KiB
TypeScript

import { prisma } from "@/lib/db";
import { lookupIpGeo } from "@/lib/geo";
export async function auditLog(
action: string,
opts: {
userId?: string | null;
details?: Record<string, unknown>;
ipAddress?: string | null;
} = {},
) {
const entry = await prisma.auditLog.create({
data: {
action,
userId: opts.userId ?? null,
details: opts.details ? JSON.stringify(opts.details) : null,
ipAddress: opts.ipAddress ?? null,
},
});
// Fire-and-forget: resolve IP location + carrier and update the entry
// with whichever fields the resolver could fill. v1.25.8 — `lookupIpGeo`
// unifies the online location + carrier lookup with the offline ASN MMDB
// (carrier now resolves from the online provider when the optional offline
// DB is absent). The whole resolve still races a 3 s timeout so a hung
// provider can't stall the audit write.
if (opts.ipAddress && action.startsWith("auth.")) {
const ipAddress = opts.ipAddress;
const timeout = new Promise<null>((resolve) =>
setTimeout(() => resolve(null), 3000),
);
Promise.race([lookupIpGeo(ipAddress), timeout])
.then((result) => {
if (!result) return;
const { location, asn, carrier } = result;
const data: {
location?: string;
asn?: number;
carrier?: string | null;
} = {};
if (location) data.location = location;
if (typeof asn === "number") data.asn = asn;
if (carrier) data.carrier = carrier;
if (Object.keys(data).length === 0) return;
return prisma.auditLog.update({
where: { id: entry.id },
data,
});
})
.catch(() => {
// Silently ignore geo lookup failures
});
}
}