122 lines
3.4 KiB
TypeScript

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<T>(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 },
);
}
}