/// // 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'), } } /** * Wem gilt die Einladung? Beim Einladungslink: niemandem Bestimmten. * * Eine persönliche Einladung nennt Adresse und Namen. Die Seite füllt beides * vor und lässt die Adresse nicht ändern — sonst legte jemand mit fremdem * Link ein Konto auf eine Adresse an, an die nie eine Mail ging. * * Das ist keine Preisgabe: Wer den Token hat, hat die Mail bekommen, in der * die Adresse ohnehin steht. */ function recipientPayload(app, invite) { const email = invite.getString('email') if (!email) { return { email: '', name: '', personal: false } } let name = '' const riderId = invite.getString('rider') if (riderId) { try { name = app.findRecordById('riders', riderId).getString('name') } catch (err) { name = '' } } return { email: email, name: name, personal: true } } /** * 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 } // Eine persönliche Einladung nennt den Fahrer, um den es geht: Sie ging // an jemanden, der längst im Kader steht und nur noch kein Konto hat. // Ohne diesen Zweig entstünde daneben ein zweiter Fahrer gleichen Namens // — dieselbe Person doppelt, einmal mit Zeiten und einmal mit Login. if (!rider) { const invitedRiderId = invite.getString('rider') if (invitedRiderId) { try { const invited = txApp.findRecordById('riders', invitedRiderId) // Nur, wenn er noch frei ist. Hat er inzwischen ein Konto, // gehört er jemand anderem; dann ist ein eigener Fahrer für // den Eingeladenen richtig. if (invited.getString('team') === team.id && !invited.getString('user')) { invited.set('user', user.id) txApp.save(invited) rider = invited } } 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, recipientPayload, joinTeam }