77 lines
2.7 KiB
TypeScript
77 lines
2.7 KiB
TypeScript
import { NextRequest, NextResponse } from "next/server";
|
|
import { prisma } from "@/lib/prisma";
|
|
import bcrypt from "bcryptjs";
|
|
|
|
async function authenticate(req: NextRequest) {
|
|
const authHeader = req.headers.get("authorization");
|
|
if (!authHeader?.startsWith("Basic ")) return null;
|
|
const decoded = Buffer.from(authHeader.slice(6), "base64").toString();
|
|
const [email, password] = decoded.split(":");
|
|
const user = await prisma.user.findUnique({ where: { email } });
|
|
if (!user) return null;
|
|
const valid = await bcrypt.compare(password, user.passwordHash);
|
|
return valid ? user : null;
|
|
}
|
|
|
|
function taskToVTodo(task: { id: string; title: string; note: string | null; completed: boolean; completedAt: Date | null; dueDate: Date | null; priority: number; createdAt: Date; updatedAt: Date }) {
|
|
const now = new Date().toISOString().replace(/[-:]/g, "").split(".")[0] + "Z";
|
|
const priorityMap: Record<number, number> = { 0: 0, 1: 9, 2: 5, 3: 1 };
|
|
let vtodo = [
|
|
"BEGIN:VTODO",
|
|
`UID:${task.id}@checkflow`,
|
|
`DTSTAMP:${now}`,
|
|
`CREATED:${task.createdAt.toISOString().replace(/[-:]/g, "").split(".")[0] + "Z"}`,
|
|
`LAST-MODIFIED:${task.updatedAt.toISOString().replace(/[-:]/g, "").split(".")[0] + "Z"}`,
|
|
`SUMMARY:${task.title.replace(/\n/g, "\\n")}`,
|
|
`STATUS:${task.completed ? "COMPLETED" : "NEEDS-ACTION"}`,
|
|
`PRIORITY:${priorityMap[task.priority] ?? 0}`,
|
|
];
|
|
if (task.note) vtodo.push(`DESCRIPTION:${task.note.replace(/\n/g, "\\n")}`);
|
|
if (task.dueDate) vtodo.push(`DUE:${task.dueDate.toISOString().replace(/[-:]/g, "").split(".")[0] + "Z"}`);
|
|
if (task.completedAt) vtodo.push(`COMPLETED:${task.completedAt.toISOString().replace(/[-:]/g, "").split(".")[0] + "Z"}`);
|
|
vtodo.push("END:VTODO");
|
|
return vtodo.join("\r\n");
|
|
}
|
|
|
|
// Handle all CalDAV/CardDAV requests
|
|
export async function GET(req: NextRequest) {
|
|
const user = await authenticate(req);
|
|
if (!user) {
|
|
return new NextResponse("Unauthorized", {
|
|
status: 401,
|
|
headers: { "WWW-Authenticate": 'Basic realm="CheckFlow"' },
|
|
});
|
|
}
|
|
|
|
const tasks = await prisma.task.findMany({
|
|
where: { userId: user.id, parentId: null },
|
|
orderBy: { sortOrder: "asc" },
|
|
});
|
|
|
|
const icsContent = [
|
|
"BEGIN:VCALENDAR",
|
|
"VERSION:2.0",
|
|
"PRODID:-//CheckFlow//CheckFlow//EN",
|
|
"CALSCALE:GREGORIAN",
|
|
"METHOD:PUBLISH",
|
|
...tasks.map(taskToVTodo),
|
|
"END:VCALENDAR",
|
|
].join("\r\n");
|
|
|
|
return new NextResponse(icsContent, {
|
|
headers: {
|
|
"Content-Type": "text/calendar; charset=utf-8",
|
|
"Content-Disposition": "attachment; filename=checkflow.ics",
|
|
},
|
|
});
|
|
}
|
|
|
|
// OPTIONS for CORS
|
|
export async function OPTIONS() {
|
|
return new NextResponse(null, {
|
|
headers: {
|
|
Allow: "GET, OPTIONS",
|
|
"DAV": "1, 2, calendar-access",
|
|
},
|
|
});
|
|
} |