feat(users): active flag, backfilled, enforced at sign-in
This commit is contained in:
parent
ed048c5bb3
commit
287d70c42c
4 changed files with 69 additions and 1 deletions
38
pocketbase/pb_migrations/1789601100_added_user_active.js
Normal file
38
pocketbase/pb_migrations/1789601100_added_user_active.js
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
/// <reference path="../pb_data/types.d.ts" />
|
||||||
|
/**
|
||||||
|
* `active` on users.
|
||||||
|
*
|
||||||
|
* REFERENCE ONLY — see pb_migrations/README.md. scripts/setup-remote.ts is
|
||||||
|
* what runs.
|
||||||
|
*
|
||||||
|
* Accounts are deactivated, never deleted: activity.actor and comments.author
|
||||||
|
* are relations to users, and removing a row turns every historical entry that
|
||||||
|
* referenced it into "Someone". An audit trail that forgets who did what is
|
||||||
|
* not an audit trail.
|
||||||
|
*
|
||||||
|
* Existing rows are backfilled to true so nobody is locked out by the upgrade.
|
||||||
|
*/
|
||||||
|
migrate(
|
||||||
|
(app) => {
|
||||||
|
const users = app.findCollectionByNameOrId("users");
|
||||||
|
|
||||||
|
users.fields.add(
|
||||||
|
new Field({ name: "active", type: "bool", required: false })
|
||||||
|
);
|
||||||
|
|
||||||
|
app.save(users);
|
||||||
|
|
||||||
|
for (const user of app.findAllRecords("users")) {
|
||||||
|
user.set("active", true);
|
||||||
|
app.save(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
(app) => {
|
||||||
|
const users = app.findCollectionByNameOrId("users");
|
||||||
|
const field = users.fields.find((f) => f.name === "active");
|
||||||
|
if (field) users.fields.removeById(field.id);
|
||||||
|
return app.save(users);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
@ -80,12 +80,33 @@ async function ensureUsersFields(pb: PocketBase): Promise<void> {
|
||||||
changed = true;
|
changed = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!have.has("active")) {
|
||||||
|
fields.push({
|
||||||
|
name: "active",
|
||||||
|
type: "bool",
|
||||||
|
required: false,
|
||||||
|
presentable: false,
|
||||||
|
hidden: false,
|
||||||
|
system: false,
|
||||||
|
});
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
if (changed) {
|
if (changed) {
|
||||||
await pb.collections.update(users.id, { fields });
|
await pb.collections.update(users.id, { fields });
|
||||||
console.log(" + users fields: role, client");
|
console.log(" + users fields: role, client, active");
|
||||||
} else {
|
} else {
|
||||||
console.log(" = users fields already present");
|
console.log(" = users fields already present");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Backfill: an account with active unset must not be locked out by this
|
||||||
|
// field appearing.
|
||||||
|
const existing = await pb.collection("users").getFullList();
|
||||||
|
for (const user of existing) {
|
||||||
|
if (user.active !== true) {
|
||||||
|
await pb.collection("users").update(user.id, { active: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const OWNER = '@request.auth.role = "owner"';
|
const OWNER = '@request.auth.role = "owner"';
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,13 @@ export async function POST(request: Request): Promise<Response> {
|
||||||
return jsonError("Invalid email or password", 401);
|
return jsonError("Invalid email or password", 401);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A deactivated account authenticates fine — PocketBase knows nothing about
|
||||||
|
// our `active` flag — so the check has to happen here. Same generic message
|
||||||
|
// as a bad password, so the response cannot be used to enumerate accounts.
|
||||||
|
if (pb.authStore.record?.active === false) {
|
||||||
|
return jsonError("Invalid email or password", 401);
|
||||||
|
}
|
||||||
|
|
||||||
const cookie = pb.authStore.exportToCookie(
|
const cookie = pb.authStore.exportToCookie(
|
||||||
{
|
{
|
||||||
httpOnly: true,
|
httpOnly: true,
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,8 @@ export interface UserRecord extends BaseRecord {
|
||||||
role: Role;
|
role: Role;
|
||||||
/** Empty string when role is "owner". */
|
/** Empty string when role is "owner". */
|
||||||
client: string;
|
client: string;
|
||||||
|
/** False disables sign-in without deleting the account or its history. */
|
||||||
|
active: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ClientRecord extends BaseRecord {
|
export interface ClientRecord extends BaseRecord {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue