122 lines
3.4 KiB
TypeScript
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 },
|
|
);
|
|
}
|
|
}
|