Files
HealthLog/prisma/migrations/0203_v123_known_devices/migration.sql
T
Marc-André Bombeck bed6588699 feat(security): alert on a new-device sign-in
On a completed sign-in (password, second-factor, or passkey) from a device or
location not seen before, send one notification through the dispatcher cascade,
honouring the user's per-channel preferences via a new SECURITY_ALERT event
type. Dedupe on a one-way, per-user fingerprint: a SHA-256 over the userId, the
coarse User-Agent family, and a country/ASN-level geo signal, stored in the new
user_known_devices ledger (migration 0203). The first sighting of a fingerprint
alerts and records; later sightings only bump last-seen and stay silent. The
raw User-Agent and IP are never persisted — only the salted hash and a coarse
label. Registration records its device silently so a fresh account does not
alert on its own first session. The whole path is fire-and-forget and never
blocks or fails a sign-in.
2026-06-27 20:34:31 +02:00

34 lines
1.7 KiB
SQL

-- v1.23 — new-device sign-in alert dedupe ledger.
--
-- One row per (user, coarse device fingerprint). The fingerprint
-- (`device_hash`) is a one-way SHA-256 hash of
-- `userId | normalised-User-Agent | coarse-location-or-asn`. The raw
-- User-Agent and IP are NEVER stored — the table holds only the salted hash
-- (the userId is part of the digest input, so the same browser on two
-- accounts produces two unrelated hashes) plus an optional coarse,
-- human-readable `label` for the security-activity surface (e.g.
-- "Firefox on macOS — Berlin, DE"; the label omits the IP).
--
-- A successful sign-in upserts on (user_id, device_hash): the first sighting
-- of a hash inserts the row and fires one new-device notification through the
-- dispatcher; subsequent sightings only bump `last_seen_at` and stay silent.
CREATE TABLE "user_known_devices" (
"id" TEXT NOT NULL,
"user_id" TEXT NOT NULL,
"device_hash" TEXT NOT NULL,
"label" TEXT,
"first_seen_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"last_seen_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "user_known_devices_pkey" PRIMARY KEY ("id")
);
-- Dedupe is per-user: a coarse fingerprint can never be inserted twice for
-- one account, so "first sighting → alert" is a single unique-constrained
-- upsert with no race window.
CREATE UNIQUE INDEX "user_known_devices_user_id_device_hash_key" ON "user_known_devices"("user_id", "device_hash");
CREATE INDEX "user_known_devices_user_id_idx" ON "user_known_devices"("user_id");
ALTER TABLE "user_known_devices" ADD CONSTRAINT "user_known_devices_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;