/// // // M7 — Public Quote Sharing via Share Token. // // Adds a `shareToken` field to the `quotes` collection so a shop owner can // generate a secret URL that lets a customer review and approve/decline // individual services without logging in. // // The `viewRule` and `updateRule` are relaxed to allow access when the // request carries a matching `shareToken` query parameter. The token is // a random UUID, making it unguessable while simple to copy/paste. // // Security: the token only grants access to the single quote record it // belongs to. All other records remain locked to the owning user only. // migrate( (db) => { const quotes = db.findCollectionByNameOrId('quotes'); if (!quotes) { console.warn('[M7] quotes collection not found — skipping'); return; } // ── Add shareToken field ───────────────────────────────────────── if (!quotes.fields.find((f) => f.name === 'shareToken')) { quotes.fields.push( new TextField({ name: 'shareToken', required: false, }) ); console.log('[M7] added shareToken field to quotes'); } // ── Relax view & update rules for public access via shareToken ── const PUBLIC_SHARE_RULE = `(userId = @request.auth.id) || (shareToken != "" && shareToken = @request.query.shareToken)`; quotes.listRule = PUBLIC_SHARE_RULE; quotes.viewRule = PUBLIC_SHARE_RULE; quotes.updateRule = PUBLIC_SHARE_RULE; // create/delete stay locked to the owning user db.save(quotes); console.log('[M7] relaxed list/view/update rules on quotes for public share access'); }, (db) => { const quotes = db.findCollectionByNameOrId('quotes'); if (!quotes) return; const idx = quotes.fields.findIndex((f) => f.name === 'shareToken'); if (idx >= 0) { quotes.fields.splice(idx, 1); db.save(quotes); } // Restore tight user-only rules quotes.listRule = 'userId = @request.auth.id'; quotes.viewRule = 'userId = @request.auth.id'; quotes.updateRule = 'userId = @request.auth.id'; db.save(quotes); console.log('[M7] rolled back shareToken — rules restored to user-only'); } );