mirror of
https://github.com/house-of-vanity/OutFleet.git
synced 2025-10-24 17:29:08 +00:00
feat: added template innbounding feature
This commit is contained in:
@@ -0,0 +1,57 @@
|
|||||||
|
import { useForm, type SubmitHandler } from 'react-hook-form'
|
||||||
|
import type { CreateTemplateForm } from '../../types';
|
||||||
|
import { useAppDispatch } from '../../../../common/hooks';
|
||||||
|
import { protocolOptions } from './util'
|
||||||
|
import type { CreateTemplateDTO } from '../../duck/dto';
|
||||||
|
import { createTemplateAction } from '../../duck';
|
||||||
|
|
||||||
|
|
||||||
|
export const AddTemplate = () => {
|
||||||
|
const dispatch = useAppDispatch()
|
||||||
|
const { register, handleSubmit, reset } = useForm<CreateTemplateForm>({
|
||||||
|
defaultValues: {
|
||||||
|
default_port: '443'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const onSubmit: SubmitHandler<CreateTemplateForm> = (values) => {
|
||||||
|
const data: CreateTemplateDTO = {
|
||||||
|
...values,
|
||||||
|
default_port: parseInt(values.default_port),
|
||||||
|
config_template: ''
|
||||||
|
}
|
||||||
|
dispatch(createTemplateAction(data)).then(() => {
|
||||||
|
reset();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form onSubmit={handleSubmit(onSubmit)} id="templateForm">
|
||||||
|
<div className="form-group">
|
||||||
|
<label>Name:</label>
|
||||||
|
<input type="text" {...register('name', { required: true })} />
|
||||||
|
</div>
|
||||||
|
<div className="form-group">
|
||||||
|
<label>Protocol:</label>
|
||||||
|
<select {...register('protocol', {required: true})}>
|
||||||
|
{Object.entries(protocolOptions).map((protocolTupple)=> (
|
||||||
|
<option value={protocolTupple[0]}>{protocolTupple[1]}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div className="form-group">
|
||||||
|
<label>Default Port:</label>
|
||||||
|
<input type="number" {...register('default_port', {required: true})} />
|
||||||
|
</div>
|
||||||
|
<div className="form-group">
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" {...register('requires_tls')}/> Requires TLS
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<button type="submit" className="btn btn-primary">
|
||||||
|
Add Template
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from './add-template'
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import type { Protocol } from '../../duck/dto'
|
||||||
|
|
||||||
|
export const protocolOptions: Record<Protocol, string> = {
|
||||||
|
vless: 'VLESS',
|
||||||
|
vmess: 'VMess',
|
||||||
|
trojan: 'Trojan',
|
||||||
|
shadowsocks: 'Shadowsocks'
|
||||||
|
}
|
||||||
|
|
||||||
2
client/src/features/templates/components/index.ts
Normal file
2
client/src/features/templates/components/index.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export * from './add-template'
|
||||||
|
export * from './template-list'
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from './template-list'
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
import { useEffect, type FC } from 'react';
|
||||||
|
import { useForm } from 'react-hook-form';
|
||||||
|
import {
|
||||||
|
Modal,
|
||||||
|
ModalContent,
|
||||||
|
ModalHeader,
|
||||||
|
ModalBody,
|
||||||
|
ModalFooter,
|
||||||
|
Button,
|
||||||
|
} from '@heroui/react';
|
||||||
|
import type { EditTemplateForm } from '../../types';
|
||||||
|
import { useAppDispatch } from '../../../../common/hooks';
|
||||||
|
import { getTemplateById } from '../../duck/api';
|
||||||
|
import { protocolOptions } from '../add-template/util';
|
||||||
|
import { updateTemplate } from '../../duck';
|
||||||
|
|
||||||
|
export interface ServerEditProps {
|
||||||
|
templateId: string;
|
||||||
|
isOpen: boolean;
|
||||||
|
onOpenChange: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TemplateEdit: FC<ServerEditProps> = (props) => {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const { templateId, isOpen, onOpenChange } = props;
|
||||||
|
const { register, handleSubmit, reset } = useForm<EditTemplateForm>();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getTemplateById(templateId).then((response) => {
|
||||||
|
const { data } = response;
|
||||||
|
reset({
|
||||||
|
...data,
|
||||||
|
default_port: String(data.default_port),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}, [templateId]);
|
||||||
|
|
||||||
|
const onSubmit = (values: EditTemplateForm) => {
|
||||||
|
const data = {
|
||||||
|
...values,
|
||||||
|
default_port: parseInt(values.default_port),
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log({data})
|
||||||
|
|
||||||
|
dispatch(
|
||||||
|
updateTemplate({
|
||||||
|
id: templateId,
|
||||||
|
server: data,
|
||||||
|
}),
|
||||||
|
).then(() => {
|
||||||
|
onOpenChange();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
|
||||||
|
<form onSubmit={handleSubmit(onSubmit)}>
|
||||||
|
<ModalContent>
|
||||||
|
{(onClose) => (
|
||||||
|
<>
|
||||||
|
<ModalHeader className="flex flex-col gap-1">
|
||||||
|
Modal Title
|
||||||
|
</ModalHeader>
|
||||||
|
<ModalBody>
|
||||||
|
<div>
|
||||||
|
<div className="form-group">
|
||||||
|
<label>Name:</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
{...register('name', { required: true })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="form-group">
|
||||||
|
<label>Protocol:</label>
|
||||||
|
<select
|
||||||
|
id="editProtocol"
|
||||||
|
{...register('protocol', { required: true })}
|
||||||
|
>
|
||||||
|
{Object.entries(protocolOptions).map((protocolTupple) => (
|
||||||
|
<option value={protocolTupple[0]}>
|
||||||
|
{protocolTupple[1]}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div className="form-group">
|
||||||
|
<label>Default Port:</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
{...register('default_port', {required: true})}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="form-group">
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" {...register('requires_tls')} />{' '}
|
||||||
|
Requires TLS
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div className="form-group">
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" {...register('is_active')} />{' '}
|
||||||
|
Active
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ModalBody>
|
||||||
|
<ModalFooter>
|
||||||
|
<Button color="primary" type="submit">
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
<Button color="danger" variant="light" onPress={onClose}>
|
||||||
|
Close
|
||||||
|
</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</ModalContent>
|
||||||
|
</form>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import type { FC } from 'react';
|
||||||
|
import type { TemplateDTO } from '../../duck';
|
||||||
|
import { TemplateView } from './template-view';
|
||||||
|
|
||||||
|
export interface TemplateListProps {
|
||||||
|
templates: TemplateDTO[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TemplateList: FC<TemplateListProps> = ({ templates }) => {
|
||||||
|
return (
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Protocol</th>
|
||||||
|
<th>Port</th>
|
||||||
|
<th>TLS</th>
|
||||||
|
<th>Active</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{templates.map((template) => (
|
||||||
|
<TemplateView template={template} key={template.id}/>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import type { FC } from 'react';
|
||||||
|
import { deleteTemplateAction, type TemplateDTO } from '../../duck';
|
||||||
|
import { useDisclosure } from '@heroui/react';
|
||||||
|
import { TemplateEdit } from './template-edit';
|
||||||
|
import { useAppDispatch } from '../../../../common/hooks';
|
||||||
|
|
||||||
|
export interface TemplateViewProps {
|
||||||
|
template: TemplateDTO;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TemplateView: FC<TemplateViewProps> = ({ template }) => {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const { isOpen, onOpen, onOpenChange } = useDisclosure();
|
||||||
|
|
||||||
|
const handleDeleteTemplate = () => {
|
||||||
|
if (confirm('Delete template?')) {
|
||||||
|
dispatch(deleteTemplateAction(template.id));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<tr>
|
||||||
|
<td>{template.name}</td>
|
||||||
|
<td>{template.protocol}</td>
|
||||||
|
<td>{template.default_port}</td>
|
||||||
|
<td>{template.requires_tls ? 'Yes' : 'No'}</td>
|
||||||
|
<td>{template.is_active ? 'Yes' : 'No'}</td>
|
||||||
|
<td>
|
||||||
|
<button className="btn btn-primary" onClick={onOpen}>
|
||||||
|
Edit
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="btn btn-danger"
|
||||||
|
onClick={handleDeleteTemplate}
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<TemplateEdit
|
||||||
|
templateId={template.id}
|
||||||
|
onOpenChange={onOpenChange}
|
||||||
|
isOpen={isOpen}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,8 +1,10 @@
|
|||||||
import { templatesSlice } from './slice';
|
import { templatesSlice } from './slice';
|
||||||
import { getTemplates } from './api';
|
import { getTemplates, createTemplate, patchTemplate, deleteTemplate } from './api';
|
||||||
import { createAsyncThunk } from '@reduxjs/toolkit';
|
import { createAsyncThunk } from '@reduxjs/toolkit';
|
||||||
import { getTemplatesState } from './selectors';
|
import { getTemplatesState } from './selectors';
|
||||||
import type { RootState } from '../../../store';
|
import type { RootState } from '../../../store';
|
||||||
|
import type { CreateTemplateDTO, EditTemplateDTO } from './dto';
|
||||||
|
import { appNotificator } from '../../../utils/notification/app-notificator';
|
||||||
|
|
||||||
const PREFFIX = 'templates';
|
const PREFFIX = 'templates';
|
||||||
|
|
||||||
@@ -27,3 +29,74 @@ export const fetchTemplates = createAsyncThunk(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
|
export const createTemplateAction = createAsyncThunk(
|
||||||
|
`${PREFFIX}/createTemplate`,
|
||||||
|
async (params: CreateTemplateDTO, { dispatch }) => {
|
||||||
|
try {
|
||||||
|
await createTemplate(params);
|
||||||
|
dispatch(fetchTemplates());
|
||||||
|
} catch (e) {
|
||||||
|
appNotificator.add({
|
||||||
|
message:
|
||||||
|
e instanceof Error
|
||||||
|
? e.message
|
||||||
|
: `Unknown error in ${PREFFIX}/createTemplate`,
|
||||||
|
type: 'error',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export const updateTemplate = createAsyncThunk(
|
||||||
|
`${PREFFIX}/updateTemplate`,
|
||||||
|
async (
|
||||||
|
params: {
|
||||||
|
id: string;
|
||||||
|
server: EditTemplateDTO;
|
||||||
|
},
|
||||||
|
{ dispatch },
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
await patchTemplate(params.id, params.server);
|
||||||
|
dispatch(fetchTemplates());
|
||||||
|
appNotificator.add({
|
||||||
|
message: 'Template updated',
|
||||||
|
type: 'success',
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
appNotificator.add({
|
||||||
|
type: 'error',
|
||||||
|
message:
|
||||||
|
e instanceof Error
|
||||||
|
? `Error updating: ${e.message}`
|
||||||
|
: `Unknown error in ${PREFFIX}/updateTemplate`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export const deleteTemplateAction = createAsyncThunk(
|
||||||
|
`${PREFFIX}/deleteTemplate`,
|
||||||
|
async (id: string, { dispatch }) => {
|
||||||
|
try {
|
||||||
|
await deleteTemplate(id);
|
||||||
|
appNotificator.add({
|
||||||
|
message: 'Server deleted',
|
||||||
|
type: 'success',
|
||||||
|
});
|
||||||
|
dispatch(fetchTemplates());
|
||||||
|
} catch (e) {
|
||||||
|
appNotificator.add({
|
||||||
|
type: 'error',
|
||||||
|
message:
|
||||||
|
e instanceof Error
|
||||||
|
? `Delete error: ${e.message}`
|
||||||
|
: `Unknown error in ${PREFFIX}/deleteTemplate`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
@@ -1,5 +1,23 @@
|
|||||||
import type { AxiosResponse } from 'axios';
|
import type { AxiosResponse } from 'axios';
|
||||||
import { api } from '../../../api/api';
|
import { api } from '../../../api/api';
|
||||||
import type { TemplateDTO } from './dto';
|
import type { TemplateDTO, CreateTemplateDTO, EditTemplateDTO } from './dto';
|
||||||
|
|
||||||
export const getTemplates = () => api.get<never, AxiosResponse<TemplateDTO[]>>('/templates');
|
export const getTemplates = () =>
|
||||||
|
api.get<never, AxiosResponse<TemplateDTO[]>>('/templates');
|
||||||
|
|
||||||
|
export const createTemplate = (params: CreateTemplateDTO) =>
|
||||||
|
api.post<AxiosResponse>('templates', params, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const getTemplateById = (id: string) =>
|
||||||
|
api.get<string, AxiosResponse<TemplateDTO>>(`/templates/${id}`);
|
||||||
|
|
||||||
|
export const patchTemplate = (id: string, server: EditTemplateDTO) =>
|
||||||
|
api.put(`/templates/${id}`, server, {
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
});
|
||||||
|
|
||||||
|
export const deleteTemplate = (id: string) => api.delete(`/templates/${id}`);
|
||||||
|
|||||||
@@ -1 +1,33 @@
|
|||||||
export interface TemplateDTO {}
|
export type Protocol = 'vless' | 'vmess' | 'trojan' | 'shadowsocks';
|
||||||
|
|
||||||
|
export interface TemplateDTO {
|
||||||
|
base_settings: Record<string, unknown>; // TODO define unknown
|
||||||
|
created_at: string;
|
||||||
|
default_port: number;
|
||||||
|
description: string;
|
||||||
|
id: string;
|
||||||
|
is_active: boolean;
|
||||||
|
name: string;
|
||||||
|
protocol: Protocol;
|
||||||
|
requires_domain: boolean;
|
||||||
|
requires_tls: boolean;
|
||||||
|
stream_settings: Record<string, unknown>; // TOD define unknown
|
||||||
|
updated_at: string;
|
||||||
|
variables: unknown[]; // TOD define unknown
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateTemplateDTO {
|
||||||
|
name: string;
|
||||||
|
protocol: Protocol;
|
||||||
|
default_port: number;
|
||||||
|
requires_tls: boolean;
|
||||||
|
config_template: '';
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EditTemplateDTO {
|
||||||
|
name: string,
|
||||||
|
protocol: Protocol
|
||||||
|
default_port: number
|
||||||
|
requires_tls: boolean
|
||||||
|
is_active: boolean
|
||||||
|
}
|
||||||
@@ -1 +1,2 @@
|
|||||||
export * from './duck'
|
export * from './duck'
|
||||||
|
export * from './components'
|
||||||
16
client/src/features/templates/types/form.ts
Normal file
16
client/src/features/templates/types/form.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import type { Protocol } from "../duck/dto"
|
||||||
|
|
||||||
|
export interface CreateTemplateForm {
|
||||||
|
name: string,
|
||||||
|
protocol: Protocol
|
||||||
|
default_port: string
|
||||||
|
requires_tls: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EditTemplateForm {
|
||||||
|
name: string,
|
||||||
|
protocol: Protocol
|
||||||
|
default_port: string
|
||||||
|
requires_tls: boolean
|
||||||
|
is_active: boolean
|
||||||
|
}
|
||||||
1
client/src/features/templates/types/index.ts
Normal file
1
client/src/features/templates/types/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from './form'
|
||||||
@@ -1,53 +1,32 @@
|
|||||||
import type { RouteObject } from 'react-router';
|
import type { RouteObject } from 'react-router';
|
||||||
|
import { AddTemplate, fetchTemplates, getTemplatesState, TemplateList } from '../../features/templates';
|
||||||
|
import { useAppDispatch, useAppSelector } from '../../common/hooks';
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
export const InboundTemplates = () => {
|
export const InboundTemplates = () => {
|
||||||
|
const dispatch = useAppDispatch()
|
||||||
|
const { loading, templates } = useAppSelector(getTemplatesState);
|
||||||
|
|
||||||
|
useEffect(()=>{
|
||||||
|
dispatch(fetchTemplates())
|
||||||
|
}, [dispatch])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div id="templates" className="tab-content active">
|
<div id="templates" className="tab-content active">
|
||||||
<div className="section">
|
<div className="section">
|
||||||
<h2>Add Template</h2>
|
<h2>Add Template</h2>
|
||||||
<form id="templateForm">
|
<AddTemplate />
|
||||||
<div className="form-group">
|
|
||||||
<label>Name:</label>
|
|
||||||
<input type="text" id="templateName" required />
|
|
||||||
</div>
|
|
||||||
<div className="form-group">
|
|
||||||
<label>Protocol:</label>
|
|
||||||
<select id="templateProtocol" required>
|
|
||||||
<option value="vless">VLESS</option>
|
|
||||||
<option value="vmess">VMess</option>
|
|
||||||
<option value="trojan">Trojan</option>
|
|
||||||
<option value="shadowsocks">Shadowsocks</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div className="form-group">
|
|
||||||
<label>Default Port:</label>
|
|
||||||
<input type="number" id="templatePort" value="443" required />
|
|
||||||
</div>
|
|
||||||
<div className="form-group">
|
|
||||||
<label>
|
|
||||||
<input type="checkbox" id="templateTls" /> Requires TLS
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div className="form-group">
|
|
||||||
<label>Configuration Template:</label>
|
|
||||||
<textarea
|
|
||||||
id="templateConfig"
|
|
||||||
rows={6}
|
|
||||||
style={{
|
|
||||||
width: '300px',
|
|
||||||
}}
|
|
||||||
></textarea>
|
|
||||||
</div>
|
|
||||||
<button type="submit" className="btn btn-primary">
|
|
||||||
Add Template
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="section">
|
<div className="section">
|
||||||
<h2>Templates List</h2>
|
<h2>Templates List</h2>
|
||||||
<div id="templatesList" className="loading">
|
<div id="templatesList" className="loading">
|
||||||
Loading...
|
{loading && 'Loading...'}
|
||||||
|
{templates.length ? (
|
||||||
|
<TemplateList templates={templates}/>
|
||||||
|
) : (
|
||||||
|
<p>No templates found</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user