From: charleswrayjr Date: Fri, 5 Sep 2025 23:18:40 +0000 (-0500) Subject: Adding vpn and docker elements as well as adding more git operations. X-Git-Url: https://git.phasecustomsoft.com/?a=commitdiff_plain;h=ae30fb56abb78441e71577cc57cb8ca7fc077e08;p=phs-admin.git Adding vpn and docker elements as well as adding more git operations. --- diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/app/routes.js b/src/app/routes.js index 67634de..72b3019 100755 --- a/src/app/routes.js +++ b/src/app/routes.js @@ -1,8 +1,9 @@ import { Route, Routes } from 'react-router-dom'; -import Login from './views/Login'; -import NotFound from './views/404/Error404Page'; +import { Login, NotFound, Dashboard, Git, VPN, Docker } from './views'; +/*import NotFound from './views/404/Error404Page'; import Dashboard from './views/Dashboard/Dashboard'; -import Git from './views/Git'; +import Git from './views/Git/Git';*/ + export default function AppRoutes() { return ( @@ -10,6 +11,8 @@ export default function AppRoutes() { } /> } /> } /> + } /> + } /> } /> ); diff --git a/src/app/services/Docker/DockerConfig.js b/src/app/services/Docker/DockerConfig.js new file mode 100644 index 0000000..5c12e52 --- /dev/null +++ b/src/app/services/Docker/DockerConfig.js @@ -0,0 +1,11 @@ +const base = 'docker/'; +const DockerConfig = { + index: base, + start: `${base}start-container`, + stop: `${base}stop-container`, + images: `${base}images`, + containers: `${base}containers`, + down: `${base}compose-down`, +}; + +export default DockerConfig; \ No newline at end of file diff --git a/src/app/services/Docker/DockerService.js b/src/app/services/Docker/DockerService.js new file mode 100644 index 0000000..d1c8d7d --- /dev/null +++ b/src/app/services/Docker/DockerService.js @@ -0,0 +1,60 @@ +import axios from 'axios'; +import { base_url } from '../../../globals'; +import DockerConfig from './DockerConfig'; + +class DockerService { + + fetchContainers = () => { + return new Promise( async ( resolve, reject ) => { + try { + const response = await axios.get( `${ base_url }${ DockerConfig.containers }` ); + resolve( response.data.containers ); // Resolve the promise with the data + } catch (error) { + reject( `Error fetching containers: ${ error.message }` ); // Reject the promise with an error message + } + } ); + }; + + fetchImages = async () => { + return new Promise( async ( resolve, reject ) => { + try { + const response = await axios.get( `${ base_url }${ DockerConfig.images }` ); + resolve( response.data.images ); // Resolve the promise with the data + } catch (error) { + console.error( error ); + reject( `Error fetching images: ${ error.message }` ); // Reject the promise with an error message + } + } ); + }; + + containerAction = async (action, id, fetchContainers) => { + return new Promise( async ( resolve, reject ) => { + try { + const response = await axios.post( `${ base_url }docker/${ action }-container`, { + containerId:id, + } ); + fetchContainers(); + resolve(response.data.message); + } catch (error) { + reject(`Error: ${ error.response?.data?.error || `Failed to ${ action } container` }`); + } + }) + }; + + composeDown = async (composeFile, fetchContainers ) => { + return new Promise( async ( resolve, reject ) => { + try { + const response = await axios.post(`${base_url}${DockerConfig.down}`, { + composeFile, + }); + resolve(response.data.message); + fetchContainers(); + } catch (error) { + reject(`Error: ${error.response?.data?.error || 'Failed to stop compose services'}`); + } + }); + }; +} + +const instance = new DockerService(); +export default instance; \ No newline at end of file diff --git a/src/app/services/Git/GitConfig.js b/src/app/services/Git/GitConfig.js new file mode 100644 index 0000000..fb47d54 --- /dev/null +++ b/src/app/services/Git/GitConfig.js @@ -0,0 +1,9 @@ +let base = 'git/'; +const GitConfig = { + index: base, + createRepo: `${base}create-repo/`, + deleteRepo: `${base}`, + cloneRepo: `${base}clone-repo/`, +}; + +export default GitConfig; \ No newline at end of file diff --git a/src/app/services/Git/GitService.js b/src/app/services/Git/GitService.js new file mode 100644 index 0000000..a381d71 --- /dev/null +++ b/src/app/services/Git/GitService.js @@ -0,0 +1,72 @@ +import axios from 'axios'; +import { base_url } from '../../../globals'; +import GitConfig from './GitConfig'; + +class GitService { + + fetchRepos = async ( token ) => { + return new Promise( async ( resolve, reject ) => { + try { + const response = await axios.get(`${base_url}${GitConfig.index}`, { headers: { Authorization: `Bearer ${token}` } }); + resolve(response.data.repos); + } catch (error) { + reject(error.response?.data?.message || error.message); + } + }) + }; + + /** + * + * @param {Object} data + * @param {Function} fetchRepos + * @returns {Promise} + */ + createRepo = (data, fetchRepos) => { + return new Promise( async ( resolve, reject ) => { + const { repoName, repoType, repoUser } = data; + try { + const response = await axios.post(`${base_url}${GitConfig.createRepo}`, { + name: repoName, + type: repoType, + user: repoType === 'private' ? repoUser : undefined, + }); + resolve(response.data.message); + fetchRepos(); + } catch (error) { + reject(`Error: ${error.response?.data?.error || 'Failed to create repository'}`); + } + }); + }; + + deleteRepo = (name, fetchRepos) => { + return new Promise( async ( resolve ) => { + try { + const response = await axios.delete(`${base_url}${GitConfig.deleteRepo}/${name}`); + resolve(response.data.message); + fetchRepos(); + } catch (error) { + resolve(`Error: ${error.response?.data?.error || 'Failed to delete repository'}`); + } + }); + }; + + cloneRepo = (data) => { + return new Promise( async ( resolve, reject ) => { + const {deployRepoName, deployPath, deployUser } = data; + try { + const response = await axios.post(`${base_url}${GitConfig.cloneRepo}`, { + repoName: deployRepoName, + deployPath, + user: deployUser, + }); + resolve(response.data.message); + } catch (error) { + reject(`Error: ${error.response?.data?.error || 'Failed to clone repository'}`); + } + }); + }; + +} + +const instance = new GitService(); +export default instance; \ No newline at end of file diff --git a/src/app/services/VPN/VPNConfig.js b/src/app/services/VPN/VPNConfig.js new file mode 100644 index 0000000..39e1127 --- /dev/null +++ b/src/app/services/VPN/VPNConfig.js @@ -0,0 +1,8 @@ +const base = 'vpn/'; +const VPNConfig = { + create: `${base}create-client/`, + delete: `${base}revoke-client/`, + index: `${base}clients/` +}; + +export default VPNConfig; \ No newline at end of file diff --git a/src/app/services/VPN/VPNService.js b/src/app/services/VPN/VPNService.js new file mode 100644 index 0000000..f60f577 --- /dev/null +++ b/src/app/services/VPN/VPNService.js @@ -0,0 +1,60 @@ +import axios from 'axios'; +import { base_url } from '../../../globals'; +import VPNConfig from './VPNConfig'; + +class VPNService { + fetchClients = () => { + return new Promise( async ( resolve ) => { + try { + const response = await axios.get(`${base_url}${VPNConfig.index}`); + resolve(response.data.clients); + } catch (error) { + resolve(`Error: ${error.response?.data?.error || 'Failed to fetch clients'}`); + } + }); + }; + + createClient = ( data, fetchClients ) => { + return new Promise( async ( resolve ) => { + const { clientName, useStaticIp, staticIp } = data; + try { + const response = await axios.post(`${base_url}${VPNConfig.create}`, { + clientName, + staticIp: useStaticIp ? staticIp : undefined, + }); + fetchClients(); + if (response.data.ovpn) { + const element = document.createElement('a'); + const file = new Blob([response.data.ovpn], { type: 'text/plain' }); + element.href = URL.createObjectURL(file); + element.download = `${clientName}.ovpn`; + document.body.appendChild(element); + element.click(); + document.body.removeChild(element); + } + resolve(response.data.message); + } catch (error) { + resolve(`Error: ${error.response?.data?.error || 'Failed to create client'}`); + } + }) + }; + + revokeClient = ( data, fetchClients ) => { + return new Promise( async ( resolve ) => { + const { revokeClientName } = data; + try { + const response = await axios.post(`${base_url}${VPNConfig.delete}`, { + clientName: revokeClientName, + }); + fetchClients(); + resolve(response.data.message); + } catch (error) { + resolve(`Error: ${error.response?.data?.error || 'Failed to revoke client'}`); + } + }); + }; + +} + +const instance = new VPNService(); +export default instance; \ No newline at end of file diff --git a/src/app/services/index.js b/src/app/services/index.js new file mode 100644 index 0000000..9c81015 --- /dev/null +++ b/src/app/services/index.js @@ -0,0 +1,5 @@ +import DockerService from './Docker/DockerService'; +import GitService from './Git/GitService'; +import VPNService from './VPN/VPNService'; + +export { DockerService, GitService, VPNService }; \ No newline at end of file diff --git a/src/app/views/Docker/Docker.jsx b/src/app/views/Docker/Docker.jsx new file mode 100644 index 0000000..8836601 --- /dev/null +++ b/src/app/views/Docker/Docker.jsx @@ -0,0 +1,108 @@ +import React, { useState, useEffect } from 'react'; +import { Container, Typography, TextField, Button } from '@mui/material'; +import { MaterialReactTable } from 'material-react-table'; +import { DockerService } from '../../services'; + + +const Docker = () => { + const [containers, setContainers] = useState([]); + const [images, setImages] = useState([]); + const [composeFile, setComposeFile] = useState(''); + const [message, setMessage] = useState(''); + + const fetchContainers = async () => { + try { + const containersData = await DockerService.fetchContainers(); // Assuming you have a service to fetch containers + setContainers(containersData); + } catch (error) { + setMessage(error); + } + }; + + const fetchImages = async () => { + try { + const imagesData = await DockerService.fetchImages(); // Assuming you have a service to fetch images + setImages(imagesData); + } catch (error) { + setMessage(error); + } + }; + + useEffect( () => { + fetchImages().catch( e => setMessage( e ) ); + fetchContainers().catch( e => setMessage( e ) ); + }, [] ); + + const handleContainerAction = async (action, containerId) => { + await DockerService.containerAction(action, containerId); + await fetchContainers(); + }; + + const handleComposeSubmit = async (event) => { + event.preventDefault(); + setMessage( await DockerService.composeDown( composeFile ) ); + }; + + return( + + Docker Containers + ( + <> + + + + ), + }, + ]} + data={containers} + /> + + Docker Images + cell.getValue().join(', ') }, + { accessorKey: 'created', header: 'Created' }, + ]} + data={images} + /> + + Stop Docker Compose Services +
+ setComposeFile(e.target.value)} + fullWidth + /> + + + + {message && {message}} +
+ ) +} + +export default Docker; \ No newline at end of file diff --git a/src/app/views/Git.jsx b/src/app/views/Git.jsx deleted file mode 100755 index 5d7f69c..0000000 --- a/src/app/views/Git.jsx +++ /dev/null @@ -1,93 +0,0 @@ -import { useState } from 'react'; -import axios from 'axios'; -import { base_url } from '../../globals'; - -const Git = () => { - const [repoName, setRepoName] = useState(''); - const [repoType, setRepoType] = useState('global'); - const [user, setUser] = useState(''); - const [message, setMessage] = useState(''); - - const handleSubmit = async (e) => { - e.preventDefault(); - if (!repoName.match(/^[a-zA-Z0-9_-]+$/)) { - setMessage('Error: Repository name must contain only letters, numbers, hyphens, or underscores.'); - return; - } - if (repoType === 'private' && !user) { - setMessage('Error: Username required for private repository.'); - return; - } - - try { - const response = await axios.post(`${base_url}git/create-repo`, { - name: repoName, - type: repoType, - user: repoType === 'private' ? user : undefined, - }); - setMessage(response.data.message); - } catch (error) { - setMessage(`Error: ${error.response?.data?.error || 'Failed to create repository'}`); - } - }; - - return ( -
-
-

Git Repository Manager

-
-
- - setRepoName(e.target.value)} - className="w-full p-2 border rounded" - placeholder="e.g., my-project" - required - /> -
-
- - -
- {repoType === 'private' && ( -
- - setUser(e.target.value)} - className="w-full p-2 border rounded" - placeholder="e.g., gituser1" - required - /> -
- )} - -
- {message && ( -

- {message} -

- )} -
-
- ); -} - -export default Git; \ No newline at end of file diff --git a/src/app/views/Git/Git.jsx b/src/app/views/Git/Git.jsx new file mode 100755 index 0000000..8f15f2f --- /dev/null +++ b/src/app/views/Git/Git.jsx @@ -0,0 +1,125 @@ +import React, { useState, useEffect } from 'react'; + +import { Container, Typography, TextField, Button, Select, MenuItem } from '@mui/material'; +import { MaterialReactTable } from 'material-react-table'; +import { GitService } from '../../services'; + +const Git = () => { + const [repoName, setRepoName] = useState(''); + const [repoType, setRepoType] = useState('global'); + const [repoUser, setRepoUser] = useState(''); + const [message, setMessage] = useState(''); + const [repos, setRepos] = useState( [] ); + const [deployRepoName, setDeployRepoName] = useState(''); + const [deployPath, setDeployPath] = useState(''); + const [deployUser, setDeployUser] = useState(''); + + const fetchRepos = async () => { + await GitService.fetchRepos().then( res => setRepos( res ) ).catch( err => setMessage( err ) ); + }; + + useEffect(() => { + fetchRepos().catch( e => setMessage( e ) ); + }, [] ); + + const handleRepoSubmit = async (e) => { + e.preventDefault(); + setMessage( await GitService.createRepo({repoName, repoType, repoUser}, fetchRepos ) ); + }; + + const handleDeleteRepo = async (name) => { + setMessage( await GitService.deleteRepo( name, fetchRepos ) ); + }; + + const handleCloneSubmit = async (e) => { + e.preventDefault(); + setMessage( await GitService.cloneRepo( { deployRepoName, deployPath, deployUser } ) ); + }; + + return( + + Create Git Repository +
+ setRepoName(e.target.value)} + fullWidth + /> + + {repoType === 'private' && ( + setRepoUser(e.target.value)} + fullWidth + /> + )} + + + + Clone Repository Locally +
+ setDeployRepoName(e.target.value)} + fullWidth + /> + setDeployPath(e.target.value)} + fullWidth + /> + setDeployUser(e.target.value)} + fullWidth + /> + + + + Repositories + ( + + ), + }, + ]} + data={repos} + enableRowActions + renderRowActions={({ row }) => ( + + )} + /> + + {message && {message}} +
+ ); +}; + +export default Git; \ No newline at end of file diff --git a/src/app/views/Login.jsx b/src/app/views/Login.jsx deleted file mode 100755 index 8dd74f2..0000000 --- a/src/app/views/Login.jsx +++ /dev/null @@ -1,242 +0,0 @@ -import { useState } from 'react'; -import { yupResolver } from '@hookform/resolvers/yup'; -import { Controller, useForm } from 'react-hook-form'; -import { - Button, - Checkbox, - FormControl, - FormControlLabel, - FormHelperText, - TextField, - Typography, - Box, - Paper, - InputAdornment, IconButton -} from '@mui/material'; -import * as yup from 'yup'; -import _ from '../../@lodash'; -// import jwtService from '../../auth/services/jwtService'; -import { Visibility, VisibilityOff } from '@mui/icons-material'; -import { useNavigate } from 'react-router-dom'; - -/** - * Form Validation Schema - */ -const schema = yup.object().shape( { - email:yup.string().email( 'You must enter a valid email' ).required( 'You must enter a email' ), - password:yup - .string() - .required( 'Please enter your password.' ) - .min( 4, 'Password is too short - must be at least 4 chars.' ), -} ); - -const defaultValues = { - email:'', - password:'', - remember:true, -}; - -function SignInPage() { - const { control, formState, handleSubmit, setError } = useForm( { - mode:'onChange', - defaultValues, - resolver:yupResolver( schema ), - } ); - const [showPassword, setShowPassword] = useState( false ); - const [signInError, setSignInError] = useState(null); - const navigate = useNavigate(); - const { isValid, dirtyFields, errors } = formState; - - function onSubmit( { email, password } ) { - setSignInError(null); - /*jwtService - .signInWithEmailAndPassword( email, password ) - .then( data => { - if(data.requires_2fa) { - navigate('/sign-in-verify', { state: { data: data } }); - } - } ) - .catch( (error) => { - const message = error?.response?.data?.message; - console.log(`Sign-in error: ${error}, sub error: ${message}`); - setSignInError(message); - } );*/ - } - - return ( -
- -
- - - Sign in - - - {signInError && - - {signInError} - - } - -
- ( - - ) } - /> - - ( - - setShowPassword( prevState => !prevState ) }> - { showPassword ? : } - - - }} - variant="outlined" - required - fullWidth - /> - ) } - /> - {errors?.password && ( - - {errors.password.message} - - )} - -
- ( - - } - /> - - ) } - /> -
- - - - - - -
-
- - - - - - - - - - - - - - - - - -
-
-
Please Login
-
-
- Enter your company employee number and password to log in. -
-
-
-
- ); -} - -export default SignInPage; diff --git a/src/app/views/Login/Login.jsx b/src/app/views/Login/Login.jsx new file mode 100755 index 0000000..b3974fc --- /dev/null +++ b/src/app/views/Login/Login.jsx @@ -0,0 +1,242 @@ +import { useState } from 'react'; +import { yupResolver } from '@hookform/resolvers/yup'; +import { Controller, useForm } from 'react-hook-form'; +import { + Button, + Checkbox, + FormControl, + FormControlLabel, + FormHelperText, + TextField, + Typography, + Box, + Paper, + InputAdornment, IconButton +} from '@mui/material'; +import * as yup from 'yup'; +import _ from '@lodash'; +// import jwtService from '../../auth/services/jwtService'; +import { Visibility, VisibilityOff } from '@mui/icons-material'; +import { useNavigate } from 'react-router-dom'; + +/** + * Form Validation Schema + */ +const schema = yup.object().shape( { + email:yup.string().email( 'You must enter a valid email' ).required( 'You must enter a email' ), + password:yup + .string() + .required( 'Please enter your password.' ) + .min( 4, 'Password is too short - must be at least 4 chars.' ), +} ); + +const defaultValues = { + email:'', + password:'', + remember:true, +}; + +function SignInPage() { + const { control, formState, handleSubmit, setError } = useForm( { + mode:'onChange', + defaultValues, + resolver:yupResolver( schema ), + } ); + const [showPassword, setShowPassword] = useState( false ); + const [signInError, setSignInError] = useState(null); + const navigate = useNavigate(); + const { isValid, dirtyFields, errors } = formState; + + function onSubmit( { email, password } ) { + setSignInError(null); + /*jwtService + .signInWithEmailAndPassword( email, password ) + .then( data => { + if(data.requires_2fa) { + navigate('/sign-in-verify', { state: { data: data } }); + } + } ) + .catch( (error) => { + const message = error?.response?.data?.message; + console.log(`Sign-in error: ${error}, sub error: ${message}`); + setSignInError(message); + } );*/ + } + + return ( +
+ +
+ + + Sign in + + + {signInError && + + {signInError} + + } + +
+ ( + + ) } + /> + + ( + + setShowPassword( prevState => !prevState ) }> + { showPassword ? : } + + + }} + variant="outlined" + required + fullWidth + /> + ) } + /> + {errors?.password && ( + + {errors.password.message} + + )} + +
+ ( + + } + /> + + ) } + /> +
+ + + + + + +
+
+ + + + + + + + + + + + + + + + + +
+
+
Please Login
+
+
+ Enter your company employee number and password to log in. +
+
+
+
+ ); +} + +export default SignInPage; diff --git a/src/app/views/VPN/VPN.jsx b/src/app/views/VPN/VPN.jsx new file mode 100644 index 0000000..e923618 --- /dev/null +++ b/src/app/views/VPN/VPN.jsx @@ -0,0 +1,131 @@ +import React, { useState, useEffect } from 'react'; +import { Container, Typography, TextField, Button, Select, MenuItem } from '@mui/material'; +import { MaterialReactTable } from 'material-react-table'; +/*import axios from 'axios';*/ +import 'tailwindcss/tailwind.css'; +import { VPNService } from '../../services'; + +const VPN = () => { + /*const [token, setToken] = useState(''); + const [username, setUsername] = useState(''); + const [password, setPassword] = useState('');*/ + const [clientName, setClientName] = useState(''); + const [staticIp, setStaticIp] = useState(''); + const [useStaticIp, setUseStaticIp] = useState(false); + const [revokeClientName, setRevokeClientName] = useState(''); + const [clients, setClients] = useState([]); + const [message, setMessage] = useState(''); + + const fetchClients = async () => { + await VPNService.fetchClients().then( res => setClients( res ) ).catch( err => setMessage( err ) ); + }; + + useEffect( () => { + fetchClients().catch( e => setMessage( e ) ); + }, [] ); + + /*const handleLogin = async (e) => { + e.preventDefault(); + try { + const response = await axios.post('http://localhost:3000/login', { username, password }); + setToken(response.data.token); + setMessage('Logged in successfully'); + } catch (error) { + setMessage(`Error: ${error.response?.data?.error || 'Login failed'}`); + } + };*/ + + const handleCreateClientSubmit = async (e) => { + e.preventDefault(); + setMessage( await VPNService.createClient({ clientName, useStaticIp, staticIp }, fetchClients ) ); + }; + + const handleRevokeClientSubmit = async (e) => { + e.preventDefault(); + setMessage( await VPNService.revokeClient(revokeClientName) ); + }; + + return ( + + Create OpenVPN Client +
+ setClientName(e.target.value)} + fullWidth + /> + + {useStaticIp && ( + setStaticIp(e.target.value)} + fullWidth + placeholder="e.g., 10.8.0.x" + /> + )} + + + + Revoke OpenVPN Client +
+ setRevokeClientName(e.target.value)} + fullWidth + /> + + + + Connected OpenVPN Clients + + + {message && {message}} +
+ ); + + /*return ( + + PHS Admin Dashboard + {!token ? ( +
+ setUsername(e.target.value)} + fullWidth + /> + setPassword(e.target.value)} + fullWidth + /> + + + ) : ( + <> + + )} +
+ );*/ +}; + +export default VPN; \ No newline at end of file diff --git a/src/app/views/index.js b/src/app/views/index.js new file mode 100644 index 0000000..8a3426a --- /dev/null +++ b/src/app/views/index.js @@ -0,0 +1,8 @@ +import NotFound from './404/Error404Page'; +import Dashboard from './Dashboard/Dashboard'; +import Login from './Login/Login'; +import Docker from './Docker/Docker'; +import VPN from './VPN/VPN'; +import Git from './Git/Git'; + +export {NotFound, Dashboard, Login, Docker, VPN, Git }; \ No newline at end of file