Files
SRCmail/app/api/sharing/route.ts
T
Bernd Rodler b98ab59f0d fix: Phase 2 QA — all 25 remaining HIGH/MEDIUM/LOW issues
HIGH fixes (7):
- H1: VNCdirectory admin i18n — 30+ translation keys added
- H2: handleSave try/catch with error toast
- H3: Free/busy accountId scoping
- H4: cancelEventBookings filter by eventId
- H5: Resource picker static apiFetch import
- H6: Sharing-store toast messages via lastMessage state
- H7: roleLabel for all resource types

MEDIUM fixes (11):
- M1: identitySignatureMap cleanup on delete
- M2: Now-line relative positioning
- M3: Radial menu disabled item keyboard nav
- M4: Radial menu stable event listener via refs
- M5: cancelBooking error on missing booking
- M6: PasswordRow isMasked state flag
- M7: Extract shared rights into lib/sharing-rights.ts
- M8: VNCtalk client server-side guard
- M9: Collabora configManager instead of process.env
- M10: CONFIG_ENV_MAP VNCdirectory fields
- M11: SENSITIVE_CONFIG_KEYS field name unification

LOW fixes (7):
- L1-L3: Unused imports removed
- L4: aria-labels on close, clear, search, spinner
- L5-L7: Comments for intentional patterns, null guard
2026-08-07 14:21:07 +02:00

195 lines
4.8 KiB
TypeScript

import type { NextRequest } from "next/server";
import { resolveRights, type SharedResourceKind } from "@/lib/sharing-rights";
type JmapMethodCall = [string, Record<string, unknown>, string];
async function jmapRequest(
serverUrl: string,
authHeader: string,
methodCalls: JmapMethodCall[],
using?: string[],
) {
const sessionResp = await fetch(`${serverUrl}/.well-known/jmap`, {
headers: { Authorization: authHeader },
});
if (!sessionResp.ok) {
return { error: `Session fetch failed: ${sessionResp.status}` };
}
const session = await sessionResp.json();
const apiUrl = session.apiUrl;
if (!apiUrl) {
return { error: "No API URL in JMAP session" };
}
const body = {
using: using || [
"urn:ietf:params:jmap:core",
"urn:ietf:params:jmap:mail",
"urn:ietf:params:jmap:principals",
],
methodCalls,
};
const resp = await fetch(apiUrl, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: authHeader,
},
body: JSON.stringify(body),
});
if (!resp.ok) {
return { error: `JMAP request failed: ${resp.status}` };
}
return await resp.json();
}
export async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url);
const action = searchParams.get("action");
const serverUrl = request.headers.get("X-JMAP-Server-Url");
const authHeader = request.headers.get("Authorization");
if (!serverUrl || !authHeader) {
return Response.json(
{ error: "Missing server URL or auth header" },
{ status: 400 },
);
}
if (action !== "principals") {
return Response.json(
{ error: "Invalid action" },
{ status: 400 },
);
}
const result = await jmapRequest(serverUrl, authHeader, [
["Principal/query", { accountId: "" }, "0"],
["Principal/get", {
accountId: "",
"#ids": {
resultOf: "0",
name: "Principal/query",
path: "/ids",
},
}, "1"],
]);
if ("error" in result) {
return Response.json(result, { status: 502 });
}
const getResp = (result as Record<string, unknown>).methodResponses as Array<[string, Record<string, unknown>, string]> | undefined;
const principals = getResp?.find((r) => r[0] === "Principal/get")?.[1]
?.list ?? [];
return Response.json({ principals });
}
export async function POST(request: NextRequest) {
const serverUrl = request.headers.get("X-JMAP-Server-Url");
const authHeader = request.headers.get("Authorization");
if (!serverUrl || !authHeader) {
return Response.json(
{ error: "Missing server URL or auth header" },
{ status: 400 },
);
}
let body: Record<string, unknown>;
try {
body = await request.json();
} catch {
return Response.json({ error: "Invalid JSON body" }, { status: 400 });
}
const { kind, resourceId, principalId, role } = body;
if (!kind || !resourceId || !principalId) {
return Response.json(
{ error: "Missing required fields: kind, resourceId, principalId" },
{ status: 400 },
);
}
let method: string;
let shareProperty: string;
switch (kind) {
case "mailbox":
method = "Mailbox/set";
shareProperty = "shareWith";
break;
case "calendar":
method = "Calendar/set";
shareProperty = "shareWith";
break;
case "addressBook":
method = "AddressBook/set";
shareProperty = "shareWith";
break;
case "file":
method = "FileNode/set";
shareProperty = "shareWith";
break;
default:
return Response.json(
{ error: `Invalid kind: ${kind}` },
{ status: 400 },
);
}
const patchValue = role === null ? null : resolveRights(kind as SharedResourceKind, role as string);
const methodCalls: JmapMethodCall[] = [
[
method,
{
accountId: "",
update: {
[resourceId as string]: {
[`${shareProperty}/${principalId}`]: patchValue,
},
},
},
"0",
],
];
const result = await jmapRequest(
serverUrl,
authHeader,
methodCalls,
);
if ("error" in result) {
return Response.json(result, { status: 502 });
}
const responses = (result as Record<string, unknown>).methodResponses as Array<[string, Record<string, unknown>, string]> | undefined;
const setResult = responses?.[0]?.[1];
if (
setResult &&
typeof setResult === "object" &&
"notUpdated" in setResult &&
setResult.notUpdated &&
typeof setResult.notUpdated === "object" &&
(resourceId as string) in setResult.notUpdated
) {
const err = (setResult.notUpdated as Record<string, Record<string, unknown>>)[resourceId as string];
return Response.json(
{ error: err.description || "Failed to update share" },
{ status: 400 },
);
}
return Response.json({ ok: true });
}