diff --git a/app/dashboard/public/locales/en.json b/app/dashboard/public/locales/en.json index 766c039..66a7620 100644 --- a/app/dashboard/public/locales/en.json +++ b/app/dashboard/public/locales/en.json @@ -1,4 +1,6 @@ { + "home": "Home", + "hosts": "Hosts", "password": "Password", "cancel": "Cancel", "apply": "Apply", @@ -9,6 +11,7 @@ "username": "Username", "name": "Name", "node": "Node", + "nodes": "Nodes", "xrayVersion": "xray version", "addNode": "Add Node", "expires": "Expires in {{time}}", @@ -36,13 +39,6 @@ "donation": "Donation", "logout": "Log out" }, - "pageHeaderTitle": { - "Home": "Home", - "Users": "Users", - "Services": "Services", - "Host": "Host", - "Nodes": "Nodes" - }, "deleteUser": { "title": "Delete User", "prompt": "Are you sure you want to delete {{username}}?", @@ -121,8 +117,6 @@ }, "nodeDialog": { "editNodeTitle": "Edit node", - "editNode": "Edit node", - "addNode": "Add node", "createNewNode": "Create node", "nodeEdited": "Node {{name}} edited.", "nodeCreated": "Node {{name}} created.", @@ -130,7 +124,25 @@ "optional": "optional", "total": "Total: ", "updateSubmit": "Submit update", - "addSubmit": "Submit" + "addSubmit": "Submit", + "title": "Using Marzban-Node, you are able to scale up your connection quality by adding different nodes on different servers.", + "addNewMarzbanNode": "Add New Marzban Node", + "certificate": "Certificate", + "connection-hint": "To setup a Marzban Node, you need to set this certificate on the node to initialize a secure connection between main server and the node", + "download-certificate": "Download certificate", + "show-certificate": "Show certificate", + "hide-certificate": "Hide certificate", + "addHostForEveryInbound": "Add this node as a new host for every inbound", + "addNode": "Add Node", + "addNodeSuccess": "Node {{name}} added successfully", + "apply": "editNode", + "nodeName": "Name", + "nodeAddress": "Address", + "nodePort": "Port", + "nodeAPIPort": "API Port", + "editNode": "Update Node", + "reconnecting": "Reconnecting...", + "reconnect": "Reconnect" }, "serviceDialog": { "editServiceTitle": "Edit service", @@ -179,26 +191,6 @@ "host.wildcard": "Use * to generate a random string (works for wildcard domain names)" }, "revoke": "Revoke", - "nodes": { - "title": "Using Marzban-Node, you are able to scale up your connection quality by adding different nodes on different servers.", - "addNewMarzbanNode": "Add New Marzban Node", - "certificate": "Certificate", - "connection-hint": "To setup a Marzban Node, you need to set this certificate on the node to initialize a secure connection between main server and the node", - "download-certificate": "Download certificate", - "show-certificate": "Show certificate", - "hide-certificate": "Hide certificate", - "addHostForEveryInbound": "Add this node as a new host for every inbound", - "addNode": "Add Node", - "addNodeSuccess": "Node {{name}} added successfully", - "apply": "editNode", - "nodeName": "Name", - "nodeAddress": "Address", - "nodePort": "Port", - "nodeAPIPort": "API Port", - "editNode": "Update Node", - "reconnecting": "Reconnecting...", - "reconnect": "Reconnect" - }, "deleteNode.title": "Delete Node", "deleteNode.prompt": "Are you sure you want to delete the {{name}} node?", "deleteNode.deleteSuccess": "Node {{name}} removed successfully", diff --git a/app/dashboard/src/components/Footer.tsx b/app/dashboard/src/components/Footer.tsx index 3dcd9fd..94f674b 100644 --- a/app/dashboard/src/components/Footer.tsx +++ b/app/dashboard/src/components/Footer.tsx @@ -1,6 +1,6 @@ import { BoxProps, HStack, Link, Text } from '@chakra-ui/react'; import { ORGANIZATION_URL, REPO_URL } from 'constants/Project'; -import { useDashboard } from 'contexts/DashboardContext'; +import { useDashboard } from 'stores'; import { FC } from 'react'; export const Footer: FC = (props) => { diff --git a/app/dashboard/src/components/StatusBadge.tsx b/app/dashboard/src/components/StatusBadge.tsx index b5a7c54..ab43bab 100644 --- a/app/dashboard/src/components/StatusBadge.tsx +++ b/app/dashboard/src/components/StatusBadge.tsx @@ -3,7 +3,7 @@ import { Badge, Text } from '@chakra-ui/react'; import { statusColors } from 'constants/Settings'; import { FC } from 'react'; import { useTranslation } from 'react-i18next'; -import { Status as StatusType } from 'types/User'; +import { Status as StatusType } from 'types'; import { relativeExpiryDate } from 'utils/dateFormatter'; type StatusProps = { diff --git a/app/dashboard/src/components/UsageFilter.tsx b/app/dashboard/src/components/UsageFilter.tsx index d37c683..36576fd 100644 --- a/app/dashboard/src/components/UsageFilter.tsx +++ b/app/dashboard/src/components/UsageFilter.tsx @@ -20,7 +20,7 @@ import { } from '@chakra-ui/react'; import { CalendarIcon, ChevronRightIcon } from '@heroicons/react/24/outline'; import { ApexOptions } from 'apexcharts'; -import { FilterUsageType } from 'types/Filter'; +import { FilterUsageType } from 'types/filter'; import dayjs, { ManipulateType } from 'dayjs'; import { FC, useRef, useState } from 'react'; import ReactDatePicker from 'react-datepicker'; diff --git a/app/dashboard/src/components/dialog/ModalHeader.tsx b/app/dashboard/src/components/dialog/ModalHeader.tsx index 1ee02c5..23a7599 100644 --- a/app/dashboard/src/components/dialog/ModalHeader.tsx +++ b/app/dashboard/src/components/dialog/ModalHeader.tsx @@ -1,6 +1,6 @@ import { Text, HStack, ModalHeader } from '@chakra-ui/react'; -import { ChakraIcon } from 'types/ChakraIcon'; +import { ChakraIcon } from 'types'; import { Icon } from '../icon'; export const DialogModalHeader = ({ HeaderIcon, title }: { HeaderIcon: ChakraIcon, title: string }) => { diff --git a/app/dashboard/src/components/header/index.tsx b/app/dashboard/src/components/header/index.tsx index 4f09473..1877895 100644 --- a/app/dashboard/src/components/header/index.tsx +++ b/app/dashboard/src/components/header/index.tsx @@ -18,8 +18,7 @@ import { } from '@heroicons/react/24/outline'; import { pages } from 'constants/Pages'; import { DONATION_URL, REPO_URL } from 'constants/Project'; -import { useDashboard } from 'contexts/DashboardContext'; -import { useNodes } from 'contexts/NodesContext'; +import { useNodes, useDashboard } from 'stores'; import differenceInDays from 'date-fns/differenceInDays'; import isValid from 'date-fns/isValid'; import { FC, ReactNode, useState } from 'react'; diff --git a/app/dashboard/src/components/sidebar/sidebar-item.tsx b/app/dashboard/src/components/sidebar/sidebar-item.tsx index 5722c8b..36dc6b6 100644 --- a/app/dashboard/src/components/sidebar/sidebar-item.tsx +++ b/app/dashboard/src/components/sidebar/sidebar-item.tsx @@ -1,18 +1,18 @@ import { Flex, Text } from '@chakra-ui/react' -import { useDashboard } from 'contexts/DashboardContext'; +import { useDashboard } from 'stores'; import { Link as ReactRouterLink } from 'react-router-dom' import { Link as ChakraLink } from '@chakra-ui/react' import { Icon } from 'components/icon'; import { FC, useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { PageType } from 'types/Page'; +import { PageType } from 'types'; export type SideBarItemProps = Pick< - PageType, - | 'name' - | 'path' - | 'itemIcon' + PageType, + | 'name' + | 'path' + | 'itemIcon' > export const SidebarItem: FC = ({ itemIcon: ItemIcon, name, path, index }) => { diff --git a/app/dashboard/src/components/Table/EmptySection.tsx b/app/dashboard/src/components/table/empty-section.tsx similarity index 100% rename from app/dashboard/src/components/Table/EmptySection.tsx rename to app/dashboard/src/components/table/empty-section.tsx diff --git a/app/dashboard/src/components/Table/Icons.ts b/app/dashboard/src/components/table/icons.ts similarity index 100% rename from app/dashboard/src/components/Table/Icons.ts rename to app/dashboard/src/components/table/icons.ts diff --git a/app/dashboard/src/components/table/index.ts b/app/dashboard/src/components/table/index.ts new file mode 100644 index 0000000..959892b --- /dev/null +++ b/app/dashboard/src/components/table/index.ts @@ -0,0 +1,5 @@ +export * from './icons'; +export * from './sort'; +export * from './pagination'; +export * from './status-sort-select'; +export * from './empty-section'; diff --git a/app/dashboard/src/components/Table/Pagination.tsx b/app/dashboard/src/components/table/pagination.tsx similarity index 98% rename from app/dashboard/src/components/Table/Pagination.tsx rename to app/dashboard/src/components/table/pagination.tsx index 995a5f8..c734d1b 100644 --- a/app/dashboard/src/components/Table/Pagination.tsx +++ b/app/dashboard/src/components/table/pagination.tsx @@ -13,7 +13,7 @@ import { } from '@heroicons/react/24/outline'; import { ChangeEvent, FC } from 'react'; import { useTranslation } from 'react-i18next'; -import { FilterType } from 'types/Filter'; +import { FilterType } from 'types'; import { setUsersPerPageLimitSize } from 'utils/userPreferenceStorage'; const PrevIcon = chakra(ArrowLongLeftIcon, { diff --git a/app/dashboard/src/components/Table/Sort.tsx b/app/dashboard/src/components/table/sort.tsx similarity index 78% rename from app/dashboard/src/components/Table/Sort.tsx rename to app/dashboard/src/components/table/sort.tsx index 98849e3..f570880 100644 --- a/app/dashboard/src/components/Table/Sort.tsx +++ b/app/dashboard/src/components/table/sort.tsx @@ -1,9 +1,9 @@ -import { SortIcon } from './Icons'; +import { SortIcon } from './icons'; import { FC } from 'react'; type SortType = { - sort: string; - column: string; + sort: string; + column: string; }; export const Sort: FC = ({ sort, column }) => { diff --git a/app/dashboard/src/components/Table/StatusSortSelect.tsx b/app/dashboard/src/components/table/status-sort-select.tsx similarity index 94% rename from app/dashboard/src/components/Table/StatusSortSelect.tsx rename to app/dashboard/src/components/table/status-sort-select.tsx index 24bf606..869fc05 100644 --- a/app/dashboard/src/components/Table/StatusSortSelect.tsx +++ b/app/dashboard/src/components/table/status-sort-select.tsx @@ -1,6 +1,6 @@ import { Select } from '@chakra-ui/react' -import { FilterType } from 'types/Filter'; +import { FilterType } from 'types'; import React from 'react' interface StatusSortSelectProps { diff --git a/app/dashboard/src/constants/Pages.tsx b/app/dashboard/src/constants/Pages.tsx index ddef501..a7f15b1 100644 --- a/app/dashboard/src/constants/Pages.tsx +++ b/app/dashboard/src/constants/Pages.tsx @@ -1,4 +1,4 @@ -import { PageType } from 'types/Page'; +import { PageType } from 'types'; import { chakra } from '@chakra-ui/react'; import { CubeIcon, HomeIcon, ServerIcon, ServerStackIcon, UsersIcon } from '@heroicons/react/24/outline'; import { IconBaseStyle } from 'constants/IconBaseStyle'; @@ -11,28 +11,28 @@ const HomeSidebarItemIcon = chakra(HomeIcon, IconBaseStyle); export const pages: PageType[] = [ { - name: 'pageHeaderTitle.Home', + name: 'home', path: '/', itemIcon: HomeSidebarItemIcon, }, { - name: 'pageHeaderTitle.Users', + name: 'users', path: '/users', itemIcon: UsersSidebarItemIcon, }, { - name: 'pageHeaderTitle.Services', + name: 'services', path: '/services', itemIcon: ServicesSidebarItemIcon, }, { - name: 'pageHeaderTitle.Nodes', + name: 'nodes', path: '/nodes', itemIcon: NodesSidebarItemIcon, }, { - name: 'pageHeaderTitle.Host', - path: '/host', + name: 'hosts', + path: '/hosts', itemIcon: HostSidebarItemIcon, }, ]; diff --git a/app/dashboard/src/constants/Proxies.tsx b/app/dashboard/src/constants/Proxies.tsx index 127316a..ba5cbb1 100644 --- a/app/dashboard/src/constants/Proxies.tsx +++ b/app/dashboard/src/constants/Proxies.tsx @@ -1,4 +1,4 @@ -import { ProxyHostSecurity } from 'types/ProxyHosts'; +import { ProxyHostSecurity } from 'types'; export const proxyHostSecurity: { title: string; value: ProxyHostSecurity }[] = [ diff --git a/app/dashboard/src/constants/Settings.tsx b/app/dashboard/src/constants/Settings.tsx index f049fb2..f8c92a3 100644 --- a/app/dashboard/src/constants/Settings.tsx +++ b/app/dashboard/src/constants/Settings.tsx @@ -6,7 +6,7 @@ import { WifiIcon, } from '@heroicons/react/24/outline'; import { ForwardRefExoticComponent, SVGProps } from 'react'; -import { DataLimitResetStrategy } from 'types/User'; +import { DataLimitResetStrategy } from 'types'; const iconProps = { baseStyle: { diff --git a/app/dashboard/src/contexts/HostsContext.ts b/app/dashboard/src/contexts/HostsContext.ts deleted file mode 100644 index 5c911ff..0000000 --- a/app/dashboard/src/contexts/HostsContext.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { fetch } from 'service/http'; -import { create } from 'zustand'; - -type HostsSchema = Record< - string, - { - remark: string; - address: string; - port: number | null; - path: string | null; - sni: string | null; - host: string | null; - }[] ->; - -type HostsStore = { - isLoading: boolean; - isPostLoading: boolean; - hosts: HostsSchema; - fetchHosts: () => void; - setHosts: (hosts: HostsSchema) => Promise; - onEditingHosts: (isEditingHosts: boolean) => void; - isEditingHosts: boolean; -}; -export const useHosts = create((set) => ({ - isLoading: false, - isPostLoading: false, - hosts: {}, - isEditingHosts: false, - onEditingHosts: (isEditingHosts: boolean) => { - set({ isEditingHosts }); - }, - fetchHosts: () => { - set({ isLoading: true }); - fetch('/hosts') - .then((hosts) => set({ hosts })) - .finally(() => set({ isLoading: false })); - }, - setHosts: (body) => { - set({ isPostLoading: true }); - return fetch('/hosts', { method: 'PUT', body }).finally(() => { - set({ isPostLoading: false }); - }); - }, -})); diff --git a/app/dashboard/src/contexts/InboundsContext.ts b/app/dashboard/src/contexts/InboundsContext.ts deleted file mode 100644 index 580d7bd..0000000 --- a/app/dashboard/src/contexts/InboundsContext.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { create } from 'zustand'; -import { subscribeWithSelector } from 'zustand/middleware'; -import { useDashboard } from './DashboardContext'; -import { fetch } from 'service/http'; -import { Inbounds } from 'types/Inbounds'; - - -export const fetchInbounds = async () => { - useDashboard.setState({ loading: true }); - return fetch('/inbounds') - .then((inbounds: Inbounds) => { - useInbounds.setState({ inbounds }); - }) - .finally(() => { - useDashboard.setState({ loading: false }); - }); -}; - - -type InboundsStateType = { - inbounds: Inbounds; - refetchInbounds: () => void; -}; - -export const useInbounds = create( - subscribeWithSelector(() => ({ - inbounds: [], - refetchInbounds: () => { - fetchInbounds(); - }, - })) -); diff --git a/app/dashboard/src/layouts/console-layout.tsx b/app/dashboard/src/layouts/console-layout.tsx index 1f768f8..be59e79 100644 --- a/app/dashboard/src/layouts/console-layout.tsx +++ b/app/dashboard/src/layouts/console-layout.tsx @@ -5,7 +5,7 @@ import { Header } from 'components/header'; import { Sidebar } from 'components/sidebar'; import { Suspense, useRef } from 'react'; import { Outlet } from 'react-router-dom'; -import { useDashboard } from 'contexts/DashboardContext'; +import { useDashboard } from 'stores'; export const ConsoleLayout = () => { const ref = useRef(null); diff --git a/app/dashboard/src/pages/home/statistics.tsx b/app/dashboard/src/pages/home/statistics.tsx index ae7c6b6..8e3c166 100644 --- a/app/dashboard/src/pages/home/statistics.tsx +++ b/app/dashboard/src/pages/home/statistics.tsx @@ -4,7 +4,7 @@ import { ChartPieIcon, UsersIcon, } from '@heroicons/react/24/outline'; -import { useDashboard } from 'contexts/DashboardContext'; +import { useDashboard } from 'stores'; import { FC } from 'react'; import { useTranslation } from 'react-i18next'; import { useQuery } from 'react-query'; @@ -75,7 +75,7 @@ export const Statistics: FC = (props) => { display="inline-block" pb="5px" > - / {numberWithCommas(systemData.total_user)} + / {numberWithCommas(systemData.total_user)} ) @@ -86,9 +86,9 @@ export const Statistics: FC = (props) => { title={t('dataUsage')} content={ systemData && - formatBytes( - systemData.incoming_bandwidth + systemData.outgoing_bandwidth - ) + formatBytes( + systemData.incoming_bandwidth + systemData.outgoing_bandwidth + ) } icon={} /> diff --git a/app/dashboard/src/pages/host/dialog.tsx b/app/dashboard/src/pages/host/dialog.tsx deleted file mode 100644 index 51357c0..0000000 --- a/app/dashboard/src/pages/host/dialog.tsx +++ /dev/null @@ -1,906 +0,0 @@ -import { - Accordion, - AccordionButton, - AccordionIcon, - AccordionItem, - AccordionPanel, - Badge, - Box, - Button, - Select as ChakraSelect, - FormControl, - FormErrorMessage, - FormLabel, - HStack, - IconButton, - InputGroup, - InputRightElement, - Modal, - ModalBody, - ModalCloseButton, - ModalContent, - ModalHeader, - ModalOverlay, - Popover, - PopoverArrow, - PopoverBody, - PopoverCloseButton, - PopoverContent, - PopoverTrigger, - Portal, - Text, - Tooltip, - VStack, - chakra, - useToast, -} from '@chakra-ui/react'; -import { InformationCircleIcon, LinkIcon } from '@heroicons/react/24/outline'; -import { zodResolver } from '@hookform/resolvers/zod'; -import { - proxyALPN, - proxyFingerprint, - proxyHostSecurity, -} from 'constants/Proxies'; -import { useHosts } from 'contexts/HostsContext'; -import { useInbounds } from 'contexts/InboundsContext'; -import { useUsers } from 'contexts/UsersContext'; -import { FC, useEffect, useState } from 'react'; -import { - FormProvider, - useFieldArray, - useForm, - useFormContext, -} from 'react-hook-form'; -import { Trans, useTranslation } from 'react-i18next'; -import 'slick-carousel/slick/slick-theme.css'; -import 'slick-carousel/slick/slick.css'; -import { z } from 'zod'; -import { DeleteIcon } from 'components/dialog/Icons'; -import { Icon } from 'components/icon'; -import { Input as CustomInput } from 'components/Input'; - -const Select = chakra(ChakraSelect, { - baseStyle: { - bg: 'white', - _dark: { - bg: 'gray.700', - }, - }, -}); - -const Input = chakra(CustomInput, { - baseStyle: { - bg: 'white', - _dark: { - bg: 'gray.700', - }, - }, -}); - -const ModalIcon = chakra(LinkIcon, { - baseStyle: { - w: 5, - h: 5, - }, -}); - -const InfoIcon = chakra(InformationCircleIcon, { - baseStyle: { - w: 4, - h: 4, - color: 'gray.400', - cursor: 'pointer', - }, -}); - -const hostsSchema = z.record( - z.string().min(1), - z.array( - z.object({ - remark: z.string().min(1, 'Remark is required'), - address: z.string().min(1, 'Address is required'), - port: z - .string() - .or(z.number()) - .nullable() - .transform((value) => { - if (typeof value === 'number') return value; - if (value !== null && !isNaN(parseInt(value))) - return Number(parseInt(value)); - return null; - }), - path: z.string().nullable(), - sni: z.string().nullable(), - host: z.string().nullable(), - security: z.string(), - alpn: z.string(), - fingerprint: z.string(), - }), - ), -); - -const Error = chakra(FormErrorMessage, { - baseStyle: { - color: 'red.400', - display: 'block', - textAlign: 'left', - w: '100%', - }, -}); - -type AccordionInboundType = { - hostKey: string; - isOpen: boolean; - toggleAccordion: () => void; -}; - -const AccordionInbound: FC = ({ - hostKey, - isOpen, - toggleAccordion, -}) => { - const { inbounds } = useInbounds(); - const inbound = [...inbounds.values()] - .flat() - .filter((inbound) => inbound.tag === hostKey)[0]; - - const form = useFormContext>(); - const { - fields: hosts, - append: addHost, - remove: removeHost, - } = useFieldArray({ - control: form.control, - name: hostKey, - }); - const { errors } = form.formState; - const { t } = useTranslation(); - const accordionErrors = errors[hostKey]; - const handleAddHost = () => { - addHost({ - host: '', - sni: '', - port: null, - path: null, - address: '', - remark: '', - security: 'inbound_default', - alpn: '', - fingerprint: '', - }); - }; - useEffect(() => { - if (accordionErrors && !isOpen) { - toggleAccordion(); - } - }, [accordionErrors]); - return ( - - - - {hostKey} - - - - - - {hosts.map((host, index) => ( - - - - - - - - - - - - - - - - - - - {t('hostsDialog.desc')} - - - {'{'}SERVER_IP{'}'} - {' '} - {t('hostsDialog.currentServer')} - - - - {'{'}USERNAME{'}'} - {' '} - {t('hostsDialog.username')} - - - - {'{'}DATA_USAGE{'}'} - {' '} - {t('hostsDialog.dataUsage')} - - - - {'{'}DATA_LEFT{'}'} - {' '} - {t('hostsDialog.remainingData')} - - - - {'{'}DATA_LIMIT{'}'} - {' '} - {t('hostsDialog.dataLimit')} - - - - {'{'}DAYS_LEFT{'}'} - {' '} - {t('hostsDialog.remainingDays')} - - - - {'{'}EXPIRE_DATE{'}'} - {' '} - {t('hostsDialog.expireDate')} - - - - {'{'}JALALI_EXPIRE_DATE{'}'} - {' '} - {t('hostsDialog.jalaliExpireDate')} - - - - {'{'}TIME_LEFT{'}'} - {' '} - {t('hostsDialog.remainingTime')} - - - - {'{'}STATUS_EMOJI{'}'} - {' '} - {t('hostsDialog.statusEmoji')} - - - - {'{'}PROTOCOL{'}'} - {' '} - {t('hostsDialog.proxyProtocol')} - - - - {'{'}TRANSPORT{'}'} - {' '} - {t('hostsDialog.proxyMethod')} - - - - - - - - - {accordionErrors && accordionErrors[index]?.remark && ( - {accordionErrors[index]?.remark?.message} - )} - - - - - - - - - - - - - - - - - - - {t('hostsDialog.desc')} - - - {'{'}SERVER_IP{'}'} - {' '} - {t('hostsDialog.currentServer')} - - - - {'{'}USERNAME{'}'} - {' '} - {t('hostsDialog.username')} - - - - {'{'}DATA_USAGE{'}'} - {' '} - {t('hostsDialog.dataUsage')} - - - - {'{'}DATA_LEFT{'}'} - {' '} - {t('hostsDialog.remainingData')} - - - - {'{'}DATA_LIMIT{'}'} - {' '} - {t('hostsDialog.dataLimit')} - - - - {'{'}DAYS_LEFT{'}'} - {' '} - {t('hostsDialog.remainingDays')} - - - - {'{'}EXPIRE_DATE{'}'} - {' '} - {t('hostsDialog.expireDate')} - - - - {'{'}JALALI_EXPIRE_DATE{'}'} - {' '} - {t('hostsDialog.jalaliExpireDate')} - - - - {'{'}TIME_LEFT{'}'} - {' '} - {t('hostsDialog.remainingTime')} - - - - {'{'}STATUS_EMOJI{'}'} - {' '} - {t('hostsDialog.statusEmoji')} - - - - {'{'}PROTOCOL{'}'} - {' '} - {t('hostsDialog.proxyProtocol')} - - - - {'{'}TRANSPORT{'}'} - {' '} - {t('hostsDialog.proxyMethod')} - - - - - - - - - {accordionErrors && accordionErrors[index]?.address && ( - {accordionErrors[index]?.address?.message} - )} - - - - - - - {t('hostsDialog.advancedOptions')} - - - - - - - - - - - - - - {t('hostsDialog.port')} - - - - - - - - - - {t('hostsDialog.port.info')} - - - - - - - - - - {t('hostsDialog.sni')} - - - - - - - - - - - {t('hostsDialog.sni.info')} - - - , - }} - /> - - - , - }} - /> - - - - - - - {accordionErrors && accordionErrors[index]?.sni && ( - {accordionErrors[index]?.sni?.message} - )} - - - - {t('hostsDialog.host')} - - - - - - - - - - - {t('hostsDialog.host.info')} - - - , - }} - /> - - - , - }} - /> - - - - - - - {accordionErrors && accordionErrors[index]?.host && ( - {accordionErrors[index]?.host?.message} - )} - - - - - {t('hostsDialog.path')} - - - - - - - - - - - {t('hostsDialog.path.info')} - - - - - - - {accordionErrors && accordionErrors[index]?.path && ( - {accordionErrors[index]?.path?.message} - )} - - - - - {t('hostsDialog.security')} - - - - - - - - - - - {t('hostsDialog.security.info')} - - - - - - - - - - - {t('hostsDialog.alpn')} - - - - - - - {t('hostsDialog.fingerprint')} - - - - - - - - - ))} - - - - - ); -}; - -export const HostsDialog: FC = () => { - const { refetchUsers } = useUsers(); - const { isEditingHosts, onEditingHosts, isLoading, hosts, fetchHosts, isPostLoading, setHosts } = useHosts(); - const toast = useToast(); - const { t } = useTranslation(); - const [openAccordions, setOpenAccordions] = useState({}); - - useEffect(() => { - if (isEditingHosts) fetchHosts(); - }, [isEditingHosts]); - const form = useForm>({ - resolver: zodResolver(hostsSchema), - }); - - useEffect(() => { - if (hosts && isEditingHosts) { - form.reset(hosts); - } - }, [hosts]); - - const onClose = () => { - setOpenAccordions({}); - onEditingHosts(false); - }; - const handleFormSubmit = (hosts: z.infer) => { - setHosts(hosts) - .then(() => { - toast({ - title: t('hostsDialog.savedSuccess'), - status: 'success', - isClosable: true, - position: 'top', - duration: 3000, - }); - refetchUsers(); - }) - .catch((err) => { - if (err?.response?.status === 409 || err?.response?.status === 400) { - toast({ - title: err.response?._data?.detail, - status: 'error', - isClosable: true, - position: 'top', - duration: 3000, - }); - } - if (err?.response?.status === 422) { - Object.keys(err.response._data.detail).forEach((key) => { - toast({ - title: err.response._data.detail[key] + ' (' + key + ')', - status: 'error', - isClosable: true, - position: 'top', - duration: 3000, - }); - }); - } - }); - }; - - const toggleAccordion = (index: number) => { - if (openAccordions[String(index)]) { - delete openAccordions[String(index)]; - } else openAccordions[String(index)] = {}; - - setOpenAccordions({ ...openAccordions }); - }; - - return ( - - - - - - - - - - - -
- - {t('hostsDialog.title')} - - {isLoading && t('hostsDialog.loading')} - {!isLoading && - hosts && - (Object.keys(hosts).length > 0 ? ( - parseInt(i))} - > - - {Object.keys(hosts).map((hostKey, index) => { - return ( - toggleAccordion(index)} - isOpen={openAccordions[String(index)]} - key={hostKey} - hostKey={hostKey} - /> - ); - })} - - - ) : ( - 'No inbound found. Please check your Xray config file.' - ))} - - - - -
-
-
-
-
- ); -}; diff --git a/app/dashboard/src/pages/hosts/dialog/default-values.ts b/app/dashboard/src/pages/hosts/dialog/default-values.ts new file mode 100644 index 0000000..f6627cc --- /dev/null +++ b/app/dashboard/src/pages/hosts/dialog/default-values.ts @@ -0,0 +1,14 @@ +import { HostSchema } from 'stores'; +export const getDefaultValues = (): HostSchema => { + return { + host: '', + sni: '', + port: null, + path: '', + address: '', + remark: '', + security: 'inbound_default', + alpn: '', + fingerprint: '', + } +}; diff --git a/app/dashboard/src/pages/hosts/dialog/icons.tsx b/app/dashboard/src/pages/hosts/dialog/icons.tsx new file mode 100644 index 0000000..2787c25 --- /dev/null +++ b/app/dashboard/src/pages/hosts/dialog/icons.tsx @@ -0,0 +1,19 @@ + +import { chakra } from '@chakra-ui/react'; +import { InformationCircleIcon, LinkIcon } from '@heroicons/react/24/outline'; + +export const ModalIcon = chakra(LinkIcon, { + baseStyle: { + w: 5, + h: 5, + }, +}); + +export const InfoIcon = chakra(InformationCircleIcon, { + baseStyle: { + w: 4, + h: 4, + color: 'gray.400', + cursor: 'pointer', + }, +}); diff --git a/app/dashboard/src/pages/hosts/dialog/index.tsx b/app/dashboard/src/pages/hosts/dialog/index.tsx new file mode 100644 index 0000000..25e7398 --- /dev/null +++ b/app/dashboard/src/pages/hosts/dialog/index.tsx @@ -0,0 +1,482 @@ +import { + Button, + Select as ChakraSelect, + FormControl, + FormLabel, + HStack, + IconButton, + Modal, + ModalBody, + ModalCloseButton, + ModalContent, + ModalOverlay, + Tooltip, + VStack, + chakra, + useToast, + Spacer, + Popover, + PopoverTrigger, + Portal, + PopoverContent, + PopoverArrow, + PopoverCloseButton, + Text, + Badge, + Alert, + AlertIcon, + InputRightElement, + InputGroup, +} from '@chakra-ui/react'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { FC, useEffect, useState } from 'react'; +import { useInbounds, HostType, HostSchema, hostSchema } from 'stores'; +import { Trans, useTranslation } from 'react-i18next'; +import { getDefaultValues } from './default-values'; +import { useForm } from 'react-hook-form'; +import 'slick-carousel/slick/slick-theme.css'; +import 'slick-carousel/slick/slick.css'; +import { AddIcon, DeleteIcon } from 'components/dialog/Icons'; +import { Input as CustomInput } from 'components/Input'; +import { DialogModalHeader, } from 'components/dialog/ModalHeader'; +import { EditIcon } from 'components/table'; +import { InfoIcon } from './icons'; +import { + proxyALPN, + proxyFingerprint, + proxyHostSecurity, +} from 'constants/Proxies'; +import { RemarkFieldPopover } from './remark-field-popover'; + +export const Select = chakra(ChakraSelect, { + baseStyle: { + bg: 'white', + _dark: { + bg: 'gray.700', + }, + }, +}); + +export const Input = chakra(CustomInput, { + baseStyle: { + bg: 'white', + _dark: { + bg: 'gray.700', + }, + }, +}); + +/* TODO: Impelement specific errors and input verifications + * */ +export const HostsDialog: FC = () => { + const { + selectedHost, isEditingHost, isCreatingHost, selectedInbound, + onEditingHost, editHost, createHost, onCreatingHost, onDeletingHost + } = useInbounds(); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(''); + + const { t } = useTranslation(); + const toast = useToast(); + const defaultValueHost: HostSchema = (isEditingHost === true && selectedHost !== null) ? selectedHost : getDefaultValues(); + const form = useForm({ + resolver: zodResolver(hostSchema), + defaultValues: defaultValueHost, + }); + const isOpen = isEditingHost || isCreatingHost; + const onClose = () => { + onCreatingHost(false); + onEditingHost(false, null); + }; + useEffect(() => { + if (isOpen) { + form.reset(defaultValueHost); + } + }, [isOpen]); + + const submit = async (values: HostSchema | HostType) => { + setLoading(true); + const methods = { edited: editHost, created: createHost }; + const method = isEditingHost ? 'edited' : 'created'; + const refId = (isEditingHost ? selectedHost?.id : null) ?? selectedInbound?.id; + setError(null); + await methods[method](refId, values) + .then(() => { + toast({ + title: t( + isEditingHost ? 'hostDialog.editHost' : 'hostDialog.createNewHost', + { hoshtName: values.host } + ), + status: 'success', + isClosable: true, + position: 'top', + duration: 3000, + }); + // refetchNodes(); + onClose(); + }) + .catch((err) => { + if (err?.response?.status === 409 || err?.response?.status === 400) + setError(err?.response?._data?.detail); + if (err?.response?.status === 422) { + Object.keys(err.response._data.detail).forEach((key) => { + setError(err?.response._data.detail[key] as string); + form.setError( + key as 'address' | 'port' | 'host' | 'path' | 'remark' | 'sni' | 'security' | 'alpn' | 'fingerprint', + { + type: 'custom', + message: err.response._data.detail[key], + } + ); + }); + } + }) + .finally(() => { + setLoading(false); + }); + }; + + return ( + + + + + + + + {loading && 'loading...'} +
submit(v))}> + + + + + + + + + + + + + + + + + + + + + + {t('hostsDialog.port')} + + + + + + + + + + {t('hostsDialog.port.info')} + + + + + + + + + + {t('hostsDialog.sni')} + + + + + + + + + + {t('hostsDialog.sni.info')} + + + , + }} + /> + + + , + }} + /> + + + + + + + + + + {t('hostsDialog.host')} + + + + + + + + + + + {t('hostsDialog.host.info')} + + + , + }} + /> + + + , + }} + /> + + + + + + + + + + {t('hostsDialog.path')} + + + + + + + + + + {t('hostsDialog.path.info')} + + + + + + + + + + {t('hostsDialog.security')} + + + + + + + + + + {t('hostsDialog.security.info')} + + + + + + + + {/* TODO: Add ALPN and fingerprint fields smooth entrance and + * pushing down animation when security field is TLS + * - Insertion and pushing down the below element + * - Slowly increasing the opacity + * Related issue: #95 + */} + {form.getValues().security === 'tls' && ( + <> + + + {t('hostsDialog.alpn')} + + + + + + {t('hostsDialog.fingerprint')} + + + + + )} + + + + { + if (selectedHost !== null) { + onDeletingHost(true, selectedHost); + onClose(); + } + }} + > + + + + + + {error && ( + + + {error} + + )} + +
+
+
+
+
+ ); +}; diff --git a/app/dashboard/src/pages/hosts/dialog/remark-field-popover.tsx b/app/dashboard/src/pages/hosts/dialog/remark-field-popover.tsx new file mode 100644 index 0000000..0e665ed --- /dev/null +++ b/app/dashboard/src/pages/hosts/dialog/remark-field-popover.tsx @@ -0,0 +1,113 @@ + +import { InfoIcon } from './icons'; +import { + Badge, + Box, + Popover, + PopoverArrow, + PopoverBody, + PopoverCloseButton, + PopoverContent, + PopoverTrigger, + Portal, + Text, +} from '@chakra-ui/react'; +import { useTranslation } from 'react-i18next'; +import 'slick-carousel/slick/slick-theme.css'; +import 'slick-carousel/slick/slick.css'; + +export const RemarkFieldPopover = () => { + const { t } = useTranslation(); + return ( + + + + + + + + + + + + + {t('hostsDialog.desc')} + + + {'{'}SERVER_IP{'}'} + {' '} + {t('hostsDialog.currentServer')} + + + + {'{'}USERNAME{'}'} + {' '} + {t('hostsDialog.username')} + + + + {'{'}DATA_USAGE{'}'} + {' '} + {t('hostsDialog.dataUsage')} + + + + {'{'}DATA_LEFT{'}'} + {' '} + {t('hostsDialog.remainingData')} + + + + {'{'}DATA_LIMIT{'}'} + {' '} + {t('hostsDialog.dataLimit')} + + + + {'{'}DAYS_LEFT{'}'} + {' '} + {t('hostsDialog.remainingDays')} + + + + {'{'}EXPIRE_DATE{'}'} + {' '} + {t('hostsDialog.expireDate')} + + + + {'{'}JALALI_EXPIRE_DATE{'}'} + {' '} + {t('hostsDialog.jalaliExpireDate')} + + + + {'{'}TIME_LEFT{'}'} + {' '} + {t('hostsDialog.remainingTime')} + + + + {'{'}STATUS_EMOJI{'}'} + {' '} + {t('hostsDialog.statusEmoji')} + + + + {'{'}PROTOCOL{'}'} + {' '} + {t('hostsDialog.proxyProtocol')} + + + + {'{'}TRANSPORT{'}'} + {' '} + {t('hostsDialog.proxyMethod')} + + + + + + + ); +} diff --git a/app/dashboard/src/pages/host/index.tsx b/app/dashboard/src/pages/hosts/index.tsx similarity index 72% rename from app/dashboard/src/pages/host/index.tsx rename to app/dashboard/src/pages/hosts/index.tsx index 16efd76..eb7101b 100644 --- a/app/dashboard/src/pages/host/index.tsx +++ b/app/dashboard/src/pages/hosts/index.tsx @@ -1,7 +1,7 @@ import { HostsDialog } from './dialog'; import Page from 'components/page'; -const HostPage = () => { +const HostsPage = () => { return ( @@ -9,4 +9,4 @@ const HostPage = () => { ) } -export default HostPage; +export default HostsPage; diff --git a/app/dashboard/src/pages/login/index.tsx b/app/dashboard/src/pages/login/index.tsx index b5f1cfa..f770ae7 100644 --- a/app/dashboard/src/pages/login/index.tsx +++ b/app/dashboard/src/pages/login/index.tsx @@ -22,7 +22,7 @@ import { fetch } from 'service/http'; import { ReactComponent as Logo } from 'assets/logo.svg'; import { useTranslation } from 'react-i18next'; import { Language } from 'components/Language'; -import { useAdmin } from 'contexts/AdminContext'; +import { useAdmin } from 'stores'; const schema = z.object({ username: z.string().min(1, 'login.fieldRequired'), diff --git a/app/dashboard/src/pages/nodes/dialog.tsx b/app/dashboard/src/pages/nodes/dialog.tsx index c3d7af2..d6507db 100644 --- a/app/dashboard/src/pages/nodes/dialog.tsx +++ b/app/dashboard/src/pages/nodes/dialog.tsx @@ -22,7 +22,7 @@ import { NodeCreate, NodeCreateSchema, useNodes, -} from 'contexts/NodesContext'; +} from 'stores'; import { FC, useEffect, useState } from 'react'; import { Controller, useForm } from 'react-hook-form'; import { useTranslation } from 'react-i18next'; @@ -133,7 +133,7 @@ export const NodesDialog: FC = () => { { { { {!editingNode && ( - {t('nodes.addHostForEveryInbound')} + {t('nodeDialog.addHostForEveryInbound')} )} diff --git a/app/dashboard/src/pages/nodes/filters.tsx b/app/dashboard/src/pages/nodes/filters.tsx index 8bb9a9b..5686420 100644 --- a/app/dashboard/src/pages/nodes/filters.tsx +++ b/app/dashboard/src/pages/nodes/filters.tsx @@ -19,13 +19,12 @@ import { XMarkIcon, } from '@heroicons/react/24/outline'; import classNames from 'classnames'; -import { useDashboard } from 'contexts/DashboardContext'; -import { useNodes } from 'contexts/NodesContext'; +import { useNodes, useDashboard } from 'stores'; import debounce from 'lodash.debounce'; import React, { FC, useEffect, useState } from 'react'; import CopyToClipboard from 'react-copy-to-clipboard'; import { useTranslation } from 'react-i18next'; -import { CopiedIcon, SubscriptionLinkIcon } from 'components/Table/Icons'; +import { CopiedIcon, SubscriptionLinkIcon } from 'components/table'; const iconProps = { baseStyle: { diff --git a/app/dashboard/src/pages/nodes/index.tsx b/app/dashboard/src/pages/nodes/index.tsx index 64cd905..2919d0c 100644 --- a/app/dashboard/src/pages/nodes/index.tsx +++ b/app/dashboard/src/pages/nodes/index.tsx @@ -6,8 +6,7 @@ import { NodesUsage } from './usage'; import { NodesTable } from './table'; import { NodesFilters } from './filters'; import { DeleteNodeModal } from './modals/delete-modal'; -import { useDashboard } from 'contexts/DashboardContext'; -import { useNodes } from 'contexts/NodesContext'; +import { useNodes, useDashboard } from 'stores'; import { pages } from 'constants/Pages'; import Page from 'components/page'; diff --git a/app/dashboard/src/pages/nodes/modals/core-settings-modal.tsx b/app/dashboard/src/pages/nodes/modals/core-settings-modal.tsx index 64f7831..fbbbe60 100644 --- a/app/dashboard/src/pages/nodes/modals/core-settings-modal.tsx +++ b/app/dashboard/src/pages/nodes/modals/core-settings-modal.tsx @@ -27,8 +27,7 @@ import { } from '@heroicons/react/24/outline'; import { joinPaths } from '@remix-run/router'; import classNames from 'classnames'; -import { useCoreSettings } from 'contexts/CoreSettingsContext'; -import { useNodes } from 'contexts/NodesContext'; +import { useNodes, useCoreSettings } from 'stores'; import debounce from 'lodash.debounce'; import { FC, useCallback, useEffect, useRef, useState } from 'react'; import { Controller, useForm } from 'react-hook-form'; diff --git a/app/dashboard/src/pages/nodes/modals/delete-modal.tsx b/app/dashboard/src/pages/nodes/modals/delete-modal.tsx index c44e774..7b61390 100644 --- a/app/dashboard/src/pages/nodes/modals/delete-modal.tsx +++ b/app/dashboard/src/pages/nodes/modals/delete-modal.tsx @@ -1,7 +1,7 @@ import { useToast, } from '@chakra-ui/react'; -import { useNodes } from 'contexts/NodesContext'; +import { useNodes } from 'stores'; import { FC, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { DeleteModal } from 'components/delete-modal'; diff --git a/app/dashboard/src/pages/nodes/table.tsx b/app/dashboard/src/pages/nodes/table.tsx index 4688da3..0ee9dbe 100644 --- a/app/dashboard/src/pages/nodes/table.tsx +++ b/app/dashboard/src/pages/nodes/table.tsx @@ -25,16 +25,16 @@ import classNames from 'classnames'; import { t } from 'i18next'; import { FC, Fragment, useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { Pagination } from 'components/Table/Pagination'; import { StatusBadge } from 'components/StatusBadge'; -import { EmptySection } from 'components/Table/EmptySection'; import { + Pagination, + EmptySection, AccordionArrowIcon, EditIcon, -} from 'components/Table/Icons'; -import { Sort } from 'components/Table/Sort'; -import { StatusSortSelect } from 'components/Table/StatusSortSelect'; -import { useNodes } from 'contexts/NodesContext'; + Sort, + StatusSortSelect, +} from 'components/table'; +import { useNodes } from 'stores'; import { Cog6ToothIcon } from '@heroicons/react/24/outline'; const iconProps = { diff --git a/app/dashboard/src/pages/nodes/usage.tsx b/app/dashboard/src/pages/nodes/usage.tsx index ba4fdd5..3678b60 100644 --- a/app/dashboard/src/pages/nodes/usage.tsx +++ b/app/dashboard/src/pages/nodes/usage.tsx @@ -6,8 +6,8 @@ import { useColorMode, } from '@chakra-ui/react'; import { ChartPieIcon } from '@heroicons/react/24/outline'; -import { FilterUsageType } from 'types/Filter'; -import { useNodes } from 'contexts/NodesContext'; +import { FilterUsageType } from 'types/filter'; +import { useNodes } from 'stores'; import dayjs from 'dayjs'; import { FC, Suspense, useEffect, useState } from 'react'; import ReactApexChart from 'react-apexcharts'; diff --git a/app/dashboard/src/pages/routes.tsx b/app/dashboard/src/pages/routes.tsx index db1637c..cd95a40 100644 --- a/app/dashboard/src/pages/routes.tsx +++ b/app/dashboard/src/pages/routes.tsx @@ -4,7 +4,7 @@ import { fetchAdminLoader } from 'service/http'; const ConsoleLayout = lazy(() => import('layouts/console-layout')); const Home = lazy(() => import('./home')); -const Host = lazy(() => import('./host')); +const Hosts = lazy(() => import('./hosts')); const Users = lazy(() => import('./users')); const Login = lazy(() => import('./login')); const Nodes = lazy(() => import('./nodes')); @@ -34,8 +34,8 @@ export const router = createHashRouter([ element: , }, { - path: '/host', - element: , + path: '/hosts', + element: , }, ], }, diff --git a/app/dashboard/src/pages/services/dialog/form-schema.ts b/app/dashboard/src/pages/services/dialog/form-schema.ts index 36f96e4..79fb2b1 100644 --- a/app/dashboard/src/pages/services/dialog/form-schema.ts +++ b/app/dashboard/src/pages/services/dialog/form-schema.ts @@ -1,6 +1,6 @@ import { z } from 'zod'; -import { ServiceCreate } from 'types/Service'; +import { ServiceCreate } from 'types'; export type FormType = Pick; export const schema = z.object({ diff --git a/app/dashboard/src/pages/services/dialog/inbounds-field.tsx b/app/dashboard/src/pages/services/dialog/inbounds-field.tsx index d9b46eb..32a320b 100644 --- a/app/dashboard/src/pages/services/dialog/inbounds-field.tsx +++ b/app/dashboard/src/pages/services/dialog/inbounds-field.tsx @@ -1,5 +1,5 @@ import { FormControl, FormLabel, FormErrorMessage, Center, Spinner, Text, Spacer, Flex } from '@chakra-ui/react'; -import { Inbounds, InboundType } from 'types/Inbounds'; +import { Inbounds, InboundType } from 'types'; import { TFunction } from 'i18next'; import { Controller } from 'react-hook-form'; import { InboundsSelectList } from './inbounds-select-list'; diff --git a/app/dashboard/src/pages/services/dialog/inbounds-select-list.tsx b/app/dashboard/src/pages/services/dialog/inbounds-select-list.tsx index 5bcb5ea..fd665dc 100644 --- a/app/dashboard/src/pages/services/dialog/inbounds-select-list.tsx +++ b/app/dashboard/src/pages/services/dialog/inbounds-select-list.tsx @@ -5,7 +5,7 @@ import { Stack, } from '@chakra-ui/react'; import { ListCheckBox } from 'components/list-check-box'; -import { Inbounds, InboundType } from 'types/Inbounds'; +import { Inbounds, InboundType } from 'types'; import { forwardRef, ReactNode } from 'react'; import { useFormContext, diff --git a/app/dashboard/src/pages/services/dialog/index.tsx b/app/dashboard/src/pages/services/dialog/index.tsx index 850019a..c79797c 100644 --- a/app/dashboard/src/pages/services/dialog/index.tsx +++ b/app/dashboard/src/pages/services/dialog/index.tsx @@ -17,7 +17,6 @@ import { IconButton, } from '@chakra-ui/react'; import { zodResolver } from '@hookform/resolvers/zod'; -import { InboundType } from 'types/Inbounds'; import { FC, useEffect, useState } from 'react'; import { FormProvider, useForm } from 'react-hook-form'; import { useTranslation } from 'react-i18next'; @@ -28,9 +27,8 @@ import { getDefaultValues } from './default-values'; import { schema, FormType } from './form-schema'; import { InboundsField } from './inbounds-field'; import { NameField } from './name-field'; -import { Service, ServiceCreate } from 'types/Service'; -import { useServices } from 'contexts/ServicesContext'; -import { useInbounds } from 'contexts/InboundsContext'; +import { InboundType, Service, ServiceCreate } from 'types'; +import { useInbounds, useServices } from 'stores'; const formatService = (service: Service): FormType => { const inbounds: number[] = service.inbounds.map((inbound: number | InboundType): number => { diff --git a/app/dashboard/src/pages/services/filters.tsx b/app/dashboard/src/pages/services/filters.tsx index 6cfd7f8..e846b0a 100644 --- a/app/dashboard/src/pages/services/filters.tsx +++ b/app/dashboard/src/pages/services/filters.tsx @@ -18,8 +18,7 @@ import { XMarkIcon, } from '@heroicons/react/24/outline'; import classNames from 'classnames'; -import { useDashboard } from 'contexts/DashboardContext'; -import { useServices } from 'contexts/ServicesContext'; +import { useDashboard, useServices } from 'stores'; import debounce from 'lodash.debounce'; import React, { FC, useState } from 'react'; import { useTranslation } from 'react-i18next'; diff --git a/app/dashboard/src/pages/services/index.tsx b/app/dashboard/src/pages/services/index.tsx index ed826a0..1a8a5ff 100644 --- a/app/dashboard/src/pages/services/index.tsx +++ b/app/dashboard/src/pages/services/index.tsx @@ -4,8 +4,7 @@ import { ServicesFilters } from './filters'; import { ServicesTable } from './table'; import { pages } from 'constants/Pages'; import { ServiceDialog } from './dialog'; -import { useServices } from 'contexts/ServicesContext'; -import { useDashboard } from 'contexts/DashboardContext'; +import { useDashboard, useServices } from 'stores'; import { DeleteServiceModal } from './modals/delete-modal'; import Page from 'components/page'; diff --git a/app/dashboard/src/pages/services/modals/delete-modal.tsx b/app/dashboard/src/pages/services/modals/delete-modal.tsx index 3bb8133..e8d8d0f 100644 --- a/app/dashboard/src/pages/services/modals/delete-modal.tsx +++ b/app/dashboard/src/pages/services/modals/delete-modal.tsx @@ -1,7 +1,7 @@ import { useToast, } from '@chakra-ui/react'; -import { useServices } from 'contexts/ServicesContext'; +import { useServices } from 'stores'; import { FC, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { DeleteModal } from 'components/delete-modal'; diff --git a/app/dashboard/src/pages/services/table.tsx b/app/dashboard/src/pages/services/table.tsx index 1bb658e..08e6fb5 100644 --- a/app/dashboard/src/pages/services/table.tsx +++ b/app/dashboard/src/pages/services/table.tsx @@ -15,10 +15,12 @@ import { import classNames from 'classnames'; import { FC, Fragment, useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { EmptySection } from 'components/Table/EmptySection'; -import { Pagination } from 'components/Table/Pagination'; -import { Sort } from 'components/Table/Sort'; -import { useServices } from 'contexts/ServicesContext'; +import { + EmptySection, + Pagination, + Sort +} from 'components/table'; +import { useServices } from 'stores'; type ServicesTableProps = {} & TableProps; diff --git a/app/dashboard/src/pages/users/dialog/form-schema.ts b/app/dashboard/src/pages/users/dialog/form-schema.ts index 868b3fd..9f452c8 100644 --- a/app/dashboard/src/pages/users/dialog/form-schema.ts +++ b/app/dashboard/src/pages/users/dialog/form-schema.ts @@ -1,6 +1,6 @@ import { z } from 'zod'; -import { UserCreate } from 'types/User'; +import { UserCreate } from 'types'; export type FormType = Pick; export const schema = z.object({ diff --git a/app/dashboard/src/pages/users/dialog/index.tsx b/app/dashboard/src/pages/users/dialog/index.tsx index af6c13e..0cb254d 100644 --- a/app/dashboard/src/pages/users/dialog/index.tsx +++ b/app/dashboard/src/pages/users/dialog/index.tsx @@ -21,7 +21,6 @@ import { HStack, } from '@chakra-ui/react'; import { zodResolver } from '@hookform/resolvers/zod'; -import { FilterUsageType } from 'types/Filter'; import dayjs from 'dayjs'; import { FC, useEffect, useState } from 'react'; import ReactApexChart from 'react-apexcharts'; @@ -29,8 +28,10 @@ import { FormProvider, useForm, useWatch } from 'react-hook-form'; import { useTranslation } from 'react-i18next'; import { User, + FilterUsageType, UserCreate, -} from 'types/User'; + Service +} from 'types'; import { UsageFilter, createUsageConfig } from 'components/UsageFilter'; import { AddIcon, EditIcon } from 'components/dialog/Icons'; import { DialogModalHeader } from 'components/dialog/ModalHeader'; @@ -45,9 +46,7 @@ import { DataLimitField } from './data-limit-field'; import { PeriodicUsageReset } from './periodic-usage-reset'; import { ExpireDateField } from './expire-date-field'; import { NoteField } from './note-field'; -import { Service } from 'types/Service'; -import { useUsers } from 'contexts/UsersContext'; -import { useServices } from 'contexts/ServicesContext'; +import { useUsers, useServices } from 'stores'; const formatUser = (user: User): FormType => { const services: number[] = user.services.map((service: number | Service): number => { diff --git a/app/dashboard/src/pages/users/dialog/service-property-status.tsx b/app/dashboard/src/pages/users/dialog/service-property-status.tsx index e1651fb..63cd06b 100644 --- a/app/dashboard/src/pages/users/dialog/service-property-status.tsx +++ b/app/dashboard/src/pages/users/dialog/service-property-status.tsx @@ -1,16 +1,15 @@ import { Flex } from '@chakra-ui/react'; import { FC, useState } from 'react'; -import { ChakraIcon } from 'types/ChakraIcon'; +import { ChakraIcon } from 'types'; type ServicePropertyStatusProps = { - value: number; - StatusIcon: ChakraIcon; + value: number; + StatusIcon: ChakraIcon; } export const ServicePropertyStatus: FC = ({ value, StatusIcon }) => { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const [valueState, _] = useState(value); + const [valueState,] = useState(value); return ( diff --git a/app/dashboard/src/pages/users/dialog/services-field.tsx b/app/dashboard/src/pages/users/dialog/services-field.tsx index 30dbb66..e81e283 100644 --- a/app/dashboard/src/pages/users/dialog/services-field.tsx +++ b/app/dashboard/src/pages/users/dialog/services-field.tsx @@ -3,7 +3,7 @@ import { UsersIcon, ServerStackIcon } from '@heroicons/react/24/outline'; import { IconBaseStyle } from 'constants/IconBaseStyle'; import { TFunction } from 'i18next'; import { Controller } from 'react-hook-form'; -import { Service } from 'types/Service'; +import { Service } from 'types'; import { ServicePropertyStatus } from './service-property-status'; import { ServicesSelectList } from './services-select-list'; diff --git a/app/dashboard/src/pages/users/dialog/services-select-list.tsx b/app/dashboard/src/pages/users/dialog/services-select-list.tsx index f320714..ee3e4b4 100644 --- a/app/dashboard/src/pages/users/dialog/services-select-list.tsx +++ b/app/dashboard/src/pages/users/dialog/services-select-list.tsx @@ -9,7 +9,7 @@ import { forwardRef, ReactNode } from 'react'; import { useFormContext, } from 'react-hook-form'; -import { Service } from 'types/Service'; +import { Service } from 'types'; interface ServicesSelectListProps { list: Service[]; diff --git a/app/dashboard/src/pages/users/filters.tsx b/app/dashboard/src/pages/users/filters.tsx index eb8c0f5..4cd071f 100644 --- a/app/dashboard/src/pages/users/filters.tsx +++ b/app/dashboard/src/pages/users/filters.tsx @@ -18,8 +18,7 @@ import { XMarkIcon, } from '@heroicons/react/24/outline'; import classNames from 'classnames'; -import { useDashboard } from 'contexts/DashboardContext'; -import { useUsers } from 'contexts/UsersContext'; +import { useDashboard, useUsers } from 'stores'; import debounce from 'lodash.debounce'; import React, { FC, useState } from 'react'; import { useTranslation } from 'react-i18next'; diff --git a/app/dashboard/src/pages/users/index.tsx b/app/dashboard/src/pages/users/index.tsx index 6fa5d9a..e906cc8 100644 --- a/app/dashboard/src/pages/users/index.tsx +++ b/app/dashboard/src/pages/users/index.tsx @@ -1,6 +1,6 @@ import { UserDialog } from './dialog'; import { UsersTable } from './table'; -import { useDashboard } from 'contexts/DashboardContext'; +import { useUsers, useDashboard } from 'stores'; import { useEffect } from 'react'; import { UsersFilters } from './filters'; import { @@ -11,7 +11,6 @@ import { RevokeSubscriptionModal } from './modals'; import { pages } from 'constants/Pages'; -import { useUsers } from 'contexts/UsersContext'; import Page from 'components/page'; diff --git a/app/dashboard/src/pages/users/modals/delete-modal.tsx b/app/dashboard/src/pages/users/modals/delete-modal.tsx index 2d8e418..69b11f4 100644 --- a/app/dashboard/src/pages/users/modals/delete-modal.tsx +++ b/app/dashboard/src/pages/users/modals/delete-modal.tsx @@ -3,7 +3,7 @@ import { } from '@chakra-ui/react'; import { FC, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { useUsers } from 'contexts/UsersContext'; +import { useUsers } from 'stores'; import { DeleteModal } from 'components/delete-modal'; type DeleteUserModalProps = { diff --git a/app/dashboard/src/pages/users/modals/qrcode-modals.tsx b/app/dashboard/src/pages/users/modals/qrcode-modals.tsx index ce43006..f1ee343 100644 --- a/app/dashboard/src/pages/users/modals/qrcode-modals.tsx +++ b/app/dashboard/src/pages/users/modals/qrcode-modals.tsx @@ -24,7 +24,7 @@ import Slider from 'react-slick'; import 'slick-carousel/slick/slick-theme.css'; import 'slick-carousel/slick/slick.css'; import { Icon } from 'components/icon'; -import { useUsers } from 'contexts/UsersContext'; +import { useUsers } from 'stores'; const QRCode = chakra(QRCodeCanvas); const NextIcon = chakra(ChevronRightIcon, { diff --git a/app/dashboard/src/pages/users/modals/reset-all-usage-modal.tsx b/app/dashboard/src/pages/users/modals/reset-all-usage-modal.tsx index f9b177c..ef4159d 100644 --- a/app/dashboard/src/pages/users/modals/reset-all-usage-modal.tsx +++ b/app/dashboard/src/pages/users/modals/reset-all-usage-modal.tsx @@ -16,7 +16,7 @@ import { FC, useState } from 'react'; import { DocumentMinusIcon } from '@heroicons/react/24/outline'; import { Icon } from 'components/icon'; import { useTranslation } from 'react-i18next'; -import { useUsers } from 'contexts/UsersContext'; +import { useUsers } from 'stores'; export const ResetIcon = chakra(DocumentMinusIcon, { baseStyle: { diff --git a/app/dashboard/src/pages/users/modals/reset-user-usage-modal.tsx b/app/dashboard/src/pages/users/modals/reset-user-usage-modal.tsx index 5191f04..b043c23 100644 --- a/app/dashboard/src/pages/users/modals/reset-user-usage-modal.tsx +++ b/app/dashboard/src/pages/users/modals/reset-user-usage-modal.tsx @@ -16,7 +16,7 @@ import { FC, useState } from 'react'; import { ArrowPathIcon } from '@heroicons/react/24/outline'; import { Icon } from 'components/icon'; import { useTranslation, Trans } from 'react-i18next'; -import { useUsers } from 'contexts/UsersContext'; +import { useUsers } from 'stores'; const ResetIcon = chakra(ArrowPathIcon, { baseStyle: { diff --git a/app/dashboard/src/pages/users/modals/revoke-subscription-modal.tsx b/app/dashboard/src/pages/users/modals/revoke-subscription-modal.tsx index 4c953b5..cff25ab 100644 --- a/app/dashboard/src/pages/users/modals/revoke-subscription-modal.tsx +++ b/app/dashboard/src/pages/users/modals/revoke-subscription-modal.tsx @@ -13,7 +13,7 @@ import { useToast, } from '@chakra-ui/react'; import { ArrowPathIcon } from '@heroicons/react/24/outline'; -import { useUsers } from 'contexts/UsersContext'; +import { useUsers } from 'stores'; import { FC, useState } from 'react'; import { Trans, useTranslation } from 'react-i18next'; import { Icon } from 'components/icon'; diff --git a/app/dashboard/src/pages/users/table/index.tsx b/app/dashboard/src/pages/users/table/index.tsx index db0e18b..f32fd3b 100644 --- a/app/dashboard/src/pages/users/table/index.tsx +++ b/app/dashboard/src/pages/users/table/index.tsx @@ -26,24 +26,24 @@ import { t } from 'i18next'; import { FC, Fragment, useEffect, useState } from 'react'; import CopyToClipboard from 'react-copy-to-clipboard'; import { useTranslation } from 'react-i18next'; -import { User } from 'types/User'; +import { User } from 'types'; import { OnlineBadge } from 'components/OnlineBadge'; import { OnlineStatus } from 'components/OnlineStatus'; -import { Pagination } from 'components/Table/Pagination'; import { StatusBadge } from 'components/StatusBadge'; -import { EmptySection } from 'components/Table/EmptySection'; import { + Pagination, + EmptySection, AccordionArrowIcon, EditIcon, CopiedIcon, CopyIcon, + StatusSortSelect, + Sort, QRIcon, SubscriptionLinkIcon -} from 'components/Table/Icons'; -import { Sort } from 'components/Table/Sort'; +} from 'components/table'; import { UsageSlider, UsageSliderCompact } from './usage-slider'; -import { StatusSortSelect } from 'components/Table/StatusSortSelect'; -import { useUsers } from 'contexts/UsersContext'; +import { useUsers } from 'stores'; type ExpandedIndex = number | number[]; diff --git a/app/dashboard/src/contexts/AdminContext.ts b/app/dashboard/src/stores/admin.store.ts similarity index 100% rename from app/dashboard/src/contexts/AdminContext.ts rename to app/dashboard/src/stores/admin.store.ts diff --git a/app/dashboard/src/contexts/CoreSettingsContext.ts b/app/dashboard/src/stores/core-settings.store.ts similarity index 100% rename from app/dashboard/src/contexts/CoreSettingsContext.ts rename to app/dashboard/src/stores/core-settings.store.ts diff --git a/app/dashboard/src/contexts/DashboardContext.ts b/app/dashboard/src/stores/dashboard.store.ts similarity index 100% rename from app/dashboard/src/contexts/DashboardContext.ts rename to app/dashboard/src/stores/dashboard.store.ts diff --git a/app/dashboard/src/stores/inbounds.store.ts b/app/dashboard/src/stores/inbounds.store.ts new file mode 100644 index 0000000..738f191 --- /dev/null +++ b/app/dashboard/src/stores/inbounds.store.ts @@ -0,0 +1,139 @@ +import { create } from 'zustand'; +import { subscribeWithSelector } from 'zustand/middleware'; +import { useDashboard } from './dashboard.store'; +import { fetch } from 'service/http'; +import { Inbounds, InboundType } from 'types/inbounds'; +import { z } from 'zod'; + +const isPortThenValue = (value: number) => (value <= 65535 && value !== 0) ? value : null; + +export const hostSchema = z.object({ + remark: z.string().min(1, 'Remark is required'), + address: z.string().min(1, 'Address is required'), + port: z + .string() + .or(z.number()) + .nullable() + .transform((value) => { + if (typeof value === 'number') return isPortThenValue(value); + if (value !== null && !isNaN(parseInt(value))) return isPortThenValue(Number(parseInt(value))); + return null; + }), + path: z.string(), + sni: z.string(), + host: z.string(), + security: z.enum(['inbound_default', 'none', 'tls']), + alpn: z.string().optional(), + fingerprint: z.string().optional(), +}); + +export type HostSchema = z.infer; +export type HostType = HostSchema & { id?: number }; +export type Hosts = HostType[]; + +export const fetchInbounds = async () => { + useDashboard.setState({ loading: true }); + return fetch('/inbounds') + .then((inbounds: Inbounds) => { + useInbounds.setState({ inbounds }); + }) + .finally(() => { + useDashboard.setState({ loading: false }); + }); +}; + +export const fetchInboundHosts = async (id: string): Promise => { + useDashboard.setState({ loading: true }); + return fetch(`/inbounds/${id}/hosts`) + .then((hosts: Hosts) => hosts) + .finally(() => { + useDashboard.setState({ loading: false }); + }); +}; + +type InboundsStateType = { + // Loading + loading: boolean; + setLoading: (value: boolean) => void; + // Inbounds + inbounds: Inbounds; + refetchInbounds: () => void; + selectedInbound: InboundType | null; + selectInbound: (inbound: InboundType) => void; + // Hosts + selectedHost: HostType | null; + selectHost: (host: HostType) => void; + isEditingHost: boolean; + onEditingHost: (isEditingHost: boolean, host: HostType | null) => void; + editHost: (inboundId: number | undefined, host: HostType) => Promise; + isCreatingHost: boolean; + onCreatingHost: (isCreatingHost: boolean) => void; + createHost: (inboundId: number | undefined, host: HostSchema) => Promise; + isDeletingHost: boolean; + onDeletingHost: (isDeletingHost: boolean, host: HostType) => void; + deleteHost: (hostId: number) => void; +}; + +export const useInbounds = create( + subscribeWithSelector((set,) => ({ + inbounds: [], + selectedHost: null, + selectedInbound: null, + loading: false, + isDeletingHost: false, + isCreatingHost: false, + isEditingHost: false, + setLoading: (value) => { + set({ loading: value }) + }, + refetchInbounds: () => { + fetchInbounds(); + }, + selectInbound: (host): void => { + set({ selectedInbound: host }) + }, + selectHost: (host: HostType): void => { + set({ selectedHost: host }) + }, + onEditingHost: (isEditingHost: boolean, host: HostType | null) => { + set({ isEditingHost, selectedHost: host }); + }, + editHost: async (hostId: number | undefined, body: HostSchema): Promise => { + if (hostId !== undefined) { + useDashboard.setState({ loading: true }); + return fetch(`/inbounds/hosts/${hostId}`, { method: 'PUT', body }) + .then(() => true) + .catch(() => false) + .finally(() => { + useDashboard.setState({ loading: false }); + }); + } + }, + onCreatingHost: (isCreatingHost: boolean) => { + set({ isCreatingHost }); + }, + createHost: async (inboundId: number | undefined, body: HostSchema): Promise => { + if (inboundId !== undefined) { + useDashboard.setState({ loading: true }); + return fetch(`/inbounds/${inboundId}/hosts/${body}`, { method: 'POST', body }) + .then(() => true) + .catch(() => false) + .finally(() => { + useDashboard.setState({ loading: false }); + }); + } + }, + onDeletingHost: (isDeletingHost: boolean, host: HostType | null) => { + set({ isDeletingHost, selectedHost: host }); + }, + deleteHost: async (hostId: number): Promise => { + useDashboard.setState({ loading: true }); + return fetch(`/inbounds/hosts/${hostId}`, { method: 'DELETE' }) + .then(() => true) + .catch(() => false) + .finally(() => { + useDashboard.setState({ loading: false }); + }); + }, + })) +); diff --git a/app/dashboard/src/stores/index.ts b/app/dashboard/src/stores/index.ts new file mode 100644 index 0000000..afc1b15 --- /dev/null +++ b/app/dashboard/src/stores/index.ts @@ -0,0 +1,7 @@ +export * from './admin.store'; +export * from './inbounds.store'; +export * from './users.store'; +export * from './services.store'; +export * from './core-settings.store'; +export * from './dashboard.store'; +export * from './nodes.store'; diff --git a/app/dashboard/src/contexts/NodesContext.ts b/app/dashboard/src/stores/nodes.store.ts similarity index 97% rename from app/dashboard/src/contexts/NodesContext.ts rename to app/dashboard/src/stores/nodes.store.ts index 7f0ba53..9d39167 100644 --- a/app/dashboard/src/contexts/NodesContext.ts +++ b/app/dashboard/src/stores/nodes.store.ts @@ -2,9 +2,9 @@ import { useQuery } from 'react-query'; import { fetch } from 'service/http'; import { create } from 'zustand'; import { z } from 'zod'; -import { FilterUsageType, NodesFilterType } from 'types/Filter'; +import { FilterUsageType, NodesFilterType } from 'types/filter'; import { getNodesPerPageLimitSize } from 'utils/userPreferenceStorage'; -import { useDashboard } from './DashboardContext'; +import { useDashboard } from './dashboard.store'; export const NodeSchema = z.object({ name: z.string().min(1), diff --git a/app/dashboard/src/contexts/ServicesContext.ts b/app/dashboard/src/stores/services.store.ts similarity index 93% rename from app/dashboard/src/contexts/ServicesContext.ts rename to app/dashboard/src/stores/services.store.ts index 1bf45ba..cea9c9a 100644 --- a/app/dashboard/src/contexts/ServicesContext.ts +++ b/app/dashboard/src/stores/services.store.ts @@ -1,13 +1,12 @@ import { queryClient } from 'service/react-query'; import { StatisticsQueryKey } from 'components/statistics-card'; -import { Service, ServiceCreate } from 'types/Service'; +import { Service, ServiceCreate } from 'types/service'; import { getServicesPerPageLimitSize } from 'utils/userPreferenceStorage'; import { create } from 'zustand'; import { subscribeWithSelector } from 'zustand/middleware'; -import { useDashboard } from 'contexts/DashboardContext'; import { fetch } from 'service/http'; -import { ServicesFilterType } from 'types/Filter'; -import { useUsers } from './UsersContext'; +import { ServicesFilterType } from 'types/filter'; +import { useDashboard, useUsers } from 'stores'; type ServicesStateType = { services: Service[], diff --git a/app/dashboard/src/contexts/UsersContext.ts b/app/dashboard/src/stores/users.store.ts similarity index 95% rename from app/dashboard/src/contexts/UsersContext.ts rename to app/dashboard/src/stores/users.store.ts index 4bd43d0..279852e 100644 --- a/app/dashboard/src/contexts/UsersContext.ts +++ b/app/dashboard/src/stores/users.store.ts @@ -1,13 +1,12 @@ -import { User, UserCreate } from 'types/User'; +import { User, UserCreate } from 'types/user'; import { subscribeWithSelector } from 'zustand/middleware'; import { create } from 'zustand'; import { getUsersPerPageLimitSize } from 'utils/userPreferenceStorage'; import { fetch } from 'service/http'; -import { useDashboard } from 'contexts/DashboardContext'; +import { useServices, useDashboard } from 'stores'; import { StatisticsQueryKey } from 'components/statistics-card'; import { queryClient } from 'service/react-query'; -import { useServices } from './ServicesContext'; -import { UsersFilterType, FilterUsageType } from 'types/Filter'; +import { UsersFilterType, FilterUsageType } from 'types/filter'; type UsersStateType = { isCreatingNewUser: boolean; diff --git a/app/dashboard/src/types/ChakraIcon.ts b/app/dashboard/src/types/chakra-icon.ts similarity index 100% rename from app/dashboard/src/types/ChakraIcon.ts rename to app/dashboard/src/types/chakra-icon.ts diff --git a/app/dashboard/src/types/Filter.ts b/app/dashboard/src/types/filter.ts similarity index 100% rename from app/dashboard/src/types/Filter.ts rename to app/dashboard/src/types/filter.ts diff --git a/app/dashboard/src/types/Inbounds.ts b/app/dashboard/src/types/inbounds.ts similarity index 81% rename from app/dashboard/src/types/Inbounds.ts rename to app/dashboard/src/types/inbounds.ts index fa39bf1..4e3caae 100644 --- a/app/dashboard/src/types/Inbounds.ts +++ b/app/dashboard/src/types/inbounds.ts @@ -1,4 +1,4 @@ -import { ProtocolType } from './Protocol'; +import { ProtocolType } from './protocol'; export type InboundType = { id: number; diff --git a/app/dashboard/src/types/index.ts b/app/dashboard/src/types/index.ts new file mode 100644 index 0000000..0a04667 --- /dev/null +++ b/app/dashboard/src/types/index.ts @@ -0,0 +1,8 @@ +export * from './inbounds'; +export * from './user'; +export * from './service'; +export * from './page'; +export * from './protocol'; +export * from './filter'; +export * from './proxy-hosts'; +export * from './chakra-icon'; diff --git a/app/dashboard/src/types/Page.ts b/app/dashboard/src/types/page.ts similarity index 66% rename from app/dashboard/src/types/Page.ts rename to app/dashboard/src/types/page.ts index 608b210..4b35bf3 100644 --- a/app/dashboard/src/types/Page.ts +++ b/app/dashboard/src/types/page.ts @@ -1,4 +1,4 @@ -import { ChakraIcon } from 'types/ChakraIcon' +import { ChakraIcon } from 'types' export interface PageType { path: string; diff --git a/app/dashboard/src/types/Protocol.ts b/app/dashboard/src/types/protocol.ts similarity index 100% rename from app/dashboard/src/types/Protocol.ts rename to app/dashboard/src/types/protocol.ts diff --git a/app/dashboard/src/types/ProxyHosts.ts b/app/dashboard/src/types/proxy-hosts.ts similarity index 100% rename from app/dashboard/src/types/ProxyHosts.ts rename to app/dashboard/src/types/proxy-hosts.ts diff --git a/app/dashboard/src/types/Service.ts b/app/dashboard/src/types/service.ts similarity index 100% rename from app/dashboard/src/types/Service.ts rename to app/dashboard/src/types/service.ts diff --git a/app/dashboard/src/types/User.ts b/app/dashboard/src/types/user.ts similarity index 100% rename from app/dashboard/src/types/User.ts rename to app/dashboard/src/types/user.ts