fix(auth): bind mailbox callbacks to pending popup
This commit is contained in:
@@ -44,6 +44,7 @@ import {
|
||||
import { useDialogActions } from "../dialogs";
|
||||
import { useI18n } from "../i18n/I18nProvider";
|
||||
import ViewStateNotice from "./ViewStateNotice";
|
||||
import { emailOAuthCallbackOrigin, emailOAuthMessage, type EmailOAuthAttempt } from "../emailOAuth";
|
||||
|
||||
function parseRawEmail(raw: string): { subject?: string; date?: string; from?: string; to?: string; body: string } {
|
||||
const lines = raw.replace(/\r\n/g, "\n").split("\n");
|
||||
@@ -155,6 +156,16 @@ export default function Correspondence({ jobId, jobContext }: { jobId: number; j
|
||||
const [manageNote, setManageNote] = useState("");
|
||||
const [manageSaving, setManageSaving] = useState(false);
|
||||
const autoRefreshKeyRef = useRef<string | null>(null);
|
||||
const gmailOAuth = useRef<{ active: boolean; generation: number; starting: boolean; attempt: EmailOAuthAttempt | null }>({ active: true, generation: 0, starting: false, attempt: null });
|
||||
const startupTranslation = useRef(t);
|
||||
|
||||
useEffect(() => { startupTranslation.current = t; }, [t]);
|
||||
|
||||
useEffect(() => {
|
||||
const session = gmailOAuth.current;
|
||||
session.active = true;
|
||||
return () => { session.active = false; session.generation += 1; session.starting = false; session.attempt = null; };
|
||||
}, [jobId]);
|
||||
|
||||
const load = useCallback(async () => {
|
||||
setMessagesLoading(true);
|
||||
@@ -292,8 +303,11 @@ export default function Correspondence({ jobId, jobContext }: { jobId: number; j
|
||||
|
||||
useEffect(() => {
|
||||
const onMessage = (event: MessageEvent) => {
|
||||
const data = event.data as { source?: string; status?: string; message?: string };
|
||||
if (data?.source !== "jobtracker-gmail-oauth") return;
|
||||
const session = gmailOAuth.current;
|
||||
if (!session.active || !session.attempt) return;
|
||||
const data = emailOAuthMessage(event, session.attempt);
|
||||
if (!data) return;
|
||||
session.attempt = null;
|
||||
if (data.status === "connected") {
|
||||
toast(data.message || t("googleLinkedSuccess"), "success");
|
||||
void loadGmailStatus();
|
||||
@@ -356,12 +370,24 @@ export default function Correspondence({ jobId, jobContext }: { jobId: number; j
|
||||
};
|
||||
|
||||
const connectGmail = async () => {
|
||||
const session = gmailOAuth.current;
|
||||
if (!session.active || session.starting) return;
|
||||
if (session.attempt && !session.attempt.popup.closed) { session.attempt.popup.focus(); return; }
|
||||
session.attempt = null;
|
||||
session.starting = true;
|
||||
const generation = session.generation;
|
||||
try {
|
||||
const res = await api.get<{ url: string }>("/gmail/connect-url");
|
||||
if (!session.active || generation !== session.generation) return;
|
||||
const origin = emailOAuthCallbackOrigin(res.data.url, "gmail");
|
||||
if (!origin) { toast(startupTranslation.current("correspondenceStartGmailFailed"), "error"); return; }
|
||||
const popup = window.open(res.data.url, "jobtracker-gmail-connect", "width=620,height=760,resizable=yes,scrollbars=yes");
|
||||
if (!popup) toast(t("correspondenceBlockedPopup"), "error");
|
||||
if (!popup) toast(startupTranslation.current("correspondenceBlockedPopup"), "error");
|
||||
else session.attempt = { provider: "gmail", origin, popup };
|
||||
} catch (error) {
|
||||
toast(getApiErrorMessage(error, t("correspondenceStartGmailFailed")), "error");
|
||||
if (session.active && generation === session.generation) toast(getApiErrorMessage(error, startupTranslation.current("correspondenceStartGmailFailed")), "error");
|
||||
} finally {
|
||||
if (generation === session.generation) session.starting = false;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import React, { useCallback, useEffect, useRef, useState } from "react";
|
||||
|
||||
import { Box, Button, Checkbox, Chip, Divider, FormControlLabel, Paper, Stack, TextField, Typography } from "@mui/material";
|
||||
import CheckCircleIcon from "@mui/icons-material/CheckCircle";
|
||||
@@ -7,6 +7,7 @@ import { api, getApiErrorMessage } from "../api";
|
||||
import { useI18n } from "../i18n/I18nProvider";
|
||||
import { useToast } from "../toast";
|
||||
import type { GmailStatus, ImapStatus, MicrosoftGraphStatus } from "../types";
|
||||
import { emailOAuthCallbackOrigin, emailOAuthMessage, type EmailOAuthAttempt, type EmailOAuthProvider } from "../emailOAuth";
|
||||
|
||||
// Settings > Account: connect/disconnect each linked-mailbox provider. Gmail and Microsoft use
|
||||
// the same OAuth-popup + postMessage handshake (mirrored server-side in GmailController /
|
||||
@@ -22,6 +23,16 @@ export default function EmailProviderConnections() {
|
||||
|
||||
const [imapForm, setImapForm] = useState({ host: "", port: 993, useSsl: true, username: "", password: "" });
|
||||
const [imapConnecting, setImapConnecting] = useState(false);
|
||||
const oauth = useRef({ mounted: true, generation: 0, starting: new Set<EmailOAuthProvider>(), attempts: new Map<EmailOAuthProvider, EmailOAuthAttempt>() });
|
||||
const startupTranslation = useRef(t);
|
||||
|
||||
useEffect(() => { startupTranslation.current = t; }, [t]);
|
||||
|
||||
useEffect(() => {
|
||||
const session = oauth.current;
|
||||
session.mounted = true;
|
||||
return () => { session.mounted = false; session.generation += 1; session.starting.clear(); session.attempts.clear(); };
|
||||
}, []);
|
||||
|
||||
const loadGmailStatus = useCallback(async () => {
|
||||
try {
|
||||
@@ -58,21 +69,18 @@ export default function EmailProviderConnections() {
|
||||
|
||||
useEffect(() => {
|
||||
const onMessage = (event: MessageEvent) => {
|
||||
const data = event.data as { source?: string; status?: string; message?: string };
|
||||
if (data?.source === "jobtracker-gmail-oauth") {
|
||||
if (!oauth.current.mounted) return;
|
||||
for (const [provider, attempt] of oauth.current.attempts) {
|
||||
const data = emailOAuthMessage(event, attempt);
|
||||
if (!data) continue;
|
||||
oauth.current.attempts.delete(provider);
|
||||
if (data.status === "connected") {
|
||||
toast(data.message || t("emailConnectionsGmailConnected"), "success");
|
||||
void loadGmailStatus();
|
||||
toast(data.message || t(provider === "gmail" ? "emailConnectionsGmailConnected" : "emailConnectionsOutlookConnected"), "success");
|
||||
void (provider === "gmail" ? loadGmailStatus() : loadMicrosoftStatus());
|
||||
} else {
|
||||
toast(data.message || t("emailConnectionsGmailFailed"), "error");
|
||||
}
|
||||
} else if (data?.source === "jobtracker-microsoft-oauth") {
|
||||
if (data.status === "connected") {
|
||||
toast(data.message || t("emailConnectionsOutlookConnected"), "success");
|
||||
void loadMicrosoftStatus();
|
||||
} else {
|
||||
toast(data.message || t("emailConnectionsOutlookFailed"), "error");
|
||||
toast(data.message || t(provider === "gmail" ? "emailConnectionsGmailFailed" : "emailConnectionsOutlookFailed"), "error");
|
||||
}
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -80,13 +88,26 @@ export default function EmailProviderConnections() {
|
||||
return () => window.removeEventListener("message", onMessage);
|
||||
}, [loadGmailStatus, loadMicrosoftStatus, t, toast]);
|
||||
|
||||
const connectViaPopup = async (connectUrlPath: string, popupName: string, providerLabel: string) => {
|
||||
const connectViaPopup = async (provider: EmailOAuthProvider, connectUrlPath: string, popupName: string, providerLabel: string) => {
|
||||
const session = oauth.current;
|
||||
if (!session.mounted || session.starting.has(provider)) return;
|
||||
const previous = session.attempts.get(provider);
|
||||
if (previous && !previous.popup.closed) { previous.popup.focus(); return; }
|
||||
session.attempts.delete(provider);
|
||||
session.starting.add(provider);
|
||||
const generation = session.generation;
|
||||
try {
|
||||
const res = await api.get<{ url: string }>(connectUrlPath);
|
||||
if (!session.mounted || generation !== session.generation) return;
|
||||
const origin = emailOAuthCallbackOrigin(res.data.url, provider);
|
||||
if (!origin) { toast(startupTranslation.current("emailConnectionsStartFailed", { provider: providerLabel }), "error"); return; }
|
||||
const popup = window.open(res.data.url, popupName, "width=620,height=760,resizable=yes,scrollbars=yes");
|
||||
if (!popup) toast(t("emailConnectionsPopupBlocked"), "error");
|
||||
if (!popup) toast(startupTranslation.current("emailConnectionsPopupBlocked"), "error");
|
||||
else session.attempts.set(provider, { provider, origin, popup });
|
||||
} catch (error) {
|
||||
toast(getApiErrorMessage(error, t("emailConnectionsStartFailed", { provider: providerLabel })), "error");
|
||||
if (session.mounted && generation === session.generation) toast(getApiErrorMessage(error, startupTranslation.current("emailConnectionsStartFailed", { provider: providerLabel })), "error");
|
||||
} finally {
|
||||
if (generation === session.generation) session.starting.delete(provider);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -130,7 +151,7 @@ export default function EmailProviderConnections() {
|
||||
label="Gmail"
|
||||
connected={Boolean(gmailStatus?.connected)}
|
||||
address={gmailStatus?.gmailAddress ?? null}
|
||||
onConnect={() => void connectViaPopup("/gmail/connect-url", "jobtracker-gmail-connect", "Gmail")}
|
||||
onConnect={() => void connectViaPopup("gmail", "/gmail/connect-url", "jobtracker-gmail-connect", "Gmail")}
|
||||
onDisconnect={() => void disconnect("/gmail/connection", loadGmailStatus, "Gmail")}
|
||||
/>
|
||||
|
||||
@@ -140,7 +161,7 @@ export default function EmailProviderConnections() {
|
||||
label="Outlook / Microsoft 365"
|
||||
connected={Boolean(microsoftStatus?.connected)}
|
||||
address={microsoftStatus?.mailAddress ?? null}
|
||||
onConnect={() => void connectViaPopup("/microsoft-graph/connect-url", "jobtracker-microsoft-connect", "Outlook")}
|
||||
onConnect={() => void connectViaPopup("microsoft", "/microsoft-graph/connect-url", "jobtracker-microsoft-connect", "Outlook")}
|
||||
onDisconnect={() => void disconnect("/microsoft-graph/connection", loadMicrosoftStatus, "Outlook")}
|
||||
/>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user