mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-28 14:15:24 +00:00
220 lines
7.2 KiB
TypeScript
220 lines
7.2 KiB
TypeScript
import { useEffect, useState } from "react";
|
|
import { useTranslation } from "react-i18next";
|
|
import { toast } from "sonner";
|
|
import { Button } from "@/components/button";
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogDescription,
|
|
DialogFooter,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
} from "@/components/dialog";
|
|
import {
|
|
getCredentials,
|
|
getHostAuthOverride,
|
|
setHostAuthOverride,
|
|
} from "@/main-axios";
|
|
import type { Credential, Host } from "@/types/ui-types";
|
|
import {
|
|
AUTH_PROTOCOL_METADATA,
|
|
type AuthOverrideProtocol,
|
|
} from "@/types/auth-protocols";
|
|
import { mapCredentials } from "./HostManagerData";
|
|
import { getConnectedRemoteApi } from "@/lib/remote-server-api";
|
|
|
|
export function HostAuthOverrideModal({
|
|
open,
|
|
onOpenChange,
|
|
host,
|
|
protocol,
|
|
}: {
|
|
open: boolean;
|
|
onOpenChange: (open: boolean) => void;
|
|
host: Host;
|
|
protocol: AuthOverrideProtocol;
|
|
}) {
|
|
const { t } = useTranslation();
|
|
const [credentials, setCredentials] = useState<Credential[]>([]);
|
|
const [selectedId, setSelectedId] = useState("");
|
|
const [initialId, setInitialId] = useState("");
|
|
const [loading, setLoading] = useState(false);
|
|
const [loadError, setLoadError] = useState(false);
|
|
const [saving, setSaving] = useState(false);
|
|
const overrideState = host.authOverrides?.[protocol];
|
|
const ownerAuthShared =
|
|
overrideState?.ownerAuthShared ??
|
|
(protocol === "ssh" ? !!host.shareSshAuth : false);
|
|
// A shared host's copy on a linked desktop: its override lives on the
|
|
// server, against the server's own credentials.
|
|
const sharedCopySyncId = host.sharedCopy ? (host.syncId ?? null) : null;
|
|
|
|
useEffect(() => {
|
|
if (!open) return;
|
|
let cancelled = false;
|
|
setLoading(true);
|
|
setLoadError(false);
|
|
|
|
const credentialsRequest = sharedCopySyncId
|
|
? getConnectedRemoteApi().then((api) => {
|
|
if (!api) throw new Error("The linked server is not reachable");
|
|
return api.get("/credentials").then((response) => response.data);
|
|
})
|
|
: getCredentials();
|
|
|
|
Promise.all([
|
|
credentialsRequest,
|
|
getHostAuthOverride(Number(host.id), protocol, sharedCopySyncId),
|
|
])
|
|
.then(([credentialResult, overrideResult]) => {
|
|
if (cancelled) return;
|
|
const nextCredentials = mapCredentials(credentialResult);
|
|
const nextId =
|
|
overrideResult.credentialId == null
|
|
? ""
|
|
: String(overrideResult.credentialId);
|
|
setCredentials(nextCredentials);
|
|
setSelectedId(nextId);
|
|
setInitialId(nextId);
|
|
})
|
|
.catch(() => {
|
|
if (!cancelled) setLoadError(true);
|
|
})
|
|
.finally(() => {
|
|
if (!cancelled) setLoading(false);
|
|
});
|
|
|
|
return () => {
|
|
cancelled = true;
|
|
};
|
|
}, [host.id, open, protocol, sharedCopySyncId]);
|
|
|
|
const handleSave = async () => {
|
|
setSaving(true);
|
|
try {
|
|
const credentialId = selectedId ? Number(selectedId) : null;
|
|
await setHostAuthOverride(
|
|
Number(host.id),
|
|
protocol,
|
|
credentialId,
|
|
sharedCopySyncId,
|
|
);
|
|
toast.success(
|
|
credentialId === null
|
|
? t(
|
|
ownerAuthShared
|
|
? "hosts.sharing.authOverrideClearedToShared"
|
|
: "hosts.sharing.authOverrideCleared",
|
|
)
|
|
: t("hosts.sharing.authOverrideSaved"),
|
|
);
|
|
window.dispatchEvent(new CustomEvent("termix:hosts-changed"));
|
|
onOpenChange(false);
|
|
} catch {
|
|
toast.error(t("hosts.sharing.authOverrideSaveError"));
|
|
} finally {
|
|
setSaving(false);
|
|
}
|
|
};
|
|
return (
|
|
<span
|
|
className="contents"
|
|
onClick={(event) => event.stopPropagation()}
|
|
onPointerDown={(event) => event.stopPropagation()}
|
|
>
|
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
<DialogContent>
|
|
<DialogHeader>
|
|
<DialogTitle>
|
|
{t("hosts.sharing.authOverrideTitleProtocol", {
|
|
protocol: AUTH_PROTOCOL_METADATA[protocol].label,
|
|
})}
|
|
</DialogTitle>
|
|
<DialogDescription>
|
|
{t(
|
|
ownerAuthShared
|
|
? "hosts.sharing.authOverrideDescriptionShared"
|
|
: "hosts.sharing.authOverrideDescriptionPrivate",
|
|
{ host: host.name },
|
|
)}
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
|
|
{loading ? (
|
|
<p className="py-4 text-center text-muted-foreground">
|
|
{t("common.loading")}
|
|
</p>
|
|
) : loadError ? (
|
|
<p className="border border-destructive/30 bg-destructive/5 p-3 text-destructive">
|
|
{t("hosts.sharing.authOverrideLoadError")}
|
|
</p>
|
|
) : (
|
|
<div className="flex flex-col gap-2">
|
|
<label
|
|
htmlFor={`auth-override-${host.id}`}
|
|
className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground"
|
|
>
|
|
{t("hosts.sharing.authOverrideCredentialLabel")}
|
|
</label>
|
|
<select
|
|
id={`auth-override-${host.id}`}
|
|
value={selectedId}
|
|
onChange={(event) => setSelectedId(event.target.value)}
|
|
className="flex h-9 w-full border border-border bg-background px-3 py-1 text-xs outline-none focus:ring-1 focus:ring-ring"
|
|
>
|
|
<option value="">
|
|
{t(
|
|
ownerAuthShared
|
|
? "hosts.sharing.useSharedAuthentication"
|
|
: "hosts.sharing.noPersonalCredential",
|
|
)}
|
|
</option>
|
|
{credentials.map((credential) => (
|
|
<option key={credential.id} value={credential.id}>
|
|
{credential.username
|
|
? `${credential.name} (${credential.username})`
|
|
: credential.name}
|
|
</option>
|
|
))}
|
|
</select>
|
|
{credentials.length === 0 && (
|
|
<p className="text-[10px] text-muted-foreground">
|
|
{t("hosts.sharing.authOverrideNoCredentials")}
|
|
</p>
|
|
)}
|
|
{overrideState?.required && selectedId === "" && (
|
|
<p className="border border-amber-500/30 bg-amber-500/5 p-2 text-[10px] text-amber-600 dark:text-amber-400">
|
|
{t("hosts.sharing.authOverrideRequired")}
|
|
</p>
|
|
)}
|
|
<p className="text-[10px] text-muted-foreground">
|
|
{t("hosts.sharing.authOverridePrivateHint")}
|
|
</p>
|
|
</div>
|
|
)}
|
|
|
|
<DialogFooter>
|
|
<Button
|
|
variant="outline"
|
|
onClick={() => onOpenChange(false)}
|
|
disabled={saving}
|
|
>
|
|
{t("common.cancel")}
|
|
</Button>
|
|
<Button
|
|
variant="outline"
|
|
className="border-accent-brand/40 text-accent-brand hover:bg-accent-brand/10 hover:text-accent-brand"
|
|
onClick={handleSave}
|
|
disabled={
|
|
loading || loadError || saving || selectedId === initialId
|
|
}
|
|
>
|
|
{saving ? t("common.saving") : t("common.save")}
|
|
</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
</span>
|
|
);
|
|
}
|