--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+ <component name="VcsDirectoryMappings">
+ <mapping directory="$PROJECT_DIR$" vcs="Git" />
+ </component>
+</project>
\ No newline at end of file
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 (
<Route path="/" element={<Dashboard />} />
<Route path="/login" element={<Login />} />
<Route path="/git" element={<Git />} />
+ <Route path="/vpn" element={<VPN />} />
+ <Route path="/docker" element={<Docker />} />
<Route path="*" element={<NotFound />} />
</Routes>
);
--- /dev/null
+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
--- /dev/null
+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
--- /dev/null
+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
--- /dev/null
+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<string>}
+ */
+ 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
--- /dev/null
+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
--- /dev/null
+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
--- /dev/null
+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
--- /dev/null
+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(
+ <Container>
+ <Typography variant="h5" className="mt-6">Docker Containers</Typography>
+ <MaterialReactTable
+ columns={[
+ { accessorKey: 'id', header: 'ID' },
+ { accessorKey: 'name', header: 'Name' },
+ { accessorKey: 'state', header: 'State' },
+ { accessorKey: 'status', header: 'Status' },
+ {
+ header: 'Actions',
+ Cell: ({ row }) => (
+ <>
+ <Button
+ variant="contained"
+ color="primary"
+ onClick={() => handleContainerAction('start', row.original.id)}
+ disabled={row.original.state === 'running'}
+ >
+ Start
+ </Button>
+ <Button
+ variant="contained"
+ color="secondary"
+ onClick={() => handleContainerAction('stop', row.original.id)}
+ disabled={row.original.state !== 'running'}
+ >
+ Stop
+ </Button>
+ </>
+ ),
+ },
+ ]}
+ data={containers}
+ />
+
+ <Typography variant="h5" className="mt-6">Docker Images</Typography>
+ <MaterialReactTable
+ columns={[
+ { accessorKey: 'id', header: 'ID' },
+ { accessorKey: 'tags', header: 'Tags', Cell: ({ cell }) => cell.getValue().join(', ') },
+ { accessorKey: 'created', header: 'Created' },
+ ]}
+ data={images}
+ />
+
+ <Typography variant="h5" className="mt-6">Stop Docker Compose Services</Typography>
+ <form onSubmit={handleComposeSubmit} className="space-y-4">
+ <TextField
+ label="Compose File Path"
+ value={composeFile}
+ onChange={(e) => setComposeFile(e.target.value)}
+ fullWidth
+ />
+ <Button type="submit" variant="contained" color="secondary">Stop Compose Services</Button>
+ </form>
+
+ {message && <Typography color={message.startsWith('Error') ? 'error' : 'success'}>{message}</Typography>}
+ </Container>
+ )
+}
+
+export default Docker;
\ No newline at end of file
+++ /dev/null
-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 (
- <div className="min-h-screen bg-gray-100 flex items-center justify-center">
- <div className="bg-white p-8 rounded-lg shadow-lg w-full max-w-md">
- <h1 className="text-2xl font-bold mb-6 text-center">Git Repository Manager</h1>
- <form onSubmit={handleSubmit}>
- <div className="mb-4">
- <label className="block text-gray-700 mb-2" htmlFor="repoName">Repository Name</label>
- <input
- id="repoName"
- type="text"
- value={repoName}
- onChange={(e) => setRepoName(e.target.value)}
- className="w-full p-2 border rounded"
- placeholder="e.g., my-project"
- required
- />
- </div>
- <div className="mb-4">
- <label className="block text-gray-700 mb-2">Repository Type</label>
- <select
- value={repoType}
- onChange={(e) => setRepoType(e.target.value)}
- className="w-full p-2 border rounded"
- >
- <option value="global">Global (Group-Shared)</option>
- <option value="private">Private (User-Specific)</option>
- </select>
- </div>
- {repoType === 'private' && (
- <div className="mb-4">
- <label className="block text-gray-700 mb-2" htmlFor="user">Username</label>
- <input
- id="user"
- type="text"
- value={user}
- onChange={(e) => setUser(e.target.value)}
- className="w-full p-2 border rounded"
- placeholder="e.g., gituser1"
- required
- />
- </div>
- )}
- <button
- type="submit"
- className="w-full bg-blue-500 text-white p-2 rounded hover:bg-blue-600"
- >
- Create Repository
- </button>
- </form>
- {message && (
- <p className={`mt-4 text-center ${message.startsWith('Error') ? 'text-red-500' : 'text-green-500'}`}>
- {message}
- </p>
- )}
- </div>
- </div>
- );
-}
-
-export default Git;
\ No newline at end of file
--- /dev/null
+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(
+ <Container>
+ <Typography variant="h5">Create Git Repository</Typography>
+ <form onSubmit={handleRepoSubmit} className="space-y-4">
+ <TextField
+ label="Repository Name"
+ value={repoName}
+ onChange={(e) => setRepoName(e.target.value)}
+ fullWidth
+ />
+ <Select
+ value={repoType}
+ onChange={(e) => setRepoType(e.target.value)}
+ fullWidth
+ >
+ <MenuItem value="global">Global (Group-Shared)</MenuItem>
+ <MenuItem value="private">Private (User-Specific)</MenuItem>
+ </Select>
+ {repoType === 'private' && (
+ <TextField
+ label="Username"
+ value={repoUser}
+ onChange={(e) => setRepoUser(e.target.value)}
+ fullWidth
+ />
+ )}
+ <Button type="submit" variant="contained" color="primary">Create Repository</Button>
+ </form>
+
+ <Typography variant="h5" className="mt-6">Clone Repository Locally</Typography>
+ <form onSubmit={handleCloneSubmit} className="space-y-4">
+ <TextField
+ label="Repository Name"
+ value={deployRepoName}
+ onChange={(e) => setDeployRepoName(e.target.value)}
+ fullWidth
+ />
+ <TextField
+ label="Deployment Path"
+ value={deployPath}
+ onChange={(e) => setDeployPath(e.target.value)}
+ fullWidth
+ />
+ <TextField
+ label="Username"
+ value={deployUser}
+ onChange={(e) => setDeployUser(e.target.value)}
+ fullWidth
+ />
+ <Button type="submit" variant="contained" color="primary">Clone Repository</Button>
+ </form>
+
+ <Typography variant="h5" className="mt-6">Repositories</Typography>
+ <MaterialReactTable
+ columns={[
+ { accessorKey: 'name', header: 'Name' },
+ { accessorKey: 'cloneUrl', header: 'Clone URL' },
+ {
+ header: 'Actions',
+ Cell: ({ row }) => (
+ <Button
+ variant="contained"
+ color="secondary"
+ onClick={() => handleDeleteRepo(row.original.name)}
+ >
+ Delete
+ </Button>
+ ),
+ },
+ ]}
+ data={repos}
+ enableRowActions
+ renderRowActions={({ row }) => (
+ <Button
+ onClick={() => navigator.clipboard.writeText(row.original.cloneUrl)}
+ >
+ Copy Clone URL
+ </Button>
+ )}
+ />
+
+ {message && <Typography color={message.startsWith('Error') ? 'error' : 'success'}>{message}</Typography>}
+ </Container>
+ );
+};
+
+export default Git;
\ No newline at end of file
+++ /dev/null
-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 (
- <div
- className="flex flex-col sm:flex-row items-center md:items-start sm:justify-center md:justify-start flex-1 min-w-0">
- <Paper
- className="h-full sm:h-auto md:flex md:items-center md:justify-end w-full sm:w-auto md:h-full md:w-1/2 py-8 px-16 sm:p-48 md:p-64 sm:rounded-2xl md:rounded-none sm:shadow md:shadow-none ltr:border-r-1 rtl:border-l-1">
- <div className="w-full max-w-320 sm:w-320 mx-auto sm:mx-0">
-
- <Typography fontSize={'2rem'} className="mt-32 text-4xl font-extrabold tracking-tight leading-tight">
- Sign in
- </Typography>
-
- {signInError &&
- <FormHelperText id="validation-helper-text" className="mt-24 mb-24 text-lg" error>
- {signInError}
- </FormHelperText>
- }
-
- <form
- name="loginForm"
- noValidate
- className="flex flex-col justify-center w-full mt-32"
- onSubmit={ handleSubmit( onSubmit ) }
- >
- <Controller
- name="employee_number"
- control={ control }
- render={ ( { field } ) => (
- <TextField
- { ...field }
- className="mb-24"
- label="Employee Number"
- autoFocus
- type="text"
- error={ !!errors.employee_number }
- helperText={ errors?.employee_number?.message }
- variant="outlined"
- required
- fullWidth
- />
- ) }
- />
-
- <Controller
- name="password"
- control={ control }
- render={ ( { field } ) => (
- <TextField
- { ...field }
- className="mb-6"
- label="Password"
- type={ showPassword ? 'text' : 'password' }
- error={ !!errors.password }
- inputProps={{
- 'aria-describedby': errors.password ? 'password-helper-text' : undefined,
- }}
- InputProps={{
- endAdornment:
- <InputAdornment position='end' >
- <IconButton aria-label='toggle password visibility' onClick={ () => setShowPassword( prevState => !prevState ) }>
- { showPassword ? <VisibilityOff/> : <Visibility/> }
- </IconButton>
- </InputAdornment>
- }}
- variant="outlined"
- required
- fullWidth
- />
- ) }
- />
- {errors?.password && (
- <FormHelperText id="password-helper-text" className="mb-24" error>
- {errors.password.message}
- </FormHelperText>
- )}
-
- <div className="flex flex-col sm:flex-row items-center justify-center sm:justify-between">
- <Controller
- name="remember"
- control={ control }
- render={ ( { field } ) => (
- <FormControl>
- <FormControlLabel
- label="Remember me"
- control={ <Checkbox size="small" { ...field } /> }
- />
- </FormControl>
- ) }
- />
- </div>
-
- <Button
- variant="contained"
- color="secondary"
- className=" w-full mt-16"
- aria-label="Sign in"
- disabled={ _.isEmpty( dirtyFields ) || !isValid }
- type="submit"
- size="large"
- >Sign in</Button>
-
- <Button
- variant="outlined"
- color="primary"
- className=" w-full mt-16"
- aria-label="Forgot Password"
- size="large"
- onClick={() => {
- navigate('/forgot-password');
- }}
- >Forgot Password</Button>
-
- </form>
- </div>
- </Paper>
-
- <Box
- className="relative hidden md:flex flex-auto items-center justify-center h-full p-64 lg:px-112 overflow-hidden"
- sx={ { backgroundColor:'primary.main' } }
- >
- <svg
- className="absolute inset-0 pointer-events-none"
- viewBox="0 0 960 540"
- width="100%"
- height="100%"
- preserveAspectRatio="xMidYMax slice"
- xmlns="http://www.w3.org/2000/svg"
- >
- <Box
- component="g"
- sx={ { color:'primary.light' } }
- className="opacity-20"
- fill="none"
- stroke="currentColor"
- strokeWidth="100"
- >
- <circle r="234" cx="196" cy="23"/>
- <circle r="234" cx="790" cy="491"/>
- </Box>
- </svg>
- <Box
- component="svg"
- className="absolute -top-64 -right-64 opacity-20"
- sx={ { color:'primary.light' } }
- viewBox="0 0 220 192"
- width="220px"
- height="192px"
- fill="none"
- >
- <defs>
- <pattern
- id="837c3e70-6c3a-44e6-8854-cc48c737b659"
- x="0"
- y="0"
- width="20"
- height="20"
- patternUnits="userSpaceOnUse"
- >
- <rect x="0" y="0" width="4" height="4" fill="currentColor"/>
- </pattern>
- </defs>
- <rect width="220" height="192" fill="url(#837c3e70-6c3a-44e6-8854-cc48c737b659)"/>
- </Box>
-
- <div className="z-10 relative w-full max-w-2xl">
- <div className="text-7xl font-bold leading-none text-grey-100">
- <div>Please Login</div>
- </div>
- <div className="mt-24 text-lg tracking-tight leading-6 text-grey-400">
- Enter your company employee number and password to log in.
- </div>
- </div>
- </Box>
- </div>
- );
-}
-
-export default SignInPage;
--- /dev/null
+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 (
+ <div
+ className="flex flex-col sm:flex-row items-center md:items-start sm:justify-center md:justify-start flex-1 min-w-0">
+ <Paper
+ className="h-full sm:h-auto md:flex md:items-center md:justify-end w-full sm:w-auto md:h-full md:w-1/2 py-8 px-16 sm:p-48 md:p-64 sm:rounded-2xl md:rounded-none sm:shadow md:shadow-none ltr:border-r-1 rtl:border-l-1">
+ <div className="w-full max-w-320 sm:w-320 mx-auto sm:mx-0">
+
+ <Typography fontSize={'2rem'} className="mt-32 text-4xl font-extrabold tracking-tight leading-tight">
+ Sign in
+ </Typography>
+
+ {signInError &&
+ <FormHelperText id="validation-helper-text" className="mt-24 mb-24 text-lg" error>
+ {signInError}
+ </FormHelperText>
+ }
+
+ <form
+ name="loginForm"
+ noValidate
+ className="flex flex-col justify-center w-full mt-32"
+ onSubmit={ handleSubmit( onSubmit ) }
+ >
+ <Controller
+ name="employee_number"
+ control={ control }
+ render={ ( { field } ) => (
+ <TextField
+ { ...field }
+ className="mb-24"
+ label="Employee Number"
+ autoFocus
+ type="text"
+ error={ !!errors.employee_number }
+ helperText={ errors?.employee_number?.message }
+ variant="outlined"
+ required
+ fullWidth
+ />
+ ) }
+ />
+
+ <Controller
+ name="password"
+ control={ control }
+ render={ ( { field } ) => (
+ <TextField
+ { ...field }
+ className="mb-6"
+ label="Password"
+ type={ showPassword ? 'text' : 'password' }
+ error={ !!errors.password }
+ inputProps={{
+ 'aria-describedby': errors.password ? 'password-helper-text' : undefined,
+ }}
+ InputProps={{
+ endAdornment:
+ <InputAdornment position='end' >
+ <IconButton aria-label='toggle password visibility' onClick={ () => setShowPassword( prevState => !prevState ) }>
+ { showPassword ? <VisibilityOff/> : <Visibility/> }
+ </IconButton>
+ </InputAdornment>
+ }}
+ variant="outlined"
+ required
+ fullWidth
+ />
+ ) }
+ />
+ {errors?.password && (
+ <FormHelperText id="password-helper-text" className="mb-24" error>
+ {errors.password.message}
+ </FormHelperText>
+ )}
+
+ <div className="flex flex-col sm:flex-row items-center justify-center sm:justify-between">
+ <Controller
+ name="remember"
+ control={ control }
+ render={ ( { field } ) => (
+ <FormControl>
+ <FormControlLabel
+ label="Remember me"
+ control={ <Checkbox size="small" { ...field } /> }
+ />
+ </FormControl>
+ ) }
+ />
+ </div>
+
+ <Button
+ variant="contained"
+ color="secondary"
+ className=" w-full mt-16"
+ aria-label="Sign in"
+ disabled={ _.isEmpty( dirtyFields ) || !isValid }
+ type="submit"
+ size="large"
+ >Sign in</Button>
+
+ <Button
+ variant="outlined"
+ color="primary"
+ className=" w-full mt-16"
+ aria-label="Forgot Password"
+ size="large"
+ onClick={() => {
+ navigate('/forgot-password');
+ }}
+ >Forgot Password</Button>
+
+ </form>
+ </div>
+ </Paper>
+
+ <Box
+ className="relative hidden md:flex flex-auto items-center justify-center h-full p-64 lg:px-112 overflow-hidden"
+ sx={ { backgroundColor:'primary.main' } }
+ >
+ <svg
+ className="absolute inset-0 pointer-events-none"
+ viewBox="0 0 960 540"
+ width="100%"
+ height="100%"
+ preserveAspectRatio="xMidYMax slice"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <Box
+ component="g"
+ sx={ { color:'primary.light' } }
+ className="opacity-20"
+ fill="none"
+ stroke="currentColor"
+ strokeWidth="100"
+ >
+ <circle r="234" cx="196" cy="23"/>
+ <circle r="234" cx="790" cy="491"/>
+ </Box>
+ </svg>
+ <Box
+ component="svg"
+ className="absolute -top-64 -right-64 opacity-20"
+ sx={ { color:'primary.light' } }
+ viewBox="0 0 220 192"
+ width="220px"
+ height="192px"
+ fill="none"
+ >
+ <defs>
+ <pattern
+ id="837c3e70-6c3a-44e6-8854-cc48c737b659"
+ x="0"
+ y="0"
+ width="20"
+ height="20"
+ patternUnits="userSpaceOnUse"
+ >
+ <rect x="0" y="0" width="4" height="4" fill="currentColor"/>
+ </pattern>
+ </defs>
+ <rect width="220" height="192" fill="url(#837c3e70-6c3a-44e6-8854-cc48c737b659)"/>
+ </Box>
+
+ <div className="z-10 relative w-full max-w-2xl">
+ <div className="text-7xl font-bold leading-none text-grey-100">
+ <div>Please Login</div>
+ </div>
+ <div className="mt-24 text-lg tracking-tight leading-6 text-grey-400">
+ Enter your company employee number and password to log in.
+ </div>
+ </div>
+ </Box>
+ </div>
+ );
+}
+
+export default SignInPage;
--- /dev/null
+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 (
+ <Container>
+ <Typography variant="h5" className="mt-6">Create OpenVPN Client</Typography>
+ <form onSubmit={handleCreateClientSubmit} className="space-y-4">
+ <TextField
+ label="Client Name"
+ value={clientName}
+ onChange={(e) => setClientName(e.target.value)}
+ fullWidth
+ />
+ <Select
+ value={useStaticIp}
+ onChange={(e) => setUseStaticIp(e.target.value)}
+ fullWidth
+ >
+ <MenuItem value={false}>Dynamic IP</MenuItem>
+ <MenuItem value={true}>Static IP</MenuItem>
+ </Select>
+ {useStaticIp && (
+ <TextField
+ label="Static IP"
+ value={staticIp}
+ onChange={(e) => setStaticIp(e.target.value)}
+ fullWidth
+ placeholder="e.g., 10.8.0.x"
+ />
+ )}
+ <Button type="submit" variant="contained" color="primary">Create Client</Button>
+ </form>
+
+ <Typography variant="h5" className="mt-6">Revoke OpenVPN Client</Typography>
+ <form onSubmit={handleRevokeClientSubmit} className="space-y-4">
+ <TextField
+ label="Client Name"
+ value={revokeClientName}
+ onChange={(e) => setRevokeClientName(e.target.value)}
+ fullWidth
+ />
+ <Button type="submit" variant="contained" color="secondary">Revoke Client</Button>
+ </form>
+
+ <Typography variant="h5" className="mt-6">Connected OpenVPN Clients</Typography>
+ <MaterialReactTable
+ columns={[
+ { accessorKey: 'name', header: 'Name' },
+ { accessorKey: 'ip', header: 'IP' },
+ { accessorKey: 'connectedSince', header: 'Connected Since' },
+ ]}
+ data={clients}
+ />
+
+ {message && <Typography color={message.startsWith('Error') ? 'error' : 'success'}>{message}</Typography>}
+ </Container>
+ );
+
+ /*return (
+ <Container>
+ <Typography variant="h4" gutterBottom>PHS Admin Dashboard</Typography>
+ {!token ? (
+ <form onSubmit={handleLogin} className="space-y-4">
+ <TextField
+ label="Username"
+ value={username}
+ onChange={(e) => setUsername(e.target.value)}
+ fullWidth
+ />
+ <TextField
+ label="Password"
+ type="password"
+ value={password}
+ onChange={(e) => setPassword(e.target.value)}
+ fullWidth
+ />
+ <Button type="submit" variant="contained" color="primary">Login</Button>
+ </form>
+ ) : (
+ <>
+ </>
+ )}
+ </Container>
+ );*/
+};
+
+export default VPN;
\ No newline at end of file
--- /dev/null
+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