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;
|
||||
}
|
||||
|
||||
if (!have.has("active")) {
|
||||
fields.push({
|
||||
name: "active",
|
||||
type: "bool",
|
||||
required: false,
|
||||
presentable: false,
|
||||
hidden: false,
|
||||
system: false,
|
||||
});
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (changed) {
|
||||
await pb.collections.update(users.id, { fields });
|
||||
console.log(" + users fields: role, client");
|
||||
console.log(" + users fields: role, client, active");
|
||||
} else {
|
||||
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"';
|
||||
|
|
|
|||
|
|
@ -31,6 +31,13 @@ export async function POST(request: Request): Promise<Response> {
|
|||
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(
|
||||
{
|
||||
httpOnly: true,
|
||||
|
|
|
|||
|
|
@ -28,6 +28,8 @@ export interface UserRecord extends BaseRecord {
|
|||
role: Role;
|
||||
/** Empty string when role is "owner". */
|
||||
client: string;
|
||||
/** False disables sign-in without deleting the account or its history. */
|
||||
active: boolean;
|
||||
}
|
||||
|
||||
export interface ClientRecord extends BaseRecord {
|
||||
|
|
|
|||
Loading…
Reference in a new issue