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 if (id !== undefined) {
49 useDashboard.setState({ loading: true });
50 return fetch(`/inbounds/${id}/hosts`)
51 .then((hosts: Hosts) => {
52 return hosts
53 })
54 .finally(() => {
55 useDashboard.setState({ loading: false });
56 });
57 } else { return [] }
58};
59
60type InboundsStateType = {
61 // Loading
62 loading: boolean;
63 setLoading: (value: boolean) => void;
64 // Inbounds
65 selectedInbound: InboundType | undefined;
66 selectInbound: (inbound: InboundType) => void;
67 refetchInbounds: () => void;
68 // Hosts
69 refetchHosts: () => void;
70 hostsFilters: HostsFilterType;
71 inboundsFilters: InboundsFilterType;
72 onHostsFilterChange: (filters: Partial<HostsFilterType>) => void;
73 onInboundsFilterChange: (filters: Partial<InboundsFilterType>) => void;
74 selectedHost: HostType | null;
75 selectHost: (host: HostType) => void;
76 isEditingHost: boolean;
77 onEditingHost: (isEditingHost: boolean, host: HostType | null) => void;
78 editHost: (inboundId: number | undefined, host: HostType) => Promise<boolean | void>;
79 isCreatingHost: boolean;
80 onCreatingHost: (isCreatingHost: boolean) => void;
81 createHost: (inboundId: number | undefined, host: HostSchema) => Promise<boolean | void>;
82 isDeletingHost: boolean;
83 onDeletingHost: (isDeletingHost: boolean, host: HostType) => void;
84 deleteHost: (hostId: number) => void;
85};
86
87export const useInbounds = create(
88 subscribeWithSelector<InboundsStateType>((set, get) => ({
89 selectedHost: null,
90 selectedInbound: undefined,
91 loading: false,
92 isDeletingHost: false,
93 isCreatingHost: false,
94 isEditingHost: false,
95 hostsFilters: {
96 name: '',
97 limit: pageSizeManagers.hosts.getPageSize(),
98 sort: '-created_at',
99 },
100 inboundsFilters: {
101 name: '',
102 limit: pageSizeManagers.inbounds.getPageSize(),
103 sort: '-created_at',
104 },
105 onHostsFilterChange: (filters) => {
106 set({
107 hostsFilters: {
108 ...get().hostsFilters,
109 ...filters,
110 },
111 });
112 },
113 onInboundsFilterChange: (filters) => {
114 set({
115 hostsFilters: {
116 ...get().inboundsFilters,
117 ...filters,
118 },
119 });
120 },
121 setLoading: (value) => {
122 set({ loading: value })
123 },
124 refetchInbounds: () => {
125 queryClient.invalidateQueries({ queryKey: [queryIds.inbounds] });
126 },
127 refetchHosts: async () => {
128 queryClient.invalidateQueries({ queryKey: [queryIds.hosts] });
129 },
130 selectInbound: (host): void => {
131 set({ selectedInbound: host })
132 },
133 selectHost: (host: HostType): void => {
134 set({ selectedHost: host })
135 },
136 onEditingHost: (isEditingHost: boolean, host: HostType | null) => {
137 set({ isEditingHost, selectedHost: host });
138 },
139 editHost: async (hostId: number | undefined, body: HostSchema): Promise<boolean | void> => {
140 if (hostId !== undefined) {
141 useDashboard.setState({ loading: true });
142 return fetch(`/inbounds/hosts/${hostId}`, { method: 'PUT', body })
143 .then(() => true)
144 .catch(() => false)
145 .finally(() => {
146 useDashboard.setState({ loading: false });
147 });
148 }
149 },
150 onCreatingHost: (isCreatingHost: boolean) => {
151 set({ isCreatingHost });
152 },
153 createHost: async (inboundId: number | undefined, body: HostSchema): Promise<boolean | void> => {
154 if (inboundId !== undefined) {
155 useDashboard.setState({ loading: true });
156 return fetch(`/inbounds/${inboundId}/hosts`, { method: 'POST', body })
157 .then(() => true)
158 .catch(() => false)
159 .finally(() => {
160 useDashboard.setState({ loading: false });
161 });
162 }
163 },
164 onDeletingHost: (isDeletingHost: boolean, host: HostType | null) => {
165 set({ isDeletingHost, selectedHost: host });
166 },
167 deleteHost: async (hostId: number): Promise<boolean> => {
168 useDashboard.setState({ loading: true });
169 return fetch(`/inbounds/hosts/${hostId}`, { method: 'DELETE' })
170 .then(() => true)
171 .catch(() => false)
172 .finally(() => {
173 useDashboard.setState({ loading: false });
174 });
175 },
176 }))
177);