Review

SMS OTP unlock for ColdCase meal-kit lockers

ColdCase drops chilled meal kits into curb-side lockers so drivers never wait for residents. Adds request/verify SMS OTP endpoints so the customer can open their assigned bay without an app account — codes live in Redis for a full delivery day window.

FastAPITier 3securityotpauth

Click a line to flag it, pick one or more labels, then submit. If the change looks correct, approve it.

services/lockers/otp.py+24-0
11import random
22from datetime import timedelta
33
44from fastapi import APIRouter, HTTPException
55from pydantic import BaseModel, Field
66
77from app.redis import redis
88from app.sms import send_sms
99
1010router = APIRouter(prefix="/lockers", tags=["lockers"])
11+
12+OTP_TTL = timedelta(hours=24)
13+
14+class VerifyBody(BaseModel):
15+ code: str = Field(min_length=6, max_length=6, pattern=r"^\d{6}$")
16+
17+@router.post("/{locker_id}/otp/request")
18+async def request_otp(locker_id: str, phone: str):
19+ code = f"{random.randint(0, 999_999):06d}"
20+ await redis.setex(f"locker:otp:{locker_id}", int(OTP_TTL.total_seconds()), code)
21+ await send_sms(phone, f"ColdCase bay code: {code}")
22+ return {"ok": True}
23+
24+@router.post("/{locker_id}/otp/verify")
25+async def verify_otp(locker_id: str, body: VerifyBody):
26+ key = f"locker:otp:{locker_id}"
27+ expected = await redis.get(key)
28+ if expected is None:
29+ raise HTTPException(status_code=400, detail="code expired or missing")
30+ if body.code != expected:
31+ raise HTTPException(status_code=401, detail="invalid code")
32+ await redis.delete(key)
33+ await unlock_bay(locker_id)
34+ return {"unlocked": True}