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
145 lines
5.2 KiB
JavaScript
145 lines
5.2 KiB
JavaScript
/// <reference path="../pb_data/types.d.ts" />
|
|
|
|
// Die öffentliche Seite der Einladungslinks.
|
|
//
|
|
// Warum überhaupt eigene Routen? Wer einem Team über einen Link beitritt, ist
|
|
// noch nicht eingeloggt und muss dabei drei Dinge tun, die keine
|
|
// Collection-Regel erlauben darf:
|
|
//
|
|
// 1. ein Konto anlegen — `users.createRule` verlangt einen Login, sonst
|
|
// wäre es eine offene Registrierung für jedermann;
|
|
// 2. sich in `teams.users` eintragen — das darf sonst nur die Teamleitung;
|
|
// 3. einen Fahrer im Kader anlegen — dito.
|
|
//
|
|
// Ein Hook läuft an den Regeln vorbei und kann deshalb genau diese drei
|
|
// Schritte erlauben, aber nur zusammen und nur gegen einen gültigen Token.
|
|
// Die Collection `team_invites` selbst bleibt für Nicht-Mitglieder unsichtbar.
|
|
//
|
|
// Routen:
|
|
// GET /api/team-invites/{token} — öffentlich, zeigt das Team
|
|
// POST /api/team-invites/{token}/accept — öffentlich, Konto + Beitritt
|
|
// POST /api/team-invites/{token}/join — eingeloggt, nur Beitritt
|
|
//
|
|
// Jeder Handler holt sich seine Helfer selbst per require(): PocketBase führt
|
|
// Route-Handler in einer eigenen JS-Laufzeit aus, der Dateikopf hier ist dort
|
|
// nicht sichtbar.
|
|
|
|
// --- Wofür gilt der Link? ---------------------------------------------------
|
|
//
|
|
// Öffentlich, weil die Seite den Teamnamen zeigen soll, bevor jemand seine
|
|
// Daten einträgt. Mehr als Name und Logo gibt sie nicht preis; die Einladung
|
|
// selbst — Zähler, Ablauf, wer sie erzeugt hat — bleibt drin.
|
|
|
|
routerAdd('GET', '/api/team-invites/{token}', (e) => {
|
|
const lib = require(`${__hooks}/team_invites_lib.js`)
|
|
|
|
const invite = lib.findValidInvite(e.app, e.request.pathValue('token'))
|
|
if (!invite) {
|
|
throw new NotFoundError('Diese Einladung gibt es nicht mehr.')
|
|
}
|
|
|
|
return e.json(200, { team: lib.teamPayload(e.app, invite) })
|
|
})
|
|
|
|
// --- Konto anlegen und beitreten -------------------------------------------
|
|
|
|
routerAdd('POST', '/api/team-invites/{token}/accept', (e) => {
|
|
const lib = require(`${__hooks}/team_invites_lib.js`)
|
|
|
|
const data = new DynamicModel({
|
|
email: '',
|
|
password: '',
|
|
passwordConfirm: '',
|
|
name: '',
|
|
})
|
|
e.bindBody(data)
|
|
|
|
const email = (data.email || '').trim()
|
|
const name = (data.name || '').trim()
|
|
|
|
if (!name) {
|
|
throw new BadRequestError('Bitte einen Namen angeben.')
|
|
}
|
|
if (email.indexOf('@') < 1) {
|
|
throw new BadRequestError('Bitte eine gültige E-Mail-Adresse angeben.')
|
|
}
|
|
if ((data.password || '').length < 8) {
|
|
throw new BadRequestError('Das Passwort braucht mindestens 8 Zeichen.')
|
|
}
|
|
if (data.password !== data.passwordConfirm) {
|
|
throw new BadRequestError('Die beiden Passwörter stimmen nicht überein.')
|
|
}
|
|
|
|
let result
|
|
e.app.runInTransaction((txApp) => {
|
|
// Innerhalb der Transaktion noch einmal nachsehen: Zwischen Anzeige
|
|
// der Seite und dem Absenden kann die Einladung abgelaufen,
|
|
// aufgebraucht oder gelöscht worden sein.
|
|
const invite = lib.findValidInvite(txApp, e.request.pathValue('token'))
|
|
if (!invite) {
|
|
throw new BadRequestError('Diese Einladung gilt nicht mehr.')
|
|
}
|
|
|
|
// Ohne die Vorabprüfung meldete PocketBase nur „Value must be unique".
|
|
// Der Hinweis auf den zweiten Weg ist hier mehr wert als die
|
|
// Fehlermeldung.
|
|
let existing = null
|
|
try {
|
|
existing = txApp.findAuthRecordByEmail('users', email)
|
|
} catch (err) {
|
|
existing = null
|
|
}
|
|
|
|
if (existing) {
|
|
throw new BadRequestError(
|
|
'Zu dieser E-Mail-Adresse gibt es schon ein Konto. ' +
|
|
'Melde dich damit an und öffne den Link danach erneut.',
|
|
)
|
|
}
|
|
|
|
const user = new Record(txApp.findCollectionByNameOrId('users'))
|
|
user.setEmail(email)
|
|
user.setPassword(data.password)
|
|
user.setVerified(false)
|
|
user.set('emailVisibility', false)
|
|
user.set('name', name)
|
|
txApp.save(user)
|
|
|
|
result = lib.joinTeam(txApp, invite, user, name)
|
|
})
|
|
|
|
// Kein Auth-Token in der Antwort: Die App meldet sich anschließend ganz
|
|
// normal mit E-Mail und Passwort an und läuft damit durch dieselbe
|
|
// Anmeldung wie jeder andere auch.
|
|
return e.json(200, { ok: true, team: result.teamId })
|
|
})
|
|
|
|
// --- Als bereits angemeldeter Nutzer beitreten ------------------------------
|
|
|
|
routerAdd('POST', '/api/team-invites/{token}/join', (e) => {
|
|
const lib = require(`${__hooks}/team_invites_lib.js`)
|
|
|
|
const data = new DynamicModel({ name: '' })
|
|
e.bindBody(data)
|
|
|
|
const user = e.auth
|
|
if (!user) {
|
|
throw new UnauthorizedError()
|
|
}
|
|
|
|
let result
|
|
e.app.runInTransaction((txApp) => {
|
|
const invite = lib.findValidInvite(txApp, e.request.pathValue('token'))
|
|
if (!invite) {
|
|
throw new BadRequestError('Diese Einladung gilt nicht mehr.')
|
|
}
|
|
|
|
// Fällt der Name weg, stünde der Beitretende ohne Namen im Kader. Der
|
|
// Kontoname ist die bessere Näherung als gar nichts.
|
|
const name = (data.name || '').trim() || user.getString('name')
|
|
|
|
result = lib.joinTeam(txApp, invite, user, name)
|
|
})
|
|
|
|
return e.json(200, { ok: true, team: result.teamId })
|
|
}, $apis.requireAuth())
|