Files
spq-v2/pb_migrations/1740000000020_users_create_role_guard.js
2026-07-12 10:01:39 -04:00

57 lines
2.1 KiB
JavaScript

/// <reference path="../pb_data/types.d.ts" />
// M20 — users.onRecordCreateRequest role guard.
//
// Closes the self-signup role-escalation hole: a client can no longer
// POST { role: 'technician', shopUserId: '<someone>' } to /api/collections/users
// to land inside another shop's technician list.
//
// Rule: role defaults to 'advisor' and shopUserId is cleared, UNLESS the
// email being registered matches a pending, unexpired technicianInvites
// record — in which case role='technician' and shopUserId comes from the
// invite (not the client).
//
// Idempotent: re-applying is safe because we only set values on create.
migrate(
(app) => {
app.onRecordCreateRequest((e) => {
if (e.collection.name !== 'users') return;
const record = e.record;
const email = (record.get('email') || '').toString().trim().toLowerCase();
if (!email) return;
// Look for a matching pending, unexpired invite.
let invite = null;
try {
invite = e.dao.findFirstRecordByFilter(
'technicianInvites',
`email = "${email.replace(/"/g, '')}" && status = "pending" && expiresAt >= @now`,
);
} catch {
invite = null;
}
if (invite) {
// Authorized technician signup — pin values from the invite.
record.set('role', 'technician');
record.set('shopUserId', invite.get('shopUserId') || '');
console.log(`[M20] technician signup authorized for ${email}`);
} else {
// Regular owner signup — force advisor, clear shop link.
record.set('role', 'advisor');
record.set('shopUserId', '');
console.log(`[M20] owner signup: role forced to advisor for ${email}`);
}
});
console.log('[M20] users create role guard installed');
},
(app) => {
// Best-effort rollback: we cannot easily unregister a JS hook in 0.39
// without restarting PB. Document that rolling this back requires a
// PB restart after removing the file. The down-migration is a no-op
// that logs the intent.
console.log('[M20] rollback requested — remove this migration file and restart PocketBase');
},
);