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