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
122 lines
3.7 KiB
JavaScript
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 }
|