diff --git a/src/ui/Homepage/HomepageAuth.tsx b/src/ui/Homepage/HomepageAuth.tsx index 43241e55..8cb5a221 100644 --- a/src/ui/Homepage/HomepageAuth.tsx +++ b/src/ui/Homepage/HomepageAuth.tsx @@ -1,10 +1,21 @@ import React, {useState, useEffect} from "react"; -import {cn} from "@/lib/utils.ts"; -import {Button} from "@/components/ui/button.tsx"; -import {Input} from "@/components/ui/input.tsx"; -import {Label} from "@/components/ui/label.tsx"; -import {Alert, AlertTitle, AlertDescription} from "@/components/ui/alert.tsx"; -import axios from "axios"; +import {cn} from "../../lib/utils.ts"; +import {Button} from "../../components/ui/button.tsx"; +import {Input} from "../../components/ui/input.tsx"; +import {Label} from "../../components/ui/label.tsx"; +import {Alert, AlertTitle, AlertDescription} from "../../components/ui/alert.tsx"; +import { + registerUser, + loginUser, + getUserInfo, + getRegistrationAllowed, + getOIDCConfig, + getUserCount, + initiatePasswordReset, + verifyPasswordResetCode, + completePasswordReset, + getOIDCAuthorizeUrl +} from "../main-axios.ts"; function setCookie(name: string, value: string, days = 7) { 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"> { setLoggedIn: (loggedIn: boolean) => void; @@ -74,14 +81,14 @@ export function HomepageAuth({ }, [loggedIn]); useEffect(() => { - API.get("/registration-allowed").then(res => { - setRegistrationAllowed(res.data.allowed); + getRegistrationAllowed().then(res => { + setRegistrationAllowed(res.allowed); }); }, []); useEffect(() => { - API.get("/oidc-config").then((response) => { - if (response.data) { + getOIDCConfig().then((response) => { + if (response) { setOidcConfigured(true); } else { setOidcConfigured(false); @@ -96,8 +103,8 @@ export function HomepageAuth({ }, []); useEffect(() => { - API.get("/count").then(res => { - if (res.data.count === 0) { + getUserCount().then(res => { + if (res.count === 0) { setFirstUser(true); setTab("signup"); } else { @@ -123,7 +130,7 @@ export function HomepageAuth({ try { let res, meRes; if (tab === "login") { - res = await API.post("/login", {username: localUsername, password}); + res = await loginUser(localUsername, password); } else { if (password !== signupConfirmPassword) { setError("Passwords do not match"); @@ -135,31 +142,37 @@ export function HomepageAuth({ setLoading(false); 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([ - API.get("/me", {headers: {Authorization: `Bearer ${res.data.token}`}}), - API.get("/db-health") + getUserInfo(), ]); + setInternalLoggedIn(true); setLoggedIn(true); - setIsAdmin(!!meRes.data.is_admin); - setUsername(meRes.data.username || null); - setUserId(meRes.data.id || null); + setIsAdmin(!!meRes.is_admin); + setUsername(meRes.username || null); + setUserId(meRes.userId || null); setDbError(null); onAuthSuccess({ - isAdmin: !!meRes.data.is_admin, - username: meRes.data.username || null, - userId: meRes.data.id || null + isAdmin: !!meRes.is_admin, + username: meRes.username || null, + userId: meRes.userId || null }); setInternalLoggedIn(true); if (tab === "signup") { setSignupConfirmPassword(""); } } catch (err: any) { - setError(err?.response?.data?.error || "Unknown error"); + setError(err?.response?.data?.error || err?.message || "Unknown error"); setInternalLoggedIn(false); setLoggedIn(false); setIsAdmin(false); @@ -176,29 +189,26 @@ export function HomepageAuth({ } } - async function initiatePasswordReset() { + async function handleInitiatePasswordReset() { setError(null); setResetLoading(true); try { - await API.post("/initiate-reset", {username: localUsername}); + const result = await initiatePasswordReset(localUsername); setResetStep("verify"); setError(null); } 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 { setResetLoading(false); } } - async function verifyResetCode() { + async function handleVerifyResetCode() { setError(null); setResetLoading(true); try { - const response = await API.post("/verify-reset-code", { - username: localUsername, - resetCode: resetCode - }); - setTempToken(response.data.tempToken); + const response = await verifyPasswordResetCode(localUsername, resetCode); + setTempToken(response.tempToken); setResetStep("newPassword"); setError(null); } catch (err: any) { @@ -208,7 +218,7 @@ export function HomepageAuth({ } } - async function completePasswordReset() { + async function handleCompletePasswordReset() { setError(null); setResetLoading(true); @@ -225,11 +235,7 @@ export function HomepageAuth({ } try { - await API.post("/complete-reset", { - username: localUsername, - tempToken: tempToken, - newPassword: newPassword - }); + await completePasswordReset(localUsername, tempToken, newPassword); setResetStep("initiate"); setResetCode(""); @@ -267,8 +273,8 @@ export function HomepageAuth({ setError(null); setOidcLoading(true); try { - const authResponse = await API.get("/oidc/authorize"); - const {auth_url: authUrl} = authResponse.data; + const authResponse = await getOIDCAuthorizeUrl(); + const {auth_url: authUrl} = authResponse; if (!authUrl || authUrl === 'undefined') { throw new Error('Invalid authorization URL received from backend'); @@ -299,18 +305,18 @@ export function HomepageAuth({ setError(null); setCookie("jwt", token); - API.get("/me", {headers: {Authorization: `Bearer ${token}`}}) + getUserInfo() .then(meRes => { setInternalLoggedIn(true); setLoggedIn(true); - setIsAdmin(!!meRes.data.is_admin); - setUsername(meRes.data.username || null); - setUserId(meRes.data.id || null); + setIsAdmin(!!meRes.is_admin); + setUsername(meRes.username || null); + setUserId(meRes.id || null); setDbError(null); onAuthSuccess({ - isAdmin: !!meRes.data.is_admin, - username: meRes.data.username || null, - userId: meRes.data.id || null + isAdmin: !!meRes.is_admin, + username: meRes.username || null, + userId: meRes.id || null }); setInternalLoggedIn(true); window.history.replaceState({}, document.title, window.location.pathname); @@ -486,7 +492,7 @@ export function HomepageAuth({ type="button" className="w-full h-11 text-base font-semibold" disabled={resetLoading || !localUsername.trim()} - onClick={initiatePasswordReset} + onClick={handleInitiatePasswordReset} > {resetLoading ? Spinner : "Send Reset Code"} @@ -519,7 +525,7 @@ export function HomepageAuth({ type="button" className="w-full h-11 text-base font-semibold" disabled={resetLoading || resetCode.length !== 6} - onClick={verifyResetCode} + onClick={handleVerifyResetCode} > {resetLoading ? Spinner : "Verify Code"} @@ -598,7 +604,7 @@ export function HomepageAuth({ type="button" className="w-full h-11 text-base font-semibold" disabled={resetLoading || !newPassword || !confirmPassword} - onClick={completePasswordReset} + onClick={handleCompletePasswordReset} > {resetLoading ? Spinner : "Reset Password"} diff --git a/src/ui/main-axios.ts b/src/ui/main-axios.ts index c1192d85..771cb493 100644 --- a/src/ui/main-axios.ts +++ b/src/ui/main-axios.ts @@ -105,35 +105,51 @@ export type ServerMetrics = { lastChecked: string; }; -const isLocalhost = window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1'; +interface AuthResponse { + token: string; +} -const sshHostApi = axios.create({ - baseURL: isLocalhost ? 'http://localhost:8081' : '', - headers: { - 'Content-Type': 'application/json', - }, -}); +interface UserInfo { + id: string; + username: string; + is_admin: boolean; +} -const tunnelApi = axios.create({ - baseURL: isLocalhost ? 'http://localhost:8083' : '', - headers: { - 'Content-Type': 'application/json', - }, -}); +interface RegistrationResponse { + allowed: boolean; +} -const fileManagerApi = axios.create({ - baseURL: isLocalhost ? 'http://localhost:8084' : '', - headers: { - 'Content-Type': 'application/json', - } -}) +interface OIDCConfig { + configured: boolean; +} -const statsApi = axios.create({ - baseURL: isLocalhost ? 'http://localhost:8085' : '', - headers: { - 'Content-Type': 'application/json', - } -}) +interface UserCount { + count: number; +} + +interface PasswordResetInitiate { + username: string; +} + +interface PasswordResetVerify { + username: string; + resetCode: string; +} + +interface PasswordResetComplete { + username: string; + tempToken: string; + newPassword: string; +} + +interface OIDCAuthorize { + auth_url: string; +} + +function setCookie(name: string, value: string, days = 7) { + const expires = new Date(Date.now() + days * 864e5).toUTCString(); + document.cookie = `${name}=${encodeURIComponent(value)}; expires=${expires}; path=/`; +} function getCookie(name: string): string | undefined { const value = `; ${document.cookie}`; @@ -141,41 +157,54 @@ function getCookie(name: string): string | undefined { if (parts.length === 2) return parts.pop()?.split(';').shift(); } -sshHostApi.interceptors.request.use((config) => { - const token = getCookie('jwt'); - if (token) { - config.headers.Authorization = `Bearer ${token}`; - } - return config; +const sshHostApi = axios.create({ + baseURL: import.meta.env.DEV ? 'http://localhost:8081/ssh' : '/ssh', + headers: { + 'Content-Type': 'application/json', + }, }); -statsApi.interceptors.request.use((config) => { - const token = getCookie('jwt'); - if (token) { - config.headers.Authorization = `Bearer ${token}`; - } - return config; +const tunnelApi = axios.create({ + baseURL: import.meta.env.DEV ? 'http://localhost:8083/ssh' : '/ssh', + headers: { + 'Content-Type': 'application/json', + }, }); -tunnelApi.interceptors.request.use((config) => { - const token = getCookie('jwt'); - if (token) { - config.headers.Authorization = `Bearer ${token}`; +const fileManagerApi = axios.create({ + baseURL: import.meta.env.DEV ? 'http://localhost:8084/ssh' : '/ssh', + headers: { + 'Content-Type': 'application/json', } - return config; }); -fileManagerApi.interceptors.request.use((config) => { - const token = getCookie('jwt'); - if (token) { - config.headers.Authorization = `Bearer ${token}`; +const statsApi = axios.create({ + baseURL: import.meta.env.DEV ? 'http://localhost:8085' : '', + headers: { + 'Content-Type': 'application/json', } - return config; +}); + +const authApi = axios.create({ + baseURL: import.meta.env.DEV ? 'http://localhost:8081/users' : '/users', + headers: { + 'Content-Type': 'application/json', + } +}); + +[sshHostApi, tunnelApi, fileManagerApi, statsApi, authApi].forEach(api => { + api.interceptors.request.use((config) => { + const token = getCookie('jwt'); + if (token) { + config.headers.Authorization = `Bearer ${token}`; + } + return config; + }); }); export async function getSSHHosts(): Promise { try { - const response = await sshHostApi.get('/ssh/db/host'); + const response = await sshHostApi.get('/db/host'); return response.data; } catch (error) { throw error; @@ -220,7 +249,7 @@ export async function createSSHHost(hostData: SSHHostData): Promise { delete dataWithoutFile.key; formData.append('data', JSON.stringify(dataWithoutFile)); - const response = await sshHostApi.post('/ssh/db/host', formData, { + const response = await sshHostApi.post('/db/host', formData, { headers: { 'Content-Type': 'multipart/form-data', }, @@ -228,7 +257,7 @@ export async function createSSHHost(hostData: SSHHostData): Promise { return response.data; } else { - const response = await sshHostApi.post('/ssh/db/host', submitData); + const response = await sshHostApi.post('/db/host', submitData); return response.data; } } catch (error) { @@ -273,7 +302,7 @@ export async function updateSSHHost(hostId: number, hostData: SSHHostData): Prom delete dataWithoutFile.key; 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: { 'Content-Type': 'multipart/form-data', }, @@ -281,7 +310,7 @@ export async function updateSSHHost(hostId: number, hostData: SSHHostData): Prom return response.data; } else { - const response = await sshHostApi.put(`/ssh/db/host/${hostId}`, submitData); + const response = await sshHostApi.put(`/db/host/${hostId}`, submitData); return response.data; } } catch (error) { @@ -296,7 +325,7 @@ export async function bulkImportSSHHosts(hosts: SSHHostData[]): Promise<{ errors: string[]; }> { try { - const response = await sshHostApi.post('/ssh/bulk-import', {hosts}); + const response = await sshHostApi.post('/bulk-import', {hosts}); return response.data; } catch (error) { throw error; @@ -305,7 +334,7 @@ export async function bulkImportSSHHosts(hosts: SSHHostData[]): Promise<{ export async function deleteSSHHost(hostId: number): Promise { try { - const response = await sshHostApi.delete(`/ssh/db/host/${hostId}`); + const response = await sshHostApi.delete(`/db/host/${hostId}`); return response.data; } catch (error) { throw error; @@ -314,7 +343,7 @@ export async function deleteSSHHost(hostId: number): Promise { export async function getSSHHostById(hostId: number): Promise { try { - const response = await sshHostApi.get(`/ssh/db/host/${hostId}`); + const response = await sshHostApi.get(`/db/host/${hostId}`); return response.data; } catch (error) { throw error; @@ -323,7 +352,7 @@ export async function getSSHHostById(hostId: number): Promise { export async function getTunnelStatuses(): Promise> { try { - const response = await tunnelApi.get('/ssh/tunnel/status'); + const response = await tunnelApi.get('/tunnel/status'); return response.data || {}; } catch (error) { throw error; @@ -337,7 +366,7 @@ export async function getTunnelStatusByName(tunnelName: string): Promise { try { - const response = await tunnelApi.post('/ssh/tunnel/connect', tunnelConfig); + const response = await tunnelApi.post('/tunnel/connect', tunnelConfig); return response.data; } catch (error) { throw error; @@ -346,7 +375,7 @@ export async function connectTunnel(tunnelConfig: TunnelConfig): Promise { export async function disconnectTunnel(tunnelName: string): Promise { try { - const response = await tunnelApi.post('/ssh/tunnel/disconnect', {tunnelName}); + const response = await tunnelApi.post('/tunnel/disconnect', {tunnelName}); return response.data; } catch (error) { throw error; @@ -355,7 +384,7 @@ export async function disconnectTunnel(tunnelName: string): Promise { export async function cancelTunnel(tunnelName: string): Promise { try { - const response = await tunnelApi.post('/ssh/tunnel/cancel', {tunnelName}); + const response = await tunnelApi.post('/tunnel/cancel', {tunnelName}); return response.data; } catch (error) { throw error; @@ -364,7 +393,7 @@ export async function cancelTunnel(tunnelName: string): Promise { export async function getFileManagerRecent(hostId: number): Promise { 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 || []; } catch (error) { return []; @@ -379,7 +408,7 @@ export async function addFileManagerRecent(file: { hostId: number }): Promise { try { - const response = await sshHostApi.post('/ssh/file_manager/recent', file); + const response = await sshHostApi.post('/file_manager/recent', file); return response.data; } catch (error) { throw error; @@ -394,7 +423,7 @@ export async function removeFileManagerRecent(file: { hostId: number }): Promise { 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; } catch (error) { throw error; @@ -403,7 +432,7 @@ export async function removeFileManagerRecent(file: { export async function getFileManagerPinned(hostId: number): Promise { 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 || []; } catch (error) { return []; @@ -418,7 +447,7 @@ export async function addFileManagerPinned(file: { hostId: number }): Promise { try { - const response = await sshHostApi.post('/ssh/file_manager/pinned', file); + const response = await sshHostApi.post('/file_manager/pinned', file); return response.data; } catch (error) { throw error; @@ -433,7 +462,7 @@ export async function removeFileManagerPinned(file: { hostId: number }): Promise { 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; } catch (error) { throw error; @@ -442,7 +471,7 @@ export async function removeFileManagerPinned(file: { export async function getFileManagerShortcuts(hostId: number): Promise { 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 || []; } catch (error) { return []; @@ -457,7 +486,7 @@ export async function addFileManagerShortcut(shortcut: { hostId: number }): Promise { try { - const response = await sshHostApi.post('/ssh/file_manager/shortcuts', shortcut); + const response = await sshHostApi.post('/file_manager/shortcuts', shortcut); return response.data; } catch (error) { throw error; @@ -472,7 +501,7 @@ export async function removeFileManagerShortcut(shortcut: { hostId: number }): Promise { 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; } catch (error) { throw error; @@ -488,7 +517,7 @@ export async function connectSSH(sessionId: string, config: { keyPassword?: string; }): Promise { try { - const response = await fileManagerApi.post('/ssh/file_manager/ssh/connect', { + const response = await fileManagerApi.post('/ssh/connect', { sessionId, ...config }); @@ -500,7 +529,7 @@ export async function connectSSH(sessionId: string, config: { export async function disconnectSSH(sessionId: string): Promise { try { - const response = await fileManagerApi.post('/ssh/file_manager/ssh/disconnect', {sessionId}); + const response = await fileManagerApi.post('/ssh/disconnect', {sessionId}); return response.data; } catch (error) { throw error; @@ -509,7 +538,7 @@ export async function disconnectSSH(sessionId: string): Promise { export async function getSSHStatus(sessionId: string): Promise<{ connected: boolean }> { try { - const response = await fileManagerApi.get('/ssh/file_manager/ssh/status', { + const response = await fileManagerApi.get('/ssh/status', { params: {sessionId} }); return response.data; @@ -520,7 +549,7 @@ export async function getSSHStatus(sessionId: string): Promise<{ connected: bool export async function listSSHFiles(sessionId: string, path: string): Promise { try { - const response = await fileManagerApi.get('/ssh/file_manager/ssh/listFiles', { + const response = await fileManagerApi.get('/ssh/listFiles', { params: {sessionId, path} }); return response.data || []; @@ -531,7 +560,7 @@ export async function listSSHFiles(sessionId: string, path: string): Promise { try { - const response = await fileManagerApi.get('/ssh/file_manager/ssh/readFile', { + const response = await fileManagerApi.get('/ssh/readFile', { params: {sessionId, path} }); return response.data; @@ -542,7 +571,7 @@ export async function readSSHFile(sessionId: string, path: string): Promise<{ co export async function writeSSHFile(sessionId: string, path: string, content: string): Promise { try { - const response = await fileManagerApi.post('/ssh/file_manager/ssh/writeFile', { + const response = await fileManagerApi.post('/ssh/writeFile', { sessionId, path, content @@ -560,7 +589,7 @@ export async function writeSSHFile(sessionId: string, path: string, content: str export async function uploadSSHFile(sessionId: string, path: string, fileName: string, content: string): Promise { try { - const response = await fileManagerApi.post('/ssh/file_manager/ssh/uploadFile', { + const response = await fileManagerApi.post('/ssh/uploadFile', { sessionId, path, fileName, @@ -574,7 +603,7 @@ export async function uploadSSHFile(sessionId: string, path: string, fileName: s export async function createSSHFile(sessionId: string, path: string, fileName: string, content: string = ''): Promise { try { - const response = await fileManagerApi.post('/ssh/file_manager/ssh/createFile', { + const response = await fileManagerApi.post('/ssh/createFile', { sessionId, path, fileName, @@ -588,7 +617,7 @@ export async function createSSHFile(sessionId: string, path: string, fileName: s export async function createSSHFolder(sessionId: string, path: string, folderName: string): Promise { try { - const response = await fileManagerApi.post('/ssh/file_manager/ssh/createFolder', { + const response = await fileManagerApi.post('/ssh/createFolder', { sessionId, path, folderName @@ -601,7 +630,7 @@ export async function createSSHFolder(sessionId: string, path: string, folderNam export async function deleteSSHItem(sessionId: string, path: string, isDirectory: boolean): Promise { try { - const response = await fileManagerApi.delete('/ssh/file_manager/ssh/deleteItem', { + const response = await fileManagerApi.delete('/ssh/deleteItem', { data: { sessionId, path, @@ -616,7 +645,7 @@ export async function deleteSSHItem(sessionId: string, path: string, isDirectory export async function renameSSHItem(sessionId: string, oldPath: string, newName: string): Promise { try { - const response = await fileManagerApi.put('/ssh/file_manager/ssh/renameItem', { + const response = await fileManagerApi.put('/ssh/renameItem', { sessionId, oldPath, newName @@ -627,7 +656,7 @@ export async function renameSSHItem(sessionId: string, oldPath: string, newName: } } -export {sshHostApi, tunnelApi, fileManagerApi}; + export async function getAllServerStatuses(): Promise> { try { @@ -654,4 +683,97 @@ export async function getServerMetricsById(id: number): Promise { } catch (error) { throw error; } -} \ No newline at end of file +} + +// Auth-related functions +export async function registerUser(username: string, password: string): Promise { + try { + const response = await authApi.post('/create', { username, password }); + return response.data; + } catch (error) { + throw error; + } +} + +export async function loginUser(username: string, password: string): Promise { + try { + const response = await authApi.post('/login', { username, password }); + return response.data; + } catch (error) { + throw error; + } +} + +export async function getUserInfo(): Promise { + try { + const response = await authApi.get('/me'); + return response.data; + } catch (error) { + throw error; + } +} + +export async function getRegistrationAllowed(): Promise<{ allowed: boolean }> { + try { + const response = await authApi.get('/registration-allowed'); + return response.data; + } catch (error) { + throw error; + } +} + +export async function getOIDCConfig(): Promise { + try { + const response = await authApi.get('/oidc-config'); + return response.data; + } catch (error) { + throw error; + } +} + +export async function getUserCount(): Promise { + try { + const response = await authApi.get('/count'); + return response.data; + } catch (error) { + throw error; + } +} + +export async function initiatePasswordReset(username: string): Promise { + try { + const response = await authApi.post('/initiate-reset', { username }); + return response.data; + } catch (error) { + throw error; + } +} + +export async function verifyPasswordResetCode(username: string, resetCode: string): Promise { + try { + const response = await authApi.post('/verify-reset-code', { username, resetCode }); + return response.data; + } catch (error) { + throw error; + } +} + +export async function completePasswordReset(username: string, tempToken: string, newPassword: string): Promise { + try { + const response = await authApi.post('/complete-reset', { username, tempToken, newPassword }); + return response.data; + } catch (error) { + throw error; + } +} + +export async function getOIDCAuthorizeUrl(): Promise { + try { + const response = await authApi.get('/oidc/authorize'); + return response.data; + } catch (error) { + throw error; + } +} + +export {sshHostApi, tunnelApi, fileManagerApi, authApi}; \ No newline at end of file