Add spec compliance improvements: seed switch alert, calendar emojis, period indicator, IP logging
- NutritionPanel: Display seed switch alert on day 15 per dashboard spec - MonthView: Add phase emojis to legend (🩸🌱🌸🌙🌑) per calendar spec - DayCell: Show period indicator (🩸) for days 1-3 per calendar spec - Auth middleware: Log client IP from x-forwarded-for/x-real-ip per observability spec - Updated NutritionGuidance type to include seedSwitchAlert field - /api/today now returns seedSwitchAlert in nutrition response Test coverage: 1005 tests (15 new tests added) - nutrition-panel.test.tsx: +4 tests - month-view.test.tsx: +1 test - day-cell.test.tsx: +5 tests - auth-middleware.test.ts: +3 tests - today/route.test.ts: +2 tests Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -42,6 +42,23 @@ export type AuthenticatedHandler<T = unknown> = (
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
/**
|
||||
* Extracts client IP address from request headers.
|
||||
* Checks x-forwarded-for and x-real-ip headers, returns "unknown" if neither present.
|
||||
*/
|
||||
function getClientIp(request: NextRequest): string {
|
||||
const forwardedFor = request.headers.get("x-forwarded-for");
|
||||
if (forwardedFor) {
|
||||
// x-forwarded-for can contain multiple IPs; first one is the client
|
||||
return forwardedFor.split(",")[0].trim();
|
||||
}
|
||||
const realIp = request.headers.get("x-real-ip");
|
||||
if (realIp) {
|
||||
return realIp;
|
||||
}
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
export function withAuth<T = unknown>(
|
||||
handler: AuthenticatedHandler<T>,
|
||||
): (request: NextRequest, context?: { params?: T }) => Promise<NextResponse> {
|
||||
@@ -57,16 +74,19 @@ export function withAuth<T = unknown>(
|
||||
const cookieStore = await cookies();
|
||||
loadAuthFromCookies(pb, cookieStore);
|
||||
|
||||
// Get client IP for logging
|
||||
const ip = getClientIp(request);
|
||||
|
||||
// Check if the user is authenticated
|
||||
if (!isAuthenticated(pb)) {
|
||||
logger.warn({ reason: "not_authenticated" }, "Auth failure");
|
||||
logger.warn({ reason: "not_authenticated", ip }, "Auth failure");
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
|
||||
// Get the current user
|
||||
const user = getCurrentUser(pb);
|
||||
if (!user) {
|
||||
logger.warn({ reason: "user_not_found" }, "Auth failure");
|
||||
logger.warn({ reason: "user_not_found", ip }, "Auth failure");
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user