Merge remote-tracking branch 'origin/dev-1.3.1' into dependabot/npm_and_yarn/prod-patch-updates-dc9bb06971
# Conflicts: # package-lock.json # package.json
This commit is contained in:
8
.github/workflows/docker-image.yml
vendored
8
.github/workflows/docker-image.yml
vendored
@@ -19,7 +19,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
fetch-depth: 1
|
fetch-depth: 1
|
||||||
|
|
||||||
@@ -37,7 +37,7 @@ jobs:
|
|||||||
network=host
|
network=host
|
||||||
|
|
||||||
- name: Cache npm dependencies
|
- name: Cache npm dependencies
|
||||||
uses: actions/cache@v3
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
~/.npm
|
~/.npm
|
||||||
@@ -48,7 +48,7 @@ jobs:
|
|||||||
${{ runner.os }}-node-
|
${{ runner.os }}-node-
|
||||||
|
|
||||||
- name: Cache Docker layers
|
- name: Cache Docker layers
|
||||||
uses: actions/cache@v3
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: /tmp/.buildx-cache
|
path: /tmp/.buildx-cache
|
||||||
key: ${{ runner.os }}-buildx-${{ github.ref_name }}-${{ hashFiles('docker/Dockerfile') }}
|
key: ${{ runner.os }}-buildx-${{ github.ref_name }}-${{ hashFiles('docker/Dockerfile') }}
|
||||||
@@ -78,7 +78,7 @@ jobs:
|
|||||||
echo "IMAGE_TAG=$IMAGE_TAG" >> $GITHUB_ENV
|
echo "IMAGE_TAG=$IMAGE_TAG" >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: Build and Push Multi-Arch Docker Image
|
- name: Build and Push Multi-Arch Docker Image
|
||||||
uses: docker/build-push-action@v5
|
uses: docker/build-push-action@v6
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: ./docker/Dockerfile
|
file: ./docker/Dockerfile
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# Stage 1: Install dependencies and build frontend
|
# Stage 1: Install dependencies and build frontend
|
||||||
FROM node:22-alpine AS deps
|
FROM node:24-alpine AS deps
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
RUN apk add --no-cache python3 make g++
|
RUN apk add --no-cache python3 make g++
|
||||||
@@ -26,7 +26,7 @@ COPY . .
|
|||||||
RUN npm run build:backend
|
RUN npm run build:backend
|
||||||
|
|
||||||
# Stage 4: Production dependencies
|
# Stage 4: Production dependencies
|
||||||
FROM node:22-alpine AS production-deps
|
FROM node:24-alpine AS production-deps
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
COPY package*.json ./
|
COPY package*.json ./
|
||||||
@@ -35,7 +35,7 @@ RUN npm ci --only=production --ignore-scripts --force && \
|
|||||||
npm cache clean --force
|
npm cache clean --force
|
||||||
|
|
||||||
# Stage 5: Build native modules
|
# Stage 5: Build native modules
|
||||||
FROM node:22-alpine AS native-builder
|
FROM node:24-alpine AS native-builder
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
RUN apk add --no-cache python3 make g++
|
RUN apk add --no-cache python3 make g++
|
||||||
@@ -46,7 +46,7 @@ RUN npm ci --only=production bcryptjs better-sqlite3 --force && \
|
|||||||
npm cache clean --force
|
npm cache clean --force
|
||||||
|
|
||||||
# Stage 6: Final image
|
# Stage 6: Final image
|
||||||
FROM node:22-alpine
|
FROM node:24-alpine
|
||||||
ENV DATA_DIR=/app/data \
|
ENV DATA_DIR=/app/data \
|
||||||
PORT=8080 \
|
PORT=8080 \
|
||||||
NODE_ENV=production
|
NODE_ENV=production
|
||||||
|
|||||||
698
package-lock.json
generated
698
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
34
package.json
34
package.json
@@ -13,24 +13,24 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@hookform/resolvers": "^5.1.1",
|
"@hookform/resolvers": "^5.1.1",
|
||||||
"@radix-ui/react-accordion": "^1.2.12",
|
"@radix-ui/react-accordion": "^1.2.11",
|
||||||
"@radix-ui/react-avatar": "^1.1.10",
|
"@radix-ui/react-avatar": "^1.1.10",
|
||||||
"@radix-ui/react-checkbox": "^1.3.3",
|
"@radix-ui/react-checkbox": "^1.3.2",
|
||||||
"@radix-ui/react-collapsible": "^1.1.11",
|
"@radix-ui/react-collapsible": "^1.1.11",
|
||||||
"@radix-ui/react-dialog": "^1.1.15",
|
"@radix-ui/react-dialog": "^1.1.15",
|
||||||
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
"@radix-ui/react-dropdown-menu": "^2.1.15",
|
||||||
"@radix-ui/react-label": "^2.1.7",
|
"@radix-ui/react-label": "^2.1.7",
|
||||||
"@radix-ui/react-popover": "^1.1.15",
|
"@radix-ui/react-popover": "^1.1.14",
|
||||||
"@radix-ui/react-progress": "^1.1.7",
|
"@radix-ui/react-progress": "^1.1.7",
|
||||||
"@radix-ui/react-scroll-area": "^1.2.10",
|
"@radix-ui/react-scroll-area": "^1.2.9",
|
||||||
"@radix-ui/react-select": "^2.2.6",
|
"@radix-ui/react-select": "^2.2.5",
|
||||||
"@radix-ui/react-separator": "^1.1.7",
|
"@radix-ui/react-separator": "^1.1.7",
|
||||||
"@radix-ui/react-slider": "^1.3.6",
|
"@radix-ui/react-slider": "^1.3.5",
|
||||||
"@radix-ui/react-slot": "^1.2.3",
|
"@radix-ui/react-slot": "^1.2.3",
|
||||||
"@radix-ui/react-switch": "^1.2.6",
|
"@radix-ui/react-switch": "^1.2.5",
|
||||||
"@radix-ui/react-tabs": "^1.1.13",
|
"@radix-ui/react-tabs": "^1.1.12",
|
||||||
"@radix-ui/react-tooltip": "^1.2.8",
|
"@radix-ui/react-tooltip": "^1.2.8",
|
||||||
"@tailwindcss/vite": "^4.1.12",
|
"@tailwindcss/vite": "^4.1.11",
|
||||||
"@types/bcryptjs": "^2.4.6",
|
"@types/bcryptjs": "^2.4.6",
|
||||||
"@types/multer": "^2.0.0",
|
"@types/multer": "^2.0.0",
|
||||||
"@uiw/codemirror-extensions-hyper-link": "^4.24.1",
|
"@uiw/codemirror-extensions-hyper-link": "^4.24.1",
|
||||||
@@ -52,8 +52,8 @@
|
|||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cookie-parser": "^1.4.7",
|
"cookie-parser": "^1.4.7",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"dotenv": "^17.2.1",
|
"dotenv": "^17.2.0",
|
||||||
"drizzle-orm": "^0.44.4",
|
"drizzle-orm": "^0.44.3",
|
||||||
"express": "^5.1.0",
|
"express": "^5.1.0",
|
||||||
"jose": "^5.2.3",
|
"jose": "^5.2.3",
|
||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
@@ -62,10 +62,10 @@
|
|||||||
"nanoid": "^5.1.5",
|
"nanoid": "^5.1.5",
|
||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
"node-fetch": "^3.3.2",
|
"node-fetch": "^3.3.2",
|
||||||
"react": "^19.1.1",
|
"react": "^19.1.0",
|
||||||
"react-dom": "^19.1.1",
|
"react-dom": "^19.1.0",
|
||||||
"react-hook-form": "^7.60.0",
|
"react-hook-form": "^7.60.0",
|
||||||
"react-resizable-panels": "^3.0.5",
|
"react-resizable-panels": "^3.0.3",
|
||||||
"react-xtermjs": "^1.0.10",
|
"react-xtermjs": "^1.0.10",
|
||||||
"sonner": "^2.0.7",
|
"sonner": "^2.0.7",
|
||||||
"ssh2": "^1.16.0",
|
"ssh2": "^1.16.0",
|
||||||
@@ -82,8 +82,8 @@
|
|||||||
"@types/express": "^5.0.3",
|
"@types/express": "^5.0.3",
|
||||||
"@types/jsonwebtoken": "^9.0.10",
|
"@types/jsonwebtoken": "^9.0.10",
|
||||||
"@types/node": "^24.0.13",
|
"@types/node": "^24.0.13",
|
||||||
"@types/react": "^19.1.11",
|
"@types/react": "^19.1.8",
|
||||||
"@types/react-dom": "^19.1.7",
|
"@types/react-dom": "^19.1.6",
|
||||||
"@types/ssh2": "^1.15.5",
|
"@types/ssh2": "^1.15.5",
|
||||||
"@types/ws": "^8.18.1",
|
"@types/ws": "^8.18.1",
|
||||||
"@vitejs/plugin-react-swc": "^3.10.2",
|
"@vitejs/plugin-react-swc": "^3.10.2",
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ async function verifyOIDCToken(idToken: string, issuerUrl: string, clientId: str
|
|||||||
const key = await importJWK(publicKey);
|
const key = await importJWK(publicKey);
|
||||||
|
|
||||||
const {payload} = await jwtVerify(idToken, key, {
|
const {payload} = await jwtVerify(idToken, key, {
|
||||||
issuer: issuerUrl,
|
issuer: [issuerUrl, issuerUrl.replace(/\/application\/o\/[^\/]+$/, '')],
|
||||||
audience: clientId,
|
audience: clientId,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,21 @@
|
|||||||
import React, {useState, useEffect} from "react";
|
import React, {useState, useEffect} from "react";
|
||||||
import {cn} from "@/lib/utils.ts";
|
import {cn} from "../../lib/utils.ts";
|
||||||
import {Button} from "@/components/ui/button.tsx";
|
import {Button} from "../../components/ui/button.tsx";
|
||||||
import {Input} from "@/components/ui/input.tsx";
|
import {Input} from "../../components/ui/input.tsx";
|
||||||
import {Label} from "@/components/ui/label.tsx";
|
import {Label} from "../../components/ui/label.tsx";
|
||||||
import {Alert, AlertTitle, AlertDescription} from "@/components/ui/alert.tsx";
|
import {Alert, AlertTitle, AlertDescription} from "../../components/ui/alert.tsx";
|
||||||
import axios from "axios";
|
import {
|
||||||
|
registerUser,
|
||||||
|
loginUser,
|
||||||
|
getUserInfo,
|
||||||
|
getRegistrationAllowed,
|
||||||
|
getOIDCConfig,
|
||||||
|
getUserCount,
|
||||||
|
initiatePasswordReset,
|
||||||
|
verifyPasswordResetCode,
|
||||||
|
completePasswordReset,
|
||||||
|
getOIDCAuthorizeUrl
|
||||||
|
} from "../main-axios.ts";
|
||||||
|
|
||||||
function setCookie(name: string, value: string, days = 7) {
|
function setCookie(name: string, value: string, days = 7) {
|
||||||
const expires = new Date(Date.now() + days * 864e5).toUTCString();
|
const expires = new Date(Date.now() + days * 864e5).toUTCString();
|
||||||
@@ -18,11 +29,7 @@ function getCookie(name: string) {
|
|||||||
}, "");
|
}, "");
|
||||||
}
|
}
|
||||||
|
|
||||||
const apiBase = import.meta.env.DEV ? "http://localhost:8081/users" : "/users";
|
|
||||||
|
|
||||||
const API = axios.create({
|
|
||||||
baseURL: apiBase,
|
|
||||||
});
|
|
||||||
|
|
||||||
interface HomepageAuthProps extends React.ComponentProps<"div"> {
|
interface HomepageAuthProps extends React.ComponentProps<"div"> {
|
||||||
setLoggedIn: (loggedIn: boolean) => void;
|
setLoggedIn: (loggedIn: boolean) => void;
|
||||||
@@ -74,14 +81,14 @@ export function HomepageAuth({
|
|||||||
}, [loggedIn]);
|
}, [loggedIn]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
API.get("/registration-allowed").then(res => {
|
getRegistrationAllowed().then(res => {
|
||||||
setRegistrationAllowed(res.data.allowed);
|
setRegistrationAllowed(res.allowed);
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
API.get("/oidc-config").then((response) => {
|
getOIDCConfig().then((response) => {
|
||||||
if (response.data) {
|
if (response) {
|
||||||
setOidcConfigured(true);
|
setOidcConfigured(true);
|
||||||
} else {
|
} else {
|
||||||
setOidcConfigured(false);
|
setOidcConfigured(false);
|
||||||
@@ -96,8 +103,8 @@ export function HomepageAuth({
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
API.get("/count").then(res => {
|
getUserCount().then(res => {
|
||||||
if (res.data.count === 0) {
|
if (res.count === 0) {
|
||||||
setFirstUser(true);
|
setFirstUser(true);
|
||||||
setTab("signup");
|
setTab("signup");
|
||||||
} else {
|
} else {
|
||||||
@@ -123,7 +130,7 @@ export function HomepageAuth({
|
|||||||
try {
|
try {
|
||||||
let res, meRes;
|
let res, meRes;
|
||||||
if (tab === "login") {
|
if (tab === "login") {
|
||||||
res = await API.post("/login", {username: localUsername, password});
|
res = await loginUser(localUsername, password);
|
||||||
} else {
|
} else {
|
||||||
if (password !== signupConfirmPassword) {
|
if (password !== signupConfirmPassword) {
|
||||||
setError("Passwords do not match");
|
setError("Passwords do not match");
|
||||||
@@ -135,31 +142,37 @@ export function HomepageAuth({
|
|||||||
setLoading(false);
|
setLoading(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await API.post("/create", {username: localUsername, password});
|
|
||||||
res = await API.post("/login", {username: localUsername, password});
|
await registerUser(localUsername, password);
|
||||||
|
res = await loginUser(localUsername, password);
|
||||||
}
|
}
|
||||||
setCookie("jwt", res.data.token);
|
|
||||||
|
if (!res || !res.token) {
|
||||||
|
throw new Error('No token received from login');
|
||||||
|
}
|
||||||
|
|
||||||
|
setCookie("jwt", res.token);
|
||||||
[meRes] = await Promise.all([
|
[meRes] = await Promise.all([
|
||||||
API.get("/me", {headers: {Authorization: `Bearer ${res.data.token}`}}),
|
getUserInfo(),
|
||||||
API.get("/db-health")
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
setInternalLoggedIn(true);
|
setInternalLoggedIn(true);
|
||||||
setLoggedIn(true);
|
setLoggedIn(true);
|
||||||
setIsAdmin(!!meRes.data.is_admin);
|
setIsAdmin(!!meRes.is_admin);
|
||||||
setUsername(meRes.data.username || null);
|
setUsername(meRes.username || null);
|
||||||
setUserId(meRes.data.id || null);
|
setUserId(meRes.userId || null);
|
||||||
setDbError(null);
|
setDbError(null);
|
||||||
onAuthSuccess({
|
onAuthSuccess({
|
||||||
isAdmin: !!meRes.data.is_admin,
|
isAdmin: !!meRes.is_admin,
|
||||||
username: meRes.data.username || null,
|
username: meRes.username || null,
|
||||||
userId: meRes.data.id || null
|
userId: meRes.userId || null
|
||||||
});
|
});
|
||||||
setInternalLoggedIn(true);
|
setInternalLoggedIn(true);
|
||||||
if (tab === "signup") {
|
if (tab === "signup") {
|
||||||
setSignupConfirmPassword("");
|
setSignupConfirmPassword("");
|
||||||
}
|
}
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
setError(err?.response?.data?.error || "Unknown error");
|
setError(err?.response?.data?.error || err?.message || "Unknown error");
|
||||||
setInternalLoggedIn(false);
|
setInternalLoggedIn(false);
|
||||||
setLoggedIn(false);
|
setLoggedIn(false);
|
||||||
setIsAdmin(false);
|
setIsAdmin(false);
|
||||||
@@ -176,29 +189,26 @@ export function HomepageAuth({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function initiatePasswordReset() {
|
async function handleInitiatePasswordReset() {
|
||||||
setError(null);
|
setError(null);
|
||||||
setResetLoading(true);
|
setResetLoading(true);
|
||||||
try {
|
try {
|
||||||
await API.post("/initiate-reset", {username: localUsername});
|
const result = await initiatePasswordReset(localUsername);
|
||||||
setResetStep("verify");
|
setResetStep("verify");
|
||||||
setError(null);
|
setError(null);
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
setError(err?.response?.data?.error || "Failed to initiate password reset");
|
setError(err?.response?.data?.error || err?.message || "Failed to initiate password reset");
|
||||||
} finally {
|
} finally {
|
||||||
setResetLoading(false);
|
setResetLoading(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function verifyResetCode() {
|
async function handleVerifyResetCode() {
|
||||||
setError(null);
|
setError(null);
|
||||||
setResetLoading(true);
|
setResetLoading(true);
|
||||||
try {
|
try {
|
||||||
const response = await API.post("/verify-reset-code", {
|
const response = await verifyPasswordResetCode(localUsername, resetCode);
|
||||||
username: localUsername,
|
setTempToken(response.tempToken);
|
||||||
resetCode: resetCode
|
|
||||||
});
|
|
||||||
setTempToken(response.data.tempToken);
|
|
||||||
setResetStep("newPassword");
|
setResetStep("newPassword");
|
||||||
setError(null);
|
setError(null);
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
@@ -208,7 +218,7 @@ export function HomepageAuth({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function completePasswordReset() {
|
async function handleCompletePasswordReset() {
|
||||||
setError(null);
|
setError(null);
|
||||||
setResetLoading(true);
|
setResetLoading(true);
|
||||||
|
|
||||||
@@ -225,11 +235,7 @@ export function HomepageAuth({
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await API.post("/complete-reset", {
|
await completePasswordReset(localUsername, tempToken, newPassword);
|
||||||
username: localUsername,
|
|
||||||
tempToken: tempToken,
|
|
||||||
newPassword: newPassword
|
|
||||||
});
|
|
||||||
|
|
||||||
setResetStep("initiate");
|
setResetStep("initiate");
|
||||||
setResetCode("");
|
setResetCode("");
|
||||||
@@ -267,8 +273,8 @@ export function HomepageAuth({
|
|||||||
setError(null);
|
setError(null);
|
||||||
setOidcLoading(true);
|
setOidcLoading(true);
|
||||||
try {
|
try {
|
||||||
const authResponse = await API.get("/oidc/authorize");
|
const authResponse = await getOIDCAuthorizeUrl();
|
||||||
const {auth_url: authUrl} = authResponse.data;
|
const {auth_url: authUrl} = authResponse;
|
||||||
|
|
||||||
if (!authUrl || authUrl === 'undefined') {
|
if (!authUrl || authUrl === 'undefined') {
|
||||||
throw new Error('Invalid authorization URL received from backend');
|
throw new Error('Invalid authorization URL received from backend');
|
||||||
@@ -299,18 +305,18 @@ export function HomepageAuth({
|
|||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
setCookie("jwt", token);
|
setCookie("jwt", token);
|
||||||
API.get("/me", {headers: {Authorization: `Bearer ${token}`}})
|
getUserInfo()
|
||||||
.then(meRes => {
|
.then(meRes => {
|
||||||
setInternalLoggedIn(true);
|
setInternalLoggedIn(true);
|
||||||
setLoggedIn(true);
|
setLoggedIn(true);
|
||||||
setIsAdmin(!!meRes.data.is_admin);
|
setIsAdmin(!!meRes.is_admin);
|
||||||
setUsername(meRes.data.username || null);
|
setUsername(meRes.username || null);
|
||||||
setUserId(meRes.data.id || null);
|
setUserId(meRes.id || null);
|
||||||
setDbError(null);
|
setDbError(null);
|
||||||
onAuthSuccess({
|
onAuthSuccess({
|
||||||
isAdmin: !!meRes.data.is_admin,
|
isAdmin: !!meRes.is_admin,
|
||||||
username: meRes.data.username || null,
|
username: meRes.username || null,
|
||||||
userId: meRes.data.id || null
|
userId: meRes.id || null
|
||||||
});
|
});
|
||||||
setInternalLoggedIn(true);
|
setInternalLoggedIn(true);
|
||||||
window.history.replaceState({}, document.title, window.location.pathname);
|
window.history.replaceState({}, document.title, window.location.pathname);
|
||||||
@@ -486,7 +492,7 @@ export function HomepageAuth({
|
|||||||
type="button"
|
type="button"
|
||||||
className="w-full h-11 text-base font-semibold"
|
className="w-full h-11 text-base font-semibold"
|
||||||
disabled={resetLoading || !localUsername.trim()}
|
disabled={resetLoading || !localUsername.trim()}
|
||||||
onClick={initiatePasswordReset}
|
onClick={handleInitiatePasswordReset}
|
||||||
>
|
>
|
||||||
{resetLoading ? Spinner : "Send Reset Code"}
|
{resetLoading ? Spinner : "Send Reset Code"}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -519,7 +525,7 @@ export function HomepageAuth({
|
|||||||
type="button"
|
type="button"
|
||||||
className="w-full h-11 text-base font-semibold"
|
className="w-full h-11 text-base font-semibold"
|
||||||
disabled={resetLoading || resetCode.length !== 6}
|
disabled={resetLoading || resetCode.length !== 6}
|
||||||
onClick={verifyResetCode}
|
onClick={handleVerifyResetCode}
|
||||||
>
|
>
|
||||||
{resetLoading ? Spinner : "Verify Code"}
|
{resetLoading ? Spinner : "Verify Code"}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -598,7 +604,7 @@ export function HomepageAuth({
|
|||||||
type="button"
|
type="button"
|
||||||
className="w-full h-11 text-base font-semibold"
|
className="w-full h-11 text-base font-semibold"
|
||||||
disabled={resetLoading || !newPassword || !confirmPassword}
|
disabled={resetLoading || !newPassword || !confirmPassword}
|
||||||
onClick={completePasswordReset}
|
onClick={handleCompletePasswordReset}
|
||||||
>
|
>
|
||||||
{resetLoading ? Spinner : "Reset Password"}
|
{resetLoading ? Spinner : "Reset Password"}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -226,7 +226,9 @@ export const TerminalComponent = forwardRef<any, SSHTerminalProps>(function SSHT
|
|||||||
|
|
||||||
const cols = terminal.cols;
|
const cols = terminal.cols;
|
||||||
const rows = terminal.rows;
|
const rows = terminal.rows;
|
||||||
const wsUrl = window.location.hostname === 'localhost' ? 'ws://localhost:8082' : `${window.location.protocol === 'https:' ? 'wss' : 'ws'}://${window.location.host}/ssh/websocket/`;
|
const wsUrl = import.meta.env.DEV
|
||||||
|
? 'ws://localhost:8082'
|
||||||
|
: `${window.location.protocol === 'https:' ? 'wss' : 'ws'}://${window.location.host}/ssh/websocket/`;
|
||||||
|
|
||||||
const ws = new WebSocket(wsUrl);
|
const ws = new WebSocket(wsUrl);
|
||||||
webSocketRef.current = ws;
|
webSocketRef.current = ws;
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
import axios from 'axios';
|
import axios, { AxiosError, AxiosInstance } from 'axios';
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// TYPES & INTERFACES
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
interface SSHHostData {
|
interface SSHHostData {
|
||||||
name?: string;
|
name?: string;
|
||||||
@@ -93,47 +97,70 @@ interface FileManagerShortcut {
|
|||||||
path: string;
|
path: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface FileManagerOperation {
|
||||||
|
name: string;
|
||||||
|
path: string;
|
||||||
|
isSSH: boolean;
|
||||||
|
sshSessionId?: string;
|
||||||
|
hostId: number;
|
||||||
|
}
|
||||||
|
|
||||||
export type ServerStatus = {
|
export type ServerStatus = {
|
||||||
status: 'online' | 'offline';
|
status: 'online' | 'offline';
|
||||||
lastChecked: string;
|
lastChecked: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
interface CpuMetrics {
|
||||||
|
percent: number | null;
|
||||||
|
cores: number | null;
|
||||||
|
load: [number, number, number] | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MemoryMetrics {
|
||||||
|
percent: number | null;
|
||||||
|
usedGiB: number | null;
|
||||||
|
totalGiB: number | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DiskMetrics {
|
||||||
|
percent: number | null;
|
||||||
|
usedHuman: string | null;
|
||||||
|
totalHuman: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
export type ServerMetrics = {
|
export type ServerMetrics = {
|
||||||
cpu: { percent: number | null; cores: number | null; load: [number, number, number] | null };
|
cpu: CpuMetrics;
|
||||||
memory: { percent: number | null; usedGiB: number | null; totalGiB: number | null };
|
memory: MemoryMetrics;
|
||||||
disk: { percent: number | null; usedHuman: string | null; totalHuman: string | null };
|
disk: DiskMetrics;
|
||||||
lastChecked: string;
|
lastChecked: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const isLocalhost = window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1';
|
interface AuthResponse {
|
||||||
|
token: string;
|
||||||
|
}
|
||||||
|
|
||||||
const sshHostApi = axios.create({
|
interface UserInfo {
|
||||||
baseURL: isLocalhost ? 'http://localhost:8081' : '',
|
id: string;
|
||||||
headers: {
|
username: string;
|
||||||
'Content-Type': 'application/json',
|
is_admin: boolean;
|
||||||
},
|
}
|
||||||
});
|
|
||||||
|
|
||||||
const tunnelApi = axios.create({
|
interface UserCount {
|
||||||
baseURL: isLocalhost ? 'http://localhost:8083' : '',
|
count: number;
|
||||||
headers: {
|
}
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const fileManagerApi = axios.create({
|
interface OIDCAuthorize {
|
||||||
baseURL: isLocalhost ? 'http://localhost:8084' : '',
|
auth_url: string;
|
||||||
headers: {
|
}
|
||||||
'Content-Type': 'application/json',
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const statsApi = axios.create({
|
// ============================================================================
|
||||||
baseURL: isLocalhost ? 'http://localhost:8085' : '',
|
// UTILITY FUNCTIONS
|
||||||
headers: {
|
// ============================================================================
|
||||||
'Content-Type': 'application/json',
|
|
||||||
}
|
function setCookie(name: string, value: string, days = 7): void {
|
||||||
})
|
const expires = new Date(Date.now() + days * 864e5).toUTCString();
|
||||||
|
document.cookie = `${name}=${encodeURIComponent(value)}; expires=${expires}; path=/`;
|
||||||
|
}
|
||||||
|
|
||||||
function getCookie(name: string): string | undefined {
|
function getCookie(name: string): string | undefined {
|
||||||
const value = `; ${document.cookie}`;
|
const value = `; ${document.cookie}`;
|
||||||
@@ -141,44 +168,116 @@ function getCookie(name: string): string | undefined {
|
|||||||
if (parts.length === 2) return parts.pop()?.split(';').shift();
|
if (parts.length === 2) return parts.pop()?.split(';').shift();
|
||||||
}
|
}
|
||||||
|
|
||||||
sshHostApi.interceptors.request.use((config) => {
|
function createApiInstance(baseURL: string): AxiosInstance {
|
||||||
const token = getCookie('jwt');
|
const instance = axios.create({
|
||||||
if (token) {
|
baseURL,
|
||||||
config.headers.Authorization = `Bearer ${token}`;
|
headers: { 'Content-Type': 'application/json' },
|
||||||
}
|
timeout: 30000,
|
||||||
return config;
|
});
|
||||||
});
|
|
||||||
|
|
||||||
statsApi.interceptors.request.use((config) => {
|
instance.interceptors.request.use((config) => {
|
||||||
const token = getCookie('jwt');
|
const token = getCookie('jwt');
|
||||||
if (token) {
|
if (token) {
|
||||||
config.headers.Authorization = `Bearer ${token}`;
|
config.headers.Authorization = `Bearer ${token}`;
|
||||||
}
|
}
|
||||||
return config;
|
return config;
|
||||||
});
|
});
|
||||||
|
|
||||||
tunnelApi.interceptors.request.use((config) => {
|
instance.interceptors.response.use(
|
||||||
const token = getCookie('jwt');
|
(response) => response,
|
||||||
if (token) {
|
(error: AxiosError) => {
|
||||||
config.headers.Authorization = `Bearer ${token}`;
|
if (error.response?.status === 401) {
|
||||||
|
// Token expired or invalid - clear cookie
|
||||||
|
document.cookie = 'jwt=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;';
|
||||||
}
|
}
|
||||||
return config;
|
return Promise.reject(error);
|
||||||
});
|
}
|
||||||
|
);
|
||||||
|
|
||||||
fileManagerApi.interceptors.request.use((config) => {
|
return instance;
|
||||||
const token = getCookie('jwt');
|
}
|
||||||
if (token) {
|
|
||||||
config.headers.Authorization = `Bearer ${token}`;
|
// ============================================================================
|
||||||
|
// API INSTANCES
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
const isDev = process.env.NODE_ENV === 'development' || window.location.hostname === 'localhost';
|
||||||
|
|
||||||
|
// SSH Host Management API (port 8081)
|
||||||
|
export const sshHostApi = createApiInstance(
|
||||||
|
isDev ? 'http://localhost:8081/ssh' : '/ssh'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Tunnel Management API (port 8083)
|
||||||
|
export const tunnelApi = createApiInstance(
|
||||||
|
isDev ? 'http://localhost:8083/ssh' : '/ssh'
|
||||||
|
);
|
||||||
|
|
||||||
|
// File Manager Operations API (port 8084) - SSH file operations
|
||||||
|
export const fileManagerApi = createApiInstance(
|
||||||
|
isDev ? 'http://localhost:8084/ssh/file_manager' : '/ssh/file_manager'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Server Statistics API (port 8085)
|
||||||
|
export const statsApi = createApiInstance(
|
||||||
|
isDev ? 'http://localhost:8085' : ''
|
||||||
|
);
|
||||||
|
|
||||||
|
// Authentication API (port 8081)
|
||||||
|
export const authApi = createApiInstance(
|
||||||
|
isDev ? 'http://localhost:8081/users' : '/users'
|
||||||
|
);
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// ERROR HANDLING
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
class ApiError extends Error {
|
||||||
|
constructor(
|
||||||
|
message: string,
|
||||||
|
public status?: number,
|
||||||
|
public code?: string
|
||||||
|
) {
|
||||||
|
super(message);
|
||||||
|
this.name = 'ApiError';
|
||||||
}
|
}
|
||||||
return config;
|
}
|
||||||
});
|
|
||||||
|
function handleApiError(error: unknown, operation: string): never {
|
||||||
|
if (axios.isAxiosError(error)) {
|
||||||
|
const status = error.response?.status;
|
||||||
|
const message = error.response?.data?.error || error.message;
|
||||||
|
|
||||||
|
if (status === 401) {
|
||||||
|
throw new ApiError('Authentication required', 401);
|
||||||
|
} else if (status === 403) {
|
||||||
|
throw new ApiError('Access denied', 403);
|
||||||
|
} else if (status === 404) {
|
||||||
|
throw new ApiError('Resource not found', 404);
|
||||||
|
} else if (status && status >= 500) {
|
||||||
|
throw new ApiError('Server error occurred', status);
|
||||||
|
} else {
|
||||||
|
throw new ApiError(message || `Failed to ${operation}`, status);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error instanceof ApiError) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new ApiError(`Unexpected error during ${operation}: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// SSH HOST MANAGEMENT
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
export async function getSSHHosts(): Promise<SSHHost[]> {
|
export async function getSSHHosts(): Promise<SSHHost[]> {
|
||||||
try {
|
try {
|
||||||
const response = await sshHostApi.get('/ssh/db/host');
|
const response = await sshHostApi.get('/db/host');
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw error;
|
handleApiError(error, 'fetch SSH hosts');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -216,23 +315,20 @@ export async function createSSHHost(hostData: SSHHostData): Promise<SSHHost> {
|
|||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('key', hostData.key);
|
formData.append('key', hostData.key);
|
||||||
|
|
||||||
const dataWithoutFile = {...submitData};
|
const dataWithoutFile = { ...submitData };
|
||||||
delete dataWithoutFile.key;
|
delete dataWithoutFile.key;
|
||||||
formData.append('data', JSON.stringify(dataWithoutFile));
|
formData.append('data', JSON.stringify(dataWithoutFile));
|
||||||
|
|
||||||
const response = await sshHostApi.post('/ssh/db/host', formData, {
|
const response = await sshHostApi.post('/db/host', formData, {
|
||||||
headers: {
|
headers: { 'Content-Type': 'multipart/form-data' },
|
||||||
'Content-Type': 'multipart/form-data',
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return response.data;
|
return response.data;
|
||||||
} else {
|
} else {
|
||||||
const response = await sshHostApi.post('/ssh/db/host', submitData);
|
const response = await sshHostApi.post('/db/host', submitData);
|
||||||
return response.data;
|
return response.data;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw error;
|
handleApiError(error, 'create SSH host');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -269,23 +365,20 @@ export async function updateSSHHost(hostId: number, hostData: SSHHostData): Prom
|
|||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('key', hostData.key);
|
formData.append('key', hostData.key);
|
||||||
|
|
||||||
const dataWithoutFile = {...submitData};
|
const dataWithoutFile = { ...submitData };
|
||||||
delete dataWithoutFile.key;
|
delete dataWithoutFile.key;
|
||||||
formData.append('data', JSON.stringify(dataWithoutFile));
|
formData.append('data', JSON.stringify(dataWithoutFile));
|
||||||
|
|
||||||
const response = await sshHostApi.put(`/ssh/db/host/${hostId}`, formData, {
|
const response = await sshHostApi.put(`/db/host/${hostId}`, formData, {
|
||||||
headers: {
|
headers: { 'Content-Type': 'multipart/form-data' },
|
||||||
'Content-Type': 'multipart/form-data',
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return response.data;
|
return response.data;
|
||||||
} else {
|
} else {
|
||||||
const response = await sshHostApi.put(`/ssh/db/host/${hostId}`, submitData);
|
const response = await sshHostApi.put(`/db/host/${hostId}`, submitData);
|
||||||
return response.data;
|
return response.data;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw error;
|
handleApiError(error, 'update SSH host');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -296,37 +389,41 @@ export async function bulkImportSSHHosts(hosts: SSHHostData[]): Promise<{
|
|||||||
errors: string[];
|
errors: string[];
|
||||||
}> {
|
}> {
|
||||||
try {
|
try {
|
||||||
const response = await sshHostApi.post('/ssh/bulk-import', {hosts});
|
const response = await sshHostApi.post('/bulk-import', { hosts });
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw error;
|
handleApiError(error, 'bulk import SSH hosts');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function deleteSSHHost(hostId: number): Promise<any> {
|
export async function deleteSSHHost(hostId: number): Promise<any> {
|
||||||
try {
|
try {
|
||||||
const response = await sshHostApi.delete(`/ssh/db/host/${hostId}`);
|
const response = await sshHostApi.delete(`/db/host/${hostId}`);
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw error;
|
handleApiError(error, 'delete SSH host');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getSSHHostById(hostId: number): Promise<SSHHost> {
|
export async function getSSHHostById(hostId: number): Promise<SSHHost> {
|
||||||
try {
|
try {
|
||||||
const response = await sshHostApi.get(`/ssh/db/host/${hostId}`);
|
const response = await sshHostApi.get(`/db/host/${hostId}`);
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw error;
|
handleApiError(error, 'fetch SSH host');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// TUNNEL MANAGEMENT
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
export async function getTunnelStatuses(): Promise<Record<string, TunnelStatus>> {
|
export async function getTunnelStatuses(): Promise<Record<string, TunnelStatus>> {
|
||||||
try {
|
try {
|
||||||
const response = await tunnelApi.get('/ssh/tunnel/status');
|
const response = await tunnelApi.get('/tunnel/status');
|
||||||
return response.data || {};
|
return response.data || {};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw error;
|
handleApiError(error, 'fetch tunnel statuses');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -337,148 +434,121 @@ export async function getTunnelStatusByName(tunnelName: string): Promise<TunnelS
|
|||||||
|
|
||||||
export async function connectTunnel(tunnelConfig: TunnelConfig): Promise<any> {
|
export async function connectTunnel(tunnelConfig: TunnelConfig): Promise<any> {
|
||||||
try {
|
try {
|
||||||
const response = await tunnelApi.post('/ssh/tunnel/connect', tunnelConfig);
|
const response = await tunnelApi.post('/tunnel/connect', tunnelConfig);
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw error;
|
handleApiError(error, 'connect tunnel');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function disconnectTunnel(tunnelName: string): Promise<any> {
|
export async function disconnectTunnel(tunnelName: string): Promise<any> {
|
||||||
try {
|
try {
|
||||||
const response = await tunnelApi.post('/ssh/tunnel/disconnect', {tunnelName});
|
const response = await tunnelApi.post('/tunnel/disconnect', { tunnelName });
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw error;
|
handleApiError(error, 'disconnect tunnel');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function cancelTunnel(tunnelName: string): Promise<any> {
|
export async function cancelTunnel(tunnelName: string): Promise<any> {
|
||||||
try {
|
try {
|
||||||
const response = await tunnelApi.post('/ssh/tunnel/cancel', {tunnelName});
|
const response = await tunnelApi.post('/tunnel/cancel', { tunnelName });
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw error;
|
handleApiError(error, 'cancel tunnel');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// FILE MANAGER METADATA (Recent, Pinned, Shortcuts)
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
export async function getFileManagerRecent(hostId: number): Promise<FileManagerFile[]> {
|
export async function getFileManagerRecent(hostId: number): Promise<FileManagerFile[]> {
|
||||||
try {
|
try {
|
||||||
const response = await sshHostApi.get(`/ssh/file_manager/recent?hostId=${hostId}`);
|
const response = await sshHostApi.get(`/file_manager/recent?hostId=${hostId}`);
|
||||||
return response.data || [];
|
return response.data || [];
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
// Don't throw for file manager metadata - return empty array
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function addFileManagerRecent(file: {
|
export async function addFileManagerRecent(file: FileManagerOperation): Promise<any> {
|
||||||
name: string;
|
|
||||||
path: string;
|
|
||||||
isSSH: boolean;
|
|
||||||
sshSessionId?: string;
|
|
||||||
hostId: number
|
|
||||||
}): Promise<any> {
|
|
||||||
try {
|
try {
|
||||||
const response = await sshHostApi.post('/ssh/file_manager/recent', file);
|
const response = await sshHostApi.post('/file_manager/recent', file);
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw error;
|
handleApiError(error, 'add recent file');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function removeFileManagerRecent(file: {
|
export async function removeFileManagerRecent(file: FileManagerOperation): Promise<any> {
|
||||||
name: string;
|
|
||||||
path: string;
|
|
||||||
isSSH: boolean;
|
|
||||||
sshSessionId?: string;
|
|
||||||
hostId: number
|
|
||||||
}): Promise<any> {
|
|
||||||
try {
|
try {
|
||||||
const response = await sshHostApi.delete('/ssh/file_manager/recent', {data: file});
|
const response = await sshHostApi.delete('/file_manager/recent', { data: file });
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw error;
|
handleApiError(error, 'remove recent file');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getFileManagerPinned(hostId: number): Promise<FileManagerFile[]> {
|
export async function getFileManagerPinned(hostId: number): Promise<FileManagerFile[]> {
|
||||||
try {
|
try {
|
||||||
const response = await sshHostApi.get(`/ssh/file_manager/pinned?hostId=${hostId}`);
|
const response = await sshHostApi.get(`/file_manager/pinned?hostId=${hostId}`);
|
||||||
return response.data || [];
|
return response.data || [];
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function addFileManagerPinned(file: {
|
export async function addFileManagerPinned(file: FileManagerOperation): Promise<any> {
|
||||||
name: string;
|
|
||||||
path: string;
|
|
||||||
isSSH: boolean;
|
|
||||||
sshSessionId?: string;
|
|
||||||
hostId: number
|
|
||||||
}): Promise<any> {
|
|
||||||
try {
|
try {
|
||||||
const response = await sshHostApi.post('/ssh/file_manager/pinned', file);
|
const response = await sshHostApi.post('/file_manager/pinned', file);
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw error;
|
handleApiError(error, 'add pinned file');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function removeFileManagerPinned(file: {
|
export async function removeFileManagerPinned(file: FileManagerOperation): Promise<any> {
|
||||||
name: string;
|
|
||||||
path: string;
|
|
||||||
isSSH: boolean;
|
|
||||||
sshSessionId?: string;
|
|
||||||
hostId: number
|
|
||||||
}): Promise<any> {
|
|
||||||
try {
|
try {
|
||||||
const response = await sshHostApi.delete('/ssh/file_manager/pinned', {data: file});
|
const response = await sshHostApi.delete('/file_manager/pinned', { data: file });
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw error;
|
handleApiError(error, 'remove pinned file');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getFileManagerShortcuts(hostId: number): Promise<FileManagerShortcut[]> {
|
export async function getFileManagerShortcuts(hostId: number): Promise<FileManagerShortcut[]> {
|
||||||
try {
|
try {
|
||||||
const response = await sshHostApi.get(`/ssh/file_manager/shortcuts?hostId=${hostId}`);
|
const response = await sshHostApi.get(`/file_manager/shortcuts?hostId=${hostId}`);
|
||||||
return response.data || [];
|
return response.data || [];
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function addFileManagerShortcut(shortcut: {
|
export async function addFileManagerShortcut(shortcut: FileManagerOperation): Promise<any> {
|
||||||
name: string;
|
|
||||||
path: string;
|
|
||||||
isSSH: boolean;
|
|
||||||
sshSessionId?: string;
|
|
||||||
hostId: number
|
|
||||||
}): Promise<any> {
|
|
||||||
try {
|
try {
|
||||||
const response = await sshHostApi.post('/ssh/file_manager/shortcuts', shortcut);
|
const response = await sshHostApi.post('/file_manager/shortcuts', shortcut);
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw error;
|
handleApiError(error, 'add shortcut');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function removeFileManagerShortcut(shortcut: {
|
export async function removeFileManagerShortcut(shortcut: FileManagerOperation): Promise<any> {
|
||||||
name: string;
|
|
||||||
path: string;
|
|
||||||
isSSH: boolean;
|
|
||||||
sshSessionId?: string;
|
|
||||||
hostId: number
|
|
||||||
}): Promise<any> {
|
|
||||||
try {
|
try {
|
||||||
const response = await sshHostApi.delete('/ssh/file_manager/shortcuts', {data: shortcut});
|
const response = await sshHostApi.delete('/file_manager/shortcuts', { data: shortcut });
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw error;
|
handleApiError(error, 'remove shortcut');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// SSH FILE OPERATIONS
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
export async function connectSSH(sessionId: string, config: {
|
export async function connectSSH(sessionId: string, config: {
|
||||||
ip: string;
|
ip: string;
|
||||||
port: number;
|
port: number;
|
||||||
@@ -488,61 +558,61 @@ export async function connectSSH(sessionId: string, config: {
|
|||||||
keyPassword?: string;
|
keyPassword?: string;
|
||||||
}): Promise<any> {
|
}): Promise<any> {
|
||||||
try {
|
try {
|
||||||
const response = await fileManagerApi.post('/ssh/file_manager/ssh/connect', {
|
const response = await fileManagerApi.post('/ssh/connect', {
|
||||||
sessionId,
|
sessionId,
|
||||||
...config
|
...config
|
||||||
});
|
});
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw error;
|
handleApiError(error, 'connect SSH');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function disconnectSSH(sessionId: string): Promise<any> {
|
export async function disconnectSSH(sessionId: string): Promise<any> {
|
||||||
try {
|
try {
|
||||||
const response = await fileManagerApi.post('/ssh/file_manager/ssh/disconnect', {sessionId});
|
const response = await fileManagerApi.post('/ssh/disconnect', { sessionId });
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw error;
|
handleApiError(error, 'disconnect SSH');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getSSHStatus(sessionId: string): Promise<{ connected: boolean }> {
|
export async function getSSHStatus(sessionId: string): Promise<{ connected: boolean }> {
|
||||||
try {
|
try {
|
||||||
const response = await fileManagerApi.get('/ssh/file_manager/ssh/status', {
|
const response = await fileManagerApi.get('/ssh/status', {
|
||||||
params: {sessionId}
|
params: { sessionId }
|
||||||
});
|
});
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw error;
|
handleApiError(error, 'get SSH status');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function listSSHFiles(sessionId: string, path: string): Promise<any[]> {
|
export async function listSSHFiles(sessionId: string, path: string): Promise<any[]> {
|
||||||
try {
|
try {
|
||||||
const response = await fileManagerApi.get('/ssh/file_manager/ssh/listFiles', {
|
const response = await fileManagerApi.get('/ssh/listFiles', {
|
||||||
params: {sessionId, path}
|
params: { sessionId, path }
|
||||||
});
|
});
|
||||||
return response.data || [];
|
return response.data || [];
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw error;
|
handleApiError(error, 'list SSH files');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function readSSHFile(sessionId: string, path: string): Promise<{ content: string; path: string }> {
|
export async function readSSHFile(sessionId: string, path: string): Promise<{ content: string; path: string }> {
|
||||||
try {
|
try {
|
||||||
const response = await fileManagerApi.get('/ssh/file_manager/ssh/readFile', {
|
const response = await fileManagerApi.get('/ssh/readFile', {
|
||||||
params: {sessionId, path}
|
params: { sessionId, path }
|
||||||
});
|
});
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw error;
|
handleApiError(error, 'read SSH file');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function writeSSHFile(sessionId: string, path: string, content: string): Promise<any> {
|
export async function writeSSHFile(sessionId: string, path: string, content: string): Promise<any> {
|
||||||
try {
|
try {
|
||||||
const response = await fileManagerApi.post('/ssh/file_manager/ssh/writeFile', {
|
const response = await fileManagerApi.post('/ssh/writeFile', {
|
||||||
sessionId,
|
sessionId,
|
||||||
path,
|
path,
|
||||||
content
|
content
|
||||||
@@ -554,13 +624,13 @@ export async function writeSSHFile(sessionId: string, path: string, content: str
|
|||||||
throw new Error('File write operation did not return success status');
|
throw new Error('File write operation did not return success status');
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw error;
|
handleApiError(error, 'write SSH file');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function uploadSSHFile(sessionId: string, path: string, fileName: string, content: string): Promise<any> {
|
export async function uploadSSHFile(sessionId: string, path: string, fileName: string, content: string): Promise<any> {
|
||||||
try {
|
try {
|
||||||
const response = await fileManagerApi.post('/ssh/file_manager/ssh/uploadFile', {
|
const response = await fileManagerApi.post('/ssh/uploadFile', {
|
||||||
sessionId,
|
sessionId,
|
||||||
path,
|
path,
|
||||||
fileName,
|
fileName,
|
||||||
@@ -568,13 +638,13 @@ export async function uploadSSHFile(sessionId: string, path: string, fileName: s
|
|||||||
});
|
});
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw error;
|
handleApiError(error, 'upload SSH file');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createSSHFile(sessionId: string, path: string, fileName: string, content: string = ''): Promise<any> {
|
export async function createSSHFile(sessionId: string, path: string, fileName: string, content: string = ''): Promise<any> {
|
||||||
try {
|
try {
|
||||||
const response = await fileManagerApi.post('/ssh/file_manager/ssh/createFile', {
|
const response = await fileManagerApi.post('/ssh/createFile', {
|
||||||
sessionId,
|
sessionId,
|
||||||
path,
|
path,
|
||||||
fileName,
|
fileName,
|
||||||
@@ -582,26 +652,26 @@ export async function createSSHFile(sessionId: string, path: string, fileName: s
|
|||||||
});
|
});
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw error;
|
handleApiError(error, 'create SSH file');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createSSHFolder(sessionId: string, path: string, folderName: string): Promise<any> {
|
export async function createSSHFolder(sessionId: string, path: string, folderName: string): Promise<any> {
|
||||||
try {
|
try {
|
||||||
const response = await fileManagerApi.post('/ssh/file_manager/ssh/createFolder', {
|
const response = await fileManagerApi.post('/ssh/createFolder', {
|
||||||
sessionId,
|
sessionId,
|
||||||
path,
|
path,
|
||||||
folderName
|
folderName
|
||||||
});
|
});
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw error;
|
handleApiError(error, 'create SSH folder');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function deleteSSHItem(sessionId: string, path: string, isDirectory: boolean): Promise<any> {
|
export async function deleteSSHItem(sessionId: string, path: string, isDirectory: boolean): Promise<any> {
|
||||||
try {
|
try {
|
||||||
const response = await fileManagerApi.delete('/ssh/file_manager/ssh/deleteItem', {
|
const response = await fileManagerApi.delete('/ssh/deleteItem', {
|
||||||
data: {
|
data: {
|
||||||
sessionId,
|
sessionId,
|
||||||
path,
|
path,
|
||||||
@@ -610,31 +680,33 @@ export async function deleteSSHItem(sessionId: string, path: string, isDirectory
|
|||||||
});
|
});
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw error;
|
handleApiError(error, 'delete SSH item');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function renameSSHItem(sessionId: string, oldPath: string, newName: string): Promise<any> {
|
export async function renameSSHItem(sessionId: string, oldPath: string, newName: string): Promise<any> {
|
||||||
try {
|
try {
|
||||||
const response = await fileManagerApi.put('/ssh/file_manager/ssh/renameItem', {
|
const response = await fileManagerApi.put('/ssh/renameItem', {
|
||||||
sessionId,
|
sessionId,
|
||||||
oldPath,
|
oldPath,
|
||||||
newName
|
newName
|
||||||
});
|
});
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw error;
|
handleApiError(error, 'rename SSH item');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export {sshHostApi, tunnelApi, fileManagerApi};
|
// ============================================================================
|
||||||
|
// SERVER STATISTICS
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
export async function getAllServerStatuses(): Promise<Record<number, ServerStatus>> {
|
export async function getAllServerStatuses(): Promise<Record<number, ServerStatus>> {
|
||||||
try {
|
try {
|
||||||
const response = await statsApi.get('/status');
|
const response = await statsApi.get('/status');
|
||||||
return response.data || {};
|
return response.data || {};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw error;
|
handleApiError(error, 'fetch server statuses');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -643,7 +715,7 @@ export async function getServerStatusById(id: number): Promise<ServerStatus> {
|
|||||||
const response = await statsApi.get(`/status/${id}`);
|
const response = await statsApi.get(`/status/${id}`);
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw error;
|
handleApiError(error, 'fetch server status');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -652,6 +724,100 @@ export async function getServerMetricsById(id: number): Promise<ServerMetrics> {
|
|||||||
const response = await statsApi.get(`/metrics/${id}`);
|
const response = await statsApi.get(`/metrics/${id}`);
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw error;
|
handleApiError(error, 'fetch server metrics');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// AUTHENTICATION
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
export async function registerUser(username: string, password: string): Promise<any> {
|
||||||
|
try {
|
||||||
|
const response = await authApi.post('/create', { username, password });
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
handleApiError(error, 'register user');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function loginUser(username: string, password: string): Promise<AuthResponse> {
|
||||||
|
try {
|
||||||
|
const response = await authApi.post('/login', { username, password });
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
handleApiError(error, 'login user');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getUserInfo(): Promise<UserInfo> {
|
||||||
|
try {
|
||||||
|
const response = await authApi.get('/me');
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
handleApiError(error, 'fetch user info');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getRegistrationAllowed(): Promise<{ allowed: boolean }> {
|
||||||
|
try {
|
||||||
|
const response = await authApi.get('/registration-allowed');
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
handleApiError(error, 'check registration status');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getOIDCConfig(): Promise<any> {
|
||||||
|
try {
|
||||||
|
const response = await authApi.get('/oidc-config');
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
handleApiError(error, 'fetch OIDC config');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getUserCount(): Promise<UserCount> {
|
||||||
|
try {
|
||||||
|
const response = await authApi.get('/count');
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
handleApiError(error, 'fetch user count');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function initiatePasswordReset(username: string): Promise<any> {
|
||||||
|
try {
|
||||||
|
const response = await authApi.post('/initiate-reset', { username });
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
handleApiError(error, 'initiate password reset');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function verifyPasswordResetCode(username: string, resetCode: string): Promise<any> {
|
||||||
|
try {
|
||||||
|
const response = await authApi.post('/verify-reset-code', { username, resetCode });
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
handleApiError(error, 'verify reset code');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function completePasswordReset(username: string, tempToken: string, newPassword: string): Promise<any> {
|
||||||
|
try {
|
||||||
|
const response = await authApi.post('/complete-reset', { username, tempToken, newPassword });
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
handleApiError(error, 'complete password reset');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getOIDCAuthorizeUrl(): Promise<OIDCAuthorize> {
|
||||||
|
try {
|
||||||
|
const response = await authApi.get('/oidc/authorize');
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
handleApiError(error, 'get OIDC authorize URL');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user