fix: cascade soft-delete node bindings and rollback on config failure

This commit is contained in:
風雪 committed 2026-08-16 21:02:27 +08:00
1 parent eb312d238d
commit 4f33edff9c
3 files changed
+45 -6

No files matched your search

+16 -1
View File
@@ -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
+2 -2
View File
@@ -17,6 +17,6 @@ export interface Listener {
export const fetchListeners = () => client.get<Listener[]>('/nodes').then(r => r.data);
export const createListener = (payload: Partial<Listener>) => client.post<Listener>('/nodes', payload).then(r => r.data);
export const updateListener = (id: number, payload: Partial<Listener>) => client.put<Listener>(`/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);
+27 -3
View File
@@ -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<ProxyUser[]>('/users').then(r => r.data);
export const createUser = (payload: Partial<ProxyUser>) => client.post<ProxyUser>('/users', payload).then(r => r.data);
export const updateUser = (id: number, payload: Partial<ProxyUser>) => client.put<ProxyUser>(`/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<ProxyUser[]>('/users').then((r) => r.data);
export const createUser = (payload: Partial<ProxyUser>) =>
client.post<ProxyUser>('/users', payload).then((r) => r.data);
export const updateUser = (id: number, payload: Partial<ProxyUser>) =>
client.put<ProxyUser>(`/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<BoundNode[]>(`/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);