/// // M20 — users.onRecordCreateRequest role guard. // // Closes the self-signup role-escalation hole: a client can no longer // POST { role: 'technician', shopUserId: '' } 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'); }, );