Bisher legte die Teamleitung jedem Fahrer von Hand ein Konto an und gab das Passwort weiter. Bei zwanzig Leuten in einer WhatsApp-Gruppe ist ein Link die kuerzere Strecke. Die oeffentliche Seite der Einladung laeuft ueber eigene Routen in pb_hooks, nicht ueber die Collection: Ohne Login ist dort nichts sichtbar, auch nicht mit Token. Der Token kommt vom Server, ein mitgeschickter wird abgewiesen. Dazu der Login-Vorspann vor jeder Regel, die @request.auth.id erwaehnt. Das ist ein Loch und keine Kosmetik: Eine leere Relation ist in PocketBase gleich dem leeren @request.auth.id einer anonymen Anfrage — ein Team ohne Admins stuende sonst offen im Netz. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01P32KoesVtABd6xWsqMKzhr
99 lines
3.9 KiB
JavaScript
99 lines
3.9 KiB
JavaScript
/// <reference path="../pb_data/types.d.ts" />
|
|
|
|
// Einladungslinks: Ein Team gibt einen Link heraus, über den sich Fahrer
|
|
// selbst ein Konto anlegen und dem Team beitreten.
|
|
//
|
|
// Bisher ging das nur andersherum — die Teamleitung legte jedem Fahrer von
|
|
// Hand ein Konto an und gab das Passwort weiter. Bei zwanzig Leuten in einer
|
|
// WhatsApp-Gruppe ist ein Link die kürzere Strecke.
|
|
//
|
|
// Der Link ist das Geheimnis. Deshalb:
|
|
//
|
|
// 1) `token` wird von PocketBase erzeugt (autogeneratePattern) und ist
|
|
// eindeutig. Ein mitgeschickter Token wird über die createRule abgelehnt,
|
|
// sonst könnte sich jemand einen leicht zu ratenden aussuchen.
|
|
//
|
|
// 2) Lesen, anlegen und löschen darf nur, wer das Team führt. Es gibt
|
|
// bewusst keine updateRule: Ein Link ändert sich nicht, er wird gelöscht
|
|
// und neu erzeugt. Den Zähler `uses` schreibt der Hook, der an den Regeln
|
|
// vorbei arbeitet.
|
|
//
|
|
// 3) Die öffentliche Seite der Einladung läuft nicht über diese Collection,
|
|
// sondern über die Routen in pb_hooks/team_invites.pb.js. Ohne Login ist
|
|
// hier nichts sichtbar — auch nicht mit Token.
|
|
//
|
|
// `expires` leer heißt: läuft nicht ab. `max_uses` 0 heißt: beliebig oft.
|
|
|
|
// Der Login-Vorspann ist nicht bloß Gürtel und Hosenträger: Ist `admins`
|
|
// leer, vergleicht PocketBase eine leere Relation mit dem leeren
|
|
// `@request.auth.id` einer anonymen Anfrage — und das ist wahr. Ohne den
|
|
// Vorspann stünde jede Einladung eines Teams ohne Admins offen im Netz.
|
|
// Siehe 1754501700_rules_require_login.js.
|
|
const ADMIN_RULE =
|
|
'@request.auth.id != ""' +
|
|
' && (team.owner.id ?= @request.auth.id || team.admins.id ?= @request.auth.id)'
|
|
|
|
migrate((app) => {
|
|
const teams = app.findCollectionByNameOrId('teams')
|
|
const users = app.findCollectionByNameOrId('users')
|
|
|
|
try {
|
|
app.findCollectionByNameOrId('team_invites')
|
|
return
|
|
} catch {
|
|
// Noch nicht da — wird gleich angelegt.
|
|
}
|
|
|
|
const invites = new Collection({
|
|
id: 'pbc_team_invites',
|
|
type: 'base',
|
|
name: 'team_invites',
|
|
listRule: ADMIN_RULE,
|
|
viewRule: ADMIN_RULE,
|
|
// Der Token kommt vom Server. `:isset = false` weist einen
|
|
// mitgeschickten ab, statt ihn stillschweigend zu übernehmen.
|
|
createRule: ADMIN_RULE + ' && @request.body.token:isset = false',
|
|
updateRule: null,
|
|
deleteRule: ADMIN_RULE,
|
|
fields: [
|
|
{
|
|
id: 'relation_invite_team',
|
|
name: 'team',
|
|
type: 'relation',
|
|
required: true,
|
|
maxSelect: 1,
|
|
collectionId: teams.id,
|
|
cascadeDelete: true,
|
|
},
|
|
{
|
|
id: 'text_invite_token',
|
|
name: 'token',
|
|
type: 'text',
|
|
required: true,
|
|
min: 32,
|
|
max: 32,
|
|
autogeneratePattern: '[a-zA-Z0-9]{32}',
|
|
},
|
|
{
|
|
id: 'relation_invite_created_by',
|
|
name: 'created_by',
|
|
type: 'relation',
|
|
maxSelect: 1,
|
|
collectionId: users.id,
|
|
cascadeDelete: false,
|
|
},
|
|
{ id: 'date_invite_expires', name: 'expires', type: 'date' },
|
|
{ id: 'number_invite_max_uses', name: 'max_uses', type: 'number', min: 0, onlyInt: true },
|
|
{ id: 'number_invite_uses', name: 'uses', type: 'number', min: 0, onlyInt: true },
|
|
{ id: 'autodate_created', name: 'created', type: 'autodate', onCreate: true, onUpdate: false },
|
|
{ id: 'autodate_updated', name: 'updated', type: 'autodate', onCreate: true, onUpdate: true },
|
|
],
|
|
indexes: [
|
|
'CREATE UNIQUE INDEX `idx_team_invites_token` ON `team_invites` (`token`)',
|
|
],
|
|
})
|
|
|
|
app.save(invites)
|
|
}, (app) => {
|
|
app.delete(app.findCollectionByNameOrId('team_invites'))
|
|
})
|