@mostajs/booking-checkin-qr
v0.1.0
Published
QR code check-in pour @mostajs/booking. Génère un QR contenant le joinToken HMAC d'une reservation, scan côté staff valide + marque attended. Backend via @mostajs/qrpanel (server-side PNG/SVG, no Chromium).
Readme
@mostajs/booking-checkin-qr
Auteur : Dr Hamid MADANI [email protected] License : AGPL-3.0-or-later Version : 0.1.0
Check-in QR code pour @mostajs/booking. Génère un QR contenant l'URL signée HMAC d'une reservation ; staff scanne à l'arrivée, l'app valide + transitionne automatiquement vers attended. Backend QR via @mostajs/qrpanel (server-side PNG/SVG, no Chromium, 12 thèmes prêts à l'emploi).
Table des matières
- Pourquoi le QR pour check-in
- Flow complet
- Quick start
- API
- Sécurité
- Patterns
- Cas d'usage
- Limites v0.1
- Modules liés
1. Pourquoi le QR pour check-in
Aux restaurants, événements, cabinets médicaux, événements sportifs, salles de cours, le check-in se fait souvent au moment de l'arrivée :
- Hôtesse / staff scanne le QR du client → enregistre l'arrivée
- Borne tablet en self-service où le client scanne → check-in solo
- Portique d'entrée connecté → contrôle automatique
Sans QR : staff doit demander email/nom + lookup base → lent + sujet aux erreurs. Avec QR : scan instantané, identification certaine, audit trail automatique.
Modèle de menace :
- ✅ Token HMAC signé (
reservation.joinTokendéjà fourni par@mostajs/booking) → impossible à forger - ✅ Constant-time compare (déjà fait côté
manager.verifyJoinToken) - ✅ Transition unique
confirmed → attended(impossible 2× = idempotent + audit) - ⚠️ Le QR contient le token en clair dans le QR → ne pas afficher publiquement (mail privé OK)
- ⚠️ Staff scanner doit être AUTH (rôle 'staff'/'host') AVANT d'appeler
scanAndAttend
2. Flow complet
┌─────────────────────────────────────────────────────────────┐
│ 1. Reservation confirmed (via booking core) │
│ reservation.joinToken = HMAC signé (déjà disponible) │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 2. App appelle adapter.generateQR(reservationId, 'png') │
│ → Buffer PNG contenant URL │
│ https://app.example.com/checkin?reservationId=X&token=Y │
└─────────────────────────────────────────────────────────────┘
│
▼
Mail / Dashboard / Apple Wallet
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 3. Client présente QR à l'arrivée │
│ Staff scanne avec téléphone (camera native) │
└─────────────────────────────────────────────────────────────┘
│
▼
GET https://app.example.com/checkin?reservationId=X&token=Y
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 4. Route Next.js → adapter.checkinHandler(req) │
│ a. authorizeScanner(req, reservation) : staff role OK ? │
│ b. manager.verifyJoinToken(id, token) : HMAC valid ? │
│ c. reservation.status === 'confirmed' ? │
│ d. manager.attendReservation(id) → status='attended' │
│ e. Response { ok: true, reservation } │
└─────────────────────────────────────────────────────────────┘3. Quick start
Installation
npm install @mostajs/booking @mostajs/qrpanel @mostajs/booking-checkin-qrSetup
import { createBookingCheckinQR } from '@mostajs/booking-checkin-qr'
import * as qrpanel from '@mostajs/qrpanel'
const checkin = createBookingCheckinQR({
manager, // BookingManager
qr: qrpanel, // pass module entier (PNG/SVG/DataURL helpers)
baseUrl: 'https://app.example.com',
checkinPath: '/checkin', // default
qrDefaults: { width: 600, errorCorrectionLevel: 'H' },
authorizeScanner: async (req, reservation) => {
const session = await getServerSession(req)
if (!session) return new Response('Unauthorized', { status: 401 })
if (!session.user.roles.includes('staff')) {
return new Response('Forbidden — staff role required', { status: 403 })
}
// Optional : vérifier que ce staff est lié à cette resource
return null // autorisé
},
onScanned: (reservation) => console.log('[checkin]', reservation.id, 'attended'),
onCheckinFailed: (reason, id) => console.warn('[checkin] failed', reason, id),
})Générer QR au moment de la confirmation
// app/api/booking/reservations/[id]/qr/route.ts
export async function GET(_req: Request, { params }: { params: Promise<{ id: string }> }) {
const { id } = await params
const png = await checkin.generateQR(id, 'png')
if (!png) return new Response('Reservation not found', { status: 404 })
return new Response(png, {
status: 200,
headers: { 'content-type': 'image/png', 'cache-control': 'private, max-age=300' },
})
}→ user voit dans son dashboard <img src="/api/booking/reservations/abc/qr"> qui affiche le QR.
Route de check-in
// app/api/checkin/route.ts
export async function GET(req: Request) {
return checkin.checkinHandler(req)
}Quand le staff scanne le QR avec son téléphone, le navigateur ouvre directement cette URL → reservation transitionne en attended.
Inclure le QR dans le mail de confirmation
// Combiné avec @mostajs/booking-notifications
import { BOOKING_NOTIFICATION_KINDS } from '@mostajs/booking-notifications'
adapter.setTemplate(BOOKING_NOTIFICATION_KINDS.RESERVATION_CONFIRMED, {
async render(ctx) {
const reservation = ctx.reservation as any
const qrDataUrl = await checkin.generateQR(reservation.id, 'dataUrl')
return {
to: (ctx as any).email,
subject: 'Votre réservation confirmée',
html: `
<h1>Réservation confirmée</h1>
<p>Présentez ce QR à l'arrivée :</p>
<img src="${qrDataUrl}" alt="QR check-in" width="300">
`,
}
},
})4. API
createBookingCheckinQR(opts) → BookingCheckinQRAdapter
interface BookingCheckinQROptions {
manager: BookingManager
qr: QrGenerator // @mostajs/qrpanel ou compatible
baseUrl: string // 'https://app.example.com'
checkinPath?: string // '/checkin' default
qrDefaults?: QrOptions
authorizeScanner?: (req, reservation) => Promise<Response | null> | Response | null
onScanned?: (reservation, req) => void | Promise<void>
onCheckinFailed?: (reason, reservationId?) => void | Promise<void>
}
interface BookingCheckinQRAdapter {
generateQR(reservationId: string, format?: 'png' | 'svg' | 'dataUrl', opts?: QrOptions):
Promise<Buffer | string | null>
buildCheckinUrl(reservation: Reservation): string
scanAndAttend(input: { reservationId: string; token: string }): Promise<ScanResult>
checkinHandler(req: Request): Promise<Response>
}
type ScanResult =
| { ok: true; reservation: Reservation }
| { ok: false; reason: 'invalid-token' | 'not-found' | 'wrong-status' | 'already-attended'; status?: number }QrGenerator interface
interface QrGenerator {
generateQrPng(text: string, opts?: QrOptions): Promise<Buffer>
generateQrSvg(text: string, opts?: QrOptions): Promise<string>
generateQrDataUrl(text: string, opts?: QrOptions): Promise<string>
}@mostajs/qrpanel v0.4+ implémente cette interface directement.
URL construite
${baseUrl}${checkinPath}?reservationId=<id>&token=<joinToken>Exemple : https://app.example.com/checkin?reservationId=abc-123&token=<base64url-sig>
5. Sécurité
Layers de défense
| Layer | Mécanisme | Module |
|---|---|---|
| Token forgé | HMAC SHA-256 + constant-time compare | @mostajs/booking (core) |
| Replay attack (réutiliser un QR) | Status check confirmed → attended (one-way) | @mostajs/booking (state machine) |
| Token sniff (man-in-middle) | HTTPS obligatoire | App + reverse proxy |
| Staff non autorisé scan | authorizeScanner callback (RBAC) | App + @mostajs/rbac |
| Brute-force token | Pas de bruteforce praticable (HMAC 256 bits) | crypto strength |
| QR partagé entre users | Le token est lié à 1 reservation = 1 user ; multi-scan = même result | state machine |
Bonnes pratiques
- ✅ HTTPS obligatoire pour la route check-in (Let's Encrypt / Caddy / Nginx)
- ✅
authorizeScannerstrict : seuls les users avec rôlestaffouhost(de la resource concernée) - ✅ Audit log :
onScannedcallback vers@mostajs/auditpour traçabilité RGPD - ✅ Rate limit la route check-in côté reverse proxy / middleware
- ❌ Ne JAMAIS afficher le QR publiquement (URL avec token = bypass auth)
- ❌ Ne pas mettre le token dans les server logs (filtrer URL avant
console.log)
6. Patterns
Pattern A — Generation lazy (PNG cached à la 1ère request)
const qrCache = new Map<string, Buffer>()
export async function GET(_req, { params }) {
const { id } = await params
const cached = qrCache.get(id)
if (cached) return new Response(cached, { headers: { 'content-type': 'image/png' }})
const png = await checkin.generateQR(id, 'png')
if (!png) return new Response('Not found', { status: 404 })
qrCache.set(id, png as Buffer)
return new Response(png, { headers: { 'content-type': 'image/png' }})
}→ Cache mémoire OK car le QR ne change pas (même token = même URL = même bytes).
Pattern B — QR signé à la volée dans le mail (DataURL embedded)
// Pas de roundtrip HTTP : le QR est inline dans le mail HTML
const dataUrl = await checkin.generateQR(reservation.id, 'dataUrl')
const html = `<img src="${dataUrl}" alt="QR check-in">`⚠️ Limite : certains clients mail bloquent les data URLs (Gmail web ok, Outlook 2019 KO). Sinon → uploader sur CDN puis href absolue.
Pattern C — Thème custom (logo entreprise au centre du QR)
// @mostajs/qrpanel supporte 12 thèmes (logo overlay, frame, etc.)
const png = await checkin.generateQR(id, 'png', { theme: 'studio-dark' })→ ECC=H (30% recovery) déjà default, donc tolérant à l'overlay logo.
Pattern D — Borne self-service (tablet caisse)
<!-- public/checkin-station.html -->
<input id="manual-token" placeholder="Ou tapez l'ID manuellement">
<button onclick="manual()">Check-in</button>
<script>
async function manual() {
const [id, token] = document.getElementById('manual-token').value.split('|')
const r = await fetch('/api/checkin', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ reservationId: id, token }),
})
// ...
}
</script>→ checkinHandler accepte body JSON aussi ({ reservationId, token }).
Pattern E — Walk-in / no-show flow
// Après l'horaire de la session, marquer no-show les confirmed non-attended
setInterval(async () => {
const past = await manager.listReservations({ status: ['confirmed'] })
for (const r of past) {
const slot = await manager.listPersistedSlots({}).then(s => s.find(x => x.id === r.slotId))
if (slot && slot.endAt < Date.now() && !r.attendedAt) {
await manager.markNoShow(r.id)
}
}
}, 5 * 60_000) // every 5 min→ Pas spécifique au QR mais complémentaire : le scan check-in et le mark no-show forment la state machine complete.
7. Cas d'usage
🍽️ Restaurant (OpenTable-like)
- Client réserve une table 4 couverts via Cal-style flow
- Mail de confirmation contient QR (data URL embedded)
- À l'arrivée, hôtesse scanne avec sa tablette →
attended+ service débute - No-show 30 min après slot.startAt →
markNoShowautomatique (free table for walk-ins)
🎫 Événement (Eventbrite-like)
- Achat ticket → reservation
pending→ paiement OK →confirmed - Ticket QR émis (Apple Wallet pass + PNG dans le mail)
- Entrée : portique scanne → permet l'accès
- Multi-scan d'un même QR : 1er succès, suivants retournent
{ ok: true, alreadyAttended: true }(idempotent)
🩺 Cabinet médical (Doctolib-like)
- Patient reçoit le QR par mail
- Borne d'accueil tablet : patient scanne lui-même son QR → check-in + ticket d'appel imprimé
- Praticien voit en temps réel les patients arrivés dans la salle d'attente
🎓 Cours / formation
- Étudiant inscrit à une session live SFU + presentiel
- Mail de rappel J-1 inclut QR
- À l'entrée de la salle physique : enseignant scanne pour valider la présence
- Attendance = base pour notation continue / attestation présence
🏋️ Salle de sport / cours collectifs
- Adhérent réserve un créneau yoga (1 sur 20 places)
- Portique d'entrée scan QR → valide créneau + déduit 1 séance du pack
8. Limites v0.1
- Pas de wallet Pass natif (Apple Wallet
.pkpass, Google Pay) — l'app génère manuellement si besoin - Pas de notification push à l'arrivée (host = "X vient d'arriver") — utiliser
onScanned+@mostajs/notifications-pushfutur - Pas de retour visuel d'erreur custom dans le QR scan (handler retourne JSON, pas HTML rendered) — l'app peut wrap
checkinHandlerpour rendre une page HTML - Manager
listReservationslinéaire : pour large volumes, l'app peut wrapper avec un cache mémoire (cf. Pattern A pour les QRs) - Pas de waitlist auto-promote au no-show — combinaison avec
@mostajs/booking-waitlistfutur
9. Modules liés
@mostajs/booking— fournitjoinTokenHMAC +verifyJoinToken+attendReservation@mostajs/qrpanel— backend QR generation server-side (peer dep)@mostajs/booking-notifications— pour envoyer le QR par mail (template confirmation)@mostajs/auth+@mostajs/rbac— autorisation scanner (authorizeScannercallback)@mostajs/audit— log des check-ins viaonScannedcallback
License : AGPL-3.0-or-later Auteur : Dr Hamid MADANI [email protected]
