import * as Sentry from "@sentry/nextjs"; import { NextResponse } from "next/server"; import { getSession } from "@/lib/session"; import { NextcloudClient } from "@/lib/webdav"; import { env } from "@/lib/env"; import { joinPath, parentPath } from "@/lib/paths"; export const runtime = "nodejs"; export const dynamic = "force-dynamic"; type SignInBody = { baseUrl?: string; username?: string; appPassword?: string; }; function json(data: T, init?: { status?: number } & ResponseInit) { return NextResponse.json(data, init); } function normalizeBaseUrl(input: string): string { // Throws on invalid URL const u = new URL(input); let s = u.toString(); if (s.endsWith("/")) s = s.slice(0, -1); return s; } function deriveRootPath(username: string): string { // Use the pattern of the configured root path to substitute the username. // Example: /remote.php/dav/files/admin -> /remote.php/dav/files/{username} const baseParent = parentPath(env.NEXTCLOUD_ROOT_PATH) || "/remote.php/dav/files"; return joinPath(baseParent, username); } export async function POST(req: Request) { try { const body = (await req.json().catch(() => ({}))) as SignInBody; const baseUrlRaw = body.baseUrl?.trim(); const username = body.username?.trim(); const appPassword = body.appPassword?.trim(); if (!baseUrlRaw || !username || !appPassword) { return json( { error: "INVALID_REQUEST", message: "baseUrl, username, and appPassword are required", }, { status: 400 }, ); } const result = await Sentry.startSpan( { op: "function", name: "api.auth.signin" }, async (span) => { let baseUrl: string; try { baseUrl = normalizeBaseUrl(baseUrlRaw); } catch { return { ok: false, status: 400 as const, payload: { error: "INVALID_BASE_URL", message: "baseUrl must be a valid URL" }, }; } span.setAttribute("baseUrl", baseUrl); span.setAttribute("username", username); // Build a per-session client rooted to the user's files path const rootPath = deriveRootPath(username); const client = new NextcloudClient(rootPath, { baseUrl, username, appPassword, }); // Validate credentials with a lightweight directory listing try { await client.listDirectory(); } catch (err) { Sentry.captureException(err); return { ok: false, status: 401 as const, payload: { error: "INVALID_CREDENTIALS", message: "Failed to authenticate with Nextcloud WebDAV", }, }; } // Persist session const res = NextResponse.json({ ok: true }); const session = await getSession(req, res); session.auth = { baseUrl, username, appPassword, createdAt: Date.now(), }; await session.save(); return { ok: true, status: 200 as const, response: res }; }, ); if (!result.ok) { return json(result.payload, { status: result.status }); } return result.response!; } catch (error) { Sentry.captureException(error); return json( { error: "SIGNIN_FAILED", message: error instanceof Error ? error.message : String(error), }, { status: 500 }, ); } }