stammtisch-hersbruck/backend/pb_hooks/team_invites_lib.js
Daniel Michelberger 7ac5e9ab4f feat: Einladungslinks, mit denen sich Fahrer selbst ein Konto anlegen
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
2026-09-07 22:09:14 +02:00

122 lines
3.7 KiB
JavaScript

/// <reference path="../pb_data/types.d.ts" />
// Gemeinsamer Code der Einladungs-Routen.
//
// Warum eine eigene Datei? PocketBase führt jeden Route-Handler in einer
// eigenen JS-Laufzeit aus. Was in team_invites.pb.js neben routerAdd steht,
// ist im Handler nicht sichtbar — der Handler kennt nur seine eigenen
// Variablen, die Globals und was er sich per require() holt.
const INVITES = 'team_invites'
/**
* Die Einladung zum Token, sofern sie gilt — sonst null.
*
* `expires` kommt als "2026-09-30 10:00:00.000Z" zurück, leer heißt: läuft
* nicht ab. `max_uses` 0 heißt: beliebig oft.
*/
function findValidInvite(app, token) {
if (!token) {
return null
}
let invite
try {
invite = app.findFirstRecordByFilter(INVITES, 'token = {:token}', { token: token })
} catch (err) {
return null
}
const expires = invite.getString('expires')
if (expires) {
const ts = Date.parse(expires.replace(' ', 'T'))
if (!isNaN(ts) && ts < Date.now()) {
return null
}
}
const maxUses = invite.getInt('max_uses')
if (maxUses > 0 && invite.getInt('uses') >= maxUses) {
return null
}
return invite
}
/** Team der Einladung samt Logo — genug, damit die Seite es benennen kann. */
function teamPayload(app, invite) {
const team = app.findRecordById('teams', invite.getString('team'))
return {
id: team.id,
collectionId: team.collection().id,
collectionName: 'teams',
name: team.getString('name'),
logo: team.getString('logo'),
}
}
/**
* Trägt einen bestehenden Nutzer ins Team ein und legt ihm einen Fahrer an,
* falls er dort noch keinen hat. Zählt die Einladung hoch.
*
* Erwartet einen txApp — beide Aufrufer stecken in einer Transaktion.
*/
function joinTeam(txApp, invite, user, name) {
const team = txApp.findRecordById('teams', invite.getString('team'))
// Mitgliedschaft über einen Filter prüfen und nicht über das Feld: Was
// `get()` bei einer Mehrfach-Relation zurückgibt, ist ein Go-Slice und
// kein JS-Array — `indexOf` darauf ist nicht verlässlich.
let isMember = false
try {
txApp.findFirstRecordByFilter(
'teams',
'id = {:team} && users.id ?= {:user}',
{ team: team.id, user: user.id },
)
isMember = true
} catch (err) {
isMember = false
}
if (!isMember) {
// "+" hängt an, statt die Liste zu ersetzen — sonst flöge bei zwei
// gleichzeitigen Beitritten einer der beiden wieder raus.
team.set('users+', user.id)
txApp.save(team)
}
// Jedes Teammitglied ist ein Fahrer. Hat der Nutzer im Team schon einen,
// bleibt es bei dem — ein zweiter wäre dieselbe Person doppelt im Kader.
let rider = null
try {
rider = txApp.findFirstRecordByFilter(
'riders',
'team = {:team} && user = {:user}',
{ team: team.id, user: user.id },
)
} catch (err) {
rider = null
}
if (!rider) {
rider = new Record(txApp.findCollectionByNameOrId('riders'))
rider.set('team', team.id)
rider.set('name', name)
rider.set('user', user.id)
txApp.save(rider)
}
// Gezählt wird, wer über den Link ins Team gekommen ist — nicht, wie oft
// jemand darauf geklickt hat. Sonst wäre ein zweiter Klick eines schon
// aufgenommenen Mitglieds bei `max_uses = 1` der letzte für alle anderen.
if (!isMember) {
invite.set('uses', invite.getInt('uses') + 1)
txApp.save(invite)
}
return { teamId: team.id, riderId: rider.id, joined: !isMember }
}
module.exports = { findValidInvite, teamPayload, joinTeam }