/* eslint-disable react-refresh/only-export-components */
import { useState, useEffect, useCallback, useRef } from "react";
import { Button } from "@/components/button";
import { Input } from "@/components/input";
import { Separator } from "@/components/separator";
import { toast } from "sonner";
import {
Eye,
EyeOff,
User,
KeyRound,
ArrowLeft,
Shield,
CheckCircle2,
ChevronDown,
ChevronUp,
} from "lucide-react";
import { useTranslation } from "react-i18next";
import {
loginUser,
registerUser,
getUserInfo,
getRegistrationAllowed,
getPasswordLoginAllowed,
getPasswordResetAllowed,
getSetupRequired,
initiatePasswordReset,
verifyPasswordResetCode,
completePasswordReset,
getOIDCAuthorizeUrl,
verifyTOTPLogin,
getServerConfig,
saveServerConfig,
isElectron,
getEmbeddedServerStatus,
getCurrentToken,
} from "@/main-axios";
import { getSSOProviders, ldapLogin } from "@/api/sso-provider-api";
import type { SSOProviderPublic } from "@/types/index";
import { ElectronServerConfig as ServerConfigComponent } from "@/auth/ElectronServerConfig";
import { ElectronLoginForm } from "@/auth/ElectronLoginForm";
import { Checkbox } from "@/components/checkbox";
import i18n from "@/i18n/i18n";
import {
removeSilentSigninFromSearch,
shouldTriggerSilentSignin,
} from "./silent-signin";
const LANGUAGES = [
{ code: "en", label: "English" },
{ code: "af", label: "Afrikaans" },
{ code: "ar", label: "العربية" },
{ code: "bn", label: "বাংলা" },
{ code: "bg", label: "Български" },
{ code: "ca", label: "Català" },
{ code: "zh-CN", label: "中文 (简体)" },
{ code: "zh-TW", label: "中文 (繁體)" },
{ code: "cs", label: "Čeština" },
{ code: "da", label: "Dansk" },
{ code: "nl", label: "Nederlands" },
{ code: "fi", label: "Suomi" },
{ code: "fr", label: "Français" },
{ code: "de", label: "Deutsch" },
{ code: "el", label: "Ελληνικά" },
{ code: "he", label: "עברית" },
{ code: "hi", label: "हिन्दी" },
{ code: "hu", label: "Magyar" },
{ code: "id", label: "Indonesia" },
{ code: "it", label: "Italiano" },
{ code: "ja", label: "日本語" },
{ code: "ko", label: "한국어" },
{ code: "no", label: "Norsk" },
{ code: "pl", label: "Polski" },
{ code: "pt-PT", label: "Português (PT)" },
{ code: "pt-BR", label: "Português (BR)" },
{ code: "ro", label: "Română" },
{ code: "ru", label: "Русский" },
{ code: "sr", label: "Српски" },
{ code: "es-ES", label: "Español" },
{ code: "sv-SE", label: "Svenska" },
{ code: "th", label: "ไทย" },
{ code: "tr", label: "Türkçe" },
{ code: "uk", label: "Українська" },
{ code: "vi", label: "Tiếng Việt" },
];
const STORAGE_KEY = "termix_auth";
export function getStoredAuth(): {
loggedIn: boolean;
username: string;
} | null {
try {
const raw = localStorage.getItem(STORAGE_KEY);
if (!raw) return null;
return JSON.parse(raw);
} catch {
return null;
}
}
export function clearStoredAuth() {
localStorage.removeItem(STORAGE_KEY);
}
function storeAuth(username: string) {
localStorage.setItem(
STORAGE_KEY,
JSON.stringify({ loggedIn: true, username }),
);
}
type AuthView = "login" | "register" | "reset" | "totp" | "external";
type ResetStep = "email" | "code" | "newpass";
interface AuthProps {
onLogin: (username: string, userId?: string, isAdmin?: boolean) => void;
}
interface ExtendedWindow extends Window {
IS_ELECTRON_WEBVIEW?: boolean;
ReactNativeWebView?: { postMessage: (msg: string) => void };
}
const isInMobileWebView = () =>
/Termix-Mobile\/(Android|iOS)/.test(navigator.userAgent) ||
!!(window as ExtendedWindow).ReactNativeWebView;
const isInElectronWebView = () => {
if (isInMobileWebView()) return false;
if ((window as ExtendedWindow).IS_ELECTRON_WEBVIEW) return true;
try {
if (window.self !== window.top) return true;
} catch {
return true;
}
return false;
};
function PasswordInput({
value,
onChange,
placeholder,
disabled,
id,
}: {
value: string;
onChange: (v: string) => void;
placeholder?: string;
disabled?: boolean;
id?: string;
}) {
const [show, setShow] = useState(false);
return (
onChange(e.target.value)}
placeholder={placeholder ?? "••••••••"}
disabled={disabled}
className="pr-9 font-mono"
/>
);
}
function FieldLabel({
htmlFor,
children,
}: {
htmlFor?: string;
children: React.ReactNode;
}) {
return (
);
}
function Field({
label,
htmlFor,
children,
}: {
label: string;
htmlFor?: string;
children: React.ReactNode;
}) {
return (
{label}
{children}
);
}
export function Auth({ onLogin }: AuthProps) {
const { t } = useTranslation();
const [view, setView] = useState("login");
const [loading, setLoading] = useState(false);
const [providerLoading, setProviderLoading] = useState<
Record
>({});
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const [confirmPassword, setConfirmPassword] = useState("");
const [rememberMe, setRememberMe] = useState(() => {
try {
return localStorage.getItem("rememberMe") === "true";
} catch {
return false;
}
});
const [totpCode, setTotpCode] = useState("");
const [totpTempToken, setTotpTempToken] = useState("");
const totpInputRef = useRef(null);
const [resetStep, setResetStep] = useState("email");
const [resetCode, setResetCode] = useState("");
const [newPassword, setNewPassword] = useState("");
const [confirmNewPassword, setConfirmNewPassword] = useState("");
const [resetTempToken, setResetTempToken] = useState("");
const [language, setLanguage] = useState(
() => localStorage.getItem("i18nextLng") ?? "en",
);
function handleLanguageChange(code: string) {
setLanguage(code);
localStorage.setItem("i18nextLng", code);
i18n.changeLanguage(code);
}
const [registrationAllowed, setRegistrationAllowed] = useState(true);
const [passwordLoginAllowed, setPasswordLoginAllowed] = useState(true);
const [passwordResetAllowed, setPasswordResetAllowed] = useState(true);
const [ssoProviders, setSsoProviders] = useState([]);
const [ssoProvidersLoaded, setSsoProvidersLoaded] = useState(false);
const [expandedLdapId, setExpandedLdapId] = useState(null);
const [ldapUsername, setLdapUsername] = useState("");
const [ldapPassword, setLdapPassword] = useState("");
const silentSigninHandledRef = useRef(false);
const [firstUser, setFirstUser] = useState(false);
const [dbConnectionFailed, setDbConnectionFailed] = useState(false);
const [dbHealthChecking, setDbHealthChecking] = useState(true);
const [showServerConfig, setShowServerConfig] = useState(
null,
);
const [currentServerUrl, setCurrentServerUrl] = useState("");
const [webviewAuthSuccess, setWebviewAuthSuccess] = useState(false);
useEffect(() => {
try {
localStorage.setItem("rememberMe", rememberMe.toString());
} catch {
// Ignore storage failures; auth state still works for the current session.
}
}, [rememberMe]);
useEffect(() => {
getRegistrationAllowed()
.then((res) => setRegistrationAllowed(res.allowed))
.catch(() => {});
getPasswordLoginAllowed()
.then((res) => setPasswordLoginAllowed(res.allowed))
.catch(() => {});
getPasswordResetAllowed()
.then((allowed) => setPasswordResetAllowed(allowed))
.catch(() => setPasswordResetAllowed(false));
getSSOProviders()
.then((providers) => setSsoProviders(providers || []))
.catch(() => setSsoProviders([]))
.finally(() => setSsoProvidersLoaded(true));
}, []);
useEffect(() => {
if (showServerConfig) return;
setDbHealthChecking(true);
getSetupRequired()
.then((res) => {
if (res.setup_required) {
setFirstUser(true);
setView("register");
}
setDbConnectionFailed(false);
})
.catch(() => setDbConnectionFailed(true))
.finally(() => setDbHealthChecking(false));
}, [showServerConfig]);
useEffect(() => {
const checkElectron = async () => {
if (isInElectronWebView()) {
setShowServerConfig(false);
return;
}
if (isElectron()) {
try {
const [config, status] = await Promise.all([
getServerConfig(),
getEmbeddedServerStatus(),
]);
if (
status?.embedded &&
status?.running &&
config &&
!config.serverUrl
) {
setShowServerConfig(false);
setCurrentServerUrl("");
return;
}
setCurrentServerUrl(config?.serverUrl || "");
setShowServerConfig(!config || !config.serverUrl);
} catch {
setShowServerConfig(true);
}
} else {
setShowServerConfig(false);
}
};
checkElectron();
}, []);
useEffect(() => {
if (view === "totp" && totpInputRef.current) totpInputRef.current.focus();
}, [view]);
useEffect(() => {
setLdapUsername("");
setLdapPassword("");
}, [expandedLdapId]);
useEffect(() => {
if (!ssoProvidersLoaded) return;
if (!passwordLoginAllowed && ssoProviders.length > 0 && view === "login") {
setView("external");
}
}, [ssoProvidersLoaded, passwordLoginAllowed, ssoProviders.length, view]);
useEffect(() => {
const urlParams = new URLSearchParams(window.location.search);
const success = urlParams.get("success");
const error = urlParams.get("error");
if (error) {
if (error === "registration_disabled")
toast.error(t("messages.registrationDisabled"));
else if (error === "user_not_allowed")
toast.error(t("messages.userNotAllowed"));
else toast.error(`${t("errors.oidcAuthFailed")}: ${error}`);
window.history.replaceState({}, document.title, window.location.pathname);
return;
}
if (success) {
if (isInMobileWebView()) {
// The OIDC callback authenticated via an HttpOnly cookie on this origin,
// so the token isn't in localStorage. Prefer a token passed in the URL
// (termix-mobile:-origin callbacks include one), otherwise read it back
// from the cookie via /users/me/token before handing it to the app.
const postToken = (token: string) => {
(window as ExtendedWindow).ReactNativeWebView?.postMessage(
JSON.stringify({ type: "AUTH_SUCCESS", token }),
);
setWebviewAuthSuccess(true);
window.history.replaceState(
{},
document.title,
window.location.pathname,
);
};
const urlToken = urlParams.get("token");
if (urlToken) {
postToken(urlToken);
} else {
getCurrentToken()
.then((token) => postToken(token ?? ""))
.catch(() => postToken(""));
}
return;
}
if (isInElectronWebView()) {
window.parent.postMessage(
{
type: "AUTH_SUCCESS",
source: "oidc_callback",
platform: "desktop",
timestamp: Date.now(),
},
"*",
);
setWebviewAuthSuccess(true);
window.history.replaceState(
{},
document.title,
window.location.pathname,
);
return;
}
getUserInfo()
.then((meRes) => {
storeAuth(meRes.username || "");
onLogin(
meRes.username || "",
meRes.userId || undefined,
!!meRes.is_admin,
);
window.history.replaceState(
{},
document.title,
window.location.pathname,
);
})
.catch(() => {
toast.error(t("errors.failedUserInfo"));
});
}
}, [onLogin, t]);
const handleElectronAuthSuccess = useCallback(
async (token: string | null) => {
try {
if (!token) {
// No token in postMessage — fall back to waiting for the HttpOnly cookie
const cookieReady = await window.electronAPI?.waitForSessionCookie?.(
"jwt",
currentServerUrl,
null,
5000,
);
if (cookieReady && !cookieReady.success)
throw new Error(cookieReady.error || "Auth cookie not ready");
}
const meRes = await getUserInfo();
if (!meRes) throw new Error("Failed to get user info");
storeAuth(meRes.username || "");
onLogin(
meRes.username || "",
meRes.userId || undefined,
!!meRes.is_admin,
);
toast.success(t("messages.loginSuccess"));
} catch {
toast.error(t("errors.failedUserInfo"));
}
},
[onLogin, currentServerUrl, t],
);
function resetAll() {
setUsername("");
setPassword("");
setConfirmPassword("");
setResetStep("email");
setResetCode("");
setNewPassword("");
setConfirmNewPassword("");
setResetTempToken("");
setTotpCode("");
setTotpTempToken("");
}
function switchView(v: AuthView) {
const currentUsername = username;
resetAll();
if (v === "reset") setUsername(currentUsername);
setView(v);
}
async function handleLogin(e: React.FormEvent) {
e.preventDefault();
if (!username.trim()) {
toast.error(t("errors.requiredField"));
return;
}
setLoading(true);
try {
const res = await loginUser(username.trim(), password, rememberMe);
if (res.requires_totp) {
setTotpTempToken(res.temp_token);
setView("totp");
return;
}
if (!res?.success) throw new Error(t("errors.loginFailed"));
if (isInMobileWebView()) {
// Native-app requests get the JWT in the login response body.
const token = res?.token ?? "";
(window as ExtendedWindow).ReactNativeWebView?.postMessage(
JSON.stringify({ type: "AUTH_SUCCESS", token }),
);
setWebviewAuthSuccess(true);
return;
}
if (isInElectronWebView()) {
window.parent.postMessage(
{
type: "AUTH_SUCCESS",
source: "auth_component",
platform: "desktop",
timestamp: Date.now(),
},
"*",
);
setWebviewAuthSuccess(true);
return;
}
const meRes = await getUserInfo();
storeAuth(meRes.username || username.trim());
toast.success(t("messages.loginSuccess"));
onLogin(
meRes.username || username.trim(),
meRes.userId || undefined,
!!meRes.is_admin,
);
} catch (err: unknown) {
const error = err as {
message?: string;
response?: { data?: { error?: string } };
};
toast.error(
error?.response?.data?.error ||
error?.message ||
t("errors.unknownError"),
);
} finally {
setLoading(false);
}
}
async function handleRegister(e: React.FormEvent) {
e.preventDefault();
if (!username.trim()) {
toast.error(t("errors.requiredField"));
return;
}
if (password.length < 6) {
toast.error(t("errors.minLength", { min: 6 }));
return;
}
if (password !== confirmPassword) {
toast.error(t("errors.passwordMismatch"));
return;
}
setLoading(true);
try {
await registerUser(username.trim(), password);
const res = await loginUser(username.trim(), password, rememberMe);
if (res.requires_totp) {
setTotpTempToken(res.temp_token);
setView("totp");
return;
}
const meRes = await getUserInfo();
storeAuth(meRes.username || username.trim());
toast.success(t("messages.registrationSuccess"));
onLogin(
meRes.username || username.trim(),
meRes.userId || undefined,
!!meRes.is_admin,
);
} catch (err: unknown) {
const error = err as {
message?: string;
response?: { data?: { error?: string } };
};
toast.error(
error?.response?.data?.error ||
error?.message ||
t("errors.unknownError"),
);
} finally {
setLoading(false);
}
}
async function handleTOTP(e: React.FormEvent) {
e.preventDefault();
if (totpCode.length !== 6) {
toast.error(t("auth.enterCode"));
return;
}
setLoading(true);
try {
const res = await verifyTOTPLogin(totpTempToken, totpCode, rememberMe);
if (!res?.success) throw new Error(t("errors.loginFailed"));
if (isInMobileWebView()) {
// Native-app requests get the JWT in the login response body.
const token = res?.token ?? "";
(window as ExtendedWindow).ReactNativeWebView?.postMessage(
JSON.stringify({ type: "AUTH_SUCCESS", token }),
);
setWebviewAuthSuccess(true);
return;
}
if (isInElectronWebView()) {
window.parent.postMessage(
{
type: "AUTH_SUCCESS",
source: "totp_auth_component",
platform: "desktop",
timestamp: Date.now(),
},
"*",
);
setWebviewAuthSuccess(true);
return;
}
storeAuth(res.username || username);
toast.success(t("messages.loginSuccess"));
onLogin(
res.username || username,
res.userId || undefined,
!!res.is_admin,
);
} catch (err: unknown) {
const error = err as {
message?: string;
response?: { data?: { error?: string; code?: string } };
};
if (error?.response?.data?.code === "SESSION_EXPIRED") {
setView("login");
toast.error(t("errors.sessionExpired"));
return;
}
toast.error(
error?.response?.data?.error ||
error?.message ||
t("errors.invalidTotpCode"),
);
} finally {
setLoading(false);
}
}
async function handleResetInitiate(e: React.FormEvent) {
e.preventDefault();
if (!username.trim()) {
toast.error(t("errors.requiredField"));
return;
}
setLoading(true);
try {
await initiatePasswordReset(username.trim());
setResetStep("code");
toast.success(t("messages.resetCodeSent"));
} catch (err: unknown) {
const error = err as {
message?: string;
response?: { data?: { error?: string } };
};
toast.error(
error?.response?.data?.error ||
error?.message ||
t("errors.failedPasswordReset"),
);
} finally {
setLoading(false);
}
}
async function handleResetVerify(e: React.FormEvent) {
e.preventDefault();
setLoading(true);
try {
const res = await verifyPasswordResetCode(username.trim(), resetCode);
setResetTempToken(res.tempToken as string);
setResetStep("newpass");
toast.success(t("messages.codeVerified"));
} catch (err: unknown) {
const error = err as {
message?: string;
response?: { data?: { error?: string } };
};
toast.error(
error?.response?.data?.error ||
error?.message ||
t("errors.failedVerifyCode"),
);
} finally {
setLoading(false);
}
}
async function handleResetComplete(e: React.FormEvent) {
e.preventDefault();
if (newPassword.length < 6) {
toast.error(t("errors.minLength", { min: 6 }));
return;
}
if (newPassword !== confirmNewPassword) {
toast.error(t("errors.passwordMismatch"));
return;
}
setLoading(true);
try {
await completePasswordReset(username.trim(), resetTempToken, newPassword);
toast.success(t("messages.passwordResetSuccess"));
switchView("login");
} catch (err: unknown) {
const error = err as {
message?: string;
response?: { data?: { error?: string } };
};
toast.error(
error?.response?.data?.error ||
error?.message ||
t("errors.failedCompleteReset"),
);
} finally {
setLoading(false);
}
}
const handleOIDCLogin = useCallback(
async (providerId?: number) => {
const loadingKey = providerId ?? -1;
setProviderLoading((prev) => ({ ...prev, [loadingKey]: true }));
try {
if (isElectron()) {
const electronAPI = (
window as unknown as {
electronAPI?: {
oidcSystemBrowserAuth?: (
authUrl: string,
port: number,
) => Promise<{
success: boolean;
token?: string;
error?: string;
}>;
};
}
).electronAPI;
if (electronAPI?.oidcSystemBrowserAuth) {
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"));
const result = await electronAPI.oidcSystemBrowserAuth(
authUrl,
callbackPort,
);
if (result.success && result.token) {
localStorage.setItem("jwt", result.token);
window.location.reload();
return;
}
throw new Error(result.error || "Authentication failed");
}
}
const authResponse = await getOIDCAuthorizeUrl(
rememberMe,
undefined,
providerId,
);
const { auth_url: authUrl } = authResponse;
if (!authUrl || authUrl === "undefined")
throw new Error(t("errors.invalidAuthUrl"));
window.location.replace(authUrl);
} catch (err: unknown) {
const error = err as {
message?: string;
response?: { data?: { error?: string } };
};
toast.error(
error?.response?.data?.error ||
error?.message ||
t("errors.failedOidcLogin"),
);
setProviderLoading((prev) => ({ ...prev, [loadingKey]: false }));
}
},
[rememberMe, t],
);
const handleLDAPLogin = useCallback(
async (providerId: number) => {
if (!ldapUsername.trim() || !ldapPassword) {
toast.error(t("errors.requiredField"));
return;
}
setProviderLoading((prev) => ({ ...prev, [providerId]: true }));
try {
await ldapLogin(
providerId,
ldapUsername.trim(),
ldapPassword,
rememberMe,
);
const meRes = await getUserInfo();
storeAuth(meRes.username || "");
toast.success(t("messages.loginSuccess"));
onLogin(
meRes.username || "",
meRes.userId || undefined,
!!meRes.is_admin,
);
} catch (err: unknown) {
const error = err as {
response?: { data?: { error?: string } };
message?: string;
};
toast.error(
error?.response?.data?.error ||
error?.message ||
t("auth.ldapLoginFailed"),
);
} finally {
setProviderLoading((prev) => ({ ...prev, [providerId]: false }));
}
},
[ldapUsername, ldapPassword, rememberMe, onLogin, t],
);
useEffect(() => {
if (!ssoProvidersLoaded || silentSigninHandledRef.current) return;
if (!shouldTriggerSilentSignin(window.location.search)) return;
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(
(p) => p.type === "oidc" || p.type === "github" || p.type === "google",
);
if (oidcProvider && !isElectron()) {
handleOIDCLogin(oidcProvider.id);
return;
}
toast.info(t("errors.silentSigninOidcUnavailable"));
}, [handleOIDCLogin, ssoProvidersLoaded, ssoProviders, t]);
// Electron server config / webview auth success screens
if (isElectron() && !isInElectronWebView()) {
if (showServerConfig === null)
return (
);
if (showServerConfig)
return (
window.location.reload()}
onUseEmbedded={async () => {
await saveServerConfig({
serverUrl: "",
lastUpdated: new Date().toISOString(),
});
setShowServerConfig(false);
setCurrentServerUrl("");
}}
onCancel={() => setShowServerConfig(false)}
isFirstTime={!currentServerUrl}
/>
);
if (!webviewAuthSuccess && showServerConfig === false && currentServerUrl)
return (
setShowServerConfig(true)}
/>
);
}
if (webviewAuthSuccess || (isInElectronWebView() && webviewAuthSuccess))
return (
{t("auth.redirectingToApp")}
);
if (dbConnectionFailed)
return (
{t("errors.databaseConnection")}
{t("messages.databaseConnectionFailed")}
{t("common.language")}
{isElectron() && currentServerUrl && (
{t("serverConfig.serverUrl")}
{currentServerUrl}
)}
);
if (dbHealthChecking && showServerConfig === false)
return (
);
const TAB_ITEMS: { id: AuthView; label: string; show: boolean }[] = [
{
id: "login",
label: t("common.login"),
show: passwordLoginAllowed && !firstUser,
},
{
id: "register",
label: t("common.register"),
show: (passwordLoginAllowed || firstUser) && registrationAllowed,
},
{
id: "external",
label: t("auth.external"),
show: ssoProviders.length > 0,
},
];
return (
{isElectron() && !isInElectronWebView() && showServerConfig === false && (
{t("serverConfig.localServer")}
)}
{/* Left decorative panel */}
TERMIX
{t("auth.tagline")}
{/* Right panel */}
{/* TOTP view */}
{view === "totp" && (
{t("auth.twoFactorAuth")}
{t("auth.enterCode")}
)}
{/* Reset password view */}
{view === "reset" && (
{t("auth.forgotPassword")}
{t("auth.resetCodeDesc")}
{(["email", "code", "newpass"] as ResetStep[]).map(
(step, i) => {
const stepIdx = ["email", "code", "newpass"].indexOf(
resetStep,
);
const done = i < stepIdx;
const active = i === stepIdx;
return (
{done ? : i + 1}
{i < 2 && (
)}
);
},
)}
{resetStep === "email" && (
)}
{resetStep === "code" && (
)}
{resetStep === "newpass" && (
)}
)}
{/* Login / Register / External */}
{(view === "login" ||
view === "register" ||
view === "external") && (
{TAB_ITEMS.filter((item) => item.show).map((item) => (
))}
{view === "login"
? t("auth.loginTitle")
: view === "register"
? t("auth.registerTitle")
: t("auth.loginWithExternal")}
{view === "login"
? t("auth.loginSubtitle", "")
: view === "register"
? t("auth.registerSubtitle", "")
: t("auth.loginWithExternalDesc")}
{view === "external" && (
{ssoProviders.map((provider) => {
const isLoading = !!providerLoading[provider.id];
if (provider.type === "ldap") {
const isExpanded = expandedLdapId === provider.id;
return (
{isExpanded && (
)}
);
}
return (
);
})}
)}
{view === "login" && (
)}
{view === "register" && (
)}
{view === "login" && registrationAllowed ? (
<>
{t("auth.noAccount", "Don't have an account?")}{" "}
>
) : view === "register" &&
passwordLoginAllowed &&
!firstUser ? (
<>
{t("auth.hasAccount", "Already have an account?")}{" "}
>
) : null}
{t("common.language")}
)}
);
}