Implement calendar ICS feed and token regeneration (P2.6, P2.7)
Add two calendar-related API endpoints: P2.6 - GET /api/calendar/[userId]/[token].ics: - Token-based authentication (no session required) - Validates calendar token against user record - Generates 90 days of phase events using generateIcsFeed() - Returns proper Content-Type and Cache-Control headers - 404 for non-existent users, 401 for invalid tokens - 10 tests covering all scenarios P2.7 - POST /api/calendar/regenerate-token: - Requires authentication via withAuth() middleware - Generates cryptographically secure 32-character hex token - Updates user's calendarToken field in database - Returns new token and formatted calendar URL - Old tokens immediately invalidated - 9 tests covering token generation and auth Total: 19 new tests, 360 tests passing 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -2,6 +2,9 @@
|
||||
// ABOUTME: Returns subscribable iCal feed with cycle phases and warnings.
|
||||
import { type NextRequest, NextResponse } from "next/server";
|
||||
|
||||
import { generateIcsFeed } from "@/lib/ics";
|
||||
import { createPocketBaseClient } from "@/lib/pocketbase";
|
||||
|
||||
interface RouteParams {
|
||||
params: Promise<{
|
||||
userId: string;
|
||||
@@ -11,13 +14,58 @@ interface RouteParams {
|
||||
|
||||
export async function GET(_request: NextRequest, { params }: RouteParams) {
|
||||
const { userId, token } = await params;
|
||||
void token; // Token will be used for validation
|
||||
// TODO: Implement ICS feed generation
|
||||
// Validate token, generate ICS content, return with correct headers
|
||||
return new NextResponse(`ICS feed for user ${userId} not implemented`, {
|
||||
status: 501,
|
||||
headers: {
|
||||
"Content-Type": "text/calendar; charset=utf-8",
|
||||
},
|
||||
});
|
||||
|
||||
try {
|
||||
// Fetch user from database
|
||||
const pb = createPocketBaseClient();
|
||||
const user = await pb.collection("users").getOne(userId);
|
||||
|
||||
// Check if user has a calendar token set
|
||||
if (!user.calendarToken) {
|
||||
return NextResponse.json(
|
||||
{ error: "Unauthorized: Calendar not configured" },
|
||||
{ status: 401 },
|
||||
);
|
||||
}
|
||||
|
||||
// Validate token (case-sensitive comparison)
|
||||
if (user.calendarToken !== token) {
|
||||
return NextResponse.json(
|
||||
{ error: "Unauthorized: Invalid token" },
|
||||
{ status: 401 },
|
||||
);
|
||||
}
|
||||
|
||||
// Generate ICS feed with 90 days of events (3 months)
|
||||
const icsContent = generateIcsFeed({
|
||||
lastPeriodDate: new Date(user.lastPeriodDate as string),
|
||||
cycleLength: user.cycleLength as number,
|
||||
monthsAhead: 3,
|
||||
});
|
||||
|
||||
// Return ICS content with appropriate headers
|
||||
return new NextResponse(icsContent, {
|
||||
status: 200,
|
||||
headers: {
|
||||
"Content-Type": "text/calendar; charset=utf-8",
|
||||
"Cache-Control": "public, max-age=3600", // Cache for 1 hour
|
||||
"Content-Disposition": "attachment; filename=phaseflow-calendar.ics",
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
// Check if error is a "not found" error from PocketBase
|
||||
if (
|
||||
error instanceof Error &&
|
||||
(error as unknown as { status?: number }).status === 404
|
||||
) {
|
||||
return NextResponse.json({ error: "User not found" }, { status: 404 });
|
||||
}
|
||||
|
||||
// Re-throw unexpected errors
|
||||
console.error("Calendar feed error:", error);
|
||||
return NextResponse.json(
|
||||
{ error: "Internal server error" },
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user