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