Skip to content

Commit 4125bf4

Browse files
committed
Require the caller of the rebuild-users script to explicitly allow different types of changes using command line options
1 parent 16a127d commit 4125bf4

File tree

3 files changed

+109
-5
lines changed

3 files changed

+109
-5
lines changed

scripts/rebuild_users.ts

+58-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,64 @@
1+
import arg from "arg";
12
import UserService from "../src/services/UserService";
3+
import { isUserDatabaseObjectKey } from "../src/interfaces/UserDatabaseObject";
4+
5+
const parseArgs = () =>
6+
arg({
7+
"--allow-field": [String],
8+
"--allow-all-fields": Boolean,
9+
"--allow-create": Boolean,
10+
"--allow-delete": Boolean,
11+
"--help": Boolean,
12+
13+
"-h": "--help",
14+
});
15+
16+
const printHelp = () => {
17+
console.log("Usage: pnpm rebuild-users [--allow-create] [--allow-delete]");
18+
console.log(" [--allow-all-fields] [--allow-field=<FIELD>]...");
19+
console.log();
20+
console.log("Rebuilds the users table from scratch based on NATS messages.");
21+
console.log();
22+
console.log("Calling this script with no options does not permit any changes");
23+
console.log("and is essentially equivalent to a dry-run. You can explicitly");
24+
console.log("allow wanted changes with the options listed below.");
25+
console.log();
26+
console.log("Options:");
27+
console.log(" --allow-create Allow creation of new users");
28+
console.log(" --allow-delete Allow deletion of existing users");
29+
console.log(" --allow-all-fields Allow changes to all fields");
30+
console.log(" --allow-field=<FIELD> Allow changes to the specified field");
31+
};
232

333
async function main() {
4-
await UserService.rebuild();
34+
const args = parseArgs();
35+
36+
if (args["--help"]) {
37+
printHelp();
38+
process.exit(0);
39+
}
40+
41+
let allowChanges: NonNullable<Parameters<typeof UserService.rebuild>[0]>["allowChanges"] = [];
42+
43+
if (args["--allow-all-fields"]) {
44+
allowChanges = undefined;
45+
} else {
46+
for (const field of args["--allow-field"] ?? []) {
47+
if (!isUserDatabaseObjectKey(field)) {
48+
console.log(`Field ${field} does not exist!`);
49+
process.exit(1);
50+
} else {
51+
allowChanges.push(field);
52+
}
53+
}
54+
}
55+
56+
await UserService.rebuild({
57+
allowCreate: args["--allow-create"] ?? false,
58+
allowRemove: args["--allow-delete"] ?? false,
59+
allowChanges,
60+
});
61+
562
process.exit(0);
663
}
764

src/interfaces/UserDatabaseObject.ts

+29
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,35 @@ export default interface UserDatabaseObject {
2323
last_seq: number;
2424
}
2525

26+
const KEYS: Array<keyof UserDatabaseObject> = [
27+
"id",
28+
"username",
29+
"name",
30+
"screen_name",
31+
"email",
32+
"residence",
33+
"phone",
34+
"hyy_member",
35+
"membership",
36+
"role",
37+
"salt",
38+
"hashed_password",
39+
"password_hash",
40+
"created",
41+
"modified",
42+
"tktl",
43+
"deleted",
44+
"hy_staff",
45+
"hy_student",
46+
"tktdt_student",
47+
"registration_ban_bypass_until",
48+
"last_seq",
49+
];
50+
51+
export function isUserDatabaseObjectKey(key: string): key is keyof UserDatabaseObject {
52+
return KEYS.includes(key as keyof UserDatabaseObject);
53+
}
54+
2655
/**
2756
* User database object with additional payment information
2857
*/

src/services/UserService.ts

+22-4
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,12 @@ const UserEvent = z.discriminatedUnion("type", [UserCreateEvent, UserImportEvent
4444

4545
type UserEvent = z.infer<typeof UserEvent>;
4646

47+
type RebuildOptions = {
48+
allowChanges?: Array<keyof UserDatabaseObject>;
49+
allowRemove?: boolean;
50+
allowCreate?: boolean;
51+
};
52+
4753
class UserService {
4854
abortSignal?: ConsumerAbortSignal;
4955

@@ -384,7 +390,7 @@ class UserService {
384390
return this.dao.withTransaction(dao => callback(new UserService(dao)));
385391
}
386392

387-
public async rebuild() {
393+
public async rebuild(options?: RebuildOptions) {
388394
const nats = await NatsService.get();
389395

390396
await this.transaction(async tsx => {
@@ -412,13 +418,17 @@ class UserService {
412418

413419
if (!after && before) {
414420
console.log(`User ${before.id} (${before.username}) disappeared during rebuild!`);
415-
error = true;
421+
422+
if (!options?.allowRemove) error = true;
423+
416424
return;
417425
}
418426

419427
if (!before && after) {
420428
console.log(`New user ${after.id} (${after.username}) appeared during rebuild!`);
421-
error = true;
429+
430+
if (!options?.allowCreate) error = true;
431+
422432
return;
423433
}
424434

@@ -444,14 +454,22 @@ class UserService {
444454
console.log(`Field ${key} of user ${id} changed: ${before} -> ${after}`);
445455
});
446456
}
457+
458+
if (options?.allowChanges) {
459+
for (const [key] of diff) {
460+
if (!options.allowChanges.includes(key)) {
461+
console.log(`Unallowed field ${key} changed for user ${key}!`);
462+
error = true;
463+
}
464+
}
465+
}
447466
}
448467
});
449468

450469
if (usersAfter.size !== usersBefore.size) {
451470
console.log(
452471
`User count does not match before (${usersBefore.size}) and after (${usersAfter.size}) the rebuild.`,
453472
);
454-
error = true;
455473
}
456474

457475
if (error) {

0 commit comments

Comments
 (0)