forked from jordanlambrecht/tracker-tracker
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathprivacy-db.ts
More file actions
81 lines (76 loc) · 2.95 KB
/
Copy pathprivacy-db.ts
File metadata and controls
81 lines (76 loc) · 2.95 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
// src/lib/privacy-db.ts
//
// Functions: createPrivacyMask, createPrivacyMaskSync, scrubSnapshotUsernames
//
// DB-aware privacy operations. Pure helpers (maskUsername, isRedacted) live
// in privacy.ts to keep that module free of I/O dependencies.
import { sql } from "drizzle-orm"
import { db } from "@/lib/db"
import { appSettings } from "@/lib/db/schema"
import { isRedacted, maskUsername, REDACTED_PREFIX } from "@/lib/privacy"
/**
* Fetches the current privacy setting and returns a masking function.
* When privacy mode is on (storeUsernames=false), the returned function
* replaces plaintext values with redacted markers. When off, values
* pass through unchanged. Already-redacted values are never double-masked.
*/
export async function createPrivacyMask(): Promise<
(value: string | null | undefined) => string | null
> {
const [settings] = await db
.select({ storeUsernames: appSettings.storeUsernames })
.from(appSettings)
.limit(1)
const privacyMode = settings ? !settings.storeUsernames : false
return (value: string | null | undefined): string | null => {
if (!value) return null
if (!privacyMode || isRedacted(value)) return value
return maskUsername(value)
}
}
/**
* Synchronous variant — accepts a pre-fetched storeUsernames boolean instead of
* querying the DB. Use when the caller already has appSettings in scope so the
* DB round-trip can be avoided.
*
* Fallback convention: pass `true` (store usernames = no masking) when the
* settings row does not exist, which matches the default column value and the
* existing createPrivacyMask() behavior.
*/
export function createPrivacyMaskSync(
storeUsernames: boolean
): (val: string | null | undefined) => string | null {
if (storeUsernames) return (val) => val ?? null
return (val) => (val ? maskUsername(val) : null)
}
/**
* Scrubs all non-redacted usernames and groups in tracker snapshots.
* Called when privacy mode is toggled on with scrubExisting=true.
*
* Uses a single batch UPDATE instead of SELECT + per-row UPDATE to avoid
* N+1 queries. The redacted format is "▓<originalLength>" — we use
* CONCAT('▓', CHAR_LENGTH(column)) and filter out already-redacted rows.
*
* Returns the number of rows scrubbed.
*/
export async function scrubSnapshotUsernames(): Promise<number> {
const prefix = REDACTED_PREFIX
const result = await db.execute(sql`
UPDATE tracker_snapshots
SET
username = CASE
WHEN username IS NOT NULL AND username NOT LIKE ${`${prefix}%`}
THEN CONCAT(${prefix}, CHAR_LENGTH(username))
ELSE username
END,
group_name = CASE
WHEN group_name IS NOT NULL AND group_name NOT LIKE ${`${prefix}%`}
THEN CONCAT(${prefix}, CHAR_LENGTH(group_name))
ELSE group_name
END
WHERE
(username IS NOT NULL AND username NOT LIKE ${`${prefix}%`})
OR (group_name IS NOT NULL AND group_name NOT LIKE ${`${prefix}%`})
`)
return (result as { rowCount?: number }).rowCount ?? 0
}