1import { create } from 'zustand';
2import { subscribeWithSelector } from 'zustand/middleware';
3import { useDashboard } from './dashboard.store';
4import { fetch } from 'service/http';
5import { Inbounds, InboundType } from 'types/inbounds';
6import { z } from 'zod';
7import { HostsFilterType, InboundsFilterType } from 'types';
8import { pageSizeManagers } from 'utils/userPreferenceStorage';
9import { queryClient } from 'service/react-query';
10
11const isPortThenValue = (value: number) => (value <= 65535 && value !== 0) ? value : null;
12
13export const hostSchema = z.object({
14 remark: z.string().min(1, 'Remark is required'),
15 address: z.string().min(1, 'Address is required'),
16 port: z
17 .string()
18 .or(z.number())
19 .nullable()
20 .transform((value) => {
21 if (typeof value === 'number') return isPortThenValue(value);
22 if (value !== null && !isNaN(parseInt(value))) return isPortThenValue(Number(parseInt(value)));
23 return null;
24 }),
25 path: z.string(),
26 sni: z.string(),
27 host: z.string(),
28 security: z.enum(['inbound_default', 'none', 'tls']),
29 alpn: z.string().optional(),
30 fingerprint: z.string().optional(),
31});
32
33export type HostSchema = z.infer<typeof hostSchema>;
34export type HostType = HostSchema & { id?: number };
35export type Hosts = HostType[];
36
37export const fetchInbounds = async () => {
38 useDashboard.setState({ loading: true });
39 return fetch('/inbounds')
40 .then((inbounds: Inbounds) => {
41 useInbounds.setState({ inbounds });
42 })
43 .finally(() => {
44 useDashboard.setState({ loading: false });
45 });
46};
47
48export const fetchInboundHosts = async (id: number): Promise<Hosts> => {
49 useDashboard.setState({ loading: true });
50 return fetch(`/inbounds/${id}/hosts`)
51 .then((hosts: Hosts) => hosts)
52 .finally(() => {
53 useDashboard.setState({ loading: false });
54 });
55};
56
57type InboundsStateType = {
58 // Loading
59 loading: boolean;
60 setLoading: (value: boolean) => void;
61 // Inbounds
62 inbounds: Inbounds;
63 refetchInbounds: () => void;
64 selectedInbound: InboundType | null;
65 selectInbound: (inbound: InboundType) => void;
66 // Hosts
67 refetchHosts: () => void;
68 hostsFilters: HostsFilterType;
69 inboundsFilters: InboundsFilterType;
70 onHostsFilterChange: (filters: Partial<HostsFilterType>) => void;
71 onInboundsFilterChange: (filters: Partial<InboundsFilterType>) => void;
72 selectedHost: HostType | null;
73 selectHost: (host: HostType) => void;
74 isEditingHost: boolean;
75 onEditingHost: (isEditingHost: boolean, host: HostType | null) => void;
76 editHost: (inboundId: number | undefined, host: HostType) => Promise<boolean | void>;
77 isCreatingHost: boolean;
78 onCreatingHost: (isCreatingHost: boolean) => void;
79 createHost: (inboundId: number | undefined, host: HostSchema) => Promise<boolean | void>;
80 isDeletingHost: boolean;
81 onDeletingHost: (isDeletingHost: boolean, host: HostType) => void;
82 deleteHost: (hostId: number) => void;
83};
84
85export const useInbounds = create(
86 subscribeWithSelector<InboundsStateType>((set, get) => ({
87 inbounds: [],
88 selectedHost: null,
89 selectedInbound: null,
90 loading: false,
91 isDeletingHost: false,
92 isCreatingHost: false,
93 isEditingHost: false,
94 hostsFilters: {
95 name: '',
96 limit: pageSizeManagers.hosts.getPageSize(),
97 sort: '-created_at',
98 },
99 inboundsFilters: {
100 name: '',
101 limit: pageSizeManagers.inbounds.getPageSize(),
102 sort: '-created_at',
103 },
104 onHostsFilterChange: (filters) => {
105 set({
106 hostsFilters: {
107 ...get().hostsFilters,
108 ...filters,
109 },
110 });
111 },
112 onInboundsFilterChange: (filters) => {
113 set({
114 hostsFilters: {
115 ...get().inboundsFilters,
116 ...filters,
117 },
118 });
119 },
120 setLoading: (value) => {
121 set({ loading: value })
122 },
123 refetchInbounds: () => {
124 fetchInbounds();
125 },
126 refetchHosts: () => {
127 queryClient.invalidateQueries('hosts');
128 },
129 selectInbound: (host): void => {
130 set({ selectedInbound: host })
131 },
132 selectHost: (host: HostType): void => {
133 set({ selectedHost: host })
134 },
135 onEditingHost: (isEditingHost: boolean, host: HostType | null) => {
136 set({ isEditingHost, selectedHost: host });
137 },
138 editHost: async (hostId: number | undefined, body: HostSchema): Promise<boolean | void> => {
139 if (hostId !== undefined) {
140 useDashboard.setState({ loading: true });
141 return fetch(`/inbounds/hosts/${hostId}`, { method: 'PUT', body })
142 .then(() => true)
143 .catch(() => false)
144 .finally(() => {
145 useDashboard.setState({ loading: false });
146 });
147 }
148 },
149 onCreatingHost: (isCreatingHost: boolean) => {
150 set({ isCreatingHost });
151 },
152 createHost: async (inboundId: number | undefined, body: HostSchema): Promise<boolean | void> => {
153 if (inboundId !== undefined) {
154 useDashboard.setState({ loading: true });
155 return fetch(`/inbounds/${inboundId}/hosts/${body}`, { method: 'POST', body })
156 .then(() => true)
157 .catch(() => false)
158 .finally(() => {
159 useDashboard.setState({ loading: false });
160 });
161 }
162 },
163 onDeletingHost: (isDeletingHost: boolean, host: HostType | null) => {
164 set({ isDeletingHost, selectedHost: host });
165 },
166 deleteHost: async (hostId: number): Promise<boolean> => {
167 useDashboard.setState({ loading: true });
168 return fetch(`/inbounds/hosts/${hostId}`, { method: 'DELETE' })
169 .then(() => true)
170 .catch(() => false)
171 .finally(() => {
172 useDashboard.setState({ loading: false });
173 });
174 },
175 }))
176);