Kein fertiger Stand, sondern ein Zwischenstand: Hier wurde im September 2026 angefangen, die App auf „Kurbeler" umzubenennen — Wortmarke, Icons, brand.ts, Benachrichtigungen, das Benutzerverzeichnis. Weitergegangen ist es dann im eigenen Repo `~/Dokumente/Projekte/kurbeler`, das die vollständige Historie dieses Repos mitgenommen hat. Committet wird das hier nicht, weil es gebraucht würde, sondern damit es nicht als Haufen unversionierter Dateien im Arbeitsverzeichnis verrottet. Wer in zwei Jahren nachsieht, findet so eine Geschichte statt eines Rätsels — und das Stammtisch-Wappen, das nur hier je existiert hat. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LTw6xVYgjMy9AQtfs1Gdyu
174 lines
5.6 KiB
JavaScript
174 lines
5.6 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'),
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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 }
|