From c2ed2729be5663a7a4c522659af4ed579b3bd4ea Mon Sep 17 00:00:00 2001 From: LukeGus Date: Mon, 28 Jul 2025 17:06:30 -0500 Subject: [PATCH] Update read me, license, tools, and auth loading --- LICENSE | 13 +++++ README.md | 55 ++++++++----------- docker/docker-compose.yml | 2 +- src/App.tsx | 7 +-- src/apps/Homepage/Homepage.tsx | 56 ++++++++++++++++++- src/apps/Homepage/HomepageAuth.tsx | 64 ++++++++-------------- src/apps/Homepage/HomepageSidebar.tsx | 8 +-- src/apps/SSH/Terminal/SSHSidebar.tsx | 8 --- src/apps/Tools/Tools.tsx | 18 ------ src/apps/Tools/ToolsSidebar.tsx | 58 -------------------- src/backend/config_editor/config_editor.ts | 34 ++++++++++++ src/backend/database/database.ts | 2 +- 12 files changed, 153 insertions(+), 172 deletions(-) create mode 100644 LICENSE delete mode 100644 src/apps/Tools/Tools.tsx delete mode 100644 src/apps/Tools/ToolsSidebar.tsx diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..d1191b80 --- /dev/null +++ b/LICENSE @@ -0,0 +1,13 @@ +Copyright 2025 Luke Gustafson + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. \ No newline at end of file diff --git a/README.md b/README.md index 9ec7805d..a1b85746 100644 --- a/README.md +++ b/README.md @@ -5,58 +5,51 @@ Discord #### Top Technologies [![React Badge](https://img.shields.io/badge/-React-61DBFB?style=flat-square&labelColor=black&logo=react&logoColor=61DBFB)](#) -[![Javascript Badge](https://img.shields.io/badge/-Javascript-F0DB4F?style=flat-square&labelColor=black&logo=javascript&logoColor=F0DB4F)](#) -[![Nodejs Badge](https://img.shields.io/badge/-Nodejs-3C873A?style=flat-square&labelColor=black&logo=node.js&logoColor=3C873A)](#) -[![HTML Badge](https://img.shields.io/badge/-HTML-E34F26?style=flat-square&labelColor=black&logo=html5&logoColor=E34F26)](#) +[![TypeScript Badge](https://img.shields.io/badge/-TypeScript-3178C6?style=flat-square&labelColor=black&logo=typescript&logoColor=3178C6)](#) +[![Node.js Badge](https://img.shields.io/badge/-Node.js-3C873A?style=flat-square&labelColor=black&logo=node.js&logoColor=3C873A)](#) +[![Vite Badge](https://img.shields.io/badge/-Vite-646CFF?style=flat-square&labelColor=black&logo=vite&logoColor=646CFF)](#) [![Tailwind CSS Badge](https://img.shields.io/badge/-TailwindCSS-38B2AC?style=flat-square&labelColor=black&logo=tailwindcss&logoColor=38B2AC)](#) [![Docker Badge](https://img.shields.io/badge/-Docker-2496ED?style=flat-square&labelColor=black&logo=docker&logoColor=2496ED)](#) -[![MongoDB Badge](https://img.shields.io/badge/-MongoDB-47A248?style=flat-square&labelColor=black&logo=mongodb&logoColor=47A248)](#) -[![MUI Joy Badge](https://img.shields.io/badge/-MUI%20Joy-007FFF?style=flat-square&labelColor=black&logo=mui&logoColor=007FFF)](#) +[![SQLite Badge](https://img.shields.io/badge/-SQLite-003B57?style=flat-square&labelColor=black&logo=sqlite&logoColor=003B57)](#) +[![Radix UI Badge](https://img.shields.io/badge/-Radix%20UI-161618?style=flat-square&labelColor=black&logo=radixui&logoColor=161618)](#)

- Termix Banner + Termix Banner

If you would like, you can support the project here!\ -[![PayPal](https://img.shields.io/badge/PayPal-00457C?style=for-the-badge&logo=paypal&logoColor=white)](https://paypal.me/LukeGustafson803) +[![GitHub Sponsor](https://img.shields.io/badge/Sponsor-LukeGus-181717?style=for-the-badge&logo=github&logoColor=white)](https://github.com/sponsors/LukeGus) # Overview -Termix is an open-source forever free self-hosted Homepage (other protocols planned, see [Planned Features](#planned-features)) server management panel inspired by [Nexterm](https://github.com/gnmyt/Nexterm). Its purpose is to provide an all-in-one docker-hosted web solution to manage your servers in one easy place. I'm using this project to help me learn [React](https://github.com/facebook/react), [Vite](https://github.com/vitejs/vite-plugin-react), and [Docker](https://www.docker.com) but also because I could never settle on a server management software that I enjoyed to use. - -> [!WARNING] -> This app is in the VERY early stages of development. Expect bugs, data loss, and unexplainable issues! For that reason, I recommend you securely tunnel your connection to Termix through a VPN. +Termix is an open-source, forever-free, self-hosted all-in-one server management platform. It provides a web-based solution for managing your servers and infrastructure through a single, intuitive interface. Termix offers SSH terminal access, SSH tunneling capabilities, and remote file configuration editing - with many more tools to come. # Features -- Homepage -- Split Screen (Up to 4) & Tab System -- User Authentication -- Save Hosts (and easily view, connect, and manage them) -- SSHTerminal Themes +- **SSH Terminal Access** - Full-featured terminal with split-screen support (up to 4 panels) and tab system +- **SSH Tunnel Management** - Create and manage SSH tunnels with automatic reconnection and health monitoring +- **Remote Config Editor** - Edit files directly on remote servers with syntax highlighting and file management +- **SSH Host Manager** - Save, organize, and manage your SSH connections with tags and folders +- **User Authentication** - Secure user management with admin controls +- **Modern UI** - Clean, responsive interface built with React, Tailwind CSS, and the amazing Shadcn +- **Docker Support** - Easy deployment with Docker and Docker Compose # Planned Features -- VNC -- RDP -- SFTP (build in file transfer) -- ChatGPT/Ollama Integration (for commands) -- Apps (like notes, AI, etc) -- User Management (roles, permissions, etc.) -- Homepage Tunneling -- More Authentication Methods -- More Security Features (like 2FA, etc.) +- **Improved Admin Control** - Ability to manage admins, and give more fine-grained control over their permissions, share hosts, reset passwords, delete accounts, etc +- **More auth types** - Add 2FA, OCID support, etc +- **Theming** - Modify themeing for all tools +- **Improved SFTP Support** - Ability to manage files easier with the config editor by uploading, creating, and removing files +- **Improved Terminal Support** - Add more terminal protocols such as VNC and RDP (anyone who has experience in integrating RDP into a web-application similar to Apache Guacamole, please contact me by creating an issue) # Installation -Visit the Termix [Wiki](https://github.com/LukeGus/Termix/wiki) for information on how to install Termix. You can also use these links to go directly to guide. [Docker](https://github.com/LukeGus/Termix/wiki/Docker) or [Manual](https://github.com/LukeGus/Termix/wiki/Manual). +Visit the Termix [Docs](https://docs.termix.site/docs) for information on how to install Termix. # Support -If you need help with Termix, you can join the [Discord](https://discord.gg/jVQGdvHDrf) server and visit the support channel. You can also open an issue or open a pull request on the [GitHub](https://github.com/LukeGus/Termix/issues) repo. If you would like to support me financially, you can on [Paypal](https://paypal.me/LukeGustafson803). +If you need help with Termix, you can join the [Discord](https://discord.gg/jVQGdvHDrf) server and visit the support channel. You can also open an issue or open a pull request on the [GitHub](https://github.com/LukeGus/Termix/issues) repo. # Show-off - -![Demo Image](../../Termix/repo-images/DemoImage1.png) -![Demo Image](../../Termix/repo-images/DemoImage2.png) +TBD # License -Distributed under the MIT license. See LICENSE for more information. +Distributed under the Apache License Version 2.0. See LICENSE for more information. \ No newline at end of file diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 9b84669d..a4c55fad 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -8,7 +8,7 @@ services: volumes: - termix-data:/app/data environment: - PORT: 8080 + PORT: "8080" volumes: termix-data: diff --git a/src/App.tsx b/src/App.tsx index 08b62989..46efa03f 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,10 +1,9 @@ -import React, {useEffect} from "react" +import React from "react" import {Homepage} from "@/apps/Homepage/Homepage.tsx" import {SSH} from "@/apps/SSH/Terminal/SSH.tsx" import {SSHTunnel} from "@/apps/SSH/Tunnel/SSHTunnel.tsx"; import {ConfigEditor} from "@/apps/SSH/Config Editor/ConfigEditor.tsx"; -import {Tools} from "@/apps/Tools/Tools.tsx"; import {SSHManager} from "@/apps/SSH/Manager/SSHManager.tsx" function App() { @@ -32,10 +31,6 @@ function App() { return - case "tools": - return } } diff --git a/src/apps/Homepage/Homepage.tsx b/src/apps/Homepage/Homepage.tsx index 3ec39d3e..7e6a7bf9 100644 --- a/src/apps/Homepage/Homepage.tsx +++ b/src/apps/Homepage/Homepage.tsx @@ -1,21 +1,71 @@ import {HomepageSidebar} from "@/apps/Homepage/HomepageSidebar.tsx"; import React, {useEffect, useState} from "react"; import {HomepageAuth} from "@/apps/Homepage/HomepageAuth.tsx"; +import axios from "axios"; interface HomepageProps { onSelectView: (view: string) => void; } +function getCookie(name: string) { + return document.cookie.split('; ').reduce((r, v) => { + const parts = v.split('='); + return parts[0] === name ? decodeURIComponent(parts[1]) : r; + }, ""); +} + +const apiBase = + typeof window !== "undefined" && window.location.hostname === "localhost" + ? "http://localhost:8081/users" + : "/users"; + +const API = axios.create({ + baseURL: apiBase, +}); + export function Homepage({onSelectView}: HomepageProps): React.ReactElement { const [loggedIn, setLoggedIn] = useState(false); const [isAdmin, setIsAdmin] = useState(false); const [username, setUsername] = useState(null); + const [authLoading, setAuthLoading] = useState(true); + const [dbError, setDbError] = useState(null); + + useEffect(() => { + const jwt = getCookie("jwt"); + if (jwt) { + setAuthLoading(true); + Promise.all([ + API.get("/me", {headers: {Authorization: `Bearer ${jwt}`}}), + API.get("/db-health") + ]) + .then(([meRes]) => { + setLoggedIn(true); + setIsAdmin(!!meRes.data.is_admin); + setUsername(meRes.data.username || null); + setDbError(null); + }) + .catch((err) => { + setLoggedIn(false); + setIsAdmin(false); + setUsername(null); + setCookie("jwt", "", -1); + if (err?.response?.data?.error?.includes("Database")) { + setDbError("Could not connect to the database. Please try again later."); + } else { + setDbError(null); + } + }) + .finally(() => setAuthLoading(false)); + } else { + setAuthLoading(false); + } + }, []); return (
@@ -28,6 +78,10 @@ export function Homepage({onSelectView}: HomepageProps): React.ReactElement { setLoggedIn={setLoggedIn} setIsAdmin={setIsAdmin} setUsername={setUsername} + loggedIn={loggedIn} + authLoading={authLoading} + dbError={dbError} + setDbError={setDbError} />
diff --git a/src/apps/Homepage/HomepageAuth.tsx b/src/apps/Homepage/HomepageAuth.tsx index ef8a1659..711c554a 100644 --- a/src/apps/Homepage/HomepageAuth.tsx +++ b/src/apps/Homepage/HomepageAuth.tsx @@ -31,9 +31,23 @@ interface HomepageAuthProps extends React.ComponentProps<"div"> { setLoggedIn: (loggedIn: boolean) => void; setIsAdmin: (isAdmin: boolean) => void; setUsername: (username: string | null) => void; + loggedIn: boolean; + authLoading: boolean; + dbError: string | null; + setDbError: (error: string | null) => void; } -export function HomepageAuth({className, setLoggedIn, setIsAdmin, setUsername, ...props}: HomepageAuthProps) { +export function HomepageAuth({ + className, + setLoggedIn, + setIsAdmin, + setUsername, + loggedIn, + authLoading, + dbError, + setDbError, + ...props + }: HomepageAuthProps) { const [tab, setTab] = useState<"login" | "signup">("login"); const [localUsername, setLocalUsername] = useState(""); const [password, setPassword] = useState(""); @@ -41,8 +55,12 @@ export function HomepageAuth({className, setLoggedIn, setIsAdmin, setUsername, . const [error, setError] = useState(null); const [internalLoggedIn, setInternalLoggedIn] = useState(false); const [firstUser, setFirstUser] = useState(false); - const [dbError, setDbError] = useState(null); const [registrationAllowed, setRegistrationAllowed] = useState(true); + + useEffect(() => { + setInternalLoggedIn(loggedIn); + }, [loggedIn]); + useEffect(() => { API.get("/registration-allowed").then(res => { setRegistrationAllowed(res.data.allowed); @@ -61,43 +79,7 @@ export function HomepageAuth({className, setLoggedIn, setIsAdmin, setUsername, . }).catch(() => { setDbError("Could not connect to the database. Please try again later."); }); - }, []); - - useEffect(() => { - const jwt = getCookie("jwt"); - if (jwt) { - setLoading(true); - Promise.all([ - API.get("/me", {headers: {Authorization: `Bearer ${jwt}`}}), - API.get("/db-health") - ]) - .then(([meRes]) => { - setInternalLoggedIn(true); - setLoggedIn(true); - setIsAdmin(!!meRes.data.is_admin); - setUsername(meRes.data.username || null); - setDbError(null); - }) - .catch((err) => { - setInternalLoggedIn(false); - setLoggedIn(false); - setIsAdmin(false); - setUsername(null); - setCookie("jwt", "", -1); - if (err?.response?.data?.error?.includes("Database")) { - setDbError("Could not connect to the database. Please try again later."); - } else { - setDbError(null); - } - }) - .finally(() => setLoading(false)); - } else { - setInternalLoggedIn(false); - setLoggedIn(false); - setIsAdmin(false); - setUsername(null); - } - }, [setLoggedIn, setIsAdmin, setUsername]); + }, [setDbError]); async function handleSubmit(e: React.FormEvent) { e.preventDefault(); @@ -179,7 +161,7 @@ export function HomepageAuth({className, setLoggedIn, setIsAdmin, setUsername, . )} - {(internalLoggedIn || (loading && getCookie("jwt"))) && ( + {(internalLoggedIn || (authLoading && getCookie("jwt"))) && (
@@ -227,7 +209,7 @@ export function HomepageAuth({className, setLoggedIn, setIsAdmin, setUsername, .
)} - {(!internalLoggedIn && (!loading || !getCookie("jwt"))) && ( + {(!internalLoggedIn && (!authLoading || !getCookie("jwt"))) && ( <>
- onSelectView("tools")} disabled={disabled}> + window.open("https://dashix.dev", "_blank")} disabled={disabled}> Tools diff --git a/src/apps/SSH/Terminal/SSHSidebar.tsx b/src/apps/SSH/Terminal/SSHSidebar.tsx index 7291b7cc..634db0d2 100644 --- a/src/apps/SSH/Terminal/SSHSidebar.tsx +++ b/src/apps/SSH/Terminal/SSHSidebar.tsx @@ -232,14 +232,6 @@ export function SSHSidebar({onSelectView, onHostConnect, allTabs, runCommandOnTa className="text-xs text-red-500 bg-red-500/10 rounded px-2 py-1 border border-red-500/20">{hostsError} )} - {!hostsLoading && !hostsError && hosts.length === 0 && ( -
-
No - hosts found. -
-
- )}
void; -} - -export function Tools({ onSelectView }: ConfigEditorProps): React.ReactElement { - return ( -
- - - Template -
- ) -} \ No newline at end of file diff --git a/src/apps/Tools/ToolsSidebar.tsx b/src/apps/Tools/ToolsSidebar.tsx deleted file mode 100644 index 859078cb..00000000 --- a/src/apps/Tools/ToolsSidebar.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import React from 'react'; - -import { - CornerDownLeft -} from "lucide-react" - -import { - Button -} from "@/components/ui/button.tsx" - -import { - Sidebar, - SidebarContent, - SidebarGroup, - SidebarGroupContent, - SidebarGroupLabel, - SidebarMenu, - SidebarMenuItem, SidebarProvider, -} from "@/components/ui/sidebar.tsx" - -import { - Separator, -} from "@/components/ui/separator.tsx" - -interface SidebarProps { - onSelectView: (view: string) => void; -} - -export function ToolsSidebar({ onSelectView }: SidebarProps): React.ReactElement { - return ( - - - - - - Termix / Tools - - - - - - {/* Sidebar Items */} - - - - - - - - - - - - ) -} \ No newline at end of file diff --git a/src/backend/config_editor/config_editor.ts b/src/backend/config_editor/config_editor.ts index a8ae3705..7c4126bc 100644 --- a/src/backend/config_editor/config_editor.ts +++ b/src/backend/config_editor/config_editor.ts @@ -83,6 +83,40 @@ app.post('/ssh/config_editor/ssh/connect', (req, res) => { readyTimeout: 20000, keepaliveInterval: 10000, keepaliveCountMax: 3, + algorithms: { + kex: [ + 'diffie-hellman-group14-sha256', + 'diffie-hellman-group14-sha1', + 'diffie-hellman-group1-sha1', + 'diffie-hellman-group-exchange-sha256', + 'diffie-hellman-group-exchange-sha1', + 'ecdh-sha2-nistp256', + 'ecdh-sha2-nistp384', + 'ecdh-sha2-nistp521' + ], + cipher: [ + 'aes128-ctr', + 'aes192-ctr', + 'aes256-ctr', + 'aes128-gcm@openssh.com', + 'aes256-gcm@openssh.com', + 'aes128-cbc', + 'aes192-cbc', + 'aes256-cbc', + '3des-cbc' + ], + hmac: [ + 'hmac-sha2-256', + 'hmac-sha2-512', + 'hmac-sha1', + 'hmac-md5' + ], + compress: [ + 'none', + 'zlib@openssh.com', + 'zlib' + ] + } }; if (sshKey && sshKey.trim()) { diff --git a/src/backend/database/database.ts b/src/backend/database/database.ts index 19efc182..aae7fb11 100644 --- a/src/backend/database/database.ts +++ b/src/backend/database/database.ts @@ -8,7 +8,7 @@ import cors from 'cors'; const app = express(); app.use(cors({ origin: '*', - methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'], + methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'], allowedHeaders: ['Content-Type', 'Authorization'] }));