Lalabase
티켓·문서·근무시간·고객대화를 한 곳에 모아 KI로 검색, 소프트웨어 기업 고객 프로젝트용, 오픈소스로 자체 서버·독일 클라우드에 배포
아직 감지된 업데이트가 없습니다. GitHub 릴리스가 나오면 여기에 보입니다.
개인 계정의 공개 저장소 소유자는 GitHub로 로그인해 운영자를 확인하고 소개를 직접 관리할 수 있습니다. 관리자 승인은 필요하지 않습니다.
로그인한 뒤 확인 파일을 만들어 이 저장소의 기본 브랜치에 30분 안에 커밋해 주세요. 다른 브랜치나 포크의 파일은 인정하지 않습니다. 파일을 올린 뒤 아래에서 확인하면 관리자 승인 없이 관리할 수 있습니다. 자격을 유지하려면 파일을 남겨 두세요.
로그인이 필요하면 로그인 화면으로 이동합니다. 로그인 후 이곳으로 돌아와 원하는 확인 버튼을 다시 눌러 주세요.
프로젝트 폴더에서 /nomorevibe verify를 실행하면 소유권을 확인하고 소개를 직접 관리할 수 있습니다.
1분 만에 확인하기README 전체를 불러오는 중입니다. 아래는 저장된 발췌입니다.
ScanHub - Plateforme Centralisée de Scan & Vérification QR (Next.js 15) API REST centralisée et panneau d'administration pour la gestion des QR codes, des billets, badges et contrôles d'accès événementiels (EventHub, JNJL et partenaires).README 전문 보기 ↗
API REST centralisée et panneau d'administration pour la gestion des QR codes, des billets, badges et contrôles d'accès événementiels (EventHub, JNJL et partenaires).
POST /api/auth/login — vérification email/mot de passe
{ requires2FA: true, tempToken } (challenge 5 min)POST /api/auth/2fa/login — vérification TOTP → session complèteaccessToken (JWT 1h) + refreshToken (7j / 30j avec rememberMe)allowMultipleSessions dans settings/)Chaque organisateur dispose d'un espace fermé (table Organization). Ses membres ne voient
et ne modifient que ses événements, billets, scans, terminaux et clés API.
| Rôle | Portée |
|---|---|
SUPER_ADMIN | Propriétaire de la plateforme. N'appartient à aucun espace, voit tout, gère les espaces (/admin/organizations) |
ADMIN | Administrateur de son espace : équipe, événements, billets, terminaux, billetteries |
AGENT | Contrôle les entrées depuis l'application mobile, dans son espace |
USER | Consultation du tableau de bord de son espace |
/auth/signup) : crée l'espace et son premier administrateur./admin/users) : l'administrateur ajoute ses agents (mot de passe temporaire affiché une fois)./events/<id>) : saisie ou import CSV, QR codes à imprimer, export CSV, révocation./integrations) : une clé API par billetterie externe.settings/plans.ts) : tout est gratuit ; la formule FREE fixe des plafonds anti-abus
(événements ouverts, billets par événement, terminaux, membres). Le propriétaire peut passer un
espace en UNLIMITED ou le suspendre.Règle pour tout nouveau code : une requête Prisma sur une donnée métier passe toujours par les
filtres scope.* de lib/tenant.ts. Le cloisonnement est vérifié par tests/e2e/tenant.e2e.cjs.
Les tokens des billets créés dans ScanHub sont stockés hachés (vérification) et chiffrés
AES-256-GCM (tokenEnc, pour ré-imprimer le QR). Changer AES_SECRET_KEY rend ces QR
non ré-affichables (ils restent valides au scan).
L'API /api/v1 utilise la même authentification que le portail web (JWT RS256 chiffré
AES-256-GCM + session en base). Il n'existe plus de secret JWT séparé.
| Appelant | Identification |
|---|---|
| Agent (app mobile) | Authorization: Bearer <accessToken> + x-device-key: <clé du terminal> |
| Partenaire (EventHub, JNJL…) | x-api-key: <clé secrète> sur /api/v1/integrations/* |
| Tableau de bord | cookie de session (ex : export CSV) |
POST /api/v1/auth/login { email, password } → { user, tokens: { accessToken, refreshToken, expiresIn } }POST /api/v1/auth/refresh { refreshToken } → nouveau accessToken (l'ancien est aussitôt invalide)POST /api/v1/auth/logout → ferme la session courante uniquementPOST /api/v1/devices) :
un code d'activation à usage unique, valable 48 h, est affiché une seule fois.POST /api/v1/devices/activate renvoie la clé du terminal
(deviceKey), à envoyer ensuite dans l'en-tête x-device-key.403, DEVICE_BLOCKED).Un compte AGENT ne peut scanner que depuis un terminal activé.
POST /api/v1/qr/verify — lecture seule.POST /api/v1/qr/consume — enregistre le passage. Pour un billet à usage unique, la transition
ACTIVE → USED est atomique : deux scans simultanés ne peuvent pas être validés tous les deux.
clientId (UUID) rend l'appel idempotent.GET /api/v1/sync/pull?eventId=… — annuaire d'un événement pour le cache hors-ligne
(tokenHash = SHA-256 du token, recalculable par le terminal).POST /api/v1/sync/push — remontée des scans hors-ligne. Le serveur revalide chaque scan ;
un billet déjà consommé ailleurs est renvoyé en CONFLICT./api/v1/integrations)Pour créer et vérifier des QR codes depuis une autre plateforme, avec la seule clé API
(en-tête x-api-key). Tout reste dans l'espace de la clé.
| Méthode | Chemin | Rôle |
|---|---|---|
GET / POST | /events | Lister / créer des événements |
POST | /qrs | Créer un code. Idempotent sur externalId : rejouer rend le même code (200, created: false) |
POST | /qrs/batch | Jusqu'à 500 codes par appel, même idempotence |
GET | /qrs/:id | Détail (:id = identifiant ScanHub ou externalId) |
GET | /qrs/:id/image | Image du QR (?format=png|svg, ?size=) |
POST | /qrs/:id/revoke, /qrs/:id/restore | Révoquer / rétablir |
POST | /verify | Vérifier sans consommer |
POST | /consume | Valider un passage (idempotencyKey ou en-tête Idempotency-Key) |
GET | /scans | Présences, lecture incrémentale par curseur (?after=) |
token), à
condition qu'il soit long et aléatoire ; les valeurs devinables sont refusées (422).tests/e2e/developer-api.e2e.mjs.ScanHub appelle l'adresse du client à chaque scan enregistré (événement scan.created).
Gestion dans Billetteries (API) ou via GET|POST /webhooks, GET|DELETE /webhooks/:id,
POST /webhooks/:id/test.
ScanHub-Signature: t=<horodatage>,v1=<HMAC-SHA256 de "t.corps">, avec le
secret whsec_… donné une seule fois à la création.WebhookDelivery)./api/cron/webhooks chaque minute avec
Authorization: Bearer $CRON_SECRET. Sans cela, les nouvelles tentatives ne partent qu'à
l'occasion des scans suivants du même espace.La documentation publique destinée aux clients est servie sur /docs.
Générées depuis Partenaires (API) (bouton « Générer / Régénérer la clé API »). Seul le hash est stocké : la clé n'est visible qu'au moment de sa génération.
tempToken 2FA limité à 5 minutes avec purpose: "2fa_challenge" dans le payload| Couche | Scope | Limite |
|---|---|---|
| In-memory (middleware/Edge) | Global par IP | 20 req / 60s |
| Redis sliding window (API routes) | Auth par IP | 10 tentatives / 15 min |
| Redis sliding window (API routes) | Signup/Email par IP | 5 req / 1h |
'strict-dynamic' + 'nonce-{nonce}' — aucun inline script autorisé sans nonceX-Frame-Options, X-Content-Type-Options, HSTS, Permissions-PolicyAdd-Padding: true pour masquer la taille de la réponseChaque action sensible est tracée dans AuditLog (Prisma) :
LOGIN_SUCCESS, LOGIN_FAILURE, LOGOUT, SIGNUP, 2FA_ENABLED, 2FA_DISABLED, 2FA_FAILURE, 2FA_SUCCESS, PASSWORD_RESET, PASSWORD_CHANGED, IMPERSONATION_START, IMPERSONATION_END, EMAIL_CHANGED, ACCOUNT_DISABLED, ACCOUNT_DELETED
Permet à un ADMIN d'auditer un compte utilisateur sans connaître son mot de passe.
POST /api/admin/impersonate { targetUserId } Authorization: Bearer <adminToken>
DELETE /api/admin/impersonate/end Authorization: Bearer <impersonationToken>
impersonationToken séparé (httpOnly) est crééstartImpersonation(targetUserId) / endImpersonation() (cookies gérés côté serveur uniquement)import { logger } from "@/lib/logger";
logger.logAuth("LOGIN_SUCCESS", userId, email);
logger.logRequest("GET", "/api/health", 200, 12);
logger.logDatabase("query", "user.findUnique", 8);
logger.logSecurity("RATE_LIMIT", ip, "Trop de tentatives");
LOG_LEVEL (env)GET /api/health
Retourne 200 { status: "ok" } si DB + Redis répondent, 503 { status: "degraded" } sinon. Utile pour les probes Kubernetes / load balancers.
ScanHub-Back-End/
├── app/
│ ├── (dashboard)/
│ │ ├── admin/
│ │ │ └── users/ # Interface d'impersonation
│ │ └── …
│ ├── api/
│ │ ├── admin/impersonate/ # POST + DELETE /end
│ │ ├── auth/
│ │ │ ├── login/
│ │ │ ├── signup/
│ │ │ ├── 2fa/
│ │ │ │ ├── setup/ # GET — génère QR code TOTP
│ │ │ │ ├── verify-setup/ # POST — confirme enrollment
│ │ │ │ └── login/ # POST — étape 2 connexion
│ │ │ ├── refresh/
│ │ │ ├── logout/
│ │ │ ├── forgot-password/
│ │ │ ├── reset-password/
│ │ │ ├── change-password/
│ │ │ ├── change-email/
│ │ │ └── verify/
│ │ ├── health/ # GET — healthcheck DB + Redis
│ │ ├── profile/
│ │ ├── upload/
│ │ └── v1/ # Endpoints REST ScanHub (QR, Sync, Scans, Events, Devices)
│ └── layout.tsx # Lit x-nonce pour CSP
├── actions/
│ ├── impersonate.ts # Server Actions (cookies httpOnly)
│ └── getUser.ts # Vérifie impersonationToken en priorité
├── components/
│ ├── ImpersonationBanner.tsx # Bannière admin session
│ ├── admin/ImpersonateButton.tsx
│ └── …
├── lib/
│ ├── tokens.ts # JWT RS256 + AES-256-GCM
│ ├── rateLimit.ts # In-memory + Redis sliding window
│ ├── redis.ts # Singleton ioredis
│ ├── audit.ts # AuditLog helper
│ ├── hibp.ts # HIBP k-anonymity
│ ├── logger.ts # Pino wrapper
│ ├── mail.ts # Nodemailer (login notif, vérif, reset)
│ ├── upload.ts # Magic number + UUID filename
│ └── geo.ts # IP → géolocalisation
├── context/SessionContext.tsx
├── middleware.ts # CSP nonce + rate limiting Edge
├── settings/index.ts # Config centralisée
└── prisma/schema.prisma
git clone <repository-url>
cd ScanHub-Back-End
npm install
cp .env.example .env.local
# Remplir les variables (voir section ci-dessous)
docker compose up -d # PostgreSQL + Redis locaux (optionnel)
npx prisma migrate deploy # Applique les migrations versionnées
npm run seed # Données de test (dev uniquement, voir ci-dessous)
npm run dev
Le seed crée trois comptes (super admin, admin, agent). Leurs mots de passe viennent de
SEED_SUPER_ADMIN_PASSWORD, SEED_ADMIN_PASSWORD et SEED_AGENT_PASSWORD ; s'ils sont absents,
un mot de passe aléatoire est généré et affiché une seule fois dans la console. Le seed refuse de
s'exécuter avec NODE_ENV=production.
SEED_ADMIN_PASSWORD='…' SEED_AGENT_PASSWORD='…' npm run seed
# Base de données PostgreSQL
DATABASE_URL="postgresql://postgres:postgres@localhost:5432/scanhub?schema=public"
# Application
NEXT_PUBLIC_APP_URL="http://localhost:3000"
# JWT (RS256) — générer avec openssl
RSA_PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----\n…"
RSA_PUBLIC_KEY="-----BEGIN PUBLIC KEY-----\n…"
# AES-256-GCM — 32 octets aléatoires en hexadécimal (64 caractères)
AES_SECRET_KEY="hex-encoded-32-bytes"
# Redis
REDIS_URL="redis://localhost:6379"
# Peppers de hachage — OBLIGATOIRES en production (32 octets aléatoires en hex)
# QR_PEPPER : hash des tokens QR. API_KEY_PEPPER : clés partenaires, clés de terminaux,
# codes d'activation. Les changer invalide les valeurs déjà enregistrées.
QR_PEPPER="…"
API_KEY_PEPPER="…"
# Email (Nodemailer)
MAIL_AUTH_USER="[email protected]"
MAIL_AUTH_PASSWORD="your-app-password"
MAIL_HOST="smtp.gmail.com"
MAIL_PORT="587"
# Optionnel
LOG_LEVEL="debug" # trace | debug | info | warn | error
openssl genrsa -out private.pem 2048
openssl rsa -in private.pem -pubout -out public.pem
node scripts/generate-aes-key.js
npm run dev # Développement
npm run build # Build production
npm run start # Serveur production
npm run lint # ESLint
npm run type-check # TypeScript
npm run db:migrate # Crée + applique une migration (dev)
npx prisma migrate deploy # Applique les migrations (production / CI)
npm run db:studio # Prisma Studio
npm run seed # Seed base de données
npm test # Tests Jest
npm run test:e2e # Tests de bout en bout (serveur lancé + base locale, voir tests/e2e/run.cjs)
npm run test:coverage # Couverture
docker build -t scanhub-backend .
docker run -p 3000:3000 \
-e DATABASE_URL="…" \
-e NEXT_PUBLIC_APP_URL="…" \
-e RSA_PRIVATE_KEY="…" \
-e RSA_PUBLIC_KEY="…" \
-e AES_SECRET_KEY="…" \
-e QR_PEPPER="…" \
-e API_KEY_PEPPER="…" \
-e REDIS_URL="…" \
scanhub-backend
L'image n'applique pas les migrations au démarrage : lancer npx prisma migrate deploy depuis
le job de déploiement avant de démarrer le conteneur.
MIT — voir LICENSE.
Développé pour ScanHub