Compare commits
18
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2116798281 | ||
|
|
a457c1770e | ||
|
|
88bca86c1e | ||
|
|
60fde141b5 | ||
|
|
e693072862 | ||
|
|
7c127d8183 | ||
|
|
cf93b24abc | ||
|
|
d103c47cda | ||
|
|
20dadea2dd | ||
|
|
e696c65f75 | ||
|
|
33ca4bae37 | ||
|
|
424dba39f7 | ||
|
|
16853a364c | ||
|
|
8f2c89f9a8 | ||
|
|
54c508e8df | ||
|
|
d021a0a87c | ||
|
|
410aa52217 | ||
|
|
a227396e54 |
@@ -9,6 +9,11 @@ import { Loader2, AlertCircle } from "lucide-react";
|
|||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { useParams } from "next/navigation";
|
import { useParams } from "next/navigation";
|
||||||
|
|
||||||
|
// Module-level guard so a Suspense/search-params remount of this client
|
||||||
|
// component can't exchange the same OAuth code twice — Keycloak rejects a
|
||||||
|
// reused code with `invalid_grant` ("Code not valid") and the login fails.
|
||||||
|
const processedAuthCodes = new Set<string>();
|
||||||
|
|
||||||
function OAuthCallbackInner() {
|
function OAuthCallbackInner() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
@@ -32,6 +37,11 @@ function OAuthCallbackInner() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Prevent a second token exchange for the same code (remount / double
|
||||||
|
// effect). Without this, the second exchange fails with "Code not valid".
|
||||||
|
if (processedAuthCodes.has(code)) return;
|
||||||
|
processedAuthCodes.add(code);
|
||||||
|
|
||||||
// Step-up re-auth for device pairing: the QR generator sent the user here
|
// Step-up re-auth for device pairing: the QR generator sent the user here
|
||||||
// via prompt=login. Don't create a login session — just confirm the fresh
|
// via prompt=login. Don't create a login session — just confirm the fresh
|
||||||
// auth (sets the short-lived pairing proof cookie) and bounce back to the
|
// auth (sets the short-lived pairing proof cookie) and bounce back to the
|
||||||
|
|||||||
@@ -71,7 +71,10 @@ type PendingScopeAction =
|
|||||||
| { type: "delete"; event: CalendarEvent; sendScheduling?: boolean };
|
| { type: "delete"; event: CalendarEvent; sendScheduling?: boolean };
|
||||||
|
|
||||||
function isRecurringEvent(event: CalendarEvent): boolean {
|
function isRecurringEvent(event: CalendarEvent): boolean {
|
||||||
return (event.recurrenceRules?.length ?? 0) > 0 || event.recurrenceId != null;
|
// Stalwart may return an empty-string `recurrenceId` for non-recurring events
|
||||||
|
// rather than null; treat that as non-recurring so editing doesn't route
|
||||||
|
// through the recurrence-scope flow for a plain single event.
|
||||||
|
return (event.recurrenceRules?.length ?? 0) > 0 || Boolean(event.recurrenceId);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function CalendarPage() {
|
export default function CalendarPage() {
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ import { Input } from "@/components/ui/input";
|
|||||||
import { FilePreviewModal } from "@/components/files/file-preview-modal";
|
import { FilePreviewModal } from "@/components/files/file-preview-modal";
|
||||||
import { isFilePreviewable } from "@/lib/file-preview";
|
import { isFilePreviewable } from "@/lib/file-preview";
|
||||||
import { appendHtmlSignature, appendPlainTextSignature } from "@/lib/signature-utils";
|
import { appendHtmlSignature, appendPlainTextSignature } from "@/lib/signature-utils";
|
||||||
|
import { useSignatureStore } from "@/stores/signature-store";
|
||||||
import { computeReplyThreadingHeaders } from "@/lib/email-threading";
|
import { computeReplyThreadingHeaders } from "@/lib/email-threading";
|
||||||
import { EML_IMPORT_ACCEPT, expandImportableEmails } from "@/lib/eml-import";
|
import { EML_IMPORT_ACCEPT, expandImportableEmails } from "@/lib/eml-import";
|
||||||
import { findDraftIdentityId, resolveReplyFrom, type ReplyFromResolution } from "@/lib/reply-identity";
|
import { findDraftIdentityId, resolveReplyFrom, type ReplyFromResolution } from "@/lib/reply-identity";
|
||||||
@@ -2615,8 +2616,16 @@ export default function Home() {
|
|||||||
|
|
||||||
// Append signature from the sending identity (fall back to primary
|
// Append signature from the sending identity (fall back to primary
|
||||||
// when the reply-from lives on the same identity but a different alias).
|
// when the reply-from lives on the same identity but a different alias).
|
||||||
|
// The signature store's reply signature takes precedence over the legacy
|
||||||
|
// identity signature, matching the composer's send path.
|
||||||
|
const signatureStore = useSignatureStore.getState();
|
||||||
|
const replySigId = signatureStore.getIdentityReplySignatureId(sendingIdentity?.id ?? '');
|
||||||
|
const replySig = replySigId ? signatureStore.getSignatureById(replySigId) : undefined;
|
||||||
|
const signatureSource = replySig
|
||||||
|
? { htmlSignature: replySig.body, textSignature: replySig.plainText }
|
||||||
|
: sendingIdentity;
|
||||||
const separator = useSettingsStore.getState().signatureSeparatorEnabled;
|
const separator = useSettingsStore.getState().signatureSeparatorEnabled;
|
||||||
const finalBody = appendPlainTextSignature(body, sendingIdentity, { separator });
|
const finalBody = appendPlainTextSignature(body, signatureSource, { separator });
|
||||||
|
|
||||||
// When the identity has an HTML signature, send a matching HTML body so the
|
// When the identity has an HTML signature, send a matching HTML body so the
|
||||||
// signature keeps its formatting; appendPlainTextSignature would otherwise
|
// signature keeps its formatting; appendPlainTextSignature would otherwise
|
||||||
@@ -2627,8 +2636,8 @@ export default function Home() {
|
|||||||
.replace(/</g, '<')
|
.replace(/</g, '<')
|
||||||
.replace(/>/g, '>')
|
.replace(/>/g, '>')
|
||||||
.replace(/\n/g, '<br>');
|
.replace(/\n/g, '<br>');
|
||||||
const finalHtmlBody = sendingIdentity?.htmlSignature?.trim()
|
const finalHtmlBody = signatureSource?.htmlSignature?.trim()
|
||||||
? appendHtmlSignature(`<div>${escapedBody}</div>`, sendingIdentity, { separator })
|
? appendHtmlSignature(`<div>${escapedBody}</div>`, signatureSource, { separator })
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
const originalEmailId = selectedEmail.id;
|
const originalEmailId = selectedEmail.id;
|
||||||
|
|||||||
@@ -0,0 +1,77 @@
|
|||||||
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
import { cookies } from 'next/headers';
|
||||||
|
import { createHmac } from 'node:crypto';
|
||||||
|
import { decryptSession } from '@/lib/auth/crypto';
|
||||||
|
import { sessionCookieName } from '@/lib/auth/session-cookie';
|
||||||
|
import { logger } from '@/lib/logger';
|
||||||
|
|
||||||
|
const JITSI_URL = (process.env.JITSI_URL || 'https://meet.src-advisory.com').replace(/\/+$/, '');
|
||||||
|
|
||||||
|
function base64url(input: Buffer): string {
|
||||||
|
return input.toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
function b64u(input: string): string {
|
||||||
|
return Buffer.from(input).toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function POST(request: NextRequest) {
|
||||||
|
try {
|
||||||
|
const appId = process.env.JITSI_APP_ID;
|
||||||
|
const appSecret = process.env.JITSI_APP_SECRET;
|
||||||
|
if (!appId || !appSecret) {
|
||||||
|
return NextResponse.json({ error: 'Jitsi is not configured' }, { status: 503 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const cookieStore = await cookies();
|
||||||
|
const sessionToken = cookieStore.get(sessionCookieName(0))?.value;
|
||||||
|
if (!sessionToken) {
|
||||||
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||||
|
}
|
||||||
|
const session = decryptSession(sessionToken);
|
||||||
|
if (!session) {
|
||||||
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||||
|
}
|
||||||
|
const email = session.username;
|
||||||
|
|
||||||
|
const body = await request.json().catch(() => ({}));
|
||||||
|
const room = typeof body.room === 'string' ? body.room.trim() : '';
|
||||||
|
if (!room || !/^[a-z0-9-]{1,100}$/i.test(room)) {
|
||||||
|
return NextResponse.json({ error: 'Invalid room name' }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const domain = new URL(JITSI_URL).hostname;
|
||||||
|
const now = Math.floor(Date.now() / 1000);
|
||||||
|
const header = { alg: 'HS256', typ: 'JWT' };
|
||||||
|
const payload = {
|
||||||
|
iss: 'bulwark-webmail',
|
||||||
|
sub: domain,
|
||||||
|
aud: appId,
|
||||||
|
room,
|
||||||
|
iat: now,
|
||||||
|
exp: now + 86400,
|
||||||
|
context: {
|
||||||
|
user: {
|
||||||
|
email,
|
||||||
|
name: email.split('@')[0],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const signingInput = `${b64u(JSON.stringify(header))}.${b64u(JSON.stringify(payload))}`;
|
||||||
|
const signature = createHmac('sha256', appSecret).update(signingInput).digest();
|
||||||
|
const token = `${signingInput}.${base64url(signature)}`;
|
||||||
|
|
||||||
|
logger.info('Jitsi token issued', { room, email });
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
token,
|
||||||
|
room,
|
||||||
|
url: `${JITSI_URL}/${encodeURIComponent(room)}`,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
const message = error instanceof Error ? error.message : 'Unknown error';
|
||||||
|
logger.error('Jitsi token issuance failed', { error: message });
|
||||||
|
return NextResponse.json({ error: message }, { status: 500 });
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -182,7 +182,8 @@ export function EventDetailPopover({
|
|||||||
|
|
||||||
const isAttendeeMode = useMemo(() => {
|
const isAttendeeMode = useMemo(() => {
|
||||||
if (!event.participants) return false;
|
if (!event.participants) return false;
|
||||||
return !event.isOrigin && !userIsOrganizer;
|
if (userIsOrganizer) return false;
|
||||||
|
return event.isOrigin === false;
|
||||||
}, [event, userIsOrganizer]);
|
}, [event, userIsOrganizer]);
|
||||||
|
|
||||||
const userParticipantId = useMemo(
|
const userParticipantId = useMemo(
|
||||||
|
|||||||
@@ -217,7 +217,9 @@ export function EventModal({
|
|||||||
const timeDisplayFmt = timeFormat === "12h" ? "h:mm a" : "HH:mm";
|
const timeDisplayFmt = timeFormat === "12h" ? "h:mm a" : "HH:mm";
|
||||||
const isEdit = !!event;
|
const isEdit = !!event;
|
||||||
const formatEventDate = useFormatEventDate();
|
const formatEventDate = useFormatEventDate();
|
||||||
const [mode, setMode] = useState<"view" | "edit">(isEdit ? "view" : "edit");
|
// Open directly in edit mode so the fields are immediately editable. The
|
||||||
|
// read-only summary (view mode) is still reachable via the Cancel button.
|
||||||
|
const [mode, setMode] = useState<"view" | "edit">("edit");
|
||||||
|
|
||||||
const userIsOrganizer = useMemo(() => {
|
const userIsOrganizer = useMemo(() => {
|
||||||
if (!event) return true;
|
if (!event) return true;
|
||||||
@@ -227,7 +229,12 @@ export function EventModal({
|
|||||||
|
|
||||||
const isAttendeeMode = useMemo(() => {
|
const isAttendeeMode = useMemo(() => {
|
||||||
if (!event || !event.participants) return false;
|
if (!event || !event.participants) return false;
|
||||||
return !event.isOrigin && !userIsOrganizer;
|
// Only enter attendee (read-only + RSVP) mode when we are definitively NOT
|
||||||
|
// the organizer AND the event explicitly did not originate from this
|
||||||
|
// account. Stalwart may omit `isOrigin`, so treat a missing value as "ours"
|
||||||
|
// (editable) rather than locking the user out of their own events.
|
||||||
|
if (userIsOrganizer) return false;
|
||||||
|
return event.isOrigin === false;
|
||||||
}, [event, userIsOrganizer]);
|
}, [event, userIsOrganizer]);
|
||||||
|
|
||||||
const userParticipantId = useMemo(() => {
|
const userParticipantId = useMemo(() => {
|
||||||
@@ -1147,8 +1154,8 @@ export function EventModal({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Action Bar */}
|
{/* Action Bar */}
|
||||||
<div className="px-6 py-3 border-t border-border flex-shrink-0 flex items-center justify-between">
|
<div className="px-6 py-3 border-t border-border flex-shrink-0 flex flex-wrap items-center gap-2">
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex flex-wrap items-center gap-1 min-w-0">
|
||||||
{onDelete && (
|
{onDelete && (
|
||||||
showDeleteConfirm ? (
|
showDeleteConfirm ? (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
@@ -1187,7 +1194,7 @@ export function EventModal({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{!showDeleteConfirm && (
|
{!showDeleteConfirm && (
|
||||||
<Button onClick={() => setMode("edit")}>
|
<Button onClick={() => setMode("edit")} className="ml-auto shrink-0">
|
||||||
<Pencil className="w-4 h-4 me-1" />
|
<Pencil className="w-4 h-4 me-1" />
|
||||||
{t("events.edit")}
|
{t("events.edit")}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -1618,8 +1625,8 @@ export function EventModal({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center justify-between px-6 py-4 border-t border-border flex-shrink-0">
|
<div className="flex flex-wrap items-center gap-2 px-6 py-4 border-t border-border flex-shrink-0">
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex flex-wrap items-center gap-1 min-w-0">
|
||||||
{isEdit && onDelete && (
|
{isEdit && onDelete && (
|
||||||
showDeleteConfirm ? (
|
showDeleteConfirm ? (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
@@ -1670,7 +1677,7 @@ export function EventModal({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2 ml-auto shrink-0">
|
||||||
<Button variant="outline" onClick={isEdit ? () => setMode("view") : onClose}>
|
<Button variant="outline" onClick={isEdit ? () => setMode("view") : onClose}>
|
||||||
{t("form.cancel")}
|
{t("form.cancel")}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -653,6 +653,15 @@ export function EmailComposer({
|
|||||||
? currentIdentity
|
? currentIdentity
|
||||||
: primaryIdentity;
|
: primaryIdentity;
|
||||||
|
|
||||||
|
// The signature store (default/reply/per-identity) takes precedence over the
|
||||||
|
// legacy per-identity html/text signature. `selectedSignature` is resolved in
|
||||||
|
// resolveStoreSignatureId for the current mode (compose → default; reply/
|
||||||
|
// forward → reply), so replies and forwards pick up the reply signature.
|
||||||
|
// Falls back to the legacy identity signature when no store signature is set.
|
||||||
|
const effectiveSignature = selectedSignature
|
||||||
|
? { htmlSignature: selectedSignature.body, textSignature: selectedSignature.plainText }
|
||||||
|
: signatureIdentity;
|
||||||
|
|
||||||
// Hold the TipTap editor instance so we can swap the embedded signature
|
// Hold the TipTap editor instance so we can swap the embedded signature
|
||||||
// when the user switches identity in "above quote" mode without rebuilding
|
// when the user switches identity in "above quote" mode without rebuilding
|
||||||
// the whole body (which would lose user edits to the surrounding draft).
|
// the whole body (which would lose user edits to the surrounding draft).
|
||||||
@@ -751,8 +760,12 @@ export function EmailComposer({
|
|||||||
sigInsertedRef.current = true;
|
sigInsertedRef.current = true;
|
||||||
if (mode === 'compose') {
|
if (mode === 'compose') {
|
||||||
editor.chain().focus('end').insertContent(`<p></p>${sig.body}`).run();
|
editor.chain().focus('end').insertContent(`<p></p>${sig.body}`).run();
|
||||||
|
// Place the caret in the empty paragraph above the signature so the user
|
||||||
|
// starts typing at the top of the new email.
|
||||||
|
editor.chain().focus('start').run();
|
||||||
} else if ((mode === 'reply' || mode === 'replyAll' || mode === 'forward') && signaturePosition === 'above_quote') {
|
} else if ((mode === 'reply' || mode === 'replyAll' || mode === 'forward') && signaturePosition === 'above_quote') {
|
||||||
editor.chain().focus('start').insertContent(sig.body).run();
|
editor.chain().focus('start').insertContent(`<p></p>${sig.body}`).run();
|
||||||
|
editor.chain().focus('start').run();
|
||||||
}
|
}
|
||||||
}, [selectedSignatureId, plainTextMode, mode, signaturePosition, getSignatureById, editorReady]);
|
}, [selectedSignatureId, plainTextMode, mode, signaturePosition, getSignatureById, editorReady]);
|
||||||
|
|
||||||
@@ -1883,6 +1896,7 @@ export function EmailComposer({
|
|||||||
// duplicate it.
|
// duplicate it.
|
||||||
const signatureAlreadyInBody =
|
const signatureAlreadyInBody =
|
||||||
shouldEmbedSignatureInNewMail ||
|
shouldEmbedSignatureInNewMail ||
|
||||||
|
(!plainTextMode && !!selectedSignature && mode === 'compose') ||
|
||||||
((mode === 'reply' || mode === 'replyAll' || mode === 'forward') &&
|
((mode === 'reply' || mode === 'replyAll' || mode === 'forward') &&
|
||||||
signaturePosition === 'above_quote');
|
signaturePosition === 'above_quote');
|
||||||
|
|
||||||
@@ -1890,11 +1904,11 @@ export function EmailComposer({
|
|||||||
const buildSignatureHtml = (): string => {
|
const buildSignatureHtml = (): string => {
|
||||||
if (signatureAlreadyInBody) return '';
|
if (signatureAlreadyInBody) return '';
|
||||||
const sep = signatureSeparatorEnabled ? `<br><br>-- <br>` : `<br><br>`;
|
const sep = signatureSeparatorEnabled ? `<br><br>-- <br>` : `<br><br>`;
|
||||||
if (signatureIdentity?.htmlSignature) {
|
if (effectiveSignature?.htmlSignature) {
|
||||||
return `${sep}${sanitizeSignatureHtml(signatureIdentity.htmlSignature)}`;
|
return `${sep}${sanitizeSignatureHtml(effectiveSignature.htmlSignature)}`;
|
||||||
}
|
}
|
||||||
if (signatureIdentity?.textSignature) {
|
if (effectiveSignature?.textSignature) {
|
||||||
return `${sep}${signatureIdentity.textSignature.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/\n/g, '<br>')}`;
|
return `${sep}${effectiveSignature.textSignature.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/\n/g, '<br>')}`;
|
||||||
}
|
}
|
||||||
return '';
|
return '';
|
||||||
};
|
};
|
||||||
@@ -1907,8 +1921,8 @@ export function EmailComposer({
|
|||||||
// In plain text mode, send text/plain only (no HTML body)
|
// In plain text mode, send text/plain only (no HTML body)
|
||||||
const signatureOpts = { separator: signatureSeparatorEnabled };
|
const signatureOpts = { separator: signatureSeparatorEnabled };
|
||||||
const finalBody = plainTextMode
|
const finalBody = plainTextMode
|
||||||
? (signatureAlreadyInBody ? body : appendPlainTextSignature(body, signatureIdentity, signatureOpts))
|
? (signatureAlreadyInBody ? body : appendPlainTextSignature(body, effectiveSignature, signatureOpts))
|
||||||
: (signatureAlreadyInBody ? htmlToPlainText(body) : appendPlainTextSignature(htmlToPlainText(body), signatureIdentity, signatureOpts));
|
: (signatureAlreadyInBody ? htmlToPlainText(body) : appendPlainTextSignature(htmlToPlainText(body), effectiveSignature, signatureOpts));
|
||||||
|
|
||||||
const rewritten = plainTextMode ? null : rewriteInlineImages(body);
|
const rewritten = plainTextMode ? null : rewriteInlineImages(body);
|
||||||
const finalHtmlBody = plainTextMode
|
const finalHtmlBody = plainTextMode
|
||||||
|
|||||||
@@ -2,11 +2,14 @@
|
|||||||
|
|
||||||
import { useState, useRef, useEffect } from "react";
|
import { useState, useRef, useEffect } from "react";
|
||||||
import { createPortal } from "react-dom";
|
import { createPortal } from "react-dom";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
import { Mail, Phone, Building, ExternalLink, Copy, Send, UserPlus } from "lucide-react";
|
import { Mail, Phone, Building, ExternalLink, Copy, Send, UserPlus } from "lucide-react";
|
||||||
import { Avatar } from "@/components/ui/avatar";
|
import { Avatar } from "@/components/ui/avatar";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { useContactStore, getContactDisplayName } from "@/stores/contact-store";
|
import { useContactStore, getContactDisplayName } from "@/stores/contact-store";
|
||||||
import { toast } from "@/stores/toast-store";
|
import { toast } from "@/stores/toast-store";
|
||||||
|
import { savePendingMailto, notifyPendingMailto } from "@/lib/protocol-handlers/session";
|
||||||
|
import { formatRecipient } from "@/lib/email-composer-utils";
|
||||||
import type { ContactCard } from "@/lib/jmap/types";
|
import type { ContactCard } from "@/lib/jmap/types";
|
||||||
|
|
||||||
interface RecipientPopoverProps {
|
interface RecipientPopoverProps {
|
||||||
@@ -125,6 +128,21 @@ export function RecipientPopover({ name, email, displayLabel, onViewContact, cla
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const handleCompose = () => {
|
||||||
|
savePendingMailto({
|
||||||
|
to: [formatRecipient(contactName, email)],
|
||||||
|
cc: [],
|
||||||
|
bcc: [],
|
||||||
|
subject: "",
|
||||||
|
body: "",
|
||||||
|
});
|
||||||
|
notifyPendingMailto();
|
||||||
|
router.push("/");
|
||||||
|
handleClose();
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<button
|
<button
|
||||||
@@ -210,14 +228,14 @@ export function RecipientPopover({ name, email, displayLabel, onViewContact, cla
|
|||||||
<Copy className="w-3.5 h-3.5" />
|
<Copy className="w-3.5 h-3.5" />
|
||||||
Copy
|
Copy
|
||||||
</button>
|
</button>
|
||||||
<a
|
<button
|
||||||
href={`mailto:${email}`}
|
onClick={handleCompose}
|
||||||
className="flex items-center gap-1.5 text-xs text-muted-foreground hover:text-foreground px-2 py-1.5 rounded hover:bg-muted transition-colors"
|
className="flex items-center gap-1.5 text-xs text-muted-foreground hover:text-foreground px-2 py-1.5 rounded hover:bg-muted transition-colors"
|
||||||
title="Send email"
|
title="Send email"
|
||||||
>
|
>
|
||||||
<Send className="w-3.5 h-3.5" />
|
<Send className="w-3.5 h-3.5" />
|
||||||
Email
|
Email
|
||||||
</a>
|
</button>
|
||||||
{onViewContact && (
|
{onViewContact && (
|
||||||
<button
|
<button
|
||||||
onClick={handleViewContact}
|
onClick={handleViewContact}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ metadata:
|
|||||||
type: Opaque
|
type: Opaque
|
||||||
stringData:
|
stringData:
|
||||||
# Core — connect to Stalwart over JMAP
|
# Core — connect to Stalwart over JMAP
|
||||||
JMAP_SERVER_URL: "https://stalwart.sandbox.vnc.de"
|
JMAP_SERVER_URL: "https://emailcore.src-advisory.com"
|
||||||
SESSION_SECRET: "REPLACE_ME__openssl_rand_base64_32"
|
SESSION_SECRET: "REPLACE_ME__openssl_rand_base64_32"
|
||||||
# Branding (theme defaults to VNClagoon in code; these set name + logo)
|
# Branding (theme defaults to VNClagoon in code; these set name + logo)
|
||||||
APP_NAME: "VNCmail+"
|
APP_NAME: "VNCmail+"
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
# Owned by CI (the bump-prod job in .gitlab-ci.yml), not by hand — same
|
# Owned by CI (bump-prod job in .gitlab-ci.yml) - regenerated every
|
||||||
# reasoning as overlays/dev/image-tag/. Starts pointed at an obviously-fake
|
# push to main. Do not hand-edit; edits here get overwritten. Bumping
|
||||||
# tag on purpose: nothing has been promoted yet, and vncmail-prod's ArgoCD
|
# this is NOT the same as deploying it - vncmail-prod's ArgoCD
|
||||||
# Application has manual sync anyway, so this being "wrong" doesn't deploy
|
# Application has manual sync, see the note in the parent
|
||||||
# anything wrong — it just means there's nothing to sync until a real
|
# kustomization.yaml.
|
||||||
# `git push` to main updates it.
|
|
||||||
apiVersion: kustomize.config.k8s.io/v1alpha1
|
apiVersion: kustomize.config.k8s.io/v1alpha1
|
||||||
kind: Component
|
kind: Component
|
||||||
images:
|
images:
|
||||||
- name: vncmail-plus
|
- name: vncmail-plus
|
||||||
newName: registry.gitlab.vnc.biz/gitlab-instance-b9b5cf2f/vncmail-plus
|
newName: registry.gitlab.vnc.biz/gitlab-instance-b9b5cf2f/vncmail-plus
|
||||||
newTag: not-yet-promoted
|
newTag: sha-cfdd091d
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import path from 'node:path';
|
|||||||
// real JMAP server round trip works end-to-end, without ever using or
|
// real JMAP server round trip works end-to-end, without ever using or
|
||||||
// guessing a real account's credentials.
|
// guessing a real account's credentials.
|
||||||
const projectRoot = path.resolve(__dirname, '..');
|
const projectRoot = path.resolve(__dirname, '..');
|
||||||
const SANDBOX_URL = 'https://stalwart.sandbox.vnc.de';
|
const SANDBOX_URL = 'https://emailcore.src-advisory.com';
|
||||||
|
|
||||||
test.describe('Electron desktop shell - live sandbox connectivity', () => {
|
test.describe('Electron desktop shell - live sandbox connectivity', () => {
|
||||||
let electronApp: ElectronApplication;
|
let electronApp: ElectronApplication;
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ test.describe('Electron desktop shell', () => {
|
|||||||
// needing a reachable JMAP server just to prove the login screen
|
// needing a reachable JMAP server just to prove the login screen
|
||||||
// renders - any non-empty JMAP_SERVER_URL is enough to reach
|
// renders - any non-empty JMAP_SERVER_URL is enough to reach
|
||||||
// "env-managed" state and serve the normal app shell.
|
// "env-managed" state and serve the normal app shell.
|
||||||
JMAP_SERVER_URL: 'https://stalwart.sandbox.vnc.de',
|
JMAP_SERVER_URL: 'https://emailcore.src-advisory.com',
|
||||||
SESSION_SECRET: 'electron-smoke-test-not-for-production',
|
SESSION_SECRET: 'electron-smoke-test-not-for-production',
|
||||||
NODE_ENV: 'production',
|
NODE_ENV: 'production',
|
||||||
},
|
},
|
||||||
|
|||||||
+1
-1
@@ -96,7 +96,7 @@ function getServerDataDirs(): Record<string, string> {
|
|||||||
*/
|
*/
|
||||||
function getDesktopDefaults(): Record<string, string> {
|
function getDesktopDefaults(): Record<string, string> {
|
||||||
return {
|
return {
|
||||||
JMAP_SERVER_URL: "https://stalwart.sandbox.vnc.de",
|
JMAP_SERVER_URL: "https://emailcore.src-advisory.com",
|
||||||
APP_NAME: "VNCmail+",
|
APP_NAME: "VNCmail+",
|
||||||
APP_SHORT_NAME: "VNCmail+",
|
APP_SHORT_NAME: "VNCmail+",
|
||||||
LOGIN_LOGO_LIGHT_URL: "/branding/SRC_Symbol.png",
|
LOGIN_LOGO_LIGHT_URL: "/branding/SRC_Symbol.png",
|
||||||
|
|||||||
@@ -71,6 +71,13 @@ const FIRST_PARTY_PLUGINS: FirstPartyPlugin[] = [
|
|||||||
// feature - the former in-host native pipeline is gone - so the long-standing
|
// feature - the former in-host native pipeline is gone - so the long-standing
|
||||||
// `smimeEnabled` policy gate now controls this plugin.
|
// `smimeEnabled` policy gate now controls this plugin.
|
||||||
{ id: 'smime', gate: 'smimeEnabled', forceEnable: true },
|
{ id: 'smime', gate: 'smimeEnabled', forceEnable: true },
|
||||||
|
// VNCdirectory deep-link. Users are managed in the directory, not the
|
||||||
|
// webmail; this plugin adds a "User management" Settings entry that opens
|
||||||
|
// the directory's user list. Force-enabled so it is always present.
|
||||||
|
{ id: 'manage-users', gate: 'manageUsersEnabled', forceEnable: true },
|
||||||
|
// SRC video meetings (VNCtalk / Jitsi). "Start a meeting" asks the server
|
||||||
|
// for a signed JWT and opens the room in meet.src-advisory.com.
|
||||||
|
{ id: 'jitsi-meet', gate: 'jitsiMeetEnabled', forceEnable: true },
|
||||||
];
|
];
|
||||||
|
|
||||||
const ID_RE = /^[a-z0-9][a-z0-9-]*[a-z0-9]$/;
|
const ID_RE = /^[a-z0-9][a-z0-9-]*[a-z0-9]$/;
|
||||||
|
|||||||
@@ -55,6 +55,8 @@ export interface FeatureGates {
|
|||||||
calendarEnabled: boolean;
|
calendarEnabled: boolean;
|
||||||
calendarTasksEnabled: boolean;
|
calendarTasksEnabled: boolean;
|
||||||
smimeEnabled: boolean;
|
smimeEnabled: boolean;
|
||||||
|
manageUsersEnabled: boolean;
|
||||||
|
jitsiMeetEnabled: boolean;
|
||||||
externalContentEnabled: boolean;
|
externalContentEnabled: boolean;
|
||||||
debugModeEnabled: boolean;
|
debugModeEnabled: boolean;
|
||||||
folderIconsEnabled: boolean;
|
folderIconsEnabled: boolean;
|
||||||
@@ -90,6 +92,8 @@ export const DEFAULT_FEATURE_GATES: FeatureGates = {
|
|||||||
calendarEnabled: true,
|
calendarEnabled: true,
|
||||||
calendarTasksEnabled: true,
|
calendarTasksEnabled: true,
|
||||||
smimeEnabled: true,
|
smimeEnabled: true,
|
||||||
|
manageUsersEnabled: true,
|
||||||
|
jitsiMeetEnabled: true,
|
||||||
externalContentEnabled: true,
|
externalContentEnabled: true,
|
||||||
debugModeEnabled: true,
|
debugModeEnabled: true,
|
||||||
folderIconsEnabled: true,
|
folderIconsEnabled: true,
|
||||||
|
|||||||
+17
-12
@@ -1014,11 +1014,16 @@ body[data-theme-skin="builtin-vnclagoon"] .rounded-2xl.border.bg-background\\/80
|
|||||||
// near-black stone-grey neutral. Dark variant brightens the red (#EF4444) on a
|
// near-black stone-grey neutral. Dark variant brightens the red (#EF4444) on a
|
||||||
// warm near-black. Info stays blue so it never collides with the red accent.
|
// warm near-black. Info stays blue so it never collides with the red accent.
|
||||||
const srcCSS = `
|
const srcCSS = `
|
||||||
@font-face { font-family: 'DM Sans'; font-style: normal; font-weight: 400; font-display: swap; src: url('/fonts/dmsans-400.woff2') format('woff2'); }
|
@font-face { font-family: 'Inter'; font-style: normal; font-weight: 100 900; font-display: swap; src: url('/fonts/inter-var-latin.woff2') format('woff2'); unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; }
|
||||||
@font-face { font-family: 'DM Sans'; font-style: normal; font-weight: 500; font-display: swap; src: url('/fonts/dmsans-500.woff2') format('woff2'); }
|
@font-face { font-family: 'Inter'; font-style: normal; font-weight: 100 900; font-display: swap; src: url('/fonts/inter-var-latin-ext.woff2') format('woff2'); unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; }
|
||||||
@font-face { font-family: 'DM Sans'; font-style: normal; font-weight: 700; font-display: swap; src: url('/fonts/dmsans-700.woff2') format('woff2'); }
|
@font-face { font-family: 'Spectral'; font-style: normal; font-weight: 400; font-display: swap; src: url('/fonts/spectral-400-latin.woff2') format('woff2'); unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; }
|
||||||
@font-face { font-family: 'Syne'; font-style: normal; font-weight: 700; font-display: swap; src: url('/fonts/syne-700.woff2') format('woff2'); }
|
@font-face { font-family: 'Spectral'; font-style: normal; font-weight: 400; font-display: swap; src: url('/fonts/spectral-400-latin-ext.woff2') format('woff2'); unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; }
|
||||||
@font-face { font-family: 'Syne'; font-style: normal; font-weight: 800; font-display: swap; src: url('/fonts/syne-800.woff2') format('woff2'); }
|
@font-face { font-family: 'Spectral'; font-style: normal; font-weight: 600; font-display: swap; src: url('/fonts/spectral-600-latin.woff2') format('woff2'); unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; }
|
||||||
|
@font-face { font-family: 'Spectral'; font-style: normal; font-weight: 600; font-display: swap; src: url('/fonts/spectral-600-latin-ext.woff2') format('woff2'); unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; }
|
||||||
|
@font-face { font-family: 'Spectral'; font-style: normal; font-weight: 700; font-display: swap; src: url('/fonts/spectral-700-latin.woff2') format('woff2'); unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; }
|
||||||
|
@font-face { font-family: 'Spectral'; font-style: normal; font-weight: 700; font-display: swap; src: url('/fonts/spectral-700-latin-ext.woff2') format('woff2'); unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; }
|
||||||
|
@font-face { font-family: 'Spectral'; font-style: normal; font-weight: 800; font-display: swap; src: url('/fonts/spectral-800-latin.woff2') format('woff2'); unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; }
|
||||||
|
@font-face { font-family: 'Spectral'; font-style: normal; font-weight: 800; font-display: swap; src: url('/fonts/spectral-800-latin-ext.woff2') format('woff2'); unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; }
|
||||||
:root {
|
:root {
|
||||||
--color-border: #e7e5e4;
|
--color-border: #e7e5e4;
|
||||||
--color-input: #e7e5e4;
|
--color-input: #e7e5e4;
|
||||||
@@ -1105,12 +1110,12 @@ const srcCSS = `
|
|||||||
// All scoped under the skin body attribute so they detach cleanly on switch-off.
|
// All scoped under the skin body attribute so they detach cleanly on switch-off.
|
||||||
const srcSkin = `
|
const srcSkin = `
|
||||||
body[data-theme-skin="builtin-src"] {
|
body[data-theme-skin="builtin-src"] {
|
||||||
font-family: "DM Sans", system-ui, -apple-system, "Segoe UI", sans-serif;
|
font-family: "Inter", system-ui, -apple-system, "Segoe UI", sans-serif;
|
||||||
}
|
}
|
||||||
body[data-theme-skin="builtin-src"] h1,
|
body[data-theme-skin="builtin-src"] h1,
|
||||||
body[data-theme-skin="builtin-src"] h2,
|
body[data-theme-skin="builtin-src"] h2,
|
||||||
body[data-theme-skin="builtin-src"] h3 {
|
body[data-theme-skin="builtin-src"] h3 {
|
||||||
font-family: "Syne", "DM Sans", sans-serif;
|
font-family: "Spectral", "Inter", Georgia, serif;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
letter-spacing: -0.01em;
|
letter-spacing: -0.01em;
|
||||||
}
|
}
|
||||||
@@ -1135,7 +1140,7 @@ body[data-theme-skin="builtin-src"] button:not(.rounded-full):not([role="switch"
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* MD3 filled button — primary surface, M3 label-large, state layers */
|
/* MD3 filled button — primary surface, M3 label-large, state layers */
|
||||||
body[data-theme-skin="builtin-src"] .bg-primary.text-primary-foreground {
|
body[data-theme-skin="builtin-src"] .bg-primary.text-primary-foreground:not(.rounded-full) {
|
||||||
border-radius: 20px !important;
|
border-radius: 20px !important;
|
||||||
padding-inline: 24px !important;
|
padding-inline: 24px !important;
|
||||||
min-height: 40px !important;
|
min-height: 40px !important;
|
||||||
@@ -1146,20 +1151,20 @@ body[data-theme-skin="builtin-src"] .bg-primary.text-primary-foreground {
|
|||||||
transition: box-shadow 200ms ease, filter 200ms ease;
|
transition: box-shadow 200ms ease, filter 200ms ease;
|
||||||
}
|
}
|
||||||
/* hover: M3 elevation 1 + 8 % on-primary state layer */
|
/* hover: M3 elevation 1 + 8 % on-primary state layer */
|
||||||
body[data-theme-skin="builtin-src"] .bg-primary.text-primary-foreground:hover {
|
body[data-theme-skin="builtin-src"] .bg-primary.text-primary-foreground:not(.rounded-full):hover {
|
||||||
box-shadow:
|
box-shadow:
|
||||||
0 1px 2px rgba(0, 0, 0, 0.30),
|
0 1px 2px rgba(0, 0, 0, 0.30),
|
||||||
0 2px 6px 2px rgba(0, 0, 0, 0.15) !important;
|
0 2px 6px 2px rgba(0, 0, 0, 0.15) !important;
|
||||||
filter: brightness(1.06);
|
filter: brightness(1.06);
|
||||||
}
|
}
|
||||||
/* focus: +12 % tint + M3 focus ring */
|
/* focus: +12 % tint + M3 focus ring */
|
||||||
body[data-theme-skin="builtin-src"] .bg-primary.text-primary-foreground:focus-visible {
|
body[data-theme-skin="builtin-src"] .bg-primary.text-primary-foreground:not(.rounded-full):focus-visible {
|
||||||
filter: brightness(1.10) !important;
|
filter: brightness(1.10) !important;
|
||||||
outline: 3px solid var(--color-ring) !important;
|
outline: 3px solid var(--color-ring) !important;
|
||||||
outline-offset: 2px !important;
|
outline-offset: 2px !important;
|
||||||
}
|
}
|
||||||
/* pressed: +12 % darker, no shadow */
|
/* pressed: +12 % darker, no shadow */
|
||||||
body[data-theme-skin="builtin-src"] .bg-primary.text-primary-foreground:active {
|
body[data-theme-skin="builtin-src"] .bg-primary.text-primary-foreground:not(.rounded-full):active {
|
||||||
box-shadow: none !important;
|
box-shadow: none !important;
|
||||||
filter: brightness(0.94) !important;
|
filter: brightness(0.94) !important;
|
||||||
}
|
}
|
||||||
@@ -1346,7 +1351,7 @@ export const BUILTIN_THEMES: InstalledTheme[] = [
|
|||||||
logoLightUrl: '/branding/SRC_Symbol.png',
|
logoLightUrl: '/branding/SRC_Symbol.png',
|
||||||
logoDarkUrl: '/branding/SRC_Symbol.png',
|
logoDarkUrl: '/branding/SRC_Symbol.png',
|
||||||
variants: ['light', 'dark'],
|
variants: ['light', 'dark'],
|
||||||
typography: { fontSans: '"DM Sans", system-ui, -apple-system, "Segoe UI", sans-serif' },
|
typography: { fontSans: '"Inter", system-ui, -apple-system, "Segoe UI", sans-serif' },
|
||||||
enabled: true,
|
enabled: true,
|
||||||
builtIn: true,
|
builtIn: true,
|
||||||
},
|
},
|
||||||
|
|||||||
+26
-10
@@ -1,4 +1,4 @@
|
|||||||
import type { Email, Mailbox, MailboxRights, StateChange, AccountStates, Thread, Identity, EmailAddress, ContactCard, AddressBook, AddressBookRights, VacationResponse, Calendar, CalendarRights, CalendarEvent, CalendarEventFilter, CalendarTask, FileNode, FileNodeFilter, FileNodeRights, Principal, PushSubscription, EmailSubmission, ScheduledEmail, SendEmailResult, SharedAccount } from "./types";
|
import type { Email, Mailbox, MailboxRights, StateChange, AccountStates, Thread, Identity, EmailAddress, ContactCard, AddressBook, AddressBookRights, VacationResponse, Calendar, CalendarRights, CalendarEvent, CalendarEventFilter, CalendarParticipant, CalendarTask, FileNode, FileNodeFilter, FileNodeRights, Principal, PushSubscription, EmailSubmission, ScheduledEmail, SendEmailResult, SharedAccount } from "./types";
|
||||||
import type { SieveScript, SieveCapabilities } from "./sieve-types";
|
import type { SieveScript, SieveCapabilities } from "./sieve-types";
|
||||||
import type { IJMAPClient } from "./client-interface";
|
import type { IJMAPClient } from "./client-interface";
|
||||||
import { toWildcardQuery } from "./search-utils";
|
import { toWildcardQuery } from "./search-utils";
|
||||||
@@ -3135,9 +3135,18 @@ export class JMAPClient implements IJMAPClient {
|
|||||||
throw new Error('No drafts mailbox found');
|
throw new Error('No drafts mailbox found');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find the organizer participant
|
// Find the organizer participant. Stalwart may not echo `roles.owner`, so
|
||||||
|
// also fall back to the event-level organizerCalendarAddress, and derive
|
||||||
|
// participant emails from email / calendarAddress / sendTo.imip.
|
||||||
|
const participantEmail = (p: CalendarParticipant): string =>
|
||||||
|
p.email
|
||||||
|
|| p.calendarAddress?.replace(/^mailto:/i, '')
|
||||||
|
|| p.sendTo?.imip?.replace(/^mailto:/i, '')
|
||||||
|
|| '';
|
||||||
const organizerEntry = Object.values(event.participants).find(p => p.roles?.owner);
|
const organizerEntry = Object.values(event.participants).find(p => p.roles?.owner);
|
||||||
const organizerEmail = organizerEntry?.email || organizerEntry?.sendTo?.imip?.replace('mailto:', '') || this.username;
|
const organizerEmail = (organizerEntry ? participantEmail(organizerEntry) : '')
|
||||||
|
|| event.organizerCalendarAddress?.replace(/^mailto:/i, '')
|
||||||
|
|| this.username;
|
||||||
const organizerName = organizerEntry?.name || '';
|
const organizerName = organizerEntry?.name || '';
|
||||||
|
|
||||||
// Resolve identity
|
// Resolve identity
|
||||||
@@ -3152,7 +3161,7 @@ export class JMAPClient implements IJMAPClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Collect attendee participants (non-organizer)
|
// Collect attendee participants (non-organizer)
|
||||||
const attendees = Object.values(event.participants).filter(p => !p.roles?.owner);
|
const attendees = Object.values(event.participants).filter(p => !p.roles?.owner && participantEmail(p) !== organizerEmail);
|
||||||
if (attendees.length === 0) return;
|
if (attendees.length === 0) return;
|
||||||
|
|
||||||
const now = new Date().toISOString().replace(/[-:]/g, '').replace(/\.\d{3}/, '');
|
const now = new Date().toISOString().replace(/[-:]/g, '').replace(/\.\d{3}/, '');
|
||||||
@@ -3212,7 +3221,7 @@ export class JMAPClient implements IJMAPClient {
|
|||||||
lines.push(`ORGANIZER${orgCn}:mailto:${organizerEmail}`);
|
lines.push(`ORGANIZER${orgCn}:mailto:${organizerEmail}`);
|
||||||
|
|
||||||
for (const attendee of attendees) {
|
for (const attendee of attendees) {
|
||||||
const email = attendee.email || attendee.sendTo?.imip?.replace('mailto:', '');
|
const email = participantEmail(attendee);
|
||||||
if (!email) continue;
|
if (!email) continue;
|
||||||
const cn = attendee.name ? `;CN=${icsParamValue(attendee.name)}` : '';
|
const cn = attendee.name ? `;CN=${icsParamValue(attendee.name)}` : '';
|
||||||
const partstat = attendee.participationStatus
|
const partstat = attendee.participationStatus
|
||||||
@@ -3228,7 +3237,7 @@ export class JMAPClient implements IJMAPClient {
|
|||||||
|
|
||||||
const subject = `Invitation: ${event.title || 'Event'}`;
|
const subject = `Invitation: ${event.title || 'Event'}`;
|
||||||
const toAddresses = attendees
|
const toAddresses = attendees
|
||||||
.map(a => ({ name: a.name || undefined, email: a.email || a.sendTo?.imip?.replace('mailto:', '') || '' }))
|
.map(a => ({ name: a.name || undefined, email: participantEmail(a) }))
|
||||||
.filter(a => a.email);
|
.filter(a => a.email);
|
||||||
|
|
||||||
if (toAddresses.length === 0) return;
|
if (toAddresses.length === 0) return;
|
||||||
@@ -3313,8 +3322,15 @@ export class JMAPClient implements IJMAPClient {
|
|||||||
throw new Error('No drafts mailbox found');
|
throw new Error('No drafts mailbox found');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const participantEmail = (p: CalendarParticipant): string =>
|
||||||
|
p.email
|
||||||
|
|| p.calendarAddress?.replace(/^mailto:/i, '')
|
||||||
|
|| p.sendTo?.imip?.replace(/^mailto:/i, '')
|
||||||
|
|| '';
|
||||||
const organizerEntry = Object.values(event.participants).find(p => p.roles?.owner);
|
const organizerEntry = Object.values(event.participants).find(p => p.roles?.owner);
|
||||||
const organizerEmail = organizerEntry?.email || organizerEntry?.sendTo?.imip?.replace('mailto:', '') || this.username;
|
const organizerEmail = (organizerEntry ? participantEmail(organizerEntry) : '')
|
||||||
|
|| event.organizerCalendarAddress?.replace(/^mailto:/i, '')
|
||||||
|
|| this.username;
|
||||||
const organizerName = organizerEntry?.name || '';
|
const organizerName = organizerEntry?.name || '';
|
||||||
|
|
||||||
const identityResponse = await this.request([
|
const identityResponse = await this.request([
|
||||||
@@ -3327,7 +3343,7 @@ export class JMAPClient implements IJMAPClient {
|
|||||||
identityId = match?.id || identities[0]?.id || this.accountId;
|
identityId = match?.id || identities[0]?.id || this.accountId;
|
||||||
}
|
}
|
||||||
|
|
||||||
const attendees = Object.values(event.participants).filter(p => !p.roles?.owner);
|
const attendees = Object.values(event.participants).filter(p => !p.roles?.owner && participantEmail(p) !== organizerEmail);
|
||||||
if (attendees.length === 0) return;
|
if (attendees.length === 0) return;
|
||||||
|
|
||||||
const now = new Date().toISOString().replace(/[-:]/g, '').replace(/\.\d{3}/, '');
|
const now = new Date().toISOString().replace(/[-:]/g, '').replace(/\.\d{3}/, '');
|
||||||
@@ -3370,7 +3386,7 @@ export class JMAPClient implements IJMAPClient {
|
|||||||
lines.push(`ORGANIZER${orgCn}:mailto:${organizerEmail}`);
|
lines.push(`ORGANIZER${orgCn}:mailto:${organizerEmail}`);
|
||||||
|
|
||||||
for (const attendee of attendees) {
|
for (const attendee of attendees) {
|
||||||
const email = attendee.email || attendee.sendTo?.imip?.replace('mailto:', '');
|
const email = participantEmail(attendee);
|
||||||
if (!email) continue;
|
if (!email) continue;
|
||||||
const cn = attendee.name ? `;CN=${icsParamValue(attendee.name)}` : '';
|
const cn = attendee.name ? `;CN=${icsParamValue(attendee.name)}` : '';
|
||||||
lines.push(`ATTENDEE${cn}:mailto:${email}`);
|
lines.push(`ATTENDEE${cn}:mailto:${email}`);
|
||||||
@@ -3382,7 +3398,7 @@ export class JMAPClient implements IJMAPClient {
|
|||||||
|
|
||||||
const subject = `Cancelled: ${event.title || 'Event'}`;
|
const subject = `Cancelled: ${event.title || 'Event'}`;
|
||||||
const toAddresses = attendees
|
const toAddresses = attendees
|
||||||
.map(a => ({ name: a.name || undefined, email: a.email || a.sendTo?.imip?.replace('mailto:', '') || '' }))
|
.map(a => ({ name: a.name || undefined, email: participantEmail(a) }))
|
||||||
.filter(a => a.email);
|
.filter(a => a.email);
|
||||||
|
|
||||||
if (toAddresses.length === 0) return;
|
if (toAddresses.length === 0) return;
|
||||||
|
|||||||
Generated
+11
@@ -62,6 +62,7 @@
|
|||||||
"@types/qrcode": "^1.5.6",
|
"@types/qrcode": "^1.5.6",
|
||||||
"@types/react": "^19.2.7",
|
"@types/react": "^19.2.7",
|
||||||
"@types/react-dom": "^19.2.3",
|
"@types/react-dom": "^19.2.3",
|
||||||
|
"@types/ws": "^8.18.1",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.59.0",
|
"@typescript-eslint/eslint-plugin": "^8.59.0",
|
||||||
"@typescript-eslint/parser": "^8.59.0",
|
"@typescript-eslint/parser": "^8.59.0",
|
||||||
"@vitejs/plugin-react": "^6.0.1",
|
"@vitejs/plugin-react": "^6.0.1",
|
||||||
@@ -4730,6 +4731,16 @@
|
|||||||
"integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==",
|
"integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/ws": {
|
||||||
|
"version": "8.18.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
|
||||||
|
"integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||||
"version": "8.59.0",
|
"version": "8.59.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.59.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.59.0.tgz",
|
||||||
|
|||||||
@@ -101,6 +101,7 @@
|
|||||||
"@types/qrcode": "^1.5.6",
|
"@types/qrcode": "^1.5.6",
|
||||||
"@types/react": "^19.2.7",
|
"@types/react": "^19.2.7",
|
||||||
"@types/react-dom": "^19.2.3",
|
"@types/react-dom": "^19.2.3",
|
||||||
|
"@types/ws": "^8.18.1",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.59.0",
|
"@typescript-eslint/eslint-plugin": "^8.59.0",
|
||||||
"@typescript-eslint/parser": "^8.59.0",
|
"@typescript-eslint/parser": "^8.59.0",
|
||||||
"@vitejs/plugin-react": "^6.0.1",
|
"@vitejs/plugin-react": "^6.0.1",
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -487,9 +487,16 @@ export const useCalendarStore = create<CalendarStore>()(
|
|||||||
}
|
}
|
||||||
|
|
||||||
set((state) => ({ events: [...state.events, mappedCreated] }));
|
set((state) => ({ events: [...state.events, mappedCreated] }));
|
||||||
// Invitation emails are sent by the server: `sendSchedulingMessages`
|
// Send invitation emails (iTIP REQUEST) to participants. Stalwart
|
||||||
// on CalendarEvent/set makes Stalwart queue the iTIP REQUEST itself.
|
// 0.16 does not reliably queue these server-side via
|
||||||
// Sending a client-side iMIP copy here produced duplicate emails.
|
// `sendSchedulingMessages`, so fall back to a client-side iMIP send.
|
||||||
|
if (sendSchedulingMessages && created.participants) {
|
||||||
|
try {
|
||||||
|
await client.sendImipInvitation(created);
|
||||||
|
} catch (e) {
|
||||||
|
debug.error('Failed to send invitation emails:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
return mappedCreated;
|
return mappedCreated;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
debug.error('Failed to create event:', error);
|
debug.error('Failed to create event:', error);
|
||||||
@@ -559,9 +566,23 @@ export const useCalendarStore = create<CalendarStore>()(
|
|||||||
return merged;
|
return merged;
|
||||||
}),
|
}),
|
||||||
}));
|
}));
|
||||||
// Update emails (iTIP REQUEST/REPLY) are sent by the server via the
|
// Send invitation emails (iTIP REQUEST) when scheduling is requested.
|
||||||
// `sendSchedulingMessages` argument already passed above - a manual
|
if (sendSchedulingMessages) {
|
||||||
// iMIP send here produced duplicate emails.
|
const mergedParticipants = cleanUpdates.participants ?? storeEvent?.participants;
|
||||||
|
if (mergedParticipants) {
|
||||||
|
const eventForInvitation = {
|
||||||
|
...(storeEvent ?? {}),
|
||||||
|
...cleanUpdates,
|
||||||
|
id: realId,
|
||||||
|
participants: mergedParticipants,
|
||||||
|
} as CalendarEvent;
|
||||||
|
try {
|
||||||
|
await client.sendImipInvitation(eventForInvitation);
|
||||||
|
} catch (e) {
|
||||||
|
debug.error('Failed to send invitation emails:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
debug.error('Failed to update event:', error);
|
debug.error('Failed to update event:', error);
|
||||||
if (isNetworkError(error)) {
|
if (isNetworkError(error)) {
|
||||||
@@ -808,9 +829,6 @@ export const useCalendarStore = create<CalendarStore>()(
|
|||||||
try {
|
try {
|
||||||
// Resolve shared event IDs and client-side expanded occurrence IDs
|
// Resolve shared event IDs and client-side expanded occurrence IDs
|
||||||
client = resolveAccountClient(client, storeEvent?.localAccountId);
|
client = resolveAccountClient(client, storeEvent?.localAccountId);
|
||||||
// Cancellation emails (iTIP CANCEL) are sent by the server via the
|
|
||||||
// `sendSchedulingMessages` argument on the destroy below - a manual
|
|
||||||
// iMIP send here produced duplicate emails.
|
|
||||||
debug.log('calendar', 'Calendar deleteEvent', {
|
debug.log('calendar', 'Calendar deleteEvent', {
|
||||||
storeId: id,
|
storeId: id,
|
||||||
realId,
|
realId,
|
||||||
@@ -823,6 +841,14 @@ export const useCalendarStore = create<CalendarStore>()(
|
|||||||
events: state.events.filter(e => e.id !== id),
|
events: state.events.filter(e => e.id !== id),
|
||||||
selectedEventId: state.selectedEventId === id ? null : state.selectedEventId,
|
selectedEventId: state.selectedEventId === id ? null : state.selectedEventId,
|
||||||
}));
|
}));
|
||||||
|
// Send cancellation emails (iTIP CANCEL) to participants.
|
||||||
|
if (sendSchedulingMessages && storeEvent?.participants) {
|
||||||
|
try {
|
||||||
|
await client.sendImipCancellation(storeEvent);
|
||||||
|
} catch (e) {
|
||||||
|
debug.error('Failed to send cancellation emails:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
debug.error('Failed to delete event:', error);
|
debug.error('Failed to delete event:', error);
|
||||||
if (isNetworkError(error)) {
|
if (isNetworkError(error)) {
|
||||||
|
|||||||
@@ -490,7 +490,7 @@ const DEFAULT_SETTINGS = {
|
|||||||
rtlEditingSupport: false,
|
rtlEditingSupport: false,
|
||||||
subAddressDelimiter: DEFAULT_SUB_ADDRESS_DELIMITER,
|
subAddressDelimiter: DEFAULT_SUB_ADDRESS_DELIMITER,
|
||||||
sendDelaySeconds: 0 as SendDelaySeconds,
|
sendDelaySeconds: 0 as SendDelaySeconds,
|
||||||
signaturePosition: 'below_quote' as SignaturePosition,
|
signaturePosition: 'above_quote' as SignaturePosition,
|
||||||
signatureSeparatorEnabled: true,
|
signatureSeparatorEnabled: true,
|
||||||
requestReadReceiptDefault: false,
|
requestReadReceiptDefault: false,
|
||||||
readReceiptResponse: 'ask' as ReadReceiptResponse,
|
readReceiptResponse: 'ask' as ReadReceiptResponse,
|
||||||
@@ -998,7 +998,7 @@ export const useSettingsStore = create<SettingsState>()(
|
|||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
name: 'settings-storage',
|
name: 'settings-storage',
|
||||||
version: 7,
|
version: 8,
|
||||||
migrate: migrateSettings,
|
migrate: migrateSettings,
|
||||||
onRehydrateStorage: () => {
|
onRehydrateStorage: () => {
|
||||||
return (state) => {
|
return (state) => {
|
||||||
@@ -1085,6 +1085,12 @@ export function migrateSettings(persisted: unknown, version: number): SettingsSt
|
|||||||
if (version < 6 || !isPlainRecord(state.preferredIdentityIds)) {
|
if (version < 6 || !isPlainRecord(state.preferredIdentityIds)) {
|
||||||
state.preferredIdentityIds = {};
|
state.preferredIdentityIds = {};
|
||||||
}
|
}
|
||||||
|
// v8: reply/forward signatures now sit above the quoted message by
|
||||||
|
// default (previously below). Migrate any persisted value so existing
|
||||||
|
// accounts pick up the new default.
|
||||||
|
if (version < 8) {
|
||||||
|
state.signaturePosition = 'above_quote';
|
||||||
|
}
|
||||||
return state as unknown as SettingsState;
|
return state as unknown as SettingsState;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"id": "jitsi-meet",
|
||||||
|
"name": "Video meetings",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"author": "SRC Advisory",
|
||||||
|
"description": "SRC video meetings (VNCtalk / Jitsi). Start a meeting from Settings: the plugin asks the server for a signed JWT and opens the room in meet.src-advisory.com.",
|
||||||
|
"type": "ui-extension",
|
||||||
|
"tier": "privileged",
|
||||||
|
"permissions": [
|
||||||
|
"ui:settings-section"
|
||||||
|
],
|
||||||
|
"entrypoint": "index.js",
|
||||||
|
"minAppVersion": "1.7.6"
|
||||||
|
}
|
||||||
Generated
+481
@@ -0,0 +1,481 @@
|
|||||||
|
{
|
||||||
|
"name": "bulwark-plugin-jitsi-meet",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"name": "bulwark-plugin-jitsi-meet",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"devDependencies": {
|
||||||
|
"esbuild": "^0.24.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/aix-ppc64": {
|
||||||
|
"version": "0.24.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.24.2.tgz",
|
||||||
|
"integrity": "sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA==",
|
||||||
|
"cpu": [
|
||||||
|
"ppc64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"aix"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/android-arm": {
|
||||||
|
"version": "0.24.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.24.2.tgz",
|
||||||
|
"integrity": "sha512-tmwl4hJkCfNHwFB3nBa8z1Uy3ypZpxqxfTQOcHX+xRByyYgunVbZ9MzUUfb0RxaHIMnbHagwAxuTL+tnNM+1/Q==",
|
||||||
|
"cpu": [
|
||||||
|
"arm"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"android"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/android-arm64": {
|
||||||
|
"version": "0.24.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.24.2.tgz",
|
||||||
|
"integrity": "sha512-cNLgeqCqV8WxfcTIOeL4OAtSmL8JjcN6m09XIgro1Wi7cF4t/THaWEa7eL5CMoMBdjoHOTh/vwTO/o2TRXIyzg==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"android"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/android-x64": {
|
||||||
|
"version": "0.24.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.24.2.tgz",
|
||||||
|
"integrity": "sha512-B6Q0YQDqMx9D7rvIcsXfmJfvUYLoP722bgfBlO5cGvNVb5V/+Y7nhBE3mHV9OpxBf4eAS2S68KZztiPaWq4XYw==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"android"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/darwin-arm64": {
|
||||||
|
"version": "0.24.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.24.2.tgz",
|
||||||
|
"integrity": "sha512-kj3AnYWc+CekmZnS5IPu9D+HWtUI49hbnyqk0FLEJDbzCIQt7hg7ucF1SQAilhtYpIujfaHr6O0UHlzzSPdOeA==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/darwin-x64": {
|
||||||
|
"version": "0.24.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.24.2.tgz",
|
||||||
|
"integrity": "sha512-WeSrmwwHaPkNR5H3yYfowhZcbriGqooyu3zI/3GGpF8AyUdsrrP0X6KumITGA9WOyiJavnGZUwPGvxvwfWPHIA==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/freebsd-arm64": {
|
||||||
|
"version": "0.24.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.24.2.tgz",
|
||||||
|
"integrity": "sha512-UN8HXjtJ0k/Mj6a9+5u6+2eZ2ERD7Edt1Q9IZiB5UZAIdPnVKDoG7mdTVGhHJIeEml60JteamR3qhsr1r8gXvg==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"freebsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/freebsd-x64": {
|
||||||
|
"version": "0.24.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.24.2.tgz",
|
||||||
|
"integrity": "sha512-TvW7wE/89PYW+IevEJXZ5sF6gJRDY/14hyIGFXdIucxCsbRmLUcjseQu1SyTko+2idmCw94TgyaEZi9HUSOe3Q==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"freebsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-arm": {
|
||||||
|
"version": "0.24.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.24.2.tgz",
|
||||||
|
"integrity": "sha512-n0WRM/gWIdU29J57hJyUdIsk0WarGd6To0s+Y+LwvlC55wt+GT/OgkwoXCXvIue1i1sSNWblHEig00GBWiJgfA==",
|
||||||
|
"cpu": [
|
||||||
|
"arm"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-arm64": {
|
||||||
|
"version": "0.24.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.24.2.tgz",
|
||||||
|
"integrity": "sha512-7HnAD6074BW43YvvUmE/35Id9/NB7BeX5EoNkK9obndmZBUk8xmJJeU7DwmUeN7tkysslb2eSl6CTrYz6oEMQg==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-ia32": {
|
||||||
|
"version": "0.24.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.24.2.tgz",
|
||||||
|
"integrity": "sha512-sfv0tGPQhcZOgTKO3oBE9xpHuUqguHvSo4jl+wjnKwFpapx+vUDcawbwPNuBIAYdRAvIDBfZVvXprIj3HA+Ugw==",
|
||||||
|
"cpu": [
|
||||||
|
"ia32"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-loong64": {
|
||||||
|
"version": "0.24.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.24.2.tgz",
|
||||||
|
"integrity": "sha512-CN9AZr8kEndGooS35ntToZLTQLHEjtVB5n7dl8ZcTZMonJ7CCfStrYhrzF97eAecqVbVJ7APOEe18RPI4KLhwQ==",
|
||||||
|
"cpu": [
|
||||||
|
"loong64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-mips64el": {
|
||||||
|
"version": "0.24.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.24.2.tgz",
|
||||||
|
"integrity": "sha512-iMkk7qr/wl3exJATwkISxI7kTcmHKE+BlymIAbHO8xanq/TjHaaVThFF6ipWzPHryoFsesNQJPE/3wFJw4+huw==",
|
||||||
|
"cpu": [
|
||||||
|
"mips64el"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-ppc64": {
|
||||||
|
"version": "0.24.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.24.2.tgz",
|
||||||
|
"integrity": "sha512-shsVrgCZ57Vr2L8mm39kO5PPIb+843FStGt7sGGoqiiWYconSxwTiuswC1VJZLCjNiMLAMh34jg4VSEQb+iEbw==",
|
||||||
|
"cpu": [
|
||||||
|
"ppc64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-riscv64": {
|
||||||
|
"version": "0.24.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.24.2.tgz",
|
||||||
|
"integrity": "sha512-4eSFWnU9Hhd68fW16GD0TINewo1L6dRrB+oLNNbYyMUAeOD2yCK5KXGK1GH4qD/kT+bTEXjsyTCiJGHPZ3eM9Q==",
|
||||||
|
"cpu": [
|
||||||
|
"riscv64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-s390x": {
|
||||||
|
"version": "0.24.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.24.2.tgz",
|
||||||
|
"integrity": "sha512-S0Bh0A53b0YHL2XEXC20bHLuGMOhFDO6GN4b3YjRLK//Ep3ql3erpNcPlEFed93hsQAjAQDNsvcK+hV90FubSw==",
|
||||||
|
"cpu": [
|
||||||
|
"s390x"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-x64": {
|
||||||
|
"version": "0.24.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.24.2.tgz",
|
||||||
|
"integrity": "sha512-8Qi4nQcCTbLnK9WoMjdC9NiTG6/E38RNICU6sUNqK0QFxCYgoARqVqxdFmWkdonVsvGqWhmm7MO0jyTqLqwj0Q==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/netbsd-arm64": {
|
||||||
|
"version": "0.24.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.24.2.tgz",
|
||||||
|
"integrity": "sha512-wuLK/VztRRpMt9zyHSazyCVdCXlpHkKm34WUyinD2lzK07FAHTq0KQvZZlXikNWkDGoT6x3TD51jKQ7gMVpopw==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"netbsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/netbsd-x64": {
|
||||||
|
"version": "0.24.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.24.2.tgz",
|
||||||
|
"integrity": "sha512-VefFaQUc4FMmJuAxmIHgUmfNiLXY438XrL4GDNV1Y1H/RW3qow68xTwjZKfj/+Plp9NANmzbH5R40Meudu8mmw==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"netbsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/openbsd-arm64": {
|
||||||
|
"version": "0.24.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.24.2.tgz",
|
||||||
|
"integrity": "sha512-YQbi46SBct6iKnszhSvdluqDmxCJA+Pu280Av9WICNwQmMxV7nLRHZfjQzwbPs3jeWnuAhE9Jy0NrnJ12Oz+0A==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"openbsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/openbsd-x64": {
|
||||||
|
"version": "0.24.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.24.2.tgz",
|
||||||
|
"integrity": "sha512-+iDS6zpNM6EnJyWv0bMGLWSWeXGN/HTaF/LXHXHwejGsVi+ooqDfMCCTerNFxEkM3wYVcExkeGXNqshc9iMaOA==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"openbsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/sunos-x64": {
|
||||||
|
"version": "0.24.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.24.2.tgz",
|
||||||
|
"integrity": "sha512-hTdsW27jcktEvpwNHJU4ZwWFGkz2zRJUz8pvddmXPtXDzVKTTINmlmga3ZzwcuMpUvLw7JkLy9QLKyGpD2Yxig==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"sunos"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/win32-arm64": {
|
||||||
|
"version": "0.24.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.24.2.tgz",
|
||||||
|
"integrity": "sha512-LihEQ2BBKVFLOC9ZItT9iFprsE9tqjDjnbulhHoFxYQtQfai7qfluVODIYxt1PgdoyQkz23+01rzwNwYfutxUQ==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/win32-ia32": {
|
||||||
|
"version": "0.24.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.24.2.tgz",
|
||||||
|
"integrity": "sha512-q+iGUwfs8tncmFC9pcnD5IvRHAzmbwQ3GPS5/ceCyHdjXubwQWI12MKWSNSMYLJMq23/IUCvJMS76PDqXe1fxA==",
|
||||||
|
"cpu": [
|
||||||
|
"ia32"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/win32-x64": {
|
||||||
|
"version": "0.24.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.24.2.tgz",
|
||||||
|
"integrity": "sha512-7VTgWzgMGvup6aSqDPLiW5zHaxYJGTO4OokMjIlrCtf+VpEL+cXKtCvg723iguPYI5oaUNdS+/V7OU2gvXVWEg==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/esbuild": {
|
||||||
|
"version": "0.24.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.24.2.tgz",
|
||||||
|
"integrity": "sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA==",
|
||||||
|
"dev": true,
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"esbuild": "bin/esbuild"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@esbuild/aix-ppc64": "0.24.2",
|
||||||
|
"@esbuild/android-arm": "0.24.2",
|
||||||
|
"@esbuild/android-arm64": "0.24.2",
|
||||||
|
"@esbuild/android-x64": "0.24.2",
|
||||||
|
"@esbuild/darwin-arm64": "0.24.2",
|
||||||
|
"@esbuild/darwin-x64": "0.24.2",
|
||||||
|
"@esbuild/freebsd-arm64": "0.24.2",
|
||||||
|
"@esbuild/freebsd-x64": "0.24.2",
|
||||||
|
"@esbuild/linux-arm": "0.24.2",
|
||||||
|
"@esbuild/linux-arm64": "0.24.2",
|
||||||
|
"@esbuild/linux-ia32": "0.24.2",
|
||||||
|
"@esbuild/linux-loong64": "0.24.2",
|
||||||
|
"@esbuild/linux-mips64el": "0.24.2",
|
||||||
|
"@esbuild/linux-ppc64": "0.24.2",
|
||||||
|
"@esbuild/linux-riscv64": "0.24.2",
|
||||||
|
"@esbuild/linux-s390x": "0.24.2",
|
||||||
|
"@esbuild/linux-x64": "0.24.2",
|
||||||
|
"@esbuild/netbsd-arm64": "0.24.2",
|
||||||
|
"@esbuild/netbsd-x64": "0.24.2",
|
||||||
|
"@esbuild/openbsd-arm64": "0.24.2",
|
||||||
|
"@esbuild/openbsd-x64": "0.24.2",
|
||||||
|
"@esbuild/sunos-x64": "0.24.2",
|
||||||
|
"@esbuild/win32-arm64": "0.24.2",
|
||||||
|
"@esbuild/win32-ia32": "0.24.2",
|
||||||
|
"@esbuild/win32-x64": "0.24.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"name": "bulwark-plugin-jitsi-meet",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"build": "esbuild src/index.js --bundle --format=cjs --platform=browser --charset=utf8 --outfile=dist/index.js --external:react --external:react-dom --external:react-dom/client --external:react/jsx-runtime --external:@plugin-host",
|
||||||
|
"dev": "npm run build -- --watch",
|
||||||
|
"package": "npm run build && cp manifest.json dist/ && cd dist && zip -X ../jitsi-meet.zip manifest.json index.js"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"esbuild": "^0.24.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
/**
|
||||||
|
* Video meetings — SRC Advisory webmail plugin (VNCtalk / Jitsi).
|
||||||
|
*
|
||||||
|
* Adds a "Video meeting" entry to Settings. "Start a meeting" generates a room
|
||||||
|
* name, asks the webmail server for a signed JWT (the server holds the shared
|
||||||
|
* JITSI_APP_SECRET — it never reaches the browser), then opens the room in
|
||||||
|
* meet.src-advisory.com with the token as a query param.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const host = require('@plugin-host');
|
||||||
|
const React = require('react');
|
||||||
|
const h = React.createElement;
|
||||||
|
const { useState } = React;
|
||||||
|
|
||||||
|
const card = {
|
||||||
|
border: '1px solid var(--color-border, #e2e8f0)',
|
||||||
|
borderRadius: '8px',
|
||||||
|
padding: '16px',
|
||||||
|
background: 'var(--color-card, #fff)',
|
||||||
|
color: 'var(--color-foreground, #0f172a)',
|
||||||
|
maxWidth: '720px',
|
||||||
|
};
|
||||||
|
const btnPrimary = {
|
||||||
|
font: 'inherit',
|
||||||
|
padding: '8px 14px',
|
||||||
|
borderRadius: '6px',
|
||||||
|
border: '1px solid var(--color-primary, #2563eb)',
|
||||||
|
background: 'var(--color-primary, #2563eb)',
|
||||||
|
color: '#fff',
|
||||||
|
cursor: 'pointer',
|
||||||
|
};
|
||||||
|
const input = {
|
||||||
|
font: 'inherit',
|
||||||
|
padding: '6px 8px',
|
||||||
|
borderRadius: '6px',
|
||||||
|
border: '1px solid var(--color-input, #cbd5e1)',
|
||||||
|
background: 'var(--color-background, #fff)',
|
||||||
|
color: 'var(--color-foreground, #0f172a)',
|
||||||
|
marginRight: '8px',
|
||||||
|
};
|
||||||
|
|
||||||
|
function generateRoomName() {
|
||||||
|
const suffix = Math.random().toString(36).slice(2, 10);
|
||||||
|
return `meeting-${suffix}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function SettingsSection() {
|
||||||
|
const [busy, setBusy] = useState(false);
|
||||||
|
const [joinRoom, setJoinRoom] = useState('');
|
||||||
|
|
||||||
|
async function openRoom(room) {
|
||||||
|
setBusy(true);
|
||||||
|
try {
|
||||||
|
const resp = await fetch('/api/jitsi/token', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ room }),
|
||||||
|
});
|
||||||
|
const data = await resp.json();
|
||||||
|
if (!resp.ok || !data.token) {
|
||||||
|
throw new Error(data.error || 'could not obtain meeting token');
|
||||||
|
}
|
||||||
|
const url = `${data.url}?jwt=${encodeURIComponent(data.token)}`;
|
||||||
|
try { host.ui.openExternalUrl(url); }
|
||||||
|
catch { window.open(url, '_blank', 'noopener,noreferrer'); }
|
||||||
|
} catch (err) {
|
||||||
|
const msg = err && err.message ? err.message : String(err);
|
||||||
|
try { host.toast.error(`Could not start meeting: ${msg}`); } catch { /* ignore */ }
|
||||||
|
} finally {
|
||||||
|
setBusy(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return h('div', { style: card },
|
||||||
|
h('div', { style: { fontWeight: 600, marginBottom: '6px' } }, 'Video meeting'),
|
||||||
|
h('div', { style: { fontSize: '13px', lineHeight: 1.5, marginBottom: '14px', color: 'var(--color-muted-foreground, #64748b)' } },
|
||||||
|
'Start or join a SRC video meeting. The room is opened in meet.src-advisory.com.'),
|
||||||
|
h('div', { style: { display: 'flex', alignItems: 'center', marginBottom: '10px' } },
|
||||||
|
h('button', { type: 'button', style: btnPrimary, disabled: busy, onClick: () => openRoom(generateRoomName()) },
|
||||||
|
busy ? 'Starting…' : 'Start a meeting →'),
|
||||||
|
),
|
||||||
|
h('div', { style: { display: 'flex', alignItems: 'center' } },
|
||||||
|
h('input', {
|
||||||
|
type: 'text',
|
||||||
|
style: input,
|
||||||
|
placeholder: 'Room name',
|
||||||
|
value: joinRoom,
|
||||||
|
onChange: (e) => setJoinRoom(e.target.value),
|
||||||
|
}),
|
||||||
|
h('button', { type: 'button', style: btnPrimary, disabled: busy || !joinRoom.trim(), onClick: () => openRoom(joinRoom.trim()) },
|
||||||
|
'Join'),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const slots = {
|
||||||
|
'settings-section': { component: SettingsSection, order: 90 },
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function activate(api) {
|
||||||
|
api.log.info('jitsi-meet plugin activated');
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"id": "manage-users",
|
||||||
|
"name": "Manage users",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"author": "SRC Advisory",
|
||||||
|
"description": "Deep-link to the VNCdirectory: users are created and assigned to SRCmail in the directory, not in the webmail. Adds a User management entry to Settings.",
|
||||||
|
"type": "ui-extension",
|
||||||
|
"tier": "privileged",
|
||||||
|
"permissions": [
|
||||||
|
"ui:settings-section"
|
||||||
|
],
|
||||||
|
"entrypoint": "index.js",
|
||||||
|
"minAppVersion": "1.7.6"
|
||||||
|
}
|
||||||
+481
@@ -0,0 +1,481 @@
|
|||||||
|
{
|
||||||
|
"name": "bulwark-plugin-manage-users",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"name": "bulwark-plugin-manage-users",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"devDependencies": {
|
||||||
|
"esbuild": "^0.24.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/aix-ppc64": {
|
||||||
|
"version": "0.24.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.24.2.tgz",
|
||||||
|
"integrity": "sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA==",
|
||||||
|
"cpu": [
|
||||||
|
"ppc64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"aix"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/android-arm": {
|
||||||
|
"version": "0.24.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.24.2.tgz",
|
||||||
|
"integrity": "sha512-tmwl4hJkCfNHwFB3nBa8z1Uy3ypZpxqxfTQOcHX+xRByyYgunVbZ9MzUUfb0RxaHIMnbHagwAxuTL+tnNM+1/Q==",
|
||||||
|
"cpu": [
|
||||||
|
"arm"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"android"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/android-arm64": {
|
||||||
|
"version": "0.24.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.24.2.tgz",
|
||||||
|
"integrity": "sha512-cNLgeqCqV8WxfcTIOeL4OAtSmL8JjcN6m09XIgro1Wi7cF4t/THaWEa7eL5CMoMBdjoHOTh/vwTO/o2TRXIyzg==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"android"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/android-x64": {
|
||||||
|
"version": "0.24.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.24.2.tgz",
|
||||||
|
"integrity": "sha512-B6Q0YQDqMx9D7rvIcsXfmJfvUYLoP722bgfBlO5cGvNVb5V/+Y7nhBE3mHV9OpxBf4eAS2S68KZztiPaWq4XYw==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"android"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/darwin-arm64": {
|
||||||
|
"version": "0.24.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.24.2.tgz",
|
||||||
|
"integrity": "sha512-kj3AnYWc+CekmZnS5IPu9D+HWtUI49hbnyqk0FLEJDbzCIQt7hg7ucF1SQAilhtYpIujfaHr6O0UHlzzSPdOeA==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/darwin-x64": {
|
||||||
|
"version": "0.24.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.24.2.tgz",
|
||||||
|
"integrity": "sha512-WeSrmwwHaPkNR5H3yYfowhZcbriGqooyu3zI/3GGpF8AyUdsrrP0X6KumITGA9WOyiJavnGZUwPGvxvwfWPHIA==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/freebsd-arm64": {
|
||||||
|
"version": "0.24.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.24.2.tgz",
|
||||||
|
"integrity": "sha512-UN8HXjtJ0k/Mj6a9+5u6+2eZ2ERD7Edt1Q9IZiB5UZAIdPnVKDoG7mdTVGhHJIeEml60JteamR3qhsr1r8gXvg==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"freebsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/freebsd-x64": {
|
||||||
|
"version": "0.24.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.24.2.tgz",
|
||||||
|
"integrity": "sha512-TvW7wE/89PYW+IevEJXZ5sF6gJRDY/14hyIGFXdIucxCsbRmLUcjseQu1SyTko+2idmCw94TgyaEZi9HUSOe3Q==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"freebsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-arm": {
|
||||||
|
"version": "0.24.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.24.2.tgz",
|
||||||
|
"integrity": "sha512-n0WRM/gWIdU29J57hJyUdIsk0WarGd6To0s+Y+LwvlC55wt+GT/OgkwoXCXvIue1i1sSNWblHEig00GBWiJgfA==",
|
||||||
|
"cpu": [
|
||||||
|
"arm"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-arm64": {
|
||||||
|
"version": "0.24.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.24.2.tgz",
|
||||||
|
"integrity": "sha512-7HnAD6074BW43YvvUmE/35Id9/NB7BeX5EoNkK9obndmZBUk8xmJJeU7DwmUeN7tkysslb2eSl6CTrYz6oEMQg==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-ia32": {
|
||||||
|
"version": "0.24.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.24.2.tgz",
|
||||||
|
"integrity": "sha512-sfv0tGPQhcZOgTKO3oBE9xpHuUqguHvSo4jl+wjnKwFpapx+vUDcawbwPNuBIAYdRAvIDBfZVvXprIj3HA+Ugw==",
|
||||||
|
"cpu": [
|
||||||
|
"ia32"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-loong64": {
|
||||||
|
"version": "0.24.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.24.2.tgz",
|
||||||
|
"integrity": "sha512-CN9AZr8kEndGooS35ntToZLTQLHEjtVB5n7dl8ZcTZMonJ7CCfStrYhrzF97eAecqVbVJ7APOEe18RPI4KLhwQ==",
|
||||||
|
"cpu": [
|
||||||
|
"loong64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-mips64el": {
|
||||||
|
"version": "0.24.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.24.2.tgz",
|
||||||
|
"integrity": "sha512-iMkk7qr/wl3exJATwkISxI7kTcmHKE+BlymIAbHO8xanq/TjHaaVThFF6ipWzPHryoFsesNQJPE/3wFJw4+huw==",
|
||||||
|
"cpu": [
|
||||||
|
"mips64el"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-ppc64": {
|
||||||
|
"version": "0.24.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.24.2.tgz",
|
||||||
|
"integrity": "sha512-shsVrgCZ57Vr2L8mm39kO5PPIb+843FStGt7sGGoqiiWYconSxwTiuswC1VJZLCjNiMLAMh34jg4VSEQb+iEbw==",
|
||||||
|
"cpu": [
|
||||||
|
"ppc64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-riscv64": {
|
||||||
|
"version": "0.24.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.24.2.tgz",
|
||||||
|
"integrity": "sha512-4eSFWnU9Hhd68fW16GD0TINewo1L6dRrB+oLNNbYyMUAeOD2yCK5KXGK1GH4qD/kT+bTEXjsyTCiJGHPZ3eM9Q==",
|
||||||
|
"cpu": [
|
||||||
|
"riscv64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-s390x": {
|
||||||
|
"version": "0.24.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.24.2.tgz",
|
||||||
|
"integrity": "sha512-S0Bh0A53b0YHL2XEXC20bHLuGMOhFDO6GN4b3YjRLK//Ep3ql3erpNcPlEFed93hsQAjAQDNsvcK+hV90FubSw==",
|
||||||
|
"cpu": [
|
||||||
|
"s390x"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-x64": {
|
||||||
|
"version": "0.24.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.24.2.tgz",
|
||||||
|
"integrity": "sha512-8Qi4nQcCTbLnK9WoMjdC9NiTG6/E38RNICU6sUNqK0QFxCYgoARqVqxdFmWkdonVsvGqWhmm7MO0jyTqLqwj0Q==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/netbsd-arm64": {
|
||||||
|
"version": "0.24.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.24.2.tgz",
|
||||||
|
"integrity": "sha512-wuLK/VztRRpMt9zyHSazyCVdCXlpHkKm34WUyinD2lzK07FAHTq0KQvZZlXikNWkDGoT6x3TD51jKQ7gMVpopw==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"netbsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/netbsd-x64": {
|
||||||
|
"version": "0.24.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.24.2.tgz",
|
||||||
|
"integrity": "sha512-VefFaQUc4FMmJuAxmIHgUmfNiLXY438XrL4GDNV1Y1H/RW3qow68xTwjZKfj/+Plp9NANmzbH5R40Meudu8mmw==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"netbsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/openbsd-arm64": {
|
||||||
|
"version": "0.24.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.24.2.tgz",
|
||||||
|
"integrity": "sha512-YQbi46SBct6iKnszhSvdluqDmxCJA+Pu280Av9WICNwQmMxV7nLRHZfjQzwbPs3jeWnuAhE9Jy0NrnJ12Oz+0A==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"openbsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/openbsd-x64": {
|
||||||
|
"version": "0.24.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.24.2.tgz",
|
||||||
|
"integrity": "sha512-+iDS6zpNM6EnJyWv0bMGLWSWeXGN/HTaF/LXHXHwejGsVi+ooqDfMCCTerNFxEkM3wYVcExkeGXNqshc9iMaOA==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"openbsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/sunos-x64": {
|
||||||
|
"version": "0.24.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.24.2.tgz",
|
||||||
|
"integrity": "sha512-hTdsW27jcktEvpwNHJU4ZwWFGkz2zRJUz8pvddmXPtXDzVKTTINmlmga3ZzwcuMpUvLw7JkLy9QLKyGpD2Yxig==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"sunos"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/win32-arm64": {
|
||||||
|
"version": "0.24.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.24.2.tgz",
|
||||||
|
"integrity": "sha512-LihEQ2BBKVFLOC9ZItT9iFprsE9tqjDjnbulhHoFxYQtQfai7qfluVODIYxt1PgdoyQkz23+01rzwNwYfutxUQ==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/win32-ia32": {
|
||||||
|
"version": "0.24.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.24.2.tgz",
|
||||||
|
"integrity": "sha512-q+iGUwfs8tncmFC9pcnD5IvRHAzmbwQ3GPS5/ceCyHdjXubwQWI12MKWSNSMYLJMq23/IUCvJMS76PDqXe1fxA==",
|
||||||
|
"cpu": [
|
||||||
|
"ia32"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/win32-x64": {
|
||||||
|
"version": "0.24.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.24.2.tgz",
|
||||||
|
"integrity": "sha512-7VTgWzgMGvup6aSqDPLiW5zHaxYJGTO4OokMjIlrCtf+VpEL+cXKtCvg723iguPYI5oaUNdS+/V7OU2gvXVWEg==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/esbuild": {
|
||||||
|
"version": "0.24.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.24.2.tgz",
|
||||||
|
"integrity": "sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA==",
|
||||||
|
"dev": true,
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"esbuild": "bin/esbuild"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@esbuild/aix-ppc64": "0.24.2",
|
||||||
|
"@esbuild/android-arm": "0.24.2",
|
||||||
|
"@esbuild/android-arm64": "0.24.2",
|
||||||
|
"@esbuild/android-x64": "0.24.2",
|
||||||
|
"@esbuild/darwin-arm64": "0.24.2",
|
||||||
|
"@esbuild/darwin-x64": "0.24.2",
|
||||||
|
"@esbuild/freebsd-arm64": "0.24.2",
|
||||||
|
"@esbuild/freebsd-x64": "0.24.2",
|
||||||
|
"@esbuild/linux-arm": "0.24.2",
|
||||||
|
"@esbuild/linux-arm64": "0.24.2",
|
||||||
|
"@esbuild/linux-ia32": "0.24.2",
|
||||||
|
"@esbuild/linux-loong64": "0.24.2",
|
||||||
|
"@esbuild/linux-mips64el": "0.24.2",
|
||||||
|
"@esbuild/linux-ppc64": "0.24.2",
|
||||||
|
"@esbuild/linux-riscv64": "0.24.2",
|
||||||
|
"@esbuild/linux-s390x": "0.24.2",
|
||||||
|
"@esbuild/linux-x64": "0.24.2",
|
||||||
|
"@esbuild/netbsd-arm64": "0.24.2",
|
||||||
|
"@esbuild/netbsd-x64": "0.24.2",
|
||||||
|
"@esbuild/openbsd-arm64": "0.24.2",
|
||||||
|
"@esbuild/openbsd-x64": "0.24.2",
|
||||||
|
"@esbuild/sunos-x64": "0.24.2",
|
||||||
|
"@esbuild/win32-arm64": "0.24.2",
|
||||||
|
"@esbuild/win32-ia32": "0.24.2",
|
||||||
|
"@esbuild/win32-x64": "0.24.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"name": "bulwark-plugin-manage-users",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"build": "esbuild src/index.js --bundle --format=cjs --platform=browser --charset=utf8 --outfile=dist/index.js --external:react --external:react-dom --external:react-dom/client --external:react/jsx-runtime --external:@plugin-host",
|
||||||
|
"dev": "npm run build -- --watch",
|
||||||
|
"package": "npm run build && cp manifest.json dist/ && cd dist && zip -X ../manage-users.zip manifest.json index.js"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"esbuild": "^0.24.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
/**
|
||||||
|
* Manage users — SRC Advisory webmail plugin.
|
||||||
|
*
|
||||||
|
* Adds a first-class "User management" entry to the Settings page. Users are
|
||||||
|
* the VNCdirectory's concern (the SRC directory), so this plugin is a thin
|
||||||
|
* deep-link: it explains where users live and opens the directory's user list
|
||||||
|
* in a new tab. It deliberately does NOT try to manage users itself.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const host = require('@plugin-host');
|
||||||
|
const React = require('react');
|
||||||
|
const h = React.createElement;
|
||||||
|
|
||||||
|
const MANAGE_USERS_URL = 'https://vncdirectory.src-advisory.com/users';
|
||||||
|
|
||||||
|
const card = {
|
||||||
|
border: '1px solid var(--color-border, #e2e8f0)',
|
||||||
|
borderRadius: '8px',
|
||||||
|
padding: '16px',
|
||||||
|
background: 'var(--color-card, #fff)',
|
||||||
|
color: 'var(--color-foreground, #0f172a)',
|
||||||
|
maxWidth: '720px',
|
||||||
|
};
|
||||||
|
const btnPrimary = {
|
||||||
|
font: 'inherit',
|
||||||
|
padding: '8px 14px',
|
||||||
|
borderRadius: '6px',
|
||||||
|
border: '1px solid var(--color-primary, #2563eb)',
|
||||||
|
background: 'var(--color-primary, #2563eb)',
|
||||||
|
color: '#fff',
|
||||||
|
cursor: 'pointer',
|
||||||
|
};
|
||||||
|
|
||||||
|
function SettingsSection() {
|
||||||
|
return h('div', { style: card },
|
||||||
|
h('div', { style: { fontWeight: 600, marginBottom: '6px' } }, 'User management'),
|
||||||
|
h('div', { style: { fontSize: '13px', lineHeight: 1.5, marginBottom: '14px', color: 'var(--color-muted-foreground, #64748b)' } },
|
||||||
|
'Users are created and managed in the VNCdirectory, not in SRCmail. ' +
|
||||||
|
'Add a user there and assign them to the SRC organization to give them a mailbox automatically.'),
|
||||||
|
h('button', {
|
||||||
|
type: 'button',
|
||||||
|
style: btnPrimary,
|
||||||
|
onClick: () => {
|
||||||
|
try { host.ui.openExternalUrl(MANAGE_USERS_URL); } catch (err) {
|
||||||
|
// Fallback if the sandbox blocks window.open: navigate a normal link.
|
||||||
|
window.open(MANAGE_USERS_URL, '_blank', 'noopener,noreferrer');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}, 'Open VNCdirectory →'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const slots = {
|
||||||
|
'settings-section': { component: SettingsSection, order: 100 },
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function activate(api) {
|
||||||
|
api.log.info('manage-users plugin activated');
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user