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
195 lines
4.8 KiB
TypeScript
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 });
|
|
}
|
|
|
|
|