diff --git a/backend/internal/node/service.go b/backend/internal/node/service.go index a2a5e9a..6e13da4 100644 --- a/backend/internal/node/service.go +++ b/backend/internal/node/service.go @@ -84,11 +84,26 @@ func (s *Service) Update(l *models.Listener) error { } func (s *Service) Delete(id uint) error { + var previous models.Listener + if err := s.db.First(&previous, id).Error; err != nil { + return fmt.Errorf("failed to load node %d: %w", id, err) + } + + // Soft-delete join bindings first so credential regeneration stays consistent. + if err := s.db.Where("listener_id = ?", id).Delete(&models.ListenerUser{}).Error; err != nil { + return fmt.Errorf("failed to delete node bindings: %w", err) + } if err := s.db.Delete(&models.Listener{}, id).Error; err != nil { return fmt.Errorf("failed to delete node: %w", err) } - return s.RegenerateConfig() + if err := s.RegenerateConfig(); err != nil { + // Best-effort restore so a config write failure does not leave a half-deleted node. + _ = s.db.Unscoped().Model(&models.Listener{}).Where("id = ?", id).Update("deleted_at", nil).Error + _ = s.db.Unscoped().Model(&models.ListenerUser{}).Where("listener_id = ?", id).Update("deleted_at", nil).Error + return fmt.Errorf("failed to regenerate config after delete: %w", err) + } + return nil } // RegenerateConfig regenerates the complete Mihomo configuration through the diff --git a/frontend/src/api/nodes.ts b/frontend/src/api/nodes.ts index 9a296f8..2bf8716 100644 --- a/frontend/src/api/nodes.ts +++ b/frontend/src/api/nodes.ts @@ -17,6 +17,6 @@ export interface Listener { export const fetchListeners = () => client.get('/nodes').then(r => r.data); export const createListener = (payload: Partial) => client.post('/nodes', payload).then(r => r.data); export const updateListener = (id: number, payload: Partial) => client.put(`/nodes/${id}`, payload).then(r => r.data); -export const deleteListener = (id: number) => client.delete(`/nodes/${id}`); -export const reloadListener = (id: number) => client.post(`/nodes/${id}/reload`); +export const deleteListener = (id: number) => client.delete(`/nodes/${id}`).then((r) => r.data); +export const reloadListener = (id: number) => client.post(`/nodes/${id}/reload`).then((r) => r.data); export const exportNodeURI = (id: number) => client.get<{ name: string; protocol: string; uris: string[] }>(`/nodes/${id}/uri`).then(r => r.data); diff --git a/frontend/src/api/users.ts b/frontend/src/api/users.ts index ac9fdca..80775c2 100644 --- a/frontend/src/api/users.ts +++ b/frontend/src/api/users.ts @@ -5,10 +5,34 @@ export interface ProxyUser { username: string; password?: string; enabled: boolean; + traffic_limit?: number; + traffic_used?: number; created_at?: string; } -export const fetchUsers = () => client.get('/users').then(r => r.data); -export const createUser = (payload: Partial) => client.post('/users', payload).then(r => r.data); -export const updateUser = (id: number, payload: Partial) => client.put(`/users/${id}`, payload).then(r => r.data); +export interface BoundNode { + id: number; + name: string; + protocol: string; + port: string; + bind_address: string; + enabled: boolean; + tls?: boolean; + udp?: boolean; + status?: string; +} + +export const fetchUsers = () => client.get('/users').then((r) => r.data); +export const createUser = (payload: Partial) => + client.post('/users', payload).then((r) => r.data); +export const updateUser = (id: number, payload: Partial) => + client.put(`/users/${id}`, payload).then((r) => r.data); export const deleteUser = (id: number) => client.delete(`/users/${id}`); + +/** List nodes currently bound to a proxy user. */ +export const fetchUserNodes = (userId: number) => + client.get(`/users/${userId}/nodes`).then((r) => r.data); + +/** Replace the set of nodes bound to a proxy user. */ +export const bindUserNodes = (userId: number, listenerIds: number[]) => + client.post(`/users/${userId}/nodes`, { listener_ids: listenerIds }).then((r) => r.data);