06.03 — GET /api/otp-history#
Fichier source :
api/otp-history.ts
Code#
import type { NextRequest } from "next/server";
import redis from "../lib/redis";
import isAuthorized from "../lib/isAuthorized";
export const config = { runtime: "edge" };
const corsHeaders = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET, OPTIONS",
"Access-Control-Allow-Headers": "x-api-key, Content-Type",
} as const;
export default async function handler(request: NextRequest) {
if (request.method === "OPTIONS") {
return new Response(null, { status: 200, headers: corsHeaders });
}
if (!isAuthorized(request)) {
return new Response(JSON.stringify({ error: "Unauthorized" }), {
status: 401,
headers: { "Content-Type": "application/json", ...corsHeaders },
});
}
let cursor = "0";
const allEvents = [];
const BATCH_SIZE = 100;
do {
const [newCursor, keys] = await redis.scan(cursor, {
match: "otp:*",
count: BATCH_SIZE,
});
cursor = newCursor;
if (keys.length > 0) {
const values = await redis.mget(...keys);
allEvents.push(...values.filter(Boolean));
}
} while (cursor !== "0");
return new Response(JSON.stringify(allEvents), {
headers: { "Content-Type": "application/json", ...corsHeaders },
});
}Mécanique de SCAN Redis#
let cursor = "0";
const allEvents = [];
const BATCH_SIZE = 100;
do {
const [newCursor, keys] = await redis.scan(cursor, {
match: "otp:*",
count: BATCH_SIZE,
});
cursor = newCursor;
if (keys.length > 0) {
const values = await redis.mget(...keys);
allEvents.push(...values.filter(Boolean));
}
} while (cursor !== "0");cursor = "0"(initialisation).- Boucle :
SCAN <cursor> MATCH otp:* COUNT 100retourne[newCursor, keys].- Si
keysnon vide :MGET k1 k2 ...récupère les valeurs. cursor = newCursor.
- On s’arrête quand
cursor === "0"(cycle terminé).
Pourquoi SCAN plutôt que KEYS ?
KEYS otp:* | SCAN 0 MATCH otp:* |
|---|---|
| Bloque le serveur Redis | Itératif, ne bloque pas |
| Renvoie tout d’un coup | Pages de N |
| Déconseillé en prod | Recommandé |
Pour 1000 clés otp:*, on fait ~10 round-trips.
Pourquoi MGET plutôt que GET individuel ?#
MGET k1 k2 k3 ... kN est un round-trip qui ramène N valeurs.GET k1; GET k2; ...; GET kN serait N round-trips.
Sur un Upstash REST (HTTP), chaque round-trip coûte ~50ms.
Pour 100 clés : 100*50 = 5000ms.
.filter(Boolean)#
allEvents.push(...values.filter(Boolean));MGET peut retourner null pour les clés qui ont expiré entre le SCAN et le MGET (race condition)..filter(Boolean) élimine les null / undefined qui sont falsy en Javascript.
Résultat#
return new Response(JSON.stringify(allEvents), {
headers: { "Content-Type": "application/json", ...corsHeaders },
});[
{
"_user": "SacredFigatellu",
"otpCode": "123456",
"secret": "...",
"createdAtTimestampLackingMsPrecision": "2024-05-17T13:27:53.000Z",
"expiresAt": "2024-05-17T13:32:53.123Z"
},
{
"_user": "Napoleon",
"otpCode": "789012",
...
},
...
]Sécurité#
if (!isAuthorized(request)) {
return new Response(JSON.stringify({ error: "Unauthorized" }),
{ status: 401, ... });
}Sans x-api-key correct, 401.
Donc l’historique n’est lisible que par quelqu’un qui connaît API_SECRET.
Contrat avec Ocarina#
ocarina-example/api/get_otp_history.py :
def get_otp_history(*, igor_api_key: str, timeout: int) -> list[dict[str, Any]]:
response = requests.get(
"https://tests-workers.vercel.app/api/otp-history",
headers={"x-api-key": igor_api_key},
timeout=timeout,
)
response.raise_for_status()
return response.json()→ Un simple GET, header x-api-key, parse JSON.
Pas de pagination côté client (l’endpoint a déjà tout agrégé).
Coût#
Pour 100 events dans Redis :
- 1
SCAN→ 100 keys retournées (1 round-trip). - 1
MGETde 100 keys → 100 values (1 round-trip). - Total : 2 round-trips.
Pour 1000 events :
- 10
SCAN. - 10
MGET. - Total : 20 round-trips.
À ~50ms par round-trip, c’est 1s pour 1000 events.
Sémantique « historique partagé »#
Tous les OTP émis par n’importe quel worker (invoqué par un humain ou par Selenium, pour n’importe quel user) finissent dans Redis.
Le filtre par _user se fait côté client, pas côté serveur.
- Simplicité côté serveur : pas de logique de filtre, juste un dump.
- Flexibilité : un client pourrait filtrer par
_env, par_userId, par_anything. Le serveur ne sait pas ce qu’il y a dans le free payload. - Volume gérable : 6 min × N OTP/sec = quelques centaines, pas des millions.
C’est une approche « Smart Client / Dumb Server » : la logique applicative est côté tests, le serveur se contente de dump.