Update read me, license, tools, and auth loading

This commit is contained in:
LukeGus
2025-07-28 17:06:30 -05:00
parent a92ed01129
commit c2ed2729be
12 changed files with 153 additions and 172 deletions

13
LICENSE Normal file
View File

@@ -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.

View File

@@ -5,58 +5,51 @@
<a href="https://discord.gg/jVQGdvHDrf"><img alt="Discord" src="https://img.shields.io/discord/1347374268253470720"></a>
#### 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)](#)
<br />
<p align="center">
<a href="https://github.com/LukeGus/Termix">
<img alt="Termix Banner" src=../../Termix/repo-images/TermixLogo.png style="width: 125px; height: auto;"> </a>
<img alt="Termix Banner" src=../../Termix/public/icon.svg style="width: 250px; height: auto;"> </a>
</p>
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.

View File

@@ -8,7 +8,7 @@ services:
volumes:
- termix-data:/app/data
environment:
PORT: 8080
PORT: "8080"
volumes:
termix-data:

View File

@@ -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 <ConfigEditor
onSelectView={setView}
/>
case "tools":
return <Tools
onSelectView={setView}
/>
}
}

View File

@@ -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<string | null>(null);
const [authLoading, setAuthLoading] = useState(true);
const [dbError, setDbError] = useState<string | null>(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 (
<div className="flex min-h-screen">
<HomepageSidebar
onSelectView={onSelectView}
disabled={!loggedIn}
disabled={!loggedIn || authLoading}
isAdmin={isAdmin}
username={loggedIn ? username : null}
/>
@@ -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}
/>
</div>
</div>

View File

@@ -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<string | null>(null);
const [internalLoggedIn, setInternalLoggedIn] = useState(false);
const [firstUser, setFirstUser] = useState(false);
const [dbError, setDbError] = useState<string | null>(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, .
</AlertDescription>
</Alert>
)}
{(internalLoggedIn || (loading && getCookie("jwt"))) && (
{(internalLoggedIn || (authLoading && getCookie("jwt"))) && (
<div className="flex flex-1 justify-center items-center p-0 m-0">
<div className="flex flex-col items-center gap-4">
<Alert className="my-2">
@@ -227,7 +209,7 @@ export function HomepageAuth({className, setLoggedIn, setIsAdmin, setUsername, .
</div>
</div>
)}
{(!internalLoggedIn && (!loading || !getCookie("jwt"))) && (
{(!internalLoggedIn && (!authLoading || !getCookie("jwt"))) && (
<>
<div className="flex gap-2 mb-6">
<button

View File

@@ -34,12 +34,6 @@ import {
import {Checkbox} from "@/components/ui/checkbox.tsx";
import axios from "axios";
import {Button} from "@/components/ui/button.tsx";
import {Homepage} from "@/apps/Homepage/Homepage.tsx";
import {SSHManager} from "@/apps/SSH/Manager/SSHManager.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";
interface SidebarProps {
onSelectView: (view: string) => void;
@@ -145,7 +139,7 @@ export function HomepageSidebar({
</SidebarMenuItem>
</div>
<SidebarMenuItem key={"Tools"}>
<SidebarMenuButton onClick={() => onSelectView("tools")} disabled={disabled}>
<SidebarMenuButton onClick={() => window.open("https://dashix.dev", "_blank")} disabled={disabled}>
<Hammer/>
<span>Tools</span>
</SidebarMenuButton>

View File

@@ -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}</div>
</div>
)}
{!hostsLoading && !hostsError && hosts.length === 0 && (
<div className="px-2 py-1 mt-2">
<div
className="text-xs text-muted-foreground bg-muted/20 rounded px-2 py-1 border border-border/20">No
hosts found.
</div>
</div>
)}
<div className="flex-1 min-h-0">
<ScrollArea className="w-full h-full">
<Accordion key={`host-accordion-${sortedFolders.length}`}

View File

@@ -1,18 +0,0 @@
import React from "react";
import {ToolsSidebar} from "@/apps/Tools/ToolsSidebar.tsx";
interface ConfigEditorProps {
onSelectView: (view: string) => void;
}
export function Tools({ onSelectView }: ConfigEditorProps): React.ReactElement {
return (
<div>
<ToolsSidebar
onSelectView={onSelectView}
/>
Template
</div>
)
}

View File

@@ -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 (
<SidebarProvider>
<Sidebar>
<SidebarContent>
<SidebarGroup>
<SidebarGroupLabel className="text-lg font-bold text-white flex items-center gap-2">
Termix / Tools
</SidebarGroupLabel>
<Separator className="p-0.25 mt-1 mb-1" />
<SidebarGroupContent className="flex flex-col flex-grow">
<SidebarMenu>
{/* Sidebar Items */}
<SidebarMenuItem key={"Homepage"}>
<Button className="w-full mt-2 mb-2 h-8" onClick={() => onSelectView("homepage")} variant="outline">
<CornerDownLeft/>
Return
</Button>
<Separator className="p-0.25 mt-1 mb-1" />
</SidebarMenuItem>
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
</SidebarContent>
</Sidebar>
</SidebarProvider>
)
}

View File

@@ -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()) {

View File

@@ -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']
}));