mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-07-15 21:03:47 +00:00
release-2.4.1 (#921)
* chore(deps-dev): bump the dev-minor-updates group across 1 directory with 12 updates (#910) Bumps the dev-minor-updates group with 12 updates in the / directory: | Package | From | To | | --- | --- | --- | | [@radix-ui/react-select](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/select) | `2.2.6` | `2.3.1` | | [@radix-ui/react-slider](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/slider) | `1.3.6` | `1.4.1` | | [@radix-ui/react-slot](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/slot) | `1.2.5` | `1.3.0` | | [@radix-ui/react-switch](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/switch) | `1.2.6` | `1.3.1` | | [cytoscape](https://github.com/cytoscape/cytoscape.js) | `3.33.4` | `3.34.0` | | [electron](https://github.com/electron/electron) | `42.2.0` | `42.4.1` | | [electron-builder](https://github.com/electron-userland/electron-builder/tree/HEAD/packages/electron-builder) | `26.8.1` | `26.15.3` | | [lucide-react](https://github.com/lucide-icons/lucide/tree/HEAD/packages/lucide-react) | `1.16.0` | `1.20.0` | | [radix-ui](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/radix-ui) | `1.4.3` | `1.6.0` | | [react-hook-form](https://github.com/react-hook-form/react-hook-form) | `7.76.1` | `7.79.0` | | [sharp](https://github.com/lovell/sharp) | `0.34.5` | `0.35.1` | | [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint) | `8.60.0` | `8.61.1` | Updates `@radix-ui/react-select` from 2.2.6 to 2.3.1 - [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/select/CHANGELOG.md) - [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/select) Updates `@radix-ui/react-slider` from 1.3.6 to 1.4.1 - [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/slider/CHANGELOG.md) - [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/slider) Updates `@radix-ui/react-slot` from 1.2.5 to 1.3.0 - [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/slot/CHANGELOG.md) - [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/slot) Updates `@radix-ui/react-switch` from 1.2.6 to 1.3.1 - [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/switch/CHANGELOG.md) - [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/switch) Updates `cytoscape` from 3.33.4 to 3.34.0 - [Release notes](https://github.com/cytoscape/cytoscape.js/releases) - [Commits](https://github.com/cytoscape/cytoscape.js/compare/v3.33.4...v3.34.0) Updates `electron` from 42.2.0 to 42.4.1 - [Release notes](https://github.com/electron/electron/releases) - [Commits](https://github.com/electron/electron/compare/v42.2.0...v42.4.1) Updates `electron-builder` from 26.8.1 to 26.15.3 - [Release notes](https://github.com/electron-userland/electron-builder/releases) - [Changelog](https://github.com/electron-userland/electron-builder/blob/master/packages/electron-builder/CHANGELOG.md) - [Commits](https://github.com/electron-userland/electron-builder/commits/electron-builder@26.15.3/packages/electron-builder) Updates `lucide-react` from 1.16.0 to 1.20.0 - [Release notes](https://github.com/lucide-icons/lucide/releases) - [Commits](https://github.com/lucide-icons/lucide/commits/1.20.0/packages/lucide-react) Updates `radix-ui` from 1.4.3 to 1.6.0 - [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/radix-ui/CHANGELOG.md) - [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/radix-ui) Updates `react-hook-form` from 7.76.1 to 7.79.0 - [Release notes](https://github.com/react-hook-form/react-hook-form/releases) - [Changelog](https://github.com/react-hook-form/react-hook-form/blob/master/CHANGELOG.md) - [Commits](https://github.com/react-hook-form/react-hook-form/compare/v7.76.1...v7.79.0) Updates `sharp` from 0.34.5 to 0.35.1 - [Release notes](https://github.com/lovell/sharp/releases) - [Commits](https://github.com/lovell/sharp/compare/v0.34.5...v0.35.1) Updates `typescript-eslint` from 8.60.0 to 8.61.1 - [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases) - [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/typescript-eslint/CHANGELOG.md) - [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.61.1/packages/typescript-eslint) --- updated-dependencies: - dependency-name: "@radix-ui/react-select" dependency-version: 2.3.1 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: dev-minor-updates - dependency-name: "@radix-ui/react-slider" dependency-version: 1.4.1 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: dev-minor-updates - dependency-name: "@radix-ui/react-slot" dependency-version: 1.3.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: dev-minor-updates - dependency-name: "@radix-ui/react-switch" dependency-version: 1.3.1 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: dev-minor-updates - dependency-name: cytoscape dependency-version: 3.34.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: dev-minor-updates - dependency-name: electron dependency-version: 42.4.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: dev-minor-updates - dependency-name: electron-builder dependency-version: 26.15.3 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: dev-minor-updates - dependency-name: lucide-react dependency-version: 1.18.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: dev-minor-updates - dependency-name: radix-ui dependency-version: 1.6.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: dev-minor-updates - dependency-name: react-hook-form dependency-version: 7.79.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: dev-minor-updates - dependency-name: sharp dependency-version: 0.35.1 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: dev-minor-updates - dependency-name: typescript-eslint dependency-version: 8.61.1 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: dev-minor-updates ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * chore(deps): bump node in /docker in the docker-major-updates group (#914) Bumps the docker-major-updates group in /docker with 1 update: node. Updates `node` from 24-slim to 26-slim --- updated-dependencies: - dependency-name: node dependency-version: 26-slim dependency-type: direct:production dependency-group: docker-major-updates ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * ci(deps): bump the github-actions group with 2 updates (#915) Bumps the github-actions group with 2 updates: [actions/checkout](https://github.com/actions/checkout) and [actions/setup-node](https://github.com/actions/setup-node). Updates `actions/checkout` from 5 to 6 - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v5...v6) Updates `actions/setup-node` from 4 to 6 - [Release notes](https://github.com/actions/setup-node/releases) - [Commits](https://github.com/actions/setup-node/compare/v4...v6) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major dependency-group: github-actions - dependency-name: actions/setup-node dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major dependency-group: github-actions ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * chore(deps): bump the prod-minor-updates group across 1 directory with 5 updates (#916) Bumps the prod-minor-updates group with 5 updates in the / directory: | Package | From | To | | --- | --- | --- | | [axios](https://github.com/axios/axios) | `1.17.0` | `1.18.0` | | [better-sqlite3](https://github.com/WiseLibs/better-sqlite3) | `12.10.0` | `12.11.1` | | [body-parser](https://github.com/expressjs/body-parser) | `2.2.2` | `2.3.0` | | [multer](https://github.com/expressjs/multer) | `2.1.1` | `2.2.0` | | [undici](https://github.com/nodejs/undici) | `8.4.0` | `8.5.0` | Updates `axios` from 1.17.0 to 1.18.0 - [Release notes](https://github.com/axios/axios/releases) - [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md) - [Commits](https://github.com/axios/axios/compare/v1.17.0...v1.18.0) Updates `better-sqlite3` from 12.10.0 to 12.11.1 - [Release notes](https://github.com/WiseLibs/better-sqlite3/releases) - [Commits](https://github.com/WiseLibs/better-sqlite3/compare/v12.10.0...v12.11.1) Updates `body-parser` from 2.2.2 to 2.3.0 - [Release notes](https://github.com/expressjs/body-parser/releases) - [Changelog](https://github.com/expressjs/body-parser/blob/master/HISTORY.md) - [Commits](https://github.com/expressjs/body-parser/compare/v2.2.2...v2.3.0) Updates `multer` from 2.1.1 to 2.2.0 - [Release notes](https://github.com/expressjs/multer/releases) - [Changelog](https://github.com/expressjs/multer/blob/main/CHANGELOG.md) - [Commits](https://github.com/expressjs/multer/compare/v2.1.1...v2.2.0) Updates `undici` from 8.4.0 to 8.5.0 - [Release notes](https://github.com/nodejs/undici/releases) - [Commits](https://github.com/nodejs/undici/compare/v8.4.0...v8.5.0) --- updated-dependencies: - dependency-name: axios dependency-version: 1.18.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: prod-minor-updates - dependency-name: better-sqlite3 dependency-version: 12.11.1 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: prod-minor-updates - dependency-name: body-parser dependency-version: 2.3.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: prod-minor-updates - dependency-name: multer dependency-version: 2.2.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: prod-minor-updates - dependency-name: undici dependency-version: 8.5.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: prod-minor-updates ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * feat: add unlink button for dual auth * fix: general bug fixes * fix: general qol/small feature additions * fix: 100mb max upload size * fix: terminal syntax not handling carriage returns or shell prompt lines properly * feat: continued general improvements * fix: terminal outputting success right after folder path * chore: update release notes * feat: continued fixes and improvements * chore: lint, format, and bump version to 2.4.1 * chore: sync Crowdin translations for 2.4.1 * fix: rebase dev branch onto main before Crowdin download * fix: use merge instead of rebase to sync main before Crowdin --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
This commit is contained in:
+66
-9
@@ -34,6 +34,7 @@ import {
|
||||
isElectron,
|
||||
getEmbeddedServerStatus,
|
||||
getCurrentToken,
|
||||
getOidcSilentLoginDefault,
|
||||
} from "@/main-axios";
|
||||
import { getSSOProviders, ldapLogin } from "@/api/sso-provider-api";
|
||||
import type { SSOProviderPublic } from "@/types/index";
|
||||
@@ -256,6 +257,9 @@ export function Auth({ onLogin }: AuthProps) {
|
||||
const [ldapUsername, setLdapUsername] = useState("");
|
||||
const [ldapPassword, setLdapPassword] = useState("");
|
||||
const silentSigninHandledRef = useRef(false);
|
||||
const [oidcSilentLoginDefault, setOidcSilentLoginDefault] = useState(false);
|
||||
const [oidcSilentLoginDefaultLoaded, setOidcSilentLoginDefaultLoaded] =
|
||||
useState(false);
|
||||
const [firstUser, setFirstUser] = useState(false);
|
||||
const [dbConnectionFailed, setDbConnectionFailed] = useState(false);
|
||||
const [dbHealthChecking, setDbHealthChecking] = useState(true);
|
||||
@@ -288,6 +292,10 @@ export function Auth({ onLogin }: AuthProps) {
|
||||
.then((providers) => setSsoProviders(providers || []))
|
||||
.catch(() => setSsoProviders([]))
|
||||
.finally(() => setSsoProvidersLoaded(true));
|
||||
getOidcSilentLoginDefault()
|
||||
.then((res) => setOidcSilentLoginDefault(res.enabled))
|
||||
.catch(() => {})
|
||||
.finally(() => setOidcSilentLoginDefaultLoaded(true));
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -312,6 +320,18 @@ export function Auth({ onLogin }: AuthProps) {
|
||||
return;
|
||||
}
|
||||
if (isElectron()) {
|
||||
const forceShow = localStorage.getItem("termix_show_server_config");
|
||||
if (forceShow === "true") {
|
||||
localStorage.removeItem("termix_show_server_config");
|
||||
try {
|
||||
const config = await getServerConfig();
|
||||
setCurrentServerUrl(config?.serverUrl || "");
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
setShowServerConfig(true);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const [config, status] = await Promise.all([
|
||||
getServerConfig(),
|
||||
@@ -732,6 +752,29 @@ export function Auth({ onLogin }: AuthProps) {
|
||||
const loadingKey = providerId ?? -1;
|
||||
setProviderLoading((prev) => ({ ...prev, [loadingKey]: true }));
|
||||
try {
|
||||
if (isInElectronWebView()) {
|
||||
// Inside the Electron iframe: delegate OIDC to the parent window so
|
||||
// the system browser opens instead of navigating the iframe (which
|
||||
// would break captcha stages like Cloudflare Turnstile).
|
||||
const callbackPort = 17832 + Math.floor(Math.random() * 100);
|
||||
const authResponse = await getOIDCAuthorizeUrl(
|
||||
rememberMe,
|
||||
callbackPort,
|
||||
providerId,
|
||||
);
|
||||
const { auth_url: authUrl } = authResponse;
|
||||
if (!authUrl) throw new Error(t("errors.invalidAuthUrl"));
|
||||
window.parent.postMessage(
|
||||
{
|
||||
type: "OIDC_SYSTEM_BROWSER_AUTH",
|
||||
source: "oidc_request",
|
||||
authUrl,
|
||||
callbackPort,
|
||||
},
|
||||
"*",
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (isElectron()) {
|
||||
const electronAPI = (
|
||||
window as unknown as {
|
||||
@@ -834,14 +877,19 @@ export function Auth({ onLogin }: AuthProps) {
|
||||
|
||||
useEffect(() => {
|
||||
if (!ssoProvidersLoaded || silentSigninHandledRef.current) return;
|
||||
if (!shouldTriggerSilentSignin(window.location.search)) return;
|
||||
if (!oidcSilentLoginDefaultLoaded) return;
|
||||
|
||||
const nextSearch = removeSilentSigninFromSearch(window.location.search);
|
||||
window.history.replaceState(
|
||||
{},
|
||||
document.title,
|
||||
`${window.location.pathname}${nextSearch}${window.location.hash}`,
|
||||
);
|
||||
const urlTriggered = shouldTriggerSilentSignin(window.location.search);
|
||||
if (!urlTriggered && !oidcSilentLoginDefault) return;
|
||||
|
||||
if (urlTriggered) {
|
||||
const nextSearch = removeSilentSigninFromSearch(window.location.search);
|
||||
window.history.replaceState(
|
||||
{},
|
||||
document.title,
|
||||
`${window.location.pathname}${nextSearch}${window.location.hash}`,
|
||||
);
|
||||
}
|
||||
|
||||
silentSigninHandledRef.current = true;
|
||||
|
||||
@@ -853,8 +901,17 @@ export function Auth({ onLogin }: AuthProps) {
|
||||
return;
|
||||
}
|
||||
|
||||
toast.info(t("errors.silentSigninOidcUnavailable"));
|
||||
}, [handleOIDCLogin, ssoProvidersLoaded, ssoProviders, t]);
|
||||
if (urlTriggered) {
|
||||
toast.info(t("errors.silentSigninOidcUnavailable"));
|
||||
}
|
||||
}, [
|
||||
handleOIDCLogin,
|
||||
ssoProvidersLoaded,
|
||||
ssoProviders,
|
||||
t,
|
||||
oidcSilentLoginDefault,
|
||||
oidcSilentLoginDefaultLoaded,
|
||||
]);
|
||||
|
||||
// Electron server config / webview auth success screens
|
||||
if (isElectron() && !isInElectronWebView()) {
|
||||
|
||||
@@ -63,13 +63,43 @@ export function ElectronLoginForm({
|
||||
try {
|
||||
if (event.source !== iframeRef.current?.contentWindow) return;
|
||||
if (!event.data || typeof event.data !== "object") return;
|
||||
const { type, platform, source, token } = event.data;
|
||||
const { type, platform, source, token, authUrl, callbackPort } =
|
||||
event.data;
|
||||
|
||||
if (
|
||||
type === "AUTH_SUCCESS" &&
|
||||
platform === "desktop" &&
|
||||
AUTH_MESSAGE_SOURCES.has(source)
|
||||
) {
|
||||
await handleAuthSuccess(token ?? null);
|
||||
return;
|
||||
}
|
||||
|
||||
// OIDC login requested from inside the iframe — open the system browser
|
||||
// so captcha stages (e.g. Cloudflare Turnstile) render correctly.
|
||||
if (type === "OIDC_SYSTEM_BROWSER_AUTH" && authUrl && callbackPort) {
|
||||
const electronAPI = (
|
||||
window as unknown as {
|
||||
electronAPI?: {
|
||||
oidcSystemBrowserAuth?: (
|
||||
url: string,
|
||||
port: number,
|
||||
) => Promise<{
|
||||
success: boolean;
|
||||
token?: string;
|
||||
error?: string;
|
||||
}>;
|
||||
};
|
||||
}
|
||||
).electronAPI;
|
||||
if (!electronAPI?.oidcSystemBrowserAuth) return;
|
||||
const result = await electronAPI.oidcSystemBrowserAuth(
|
||||
authUrl,
|
||||
callbackPort,
|
||||
);
|
||||
if (result.success && result.token) {
|
||||
await handleAuthSuccess(result.token);
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import React, { useState, useEffect, useRef } from "react";
|
||||
import { Button } from "@/components/button.tsx";
|
||||
import { Input } from "@/components/input.tsx";
|
||||
import { Label } from "@/components/label.tsx";
|
||||
@@ -12,7 +12,34 @@ import {
|
||||
setEmbeddedMode,
|
||||
type ServerConfig,
|
||||
} from "@/main-axios.ts";
|
||||
import { Server, Monitor, Loader2 } from "lucide-react";
|
||||
import { Server, Monitor, Loader2, ChevronDown, X } from "lucide-react";
|
||||
|
||||
const SAVED_URLS_KEY = "termix_saved_server_urls";
|
||||
const MAX_SAVED_URLS = 5;
|
||||
|
||||
function getSavedUrls(): string[] {
|
||||
try {
|
||||
const raw = localStorage.getItem(SAVED_URLS_KEY);
|
||||
if (!raw) return [];
|
||||
return JSON.parse(raw);
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function addSavedUrl(url: string) {
|
||||
const urls = getSavedUrls().filter((u) => u !== url);
|
||||
urls.unshift(url);
|
||||
localStorage.setItem(
|
||||
SAVED_URLS_KEY,
|
||||
JSON.stringify(urls.slice(0, MAX_SAVED_URLS)),
|
||||
);
|
||||
}
|
||||
|
||||
function removeSavedUrl(url: string) {
|
||||
const urls = getSavedUrls().filter((u) => u !== url);
|
||||
localStorage.setItem(SAVED_URLS_KEY, JSON.stringify(urls));
|
||||
}
|
||||
|
||||
interface ServerConfigProps {
|
||||
onServerConfigured: (serverUrl: string) => void;
|
||||
@@ -36,12 +63,31 @@ export function ElectronServerConfig({
|
||||
const [embeddedAvailable, setEmbeddedAvailable] = useState<boolean | null>(
|
||||
null,
|
||||
);
|
||||
const [savedUrls, setSavedUrls] = useState<string[]>([]);
|
||||
const [dropdownOpen, setDropdownOpen] = useState(false);
|
||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
loadServerConfig();
|
||||
checkEmbeddedBackend();
|
||||
setSavedUrls(getSavedUrls());
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
function handleClickOutside(e: MouseEvent) {
|
||||
if (
|
||||
dropdownRef.current &&
|
||||
!dropdownRef.current.contains(e.target as Node)
|
||||
) {
|
||||
setDropdownOpen(false);
|
||||
}
|
||||
}
|
||||
if (dropdownOpen) {
|
||||
document.addEventListener("mousedown", handleClickOutside);
|
||||
}
|
||||
return () => document.removeEventListener("mousedown", handleClickOutside);
|
||||
}, [dropdownOpen]);
|
||||
|
||||
const loadServerConfig = async () => {
|
||||
try {
|
||||
const config = await getServerConfig();
|
||||
@@ -151,6 +197,8 @@ export function ElectronServerConfig({
|
||||
const success = await saveServerConfig(config);
|
||||
|
||||
if (success) {
|
||||
addSavedUrl(normalizedUrl);
|
||||
setSavedUrls(getSavedUrls());
|
||||
onServerConfigured(normalizedUrl);
|
||||
} else {
|
||||
setError(t("serverConfig.saveFailed"));
|
||||
@@ -220,14 +268,70 @@ export function ElectronServerConfig({
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<Label htmlFor="server-url">{t("serverConfig.serverUrl")}</Label>
|
||||
<Input
|
||||
id="server-url"
|
||||
type="text"
|
||||
placeholder="https://your-server.com"
|
||||
value={serverUrl}
|
||||
onChange={(e) => handleUrlChange(e.target.value)}
|
||||
disabled={loading || embeddedLoading}
|
||||
/>
|
||||
<div className="relative" ref={dropdownRef}>
|
||||
<Input
|
||||
id="server-url"
|
||||
type="text"
|
||||
placeholder="https://your-server.com"
|
||||
value={serverUrl}
|
||||
onChange={(e) => handleUrlChange(e.target.value)}
|
||||
disabled={loading || embeddedLoading}
|
||||
className={savedUrls.length > 0 ? "pr-9" : ""}
|
||||
onFocus={() => {
|
||||
if (savedUrls.length > 0) setDropdownOpen(true);
|
||||
}}
|
||||
/>
|
||||
{savedUrls.length > 0 && (
|
||||
<button
|
||||
type="button"
|
||||
tabIndex={-1}
|
||||
onClick={() => setDropdownOpen((o) => !o)}
|
||||
disabled={loading || embeddedLoading}
|
||||
className="absolute right-2.5 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground transition-colors"
|
||||
aria-label={t("serverConfig.savedServers")}
|
||||
>
|
||||
<ChevronDown className="size-4" />
|
||||
</button>
|
||||
)}
|
||||
{dropdownOpen && savedUrls.length > 0 && (
|
||||
<div className="absolute z-50 w-full top-full mt-1 border border-border bg-card shadow-md">
|
||||
<p className="px-2.5 py-1.5 text-[10px] font-bold uppercase tracking-widest text-muted-foreground border-b border-border">
|
||||
{t("serverConfig.savedServers")}
|
||||
</p>
|
||||
{savedUrls.map((url) => (
|
||||
<div
|
||||
key={url}
|
||||
className="flex items-center justify-between group hover:bg-muted transition-colors"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className="flex-1 text-left px-2.5 py-2 text-sm font-mono truncate"
|
||||
onClick={() => {
|
||||
handleUrlChange(url);
|
||||
setDropdownOpen(false);
|
||||
}}
|
||||
>
|
||||
{url}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="px-2 py-2 text-muted-foreground hover:text-destructive transition-colors shrink-0"
|
||||
title={t("serverConfig.removeServer")}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
removeSavedUrl(url);
|
||||
const updated = getSavedUrls();
|
||||
setSavedUrls(updated);
|
||||
if (updated.length === 0) setDropdownOpen(false);
|
||||
}}
|
||||
>
|
||||
<X className="size-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{serverUrl.trim().startsWith("https://") && (
|
||||
|
||||
+174
-107
@@ -29,6 +29,7 @@ import {
|
||||
isElectron,
|
||||
getEmbeddedServerStatus,
|
||||
getCurrentToken,
|
||||
getOidcSilentLoginDefault,
|
||||
} from "@/main-axios";
|
||||
import { getSSOProviders, ldapLogin } from "@/api/sso-provider-api";
|
||||
import type { SSOProviderPublic } from "@/types/index";
|
||||
@@ -129,6 +130,9 @@ export function Auth({
|
||||
const [ldapPassword, setLdapPassword] = useState("");
|
||||
const [ldapLoading, setLdapLoading] = useState(false);
|
||||
const silentSigninHandledRef = useRef(false);
|
||||
const [oidcSilentLoginDefault, setOidcSilentLoginDefault] = useState(false);
|
||||
const [oidcSilentLoginDefaultLoaded, setOidcSilentLoginDefaultLoaded] =
|
||||
useState(false);
|
||||
|
||||
const [resetStep, setResetStep] = useState<
|
||||
"initiate" | "verify" | "newPassword"
|
||||
@@ -258,6 +262,17 @@ export function Auth({
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
getOidcSilentLoginDefault()
|
||||
.then((res) => {
|
||||
setOidcSilentLoginDefault(res.enabled);
|
||||
})
|
||||
.catch(() => {})
|
||||
.finally(() => {
|
||||
setOidcSilentLoginDefaultLoaded(true);
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (showServerConfig) {
|
||||
return;
|
||||
@@ -683,8 +698,19 @@ export function Auth({
|
||||
setLdapLoading(true);
|
||||
try {
|
||||
await ldapLogin(providerId, ldapUsername, ldapPassword, rememberMe);
|
||||
const userInfo = await getUserInfo();
|
||||
onLogin(userInfo);
|
||||
const meRes = await getUserInfo();
|
||||
setInternalLoggedIn(true);
|
||||
setLoggedIn(true);
|
||||
setIsAdmin(!!meRes.is_admin);
|
||||
setUsername(meRes.username || null);
|
||||
setUserId(meRes.userId || null);
|
||||
setDbError(null);
|
||||
onAuthSuccess({
|
||||
isAdmin: !!meRes.is_admin,
|
||||
username: meRes.username || null,
|
||||
userId: meRes.userId || null,
|
||||
});
|
||||
toast.success(t("messages.loginSuccess"));
|
||||
} catch (err: unknown) {
|
||||
const error = err as {
|
||||
response?: { data?: { error?: string } };
|
||||
@@ -699,19 +725,35 @@ export function Auth({
|
||||
setLdapLoading(false);
|
||||
}
|
||||
},
|
||||
[ldapUsername, ldapPassword, rememberMe, onLogin, t],
|
||||
[
|
||||
ldapUsername,
|
||||
ldapPassword,
|
||||
rememberMe,
|
||||
onAuthSuccess,
|
||||
setLoggedIn,
|
||||
setIsAdmin,
|
||||
setUsername,
|
||||
setUserId,
|
||||
setDbError,
|
||||
t,
|
||||
],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!ssoProvidersLoaded || silentSigninHandledRef.current) return;
|
||||
if (!shouldTriggerSilentSignin(window.location.search)) return;
|
||||
if (!oidcSilentLoginDefaultLoaded) return;
|
||||
|
||||
const nextSearch = removeSilentSigninFromSearch(window.location.search);
|
||||
window.history.replaceState(
|
||||
{},
|
||||
document.title,
|
||||
`${window.location.pathname}${nextSearch}${window.location.hash}`,
|
||||
);
|
||||
const urlTriggered = shouldTriggerSilentSignin(window.location.search);
|
||||
if (!urlTriggered && !oidcSilentLoginDefault) return;
|
||||
|
||||
if (urlTriggered) {
|
||||
const nextSearch = removeSilentSigninFromSearch(window.location.search);
|
||||
window.history.replaceState(
|
||||
{},
|
||||
document.title,
|
||||
`${window.location.pathname}${nextSearch}${window.location.hash}`,
|
||||
);
|
||||
}
|
||||
|
||||
silentSigninHandledRef.current = true;
|
||||
const oidcProvider = ssoProviders.find(
|
||||
@@ -728,8 +770,17 @@ export function Auth({
|
||||
return;
|
||||
}
|
||||
|
||||
toast.info(t("errors.silentSigninOidcUnavailable"));
|
||||
}, [handleOIDCLogin, ssoProvidersLoaded, ssoProviders, t]);
|
||||
if (urlTriggered) {
|
||||
toast.info(t("errors.silentSigninOidcUnavailable"));
|
||||
}
|
||||
}, [
|
||||
handleOIDCLogin,
|
||||
ssoProvidersLoaded,
|
||||
ssoProviders,
|
||||
t,
|
||||
oidcSilentLoginDefault,
|
||||
oidcSilentLoginDefaultLoaded,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
@@ -1515,106 +1566,122 @@ export function Auth({
|
||||
className="flex flex-col gap-5"
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor="username">
|
||||
{t("common.username")}
|
||||
</Label>
|
||||
<Input
|
||||
id="username"
|
||||
type="text"
|
||||
required
|
||||
className="h-11 text-base"
|
||||
value={localUsername}
|
||||
onChange={(e) =>
|
||||
setLocalUsername(e.target.value)
|
||||
}
|
||||
disabled={loading || loggedIn}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor="password">
|
||||
{t("common.password")}
|
||||
</Label>
|
||||
<PasswordInput
|
||||
id="password"
|
||||
required
|
||||
className="h-11 text-base"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
disabled={loading || loggedIn}
|
||||
/>
|
||||
</div>
|
||||
{tab === "login" && (
|
||||
<div className="flex items-center gap-2">
|
||||
<Checkbox
|
||||
id="rememberMe"
|
||||
checked={rememberMe}
|
||||
onCheckedChange={(checked) =>
|
||||
setRememberMe(checked === true)
|
||||
}
|
||||
disabled={loading || loggedIn}
|
||||
/>
|
||||
<Label
|
||||
htmlFor="rememberMe"
|
||||
className="text-sm font-normal cursor-pointer"
|
||||
{!passwordLoginAllowed &&
|
||||
!firstUser &&
|
||||
tab === "login" ? (
|
||||
<p className="text-center text-muted-foreground text-sm">
|
||||
{t("auth.passwordLoginDisabledDesc")}
|
||||
</p>
|
||||
) : (
|
||||
<>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor="username">
|
||||
{t("common.username")}
|
||||
</Label>
|
||||
<Input
|
||||
id="username"
|
||||
type="text"
|
||||
required
|
||||
className="h-11 text-base"
|
||||
value={localUsername}
|
||||
onChange={(e) =>
|
||||
setLocalUsername(e.target.value)
|
||||
}
|
||||
disabled={loading || loggedIn}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor="password">
|
||||
{t("common.password")}
|
||||
</Label>
|
||||
<PasswordInput
|
||||
id="password"
|
||||
required
|
||||
className="h-11 text-base"
|
||||
value={password}
|
||||
onChange={(e) =>
|
||||
setPassword(e.target.value)
|
||||
}
|
||||
disabled={loading || loggedIn}
|
||||
/>
|
||||
</div>
|
||||
{tab === "login" && (
|
||||
<div className="flex items-center gap-2">
|
||||
<Checkbox
|
||||
id="rememberMe"
|
||||
checked={rememberMe}
|
||||
onCheckedChange={(checked) =>
|
||||
setRememberMe(checked === true)
|
||||
}
|
||||
disabled={loading || loggedIn}
|
||||
/>
|
||||
<Label
|
||||
htmlFor="rememberMe"
|
||||
className="text-sm font-normal cursor-pointer"
|
||||
>
|
||||
{t("auth.rememberMe")}
|
||||
</Label>
|
||||
</div>
|
||||
)}
|
||||
{tab === "signup" && (
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor="signup-confirm-password">
|
||||
{t("common.confirmPassword")}
|
||||
</Label>
|
||||
<PasswordInput
|
||||
id="signup-confirm-password"
|
||||
required
|
||||
className="h-11 text-base"
|
||||
value={signupConfirmPassword}
|
||||
onChange={(e) =>
|
||||
setSignupConfirmPassword(e.target.value)
|
||||
}
|
||||
disabled={loading || loggedIn}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<Button
|
||||
type="submit"
|
||||
className="w-full h-11 mt-2 text-base font-semibold"
|
||||
disabled={loading || internalLoggedIn}
|
||||
>
|
||||
{t("auth.rememberMe")}
|
||||
</Label>
|
||||
</div>
|
||||
)}
|
||||
{tab === "signup" && (
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor="signup-confirm-password">
|
||||
{t("common.confirmPassword")}
|
||||
</Label>
|
||||
<PasswordInput
|
||||
id="signup-confirm-password"
|
||||
required
|
||||
className="h-11 text-base"
|
||||
value={signupConfirmPassword}
|
||||
onChange={(e) =>
|
||||
setSignupConfirmPassword(e.target.value)
|
||||
}
|
||||
disabled={loading || loggedIn}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<Button
|
||||
type="submit"
|
||||
className="w-full h-11 mt-2 text-base font-semibold"
|
||||
disabled={loading || internalLoggedIn}
|
||||
>
|
||||
{loading
|
||||
? Spinner
|
||||
: tab === "login"
|
||||
? t("common.login")
|
||||
: t("auth.signUp")}
|
||||
</Button>
|
||||
{tab === "login" && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
className="w-full h-11 text-base font-semibold"
|
||||
disabled={loading || loggedIn}
|
||||
onClick={() => {
|
||||
setTab("reset");
|
||||
resetPasswordState();
|
||||
clearFormFields();
|
||||
}}
|
||||
>
|
||||
{t("auth.resetPasswordButton")}
|
||||
</Button>
|
||||
{loading
|
||||
? Spinner
|
||||
: tab === "login"
|
||||
? t("common.login")
|
||||
: t("auth.signUp")}
|
||||
</Button>
|
||||
{tab === "login" && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
className="w-full h-11 text-base font-semibold"
|
||||
disabled={loading || loggedIn}
|
||||
onClick={() => {
|
||||
setTab("reset");
|
||||
resetPasswordState();
|
||||
clearFormFields();
|
||||
}}
|
||||
>
|
||||
{t("auth.resetPasswordButton")}
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{ssoProviders.length > 0 && !isElectron() && (
|
||||
<div className="flex flex-col gap-3 pt-2">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex-1 border-t border-border" />
|
||||
<span className="text-xs text-muted-foreground px-1 whitespace-nowrap">
|
||||
{t("auth.orContinueWith")}
|
||||
</span>
|
||||
<div className="flex-1 border-t border-border" />
|
||||
</div>
|
||||
{(passwordLoginAllowed ||
|
||||
firstUser ||
|
||||
tab === "signup") && (
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex-1 border-t border-border" />
|
||||
<span className="text-xs text-muted-foreground px-1 whitespace-nowrap">
|
||||
{t("auth.orContinueWith")}
|
||||
</span>
|
||||
<div className="flex-1 border-t border-border" />
|
||||
</div>
|
||||
)}
|
||||
{ssoProviders.map((provider) => {
|
||||
if (provider.type === "ldap") {
|
||||
const isExpanded =
|
||||
|
||||
Reference in New Issue
Block a user