log every auth attempt that names an identity the platform does not know
This commit is contained in:
@@ -0,0 +1,57 @@
|
||||
import { getUsers, getUserByEmail } from 'officerdb';
|
||||
|
||||
// Who this platform knows about, loaded once at launch.
|
||||
//
|
||||
// This is a CLASSIFIER, never a gate. Nothing here decides whether a request is allowed — the signin
|
||||
// handler still does its own database lookup and is the only thing that authenticates. The snapshot
|
||||
// exists so the audit middleware can answer one question cheaply, on the hot path of an unauthenticated
|
||||
// endpoint: "is the name in this request even one of ours?" A wrong answer costs a spurious log line or
|
||||
// a missing one, never access.
|
||||
//
|
||||
// It holds emails AND usernames because both are identities a caller can present, and both are unique
|
||||
// columns on `users`. Matching is case-insensitive: an attempt on OWNER@example.com is the owner's own
|
||||
// address typed differently, not a stranger, and logging it as an intrusion would train the owner to
|
||||
// ignore the file.
|
||||
//
|
||||
// Single-user is a hard invariant here, so this set has two entries in practice. It is a Set rather
|
||||
// than a string because that invariant is the platform's, not this file's, and a Set costs nothing.
|
||||
|
||||
let snapshot: Set<string> | null = null;
|
||||
|
||||
const key = (identity: string) => identity.trim().toLowerCase();
|
||||
|
||||
const identitiesOf = (user: { email: string; username: string | null }): string[] =>
|
||||
[user.email, user.username].filter((value): value is string => typeof value === 'string' && !!value.trim());
|
||||
|
||||
/** Read every account into memory. Called once at launch; safe to call again to refresh. */
|
||||
export async function loadKnownUsers(): Promise<number> {
|
||||
const users = await getUsers();
|
||||
snapshot = new Set(users.flatMap(identitiesOf).map(key));
|
||||
return snapshot.size;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fold a newly created account in. Bootstrap is the only path that mints one, and without this the
|
||||
* owner's very first sign-in after creating the account would be logged as a stranger — the snapshot
|
||||
* having been taken while the user table was still empty.
|
||||
*/
|
||||
export function rememberUser(user: { email: string; username: string | null }): void {
|
||||
if (!snapshot) return;
|
||||
for (const identity of identitiesOf(user)) snapshot.add(key(identity));
|
||||
}
|
||||
|
||||
/** How many identities are loaded, or null if the launch-time load has not succeeded. */
|
||||
export const knownIdentityCount = (): number | null => snapshot?.size ?? null;
|
||||
|
||||
/**
|
||||
* Whether an identity belongs to an account. Falls back to the database when the launch-time load
|
||||
* never ran or failed — a Postgres blip at boot must not turn every later login into an alert.
|
||||
*/
|
||||
export async function isKnownIdentity(identity: string): Promise<boolean> {
|
||||
const wanted = key(identity);
|
||||
if (!wanted) return false;
|
||||
if (snapshot) return snapshot.has(wanted);
|
||||
|
||||
const user = await getUserByEmail(identity).catch(() => undefined);
|
||||
return !!user;
|
||||
}
|
||||
Reference in New Issue
Block a user