diff --git a/server/src/models/User.ts b/server/src/models/User.ts index 40eb2198..9bc6affb 100644 --- a/server/src/models/User.ts +++ b/server/src/models/User.ts @@ -6,6 +6,9 @@ interface UserAttributes { email: string; password: string; api_key?: string | null; + proxy_url?: string | null; + proxy_username?: string | null; + proxy_password?: string | null; } // Optional fields for creating a new user @@ -16,6 +19,9 @@ class User extends Model implements User public email!: string; public password!: string; public api_key!: string | null; + public proxy_url!: string | null; + public proxy_username!: string | null; + public proxy_password!: string | null; } User.init( @@ -39,7 +45,26 @@ User.init( }, api_key: { type: DataTypes.STRING, - allowNull: true, + allowNull: true, + }, + proxy_url: { + type: DataTypes.STRING, + allowNull: true, + }, + proxy_username: { + type: DataTypes.STRING, + allowNull: true, + validate: { + isProxyPasswordRequired(value: string | null) { + if (value && !this.proxy_password) { + throw new Error('Proxy password is required when proxy username is provided'); + } + }, + }, + }, + proxy_password: { + type: DataTypes.STRING, + allowNull: true, }, }, { diff --git a/server/src/routes/index.ts b/server/src/routes/index.ts index 72be1ebd..bc616273 100644 --- a/server/src/routes/index.ts +++ b/server/src/routes/index.ts @@ -3,6 +3,7 @@ import { router as workflow } from './workflow'; import { router as storage } from './storage'; import { router as auth } from './auth'; import { router as integration } from './integration'; +import { router as proxy } from './proxy'; export { record, @@ -10,4 +11,5 @@ export { storage, auth, integration, + proxy }; diff --git a/server/src/routes/proxy.ts b/server/src/routes/proxy.ts new file mode 100644 index 00000000..ff20d31e --- /dev/null +++ b/server/src/routes/proxy.ts @@ -0,0 +1,53 @@ +import { Router, Request, Response } from 'express'; +import User from '../models/User'; +import { hashPassword } from '../utils/auth'; +import { requireSignIn } from '../middlewares/auth'; + +export const router = Router(); + +interface AuthenticatedRequest extends Request { + user?: { id: string }; +} + +router.post('/config', requireSignIn, async (req: AuthenticatedRequest, res: Response) => { + const { server_url, username, password } = req.body; + + try { + + if (!req.user) { + return res.status(401).json({ ok: false, error: 'Unauthorized' }); + } + + const user = await User.findByPk(req.user.id, { + attributes: { exclude: ['password'] }, + }); + + if (!user) { + return res.status(404).json({ message: 'User not found' }); + } + + if (!server_url) { + return res.status(400).send('Proxy URL is required'); + } + + let hashedProxyUsername: string | null = null; + let hashedProxyPassword: string | null = null; + + if (username && password) { + hashedProxyUsername = await hashPassword(username); + hashedProxyPassword = await hashPassword(password); + } else if (username && !password) { + return res.status(400).send('Proxy password is required when proxy username is provided'); + } + + user.proxy_url = server_url; + user.proxy_username = hashedProxyUsername; + user.proxy_password = hashedProxyPassword; + + await user.save(); + + res.status(200).send('Proxy configuration saved successfully'); + } catch (error: any) { + res.status(500).send(`Could not save proxy configuration - ${error.message}`); + } +}); diff --git a/server/src/server.ts b/server/src/server.ts index 0d0512fc..ba44d260 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -3,7 +3,7 @@ import http from 'http'; import cors from 'cors'; import dotenv from 'dotenv'; dotenv.config(); -import { record, workflow, storage, auth, integration } from './routes'; +import { record, workflow, storage, auth, integration, proxy } from './routes'; import { BrowserPool } from "./browser-management/classes/BrowserPool"; import logger from './logger'; import { connectDB, syncDB } from './db/config'; @@ -46,6 +46,7 @@ app.use('/workflow', workflow); app.use('/storage', storage); app.use('/auth', auth); app.use('/integration', integration); +app.use('/proxy', proxy); app.get('/', function (req, res) { return res.send('Maxun server started 🚀'); diff --git a/src/api/proxy.ts b/src/api/proxy.ts new file mode 100644 index 00000000..9ae00c55 --- /dev/null +++ b/src/api/proxy.ts @@ -0,0 +1,15 @@ +import { default as axios } from "axios"; + +export const sendProxyConfig = async (proxyConfig: { server_url: string, username?: string, password?: string }): Promise => { + try { + const response = await axios.post(`http://localhost:8080/proxy/config`, proxyConfig); + if (response.status === 200) { + return response.data; + } else { + throw new Error(`Failed to submit proxy configuration. Try again.`); + } + } catch (error: any) { + console.log(error); + return false; + } +} \ No newline at end of file diff --git a/src/components/organisms/MainMenu.tsx b/src/components/organisms/MainMenu.tsx index 55dcd27c..50a9f849 100644 --- a/src/components/organisms/MainMenu.tsx +++ b/src/components/organisms/MainMenu.tsx @@ -3,7 +3,6 @@ import Tabs from '@mui/material/Tabs'; import Tab from '@mui/material/Tab'; import Box from '@mui/material/Box'; import { Paper } from "@mui/material"; -import styled from "styled-components"; interface MainMenuProps { value: string; @@ -38,13 +37,17 @@ export const MainMenu = ({ value = 'recordings', handleChangeContent }: MainMenu > - + + ); diff --git a/src/components/organisms/ProxyForm.tsx b/src/components/organisms/ProxyForm.tsx new file mode 100644 index 00000000..62869fc9 --- /dev/null +++ b/src/components/organisms/ProxyForm.tsx @@ -0,0 +1,153 @@ +import React, { useState } from 'react'; +import { styled } from '@mui/system'; +import { TextField, Button, Switch, FormControlLabel, Box, Typography } from '@mui/material'; +import { sendProxyConfig } from '../../api/proxy'; +import { useGlobalInfoStore } from '../../context/globalInfo'; + +const FormContainer = styled(Box)({ + display: 'flex', + flexDirection: 'column', + gap: '16px', + padding: '20px', + borderRadius: '8px', +}); + +const FormControl = styled(Box)({ + marginBottom: '16px', +}); + +const ProxyForm: React.FC = () => { + const [proxyConfig, setProxyConfig] = useState({ + server_url: '', + username: '', + password: '', + }); + const [requiresAuth, setRequiresAuth] = useState(false); + const [errors, setErrors] = useState({ + server_url: '', + username: '', + password: '', + }); + + const { notify } = useGlobalInfoStore(); + + const validateForm = () => { + let valid = true; + let errorMessages = { server_url: '', username: '', password: '' }; + + if (!proxyConfig.server_url) { + errorMessages.server_url = 'Server URL is required'; + valid = false; + } + + if (requiresAuth) { + if (!proxyConfig.username) { + errorMessages.username = 'Username is required for authenticated proxies'; + valid = false; + } + if (!proxyConfig.password) { + errorMessages.password = 'Password is required for authenticated proxies'; + valid = false; + } + } + + setErrors(errorMessages); + return valid; + }; + + const handleChange = (e: React.ChangeEvent) => { + const { name, value } = e.target; + setProxyConfig({ ...proxyConfig, [name]: value }); + }; + + const handleAuthToggle = (e: React.ChangeEvent) => { + setRequiresAuth(e.target.checked); + if (!e.target.checked) { + setProxyConfig({ ...proxyConfig, username: '', password: '' }); + setErrors({ ...errors, username: '', password: '' }); + } + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + if (!validateForm()) { + return; + } + + await sendProxyConfig(proxyConfig).then((response) => { + if (response) { + notify('success', 'Proxy configuration submitted successfully'); + } else { + notify('error', 'Failed to submit proxy configuration. Try again.'); + } + }); + }; + + return ( + +
+ + Proxy Configuration + + + + + + } + label="Requires Authentication?" + /> + + {requiresAuth && ( + <> + + + + + + + + )} + +
+
+ ); +}; + +export default ProxyForm; diff --git a/src/pages/MainPage.tsx b/src/pages/MainPage.tsx index b3a36349..a6a62258 100644 --- a/src/pages/MainPage.tsx +++ b/src/pages/MainPage.tsx @@ -3,6 +3,7 @@ import { MainMenu } from "../components/organisms/MainMenu"; import { Grid, Stack } from "@mui/material"; import { Recordings } from "../components/organisms/Recordings"; import { Runs } from "../components/organisms/Runs"; +import ProxyForm from '../components/organisms/ProxyForm'; import { useGlobalInfoStore } from "../context/globalInfo"; import { createRunForStoredRecording, interpretStoredRecording, notifyAboutAbort, scheduleStoredRecording } from "../api/storage"; import { handleUploadCredentials } from "../api/integration" @@ -142,6 +143,8 @@ export const MainPage = ({ handleEditRecording }: MainPageProps) => { runId={ids.runId} runningRecordingName={runningRecordingName} />; + case 'proxy': + return ; default: return null; }