From f572b28711446b2fb983dd4245439443bda3f95b Mon Sep 17 00:00:00 2001 From: Home Date: Sat, 18 Oct 2025 17:38:41 +0300 Subject: [PATCH] feat: added template innbounding feature --- .../components/add-template/add-template.tsx | 57 ++++++++ .../components/add-template/index.ts | 1 + .../templates/components/add-template/util.ts | 9 ++ .../features/templates/components/index.ts | 2 + .../components/template-list/index.ts | 1 + .../template-list/template-edit.tsx | 122 ++++++++++++++++++ .../template-list/template-list.tsx | 29 +++++ .../template-list/template-view.tsx | 48 +++++++ client/src/features/templates/duck/actions.ts | 75 ++++++++++- client/src/features/templates/duck/api.ts | 22 +++- client/src/features/templates/duck/dto.ts | 34 ++++- client/src/features/templates/index.ts | 3 +- client/src/features/templates/types/form.ts | 16 +++ client/src/features/templates/types/index.ts | 1 + .../inbound-templates/inboud-templates.tsx | 57 +++----- 15 files changed, 433 insertions(+), 44 deletions(-) create mode 100644 client/src/features/templates/components/add-template/add-template.tsx create mode 100644 client/src/features/templates/components/add-template/index.ts create mode 100644 client/src/features/templates/components/add-template/util.ts create mode 100644 client/src/features/templates/components/index.ts create mode 100644 client/src/features/templates/components/template-list/index.ts create mode 100644 client/src/features/templates/components/template-list/template-edit.tsx create mode 100644 client/src/features/templates/components/template-list/template-list.tsx create mode 100644 client/src/features/templates/components/template-list/template-view.tsx create mode 100644 client/src/features/templates/types/form.ts create mode 100644 client/src/features/templates/types/index.ts diff --git a/client/src/features/templates/components/add-template/add-template.tsx b/client/src/features/templates/components/add-template/add-template.tsx new file mode 100644 index 0000000..471d083 --- /dev/null +++ b/client/src/features/templates/components/add-template/add-template.tsx @@ -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({ + defaultValues: { + default_port: '443' + } + }); + + const onSubmit: SubmitHandler = (values) => { + const data: CreateTemplateDTO = { + ...values, + default_port: parseInt(values.default_port), + config_template: '' + } + dispatch(createTemplateAction(data)).then(() => { + reset(); + }); + }; + + + return ( +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+ +
+ ) +} \ No newline at end of file diff --git a/client/src/features/templates/components/add-template/index.ts b/client/src/features/templates/components/add-template/index.ts new file mode 100644 index 0000000..48fd01a --- /dev/null +++ b/client/src/features/templates/components/add-template/index.ts @@ -0,0 +1 @@ +export * from './add-template' \ No newline at end of file diff --git a/client/src/features/templates/components/add-template/util.ts b/client/src/features/templates/components/add-template/util.ts new file mode 100644 index 0000000..ff762a6 --- /dev/null +++ b/client/src/features/templates/components/add-template/util.ts @@ -0,0 +1,9 @@ +import type { Protocol } from '../../duck/dto' + +export const protocolOptions: Record = { + vless: 'VLESS', + vmess: 'VMess', + trojan: 'Trojan', + shadowsocks: 'Shadowsocks' +} + diff --git a/client/src/features/templates/components/index.ts b/client/src/features/templates/components/index.ts new file mode 100644 index 0000000..bd8f65f --- /dev/null +++ b/client/src/features/templates/components/index.ts @@ -0,0 +1,2 @@ +export * from './add-template' +export * from './template-list' \ No newline at end of file diff --git a/client/src/features/templates/components/template-list/index.ts b/client/src/features/templates/components/template-list/index.ts new file mode 100644 index 0000000..630ec58 --- /dev/null +++ b/client/src/features/templates/components/template-list/index.ts @@ -0,0 +1 @@ +export * from './template-list' \ No newline at end of file diff --git a/client/src/features/templates/components/template-list/template-edit.tsx b/client/src/features/templates/components/template-list/template-edit.tsx new file mode 100644 index 0000000..7bb58ed --- /dev/null +++ b/client/src/features/templates/components/template-list/template-edit.tsx @@ -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 = (props) => { + const dispatch = useAppDispatch(); + const { templateId, isOpen, onOpenChange } = props; + const { register, handleSubmit, reset } = useForm(); + + 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 ( + +
+ + {(onClose) => ( + <> + + Modal Title + + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+ +
+
+
+ + + + + + )} +
+
+
+ ); +}; diff --git a/client/src/features/templates/components/template-list/template-list.tsx b/client/src/features/templates/components/template-list/template-list.tsx new file mode 100644 index 0000000..5c77118 --- /dev/null +++ b/client/src/features/templates/components/template-list/template-list.tsx @@ -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 = ({ templates }) => { + return ( + + + + + + + + + + + + + {templates.map((template) => ( + + ))} + +
NameProtocolPortTLSActiveActions
+ ); +}; diff --git a/client/src/features/templates/components/template-list/template-view.tsx b/client/src/features/templates/components/template-list/template-view.tsx new file mode 100644 index 0000000..80197b8 --- /dev/null +++ b/client/src/features/templates/components/template-list/template-view.tsx @@ -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 = ({ template }) => { + const dispatch = useAppDispatch(); + const { isOpen, onOpen, onOpenChange } = useDisclosure(); + + const handleDeleteTemplate = () => { + if (confirm('Delete template?')) { + dispatch(deleteTemplateAction(template.id)); + } + }; + + return ( + <> + + {template.name} + {template.protocol} + {template.default_port} + {template.requires_tls ? 'Yes' : 'No'} + {template.is_active ? 'Yes' : 'No'} + + + + + + + + ); +}; diff --git a/client/src/features/templates/duck/actions.ts b/client/src/features/templates/duck/actions.ts index 77c8799..bb28966 100644 --- a/client/src/features/templates/duck/actions.ts +++ b/client/src/features/templates/duck/actions.ts @@ -1,8 +1,10 @@ import { templatesSlice } from './slice'; -import { getTemplates } from './api'; +import { getTemplates, createTemplate, patchTemplate, deleteTemplate } from './api'; import { createAsyncThunk } from '@reduxjs/toolkit'; import { getTemplatesState } from './selectors'; import type { RootState } from '../../../store'; +import type { CreateTemplateDTO, EditTemplateDTO } from './dto'; +import { appNotificator } from '../../../utils/notification/app-notificator'; 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`, + }); + } + }, +); \ No newline at end of file diff --git a/client/src/features/templates/duck/api.ts b/client/src/features/templates/duck/api.ts index f30a79e..ac2edb2 100644 --- a/client/src/features/templates/duck/api.ts +++ b/client/src/features/templates/duck/api.ts @@ -1,5 +1,23 @@ import type { AxiosResponse } from 'axios'; import { api } from '../../../api/api'; -import type { TemplateDTO } from './dto'; +import type { TemplateDTO, CreateTemplateDTO, EditTemplateDTO } from './dto'; -export const getTemplates = () => api.get>('/templates'); \ No newline at end of file +export const getTemplates = () => + api.get>('/templates'); + +export const createTemplate = (params: CreateTemplateDTO) => + api.post('templates', params, { + headers: { + 'Content-Type': 'application/json', + }, + }); + +export const getTemplateById = (id: string) => + api.get>(`/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}`); diff --git a/client/src/features/templates/duck/dto.ts b/client/src/features/templates/duck/dto.ts index ecb4dc2..426a987 100644 --- a/client/src/features/templates/duck/dto.ts +++ b/client/src/features/templates/duck/dto.ts @@ -1 +1,33 @@ -export interface TemplateDTO {} \ No newline at end of file +export type Protocol = 'vless' | 'vmess' | 'trojan' | 'shadowsocks'; + +export interface TemplateDTO { + base_settings: Record; // 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; // 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 +} \ No newline at end of file diff --git a/client/src/features/templates/index.ts b/client/src/features/templates/index.ts index edc3f9b..f37e401 100644 --- a/client/src/features/templates/index.ts +++ b/client/src/features/templates/index.ts @@ -1 +1,2 @@ -export * from './duck' \ No newline at end of file +export * from './duck' +export * from './components' \ No newline at end of file diff --git a/client/src/features/templates/types/form.ts b/client/src/features/templates/types/form.ts new file mode 100644 index 0000000..d54ec51 --- /dev/null +++ b/client/src/features/templates/types/form.ts @@ -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 +} diff --git a/client/src/features/templates/types/index.ts b/client/src/features/templates/types/index.ts new file mode 100644 index 0000000..b26032e --- /dev/null +++ b/client/src/features/templates/types/index.ts @@ -0,0 +1 @@ +export * from './form' \ No newline at end of file diff --git a/client/src/pages/inbound-templates/inboud-templates.tsx b/client/src/pages/inbound-templates/inboud-templates.tsx index 160210f..78fa026 100644 --- a/client/src/pages/inbound-templates/inboud-templates.tsx +++ b/client/src/pages/inbound-templates/inboud-templates.tsx @@ -1,53 +1,32 @@ 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 = () => { + const dispatch = useAppDispatch() + const { loading, templates } = useAppSelector(getTemplatesState); + + useEffect(()=>{ + dispatch(fetchTemplates()) + }, [dispatch]) + return (

Add Template

-
-
- - -
-
- - -
-
- - -
-
- -
-
- - -
- -
+

Templates List

- Loading... + {loading && 'Loading...'} + {templates.length ? ( + + ) : ( +

No templates found

+ )}
@@ -57,4 +36,4 @@ export const InboundTemplates = () => { export const InboundTemplatesRoute: RouteObject = { path: '/inbound-templates', Component: InboundTemplates, -}; +}; \ No newline at end of file