fix: Work more on TOTP, renamed homepage to dashboard and began improvements
This commit is contained in:
@@ -290,12 +290,6 @@ app.post("/ssh/file_manager/ssh/connect", async (req, res) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (
|
if (
|
||||||
resolvedCredentials.authType === "password" &&
|
|
||||||
resolvedCredentials.password &&
|
|
||||||
resolvedCredentials.password.trim()
|
|
||||||
) {
|
|
||||||
config.password = resolvedCredentials.password;
|
|
||||||
} else if (
|
|
||||||
resolvedCredentials.authType === "key" &&
|
resolvedCredentials.authType === "key" &&
|
||||||
resolvedCredentials.sshKey &&
|
resolvedCredentials.sshKey &&
|
||||||
resolvedCredentials.sshKey.trim()
|
resolvedCredentials.sshKey.trim()
|
||||||
@@ -326,6 +320,22 @@ app.post("/ssh/file_manager/ssh/connect", async (req, res) => {
|
|||||||
});
|
});
|
||||||
return res.status(400).json({ error: "Invalid SSH key format" });
|
return res.status(400).json({ error: "Invalid SSH key format" });
|
||||||
}
|
}
|
||||||
|
} else if (resolvedCredentials.authType === "password") {
|
||||||
|
if (!resolvedCredentials.password || !resolvedCredentials.password.trim()) {
|
||||||
|
fileLogger.warn(
|
||||||
|
"Password authentication requested but no password provided",
|
||||||
|
{
|
||||||
|
operation: "file_connect",
|
||||||
|
sessionId,
|
||||||
|
hostId,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
return res
|
||||||
|
.status(400)
|
||||||
|
.json({ error: "Password required for password authentication" });
|
||||||
|
}
|
||||||
|
// Set password to offer both password and keyboard-interactive methods
|
||||||
|
config.password = resolvedCredentials.password;
|
||||||
} else {
|
} else {
|
||||||
fileLogger.warn(
|
fileLogger.warn(
|
||||||
"No valid authentication method provided for file manager",
|
"No valid authentication method provided for file manager",
|
||||||
@@ -403,6 +413,22 @@ app.post("/ssh/file_manager/ssh/connect", async (req, res) => {
|
|||||||
if (responseSent) return;
|
if (responseSent) return;
|
||||||
responseSent = true;
|
responseSent = true;
|
||||||
|
|
||||||
|
if (pendingTOTPSessions[sessionId]) {
|
||||||
|
fileLogger.warn(
|
||||||
|
"TOTP session already exists, cleaning up old client",
|
||||||
|
{
|
||||||
|
operation: "file_keyboard_interactive",
|
||||||
|
hostId,
|
||||||
|
sessionId,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
try {
|
||||||
|
pendingTOTPSessions[sessionId].client.end();
|
||||||
|
} catch (e) {
|
||||||
|
// Ignore cleanup errors
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pendingTOTPSessions[sessionId] = {
|
pendingTOTPSessions[sessionId] = {
|
||||||
client,
|
client,
|
||||||
finish,
|
finish,
|
||||||
@@ -411,6 +437,13 @@ app.post("/ssh/file_manager/ssh/connect", async (req, res) => {
|
|||||||
sessionId,
|
sessionId,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
fileLogger.info("Created TOTP session", {
|
||||||
|
operation: "file_keyboard_interactive_totp",
|
||||||
|
hostId,
|
||||||
|
sessionId,
|
||||||
|
prompt: totpPrompt.prompt,
|
||||||
|
});
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
requires_totp: true,
|
requires_totp: true,
|
||||||
sessionId,
|
sessionId,
|
||||||
@@ -456,25 +489,38 @@ app.post("/ssh/file_manager/ssh/connect-totp", async (req, res) => {
|
|||||||
operation: "file_totp_verify",
|
operation: "file_totp_verify",
|
||||||
sessionId,
|
sessionId,
|
||||||
userId,
|
userId,
|
||||||
|
availableSessions: Object.keys(pendingTOTPSessions),
|
||||||
});
|
});
|
||||||
return res
|
return res
|
||||||
.status(404)
|
.status(404)
|
||||||
.json({ error: "TOTP session expired. Please reconnect." });
|
.json({ error: "TOTP session expired. Please reconnect." });
|
||||||
}
|
}
|
||||||
|
|
||||||
delete pendingTOTPSessions[sessionId];
|
if (Date.now() - session.createdAt > 180000) {
|
||||||
|
delete pendingTOTPSessions[sessionId];
|
||||||
if (Date.now() - session.createdAt > 120000) {
|
|
||||||
try {
|
try {
|
||||||
session.client.end();
|
session.client.end();
|
||||||
} catch {
|
} catch {
|
||||||
// Ignore errors when closing timed out session
|
// Ignore errors when closing timed out session
|
||||||
}
|
}
|
||||||
|
fileLogger.warn("TOTP session timeout before code submission", {
|
||||||
|
operation: "file_totp_verify",
|
||||||
|
sessionId,
|
||||||
|
userId,
|
||||||
|
age: Date.now() - session.createdAt,
|
||||||
|
});
|
||||||
return res
|
return res
|
||||||
.status(408)
|
.status(408)
|
||||||
.json({ error: "TOTP session timeout. Please reconnect." });
|
.json({ error: "TOTP session timeout. Please reconnect." });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fileLogger.info("Submitting TOTP code to SSH server", {
|
||||||
|
operation: "file_totp_verify",
|
||||||
|
sessionId,
|
||||||
|
userId,
|
||||||
|
codeLength: totpCode.length,
|
||||||
|
});
|
||||||
|
|
||||||
session.finish([totpCode]);
|
session.finish([totpCode]);
|
||||||
|
|
||||||
let responseSent = false;
|
let responseSent = false;
|
||||||
@@ -483,6 +529,8 @@ app.post("/ssh/file_manager/ssh/connect-totp", async (req, res) => {
|
|||||||
if (responseSent) return;
|
if (responseSent) return;
|
||||||
responseSent = true;
|
responseSent = true;
|
||||||
|
|
||||||
|
delete pendingTOTPSessions[sessionId];
|
||||||
|
|
||||||
sshSessions[sessionId] = {
|
sshSessions[sessionId] = {
|
||||||
client: session.client,
|
client: session.client,
|
||||||
isConnected: true,
|
isConnected: true,
|
||||||
@@ -506,6 +554,8 @@ app.post("/ssh/file_manager/ssh/connect-totp", async (req, res) => {
|
|||||||
if (responseSent) return;
|
if (responseSent) return;
|
||||||
responseSent = true;
|
responseSent = true;
|
||||||
|
|
||||||
|
delete pendingTOTPSessions[sessionId];
|
||||||
|
|
||||||
fileLogger.error("TOTP verification failed", {
|
fileLogger.error("TOTP verification failed", {
|
||||||
operation: "file_totp_verify",
|
operation: "file_totp_verify",
|
||||||
sessionId,
|
sessionId,
|
||||||
@@ -519,6 +569,7 @@ app.post("/ssh/file_manager/ssh/connect-totp", async (req, res) => {
|
|||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (!responseSent) {
|
if (!responseSent) {
|
||||||
responseSent = true;
|
responseSent = true;
|
||||||
|
delete pendingTOTPSessions[sessionId];
|
||||||
res.status(408).json({ error: "TOTP verification timeout" });
|
res.status(408).json({ error: "TOTP verification timeout" });
|
||||||
}
|
}
|
||||||
}, 60000);
|
}, 60000);
|
||||||
|
|||||||
@@ -577,7 +577,7 @@ function buildSshConfig(host: SSHHostWithCredentials): ConnectConfig {
|
|||||||
if (!host.password) {
|
if (!host.password) {
|
||||||
throw new Error(`No password available for host ${host.ip}`);
|
throw new Error(`No password available for host ${host.ip}`);
|
||||||
}
|
}
|
||||||
(base as Record<string, unknown>).password = host.password;
|
// Don't set password in config - let keyboard-interactive handle it
|
||||||
} else if (host.authType === "key") {
|
} else if (host.authType === "key") {
|
||||||
if (!host.key) {
|
if (!host.key) {
|
||||||
throw new Error(`No SSH key available for host ${host.ip}`);
|
throw new Error(`No SSH key available for host ${host.ip}`);
|
||||||
|
|||||||
@@ -672,7 +672,25 @@ wss.on("connection", async (ws: WebSocket, req) => {
|
|||||||
totpCode,
|
totpCode,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
finish([totpCode]);
|
// Respond to ALL prompts, not just TOTP
|
||||||
|
const responses = prompts.map((p, index) => {
|
||||||
|
if (index === totpPromptIndex) {
|
||||||
|
return totpCode;
|
||||||
|
}
|
||||||
|
if (/password/i.test(p.prompt) && resolvedCredentials.password) {
|
||||||
|
return resolvedCredentials.password;
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
});
|
||||||
|
|
||||||
|
sshLogger.info("Full keyboard-interactive response", {
|
||||||
|
operation: "totp_full_response",
|
||||||
|
hostId: id,
|
||||||
|
totalPrompts: prompts.length,
|
||||||
|
responsesProvided: responses.filter((r) => r !== "").length,
|
||||||
|
});
|
||||||
|
|
||||||
|
finish(responses);
|
||||||
};
|
};
|
||||||
ws.send(
|
ws.send(
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
@@ -768,15 +786,8 @@ wss.on("connection", async (ws: WebSocket, req) => {
|
|||||||
compress: ["none", "zlib@openssh.com", "zlib"],
|
compress: ["none", "zlib@openssh.com", "zlib"],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
if (
|
|
||||||
resolvedCredentials.authType === "password" &&
|
if (resolvedCredentials.authType === "key" && resolvedCredentials.key) {
|
||||||
resolvedCredentials.password
|
|
||||||
) {
|
|
||||||
connectConfig.password = resolvedCredentials.password;
|
|
||||||
} else if (
|
|
||||||
resolvedCredentials.authType === "key" &&
|
|
||||||
resolvedCredentials.key
|
|
||||||
) {
|
|
||||||
try {
|
try {
|
||||||
if (
|
if (
|
||||||
!resolvedCredentials.key.includes("-----BEGIN") ||
|
!resolvedCredentials.key.includes("-----BEGIN") ||
|
||||||
@@ -814,6 +825,22 @@ wss.on("connection", async (ws: WebSocket, req) => {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
|
} else if (resolvedCredentials.authType === "password") {
|
||||||
|
if (!resolvedCredentials.password) {
|
||||||
|
sshLogger.error(
|
||||||
|
"Password authentication requested but no password provided",
|
||||||
|
);
|
||||||
|
ws.send(
|
||||||
|
JSON.stringify({
|
||||||
|
type: "error",
|
||||||
|
message:
|
||||||
|
"Password authentication requested but no password provided",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Set password to offer both password and keyboard-interactive methods
|
||||||
|
connectConfig.password = resolvedCredentials.password;
|
||||||
} else {
|
} else {
|
||||||
sshLogger.error("No valid authentication method provided");
|
sshLogger.error("No valid authentication method provided");
|
||||||
ws.send(
|
ws.send(
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import { getReleasesRSS, getVersionInfo } from "@/ui/main-axios.ts";
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { BookOpen, X } from "lucide-react";
|
import { BookOpen, X } from "lucide-react";
|
||||||
|
|
||||||
interface HomepageUpdateLogProps extends React.ComponentProps<"div"> {
|
interface UpdateLogProps extends React.ComponentProps<"div"> {
|
||||||
loggedIn: boolean;
|
loggedIn: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,7 +59,7 @@ interface VersionResponse {
|
|||||||
cache_age?: number;
|
cache_age?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function HomepageUpdateLog({ loggedIn }: HomepageUpdateLogProps) {
|
export function UpdateLog({ loggedIn }: UpdateLogProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [releases, setReleases] = useState<RSSResponse | null>(null);
|
const [releases, setReleases] = useState<RSSResponse | null>(null);
|
||||||
const [versionInfo, setVersionInfo] = useState<VersionResponse | null>(null);
|
const [versionInfo, setVersionInfo] = useState<VersionResponse | null>(null);
|
||||||
204
src/ui/Desktop/Apps/Dashboard/Dashboard.tsx
Normal file
204
src/ui/Desktop/Apps/Dashboard/Dashboard.tsx
Normal file
@@ -0,0 +1,204 @@
|
|||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
import { Auth } from "@/ui/Desktop/Authentication/Auth.tsx";
|
||||||
|
import { UpdateLog } from "@/ui/Desktop/Apps/Dashboard/Apps/UpdateLog.tsx";
|
||||||
|
import { AlertManager } from "@/ui/Desktop/Apps/Dashboard/Apps/Alerts/AlertManager.tsx";
|
||||||
|
import { Button } from "@/components/ui/button.tsx";
|
||||||
|
import { getUserInfo, getDatabaseHealth, getCookie } from "@/ui/main-axios.ts";
|
||||||
|
import { useSidebar } from "@/components/ui/sidebar.tsx";
|
||||||
|
import { Separator } from "@/components/ui/separator.tsx";
|
||||||
|
import { ChartLine, History } from "lucide-react";
|
||||||
|
import { Status } from "@/components/ui/shadcn-io/status";
|
||||||
|
|
||||||
|
interface DashboardProps {
|
||||||
|
onSelectView: (view: string) => void;
|
||||||
|
isAuthenticated: boolean;
|
||||||
|
authLoading: boolean;
|
||||||
|
onAuthSuccess: (authData: {
|
||||||
|
isAdmin: boolean;
|
||||||
|
username: string | null;
|
||||||
|
userId: string | null;
|
||||||
|
}) => void;
|
||||||
|
isTopbarOpen: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Dashboard({
|
||||||
|
isAuthenticated,
|
||||||
|
authLoading,
|
||||||
|
onAuthSuccess,
|
||||||
|
isTopbarOpen,
|
||||||
|
}: DashboardProps): React.ReactElement {
|
||||||
|
const [loggedIn, setLoggedIn] = useState(isAuthenticated);
|
||||||
|
const [, setIsAdmin] = useState(false);
|
||||||
|
const [, setUsername] = useState<string | null>(null);
|
||||||
|
const [userId, setUserId] = useState<string | null>(null);
|
||||||
|
const [dbError, setDbError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
let sidebarState: "expanded" | "collapsed" = "expanded";
|
||||||
|
try {
|
||||||
|
const sidebar = useSidebar();
|
||||||
|
sidebarState = sidebar.state;
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
const topMarginPx = isTopbarOpen ? 74 : 26;
|
||||||
|
const leftMarginPx = sidebarState === "collapsed" ? 26 : 8;
|
||||||
|
const bottomMarginPx = 8;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setLoggedIn(isAuthenticated);
|
||||||
|
}, [isAuthenticated]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isAuthenticated) {
|
||||||
|
if (getCookie("jwt")) {
|
||||||
|
getUserInfo()
|
||||||
|
.then((meRes) => {
|
||||||
|
setIsAdmin(!!meRes.is_admin);
|
||||||
|
setUsername(meRes.username || null);
|
||||||
|
setUserId(meRes.userId || null);
|
||||||
|
setDbError(null);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
setIsAdmin(false);
|
||||||
|
setUsername(null);
|
||||||
|
setUserId(null);
|
||||||
|
|
||||||
|
const errorCode = err?.response?.data?.code;
|
||||||
|
if (errorCode === "SESSION_EXPIRED") {
|
||||||
|
console.warn("Session expired - please log in again");
|
||||||
|
setDbError("Session expired - please log in again");
|
||||||
|
} else {
|
||||||
|
setDbError(null);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
getDatabaseHealth()
|
||||||
|
.then(() => {
|
||||||
|
setDbError(null);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
if (err?.response?.data?.error?.includes("Database")) {
|
||||||
|
setDbError(
|
||||||
|
"Could not connect to the database. Please try again later.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [isAuthenticated]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{!loggedIn ? (
|
||||||
|
<div className="w-full h-full flex items-center justify-center">
|
||||||
|
<Auth
|
||||||
|
setLoggedIn={setLoggedIn}
|
||||||
|
setIsAdmin={setIsAdmin}
|
||||||
|
setUsername={setUsername}
|
||||||
|
setUserId={setUserId}
|
||||||
|
loggedIn={loggedIn}
|
||||||
|
authLoading={authLoading}
|
||||||
|
dbError={dbError}
|
||||||
|
setDbError={setDbError}
|
||||||
|
onAuthSuccess={onAuthSuccess}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div
|
||||||
|
className="bg-dark-bg text-white rounded-lg border-2 border-dark-border overflow-hidden flex"
|
||||||
|
style={{
|
||||||
|
marginLeft: leftMarginPx,
|
||||||
|
marginRight: 17,
|
||||||
|
marginTop: topMarginPx,
|
||||||
|
marginBottom: bottomMarginPx,
|
||||||
|
height: `calc(100vh - ${topMarginPx + bottomMarginPx}px)`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="flex flex-col relative z-10 w-full h-full">
|
||||||
|
<div className="flex flex-row items-center justify-between w-full px-3 mt-3">
|
||||||
|
<div className="text-2xl text-white font-semibold">Dashboard</div>
|
||||||
|
<div className="flex flex-row gap-3">
|
||||||
|
<Button
|
||||||
|
className="font-semibold"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() =>
|
||||||
|
window.open(
|
||||||
|
"https://github.com/Termix-SSH/Termix",
|
||||||
|
"_blank",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
GitHub
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
className="font-semibold"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() =>
|
||||||
|
window.open(
|
||||||
|
"https://github.com/Termix-SSH/Support/issues/new",
|
||||||
|
"_blank",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Support
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
className="font-semibold"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() =>
|
||||||
|
window.open(
|
||||||
|
"https://discord.com/invite/jVQGdvHDrf",
|
||||||
|
"_blank",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Discord
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
className="font-semibold"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() =>
|
||||||
|
window.open("https://github.com/sponsors/LukeGus", "_blank")
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Donate
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Separator className="mt-3 p-0.25" />
|
||||||
|
|
||||||
|
<div className="flex flex-col h-screen my-5 mx-5 gap-4">
|
||||||
|
<div className="flex flex-row flex-1 gap-4">
|
||||||
|
<div className="flex-1 border-2 border-dark-border rounded-md bg-dark-bg-darker">
|
||||||
|
<div className="flex flex-col mx-3 my-2">
|
||||||
|
<p className="text-xl font-semibold mb-3 flex flex-row">
|
||||||
|
<ChartLine className="mr-3" />
|
||||||
|
Server Status
|
||||||
|
</p>
|
||||||
|
<div className="flex flex-row items-center">
|
||||||
|
<History color="#7393B3" />
|
||||||
|
<p className="ml-3">Version</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 border-2 border-dark-border rounded-md bg-dark-bg-darker">
|
||||||
|
test
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-row flex-1 gap-4">
|
||||||
|
<div className="flex-1 border-2 border-dark-border rounded-md bg-dark-bg-darker">
|
||||||
|
test
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 border-2 border-dark-border rounded-md bg-dark-bg-darker">
|
||||||
|
test
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<AlertManager userId={userId} loggedIn={loggedIn} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,171 +0,0 @@
|
|||||||
import React, { useEffect, useState } from "react";
|
|
||||||
import { Auth } from "@/ui/Desktop/Authentication/Auth.tsx";
|
|
||||||
import { HomepageUpdateLog } from "@/ui/Desktop/Apps/Homepage/Apps/UpdateLog.tsx";
|
|
||||||
import { AlertManager } from "@/ui/Desktop/Apps/Homepage/Apps/Alerts/AlertManager.tsx";
|
|
||||||
import { Button } from "@/components/ui/button.tsx";
|
|
||||||
import { getUserInfo, getDatabaseHealth, getCookie } from "@/ui/main-axios.ts";
|
|
||||||
import { useSidebar } from "@/components/ui/sidebar.tsx";
|
|
||||||
|
|
||||||
interface HomepageProps {
|
|
||||||
onSelectView: (view: string) => void;
|
|
||||||
isAuthenticated: boolean;
|
|
||||||
authLoading: boolean;
|
|
||||||
onAuthSuccess: (authData: {
|
|
||||||
isAdmin: boolean;
|
|
||||||
username: string | null;
|
|
||||||
userId: string | null;
|
|
||||||
}) => void;
|
|
||||||
isTopbarOpen: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function Homepage({
|
|
||||||
isAuthenticated,
|
|
||||||
authLoading,
|
|
||||||
onAuthSuccess,
|
|
||||||
isTopbarOpen,
|
|
||||||
}: HomepageProps): React.ReactElement {
|
|
||||||
const [loggedIn, setLoggedIn] = useState(isAuthenticated);
|
|
||||||
const [, setIsAdmin] = useState(false);
|
|
||||||
const [, setUsername] = useState<string | null>(null);
|
|
||||||
const [userId, setUserId] = useState<string | null>(null);
|
|
||||||
const [dbError, setDbError] = useState<string | null>(null);
|
|
||||||
|
|
||||||
let sidebarState: "expanded" | "collapsed" = "expanded";
|
|
||||||
try {
|
|
||||||
const sidebar = useSidebar();
|
|
||||||
sidebarState = sidebar.state;
|
|
||||||
} catch {}
|
|
||||||
|
|
||||||
const topMarginPx = isTopbarOpen ? 74 : 26;
|
|
||||||
const leftMarginPx = sidebarState === "collapsed" ? 26 : 8;
|
|
||||||
const bottomMarginPx = 8;
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setLoggedIn(isAuthenticated);
|
|
||||||
}, [isAuthenticated]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (isAuthenticated) {
|
|
||||||
if (getCookie("jwt")) {
|
|
||||||
getUserInfo()
|
|
||||||
.then((meRes) => {
|
|
||||||
setIsAdmin(!!meRes.is_admin);
|
|
||||||
setUsername(meRes.username || null);
|
|
||||||
setUserId(meRes.userId || null);
|
|
||||||
setDbError(null);
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
setIsAdmin(false);
|
|
||||||
setUsername(null);
|
|
||||||
setUserId(null);
|
|
||||||
|
|
||||||
const errorCode = err?.response?.data?.code;
|
|
||||||
if (errorCode === "SESSION_EXPIRED") {
|
|
||||||
console.warn("Session expired - please log in again");
|
|
||||||
setDbError("Session expired - please log in again");
|
|
||||||
} else {
|
|
||||||
setDbError(null);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
getDatabaseHealth()
|
|
||||||
.then(() => {
|
|
||||||
setDbError(null);
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
if (err?.response?.data?.error?.includes("Database")) {
|
|
||||||
setDbError(
|
|
||||||
"Could not connect to the database. Please try again later.",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [isAuthenticated]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{!loggedIn ? (
|
|
||||||
<div className="w-full h-full flex items-center justify-center">
|
|
||||||
<Auth
|
|
||||||
setLoggedIn={setLoggedIn}
|
|
||||||
setIsAdmin={setIsAdmin}
|
|
||||||
setUsername={setUsername}
|
|
||||||
setUserId={setUserId}
|
|
||||||
loggedIn={loggedIn}
|
|
||||||
authLoading={authLoading}
|
|
||||||
dbError={dbError}
|
|
||||||
setDbError={setDbError}
|
|
||||||
onAuthSuccess={onAuthSuccess}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div
|
|
||||||
className="bg-dark-bg text-white rounded-lg border-2 border-dark-border overflow-hidden flex items-center justify-center"
|
|
||||||
style={{
|
|
||||||
marginLeft: leftMarginPx,
|
|
||||||
marginRight: 17,
|
|
||||||
marginTop: topMarginPx,
|
|
||||||
marginBottom: bottomMarginPx,
|
|
||||||
height: `calc(100vh - ${topMarginPx + bottomMarginPx}px)`,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className="flex flex-col items-center justify-center gap-6 relative z-10">
|
|
||||||
<HomepageUpdateLog loggedIn={loggedIn} />
|
|
||||||
|
|
||||||
<div className="flex flex-row items-center gap-3 flex-wrap justify-center">
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
className="text-sm border-dark-border text-gray-300 hover:text-white hover:bg-dark-bg transition-colors"
|
|
||||||
onClick={() =>
|
|
||||||
window.open("https://github.com/Termix-SSH/Termix", "_blank")
|
|
||||||
}
|
|
||||||
>
|
|
||||||
GitHub
|
|
||||||
</Button>
|
|
||||||
<div className="w-px h-4 bg-dark-border"></div>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
className="text-sm border-dark-border text-gray-300 hover:text-white hover:bg-dark-bg transition-colors"
|
|
||||||
onClick={() =>
|
|
||||||
window.open(
|
|
||||||
"https://github.com/Termix-SSH/Termix/issues/new",
|
|
||||||
"_blank",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
Feedback
|
|
||||||
</Button>
|
|
||||||
<div className="w-px h-4 bg-dark-border"></div>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
className="text-sm border-dark-border text-gray-300 hover:text-white hover:bg-dark-bg transition-colors"
|
|
||||||
onClick={() =>
|
|
||||||
window.open("https://discord.com/invite/jVQGdvHDrf", "_blank")
|
|
||||||
}
|
|
||||||
>
|
|
||||||
Discord
|
|
||||||
</Button>
|
|
||||||
<div className="w-px h-4 bg-dark-border"></div>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
className="text-sm border-dark-border text-gray-300 hover:text-white hover:bg-dark-bg transition-colors"
|
|
||||||
onClick={() =>
|
|
||||||
window.open("https://github.com/sponsors/LukeGus", "_blank")
|
|
||||||
}
|
|
||||||
>
|
|
||||||
Donate
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<AlertManager userId={userId} loggedIn={loggedIn} />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -25,7 +25,7 @@ import {
|
|||||||
} from "../../main-axios.ts";
|
} from "../../main-axios.ts";
|
||||||
import { ElectronServerConfig as ServerConfigComponent } from "@/ui/Desktop/Authentication/ElectronServerConfig.tsx";
|
import { ElectronServerConfig as ServerConfigComponent } from "@/ui/Desktop/Authentication/ElectronServerConfig.tsx";
|
||||||
|
|
||||||
interface HomepageAuthProps extends React.ComponentProps<"div"> {
|
interface AuthProps extends React.ComponentProps<"div"> {
|
||||||
setLoggedIn: (loggedIn: boolean) => void;
|
setLoggedIn: (loggedIn: boolean) => void;
|
||||||
setIsAdmin: (isAdmin: boolean) => void;
|
setIsAdmin: (isAdmin: boolean) => void;
|
||||||
setUsername: (username: string | null) => void;
|
setUsername: (username: string | null) => void;
|
||||||
@@ -51,7 +51,7 @@ export function Auth({
|
|||||||
setDbError,
|
setDbError,
|
||||||
onAuthSuccess,
|
onAuthSuccess,
|
||||||
...props
|
...props
|
||||||
}: HomepageAuthProps) {
|
}: AuthProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [tab, setTab] = useState<"login" | "signup" | "external" | "reset">(
|
const [tab, setTab] = useState<"login" | "signup" | "external" | "reset">(
|
||||||
"login",
|
"login",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React, { useState, useEffect } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import { LeftSidebar } from "@/ui/Desktop/Navigation/LeftSidebar.tsx";
|
import { LeftSidebar } from "@/ui/Desktop/Navigation/LeftSidebar.tsx";
|
||||||
import { Homepage } from "@/ui/Desktop/Apps/Homepage/Homepage.tsx";
|
import { Dashboard } from "@/ui/Desktop/Apps/Dashboard/Dashboard.tsx";
|
||||||
import { AppView } from "@/ui/Desktop/Navigation/AppView.tsx";
|
import { AppView } from "@/ui/Desktop/Navigation/AppView.tsx";
|
||||||
import { HostManager } from "@/ui/Desktop/Apps/Host Manager/HostManager.tsx";
|
import { HostManager } from "@/ui/Desktop/Apps/Host Manager/HostManager.tsx";
|
||||||
import {
|
import {
|
||||||
@@ -123,7 +123,7 @@ function AppContent() {
|
|||||||
|
|
||||||
{!isAuthenticated && !authLoading && !showVersionCheck && (
|
{!isAuthenticated && !authLoading && !showVersionCheck && (
|
||||||
<div className="fixed inset-0 flex items-center justify-center z-[10000]">
|
<div className="fixed inset-0 flex items-center justify-center z-[10000]">
|
||||||
<Homepage
|
<Dashboard
|
||||||
onSelectView={handleSelectView}
|
onSelectView={handleSelectView}
|
||||||
isAuthenticated={isAuthenticated}
|
isAuthenticated={isAuthenticated}
|
||||||
authLoading={authLoading}
|
authLoading={authLoading}
|
||||||
@@ -149,7 +149,7 @@ function AppContent() {
|
|||||||
|
|
||||||
{showHome && (
|
{showHome && (
|
||||||
<div className="h-screen w-full visible pointer-events-auto static overflow-hidden">
|
<div className="h-screen w-full visible pointer-events-auto static overflow-hidden">
|
||||||
<Homepage
|
<Dashboard
|
||||||
onSelectView={handleSelectView}
|
onSelectView={handleSelectView}
|
||||||
isAuthenticated={isAuthenticated}
|
isAuthenticated={isAuthenticated}
|
||||||
authLoading={authLoading}
|
authLoading={authLoading}
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ import {
|
|||||||
} from "@/ui/main-axios.ts";
|
} from "@/ui/main-axios.ts";
|
||||||
import { PasswordInput } from "@/components/ui/password-input.tsx";
|
import { PasswordInput } from "@/components/ui/password-input.tsx";
|
||||||
|
|
||||||
interface HomepageAuthProps extends React.ComponentProps<"div"> {
|
interface AuthProps extends React.ComponentProps<"div"> {
|
||||||
setLoggedIn: (loggedIn: boolean) => void;
|
setLoggedIn: (loggedIn: boolean) => void;
|
||||||
setIsAdmin: (isAdmin: boolean) => void;
|
setIsAdmin: (isAdmin: boolean) => void;
|
||||||
setUsername: (username: string | null) => void;
|
setUsername: (username: string | null) => void;
|
||||||
@@ -40,7 +40,7 @@ interface HomepageAuthProps extends React.ComponentProps<"div"> {
|
|||||||
}) => void;
|
}) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function HomepageAuth({
|
export function Auth({
|
||||||
className,
|
className,
|
||||||
setLoggedIn,
|
setLoggedIn,
|
||||||
setIsAdmin,
|
setIsAdmin,
|
||||||
@@ -52,7 +52,7 @@ export function HomepageAuth({
|
|||||||
setDbError,
|
setDbError,
|
||||||
onAuthSuccess,
|
onAuthSuccess,
|
||||||
...props
|
...props
|
||||||
}: HomepageAuthProps) {
|
}: AuthProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [tab, setTab] = useState<"login" | "signup" | "external" | "reset">(
|
const [tab, setTab] = useState<"login" | "signup" | "external" | "reset">(
|
||||||
"login",
|
"login",
|
||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
useTabs,
|
useTabs,
|
||||||
} from "@/ui/Mobile/Navigation/Tabs/TabContext.tsx";
|
} from "@/ui/Mobile/Navigation/Tabs/TabContext.tsx";
|
||||||
import { getUserInfo } from "@/ui/main-axios.ts";
|
import { getUserInfo } from "@/ui/main-axios.ts";
|
||||||
import { HomepageAuth } from "@/ui/Mobile/Homepage/HomepageAuth.tsx";
|
import { Auth } from "@/ui/Mobile/Authentication/Auth.tsx";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Toaster } from "@/components/ui/sonner.tsx";
|
import { Toaster } from "@/components/ui/sonner.tsx";
|
||||||
|
|
||||||
@@ -124,7 +124,7 @@ const AppContent: FC = () => {
|
|||||||
if (!isAuthenticated) {
|
if (!isAuthenticated) {
|
||||||
return (
|
return (
|
||||||
<div className="h-screen w-screen flex items-center justify-center bg-dark-bg p-4">
|
<div className="h-screen w-screen flex items-center justify-center bg-dark-bg p-4">
|
||||||
<HomepageAuth
|
<Auth
|
||||||
setLoggedIn={setIsAuthenticated}
|
setLoggedIn={setIsAuthenticated}
|
||||||
setIsAdmin={setIsAdmin}
|
setIsAdmin={setIsAdmin}
|
||||||
setUsername={setUsername}
|
setUsername={setUsername}
|
||||||
|
|||||||
Reference in New Issue
Block a user