fix(inbounds): forwarding echo lost in DBInbound — add the missing field
The edit modal seeds from the list row wrapped in DBInbound, whose cloneProps only copies keys the instance already owns. DBInbound had no forwarding field, so the stored ForwardingSpec JSON was silently dropped on every row wrap and rawInboundToFormValues fell back to direct — saved forwarding compiled and worked, but never echoed back into the form. Backend/API carried the field correctly all along. Regression test covers managed/manual/direct through the exact row -> DBInbound -> form-values chain.
This commit is contained in:
@@ -39,6 +39,7 @@ export type DBInboundInit = Partial<{
|
|||||||
streamSettings: RawJsonField;
|
streamSettings: RawJsonField;
|
||||||
tag: string;
|
tag: string;
|
||||||
sniffing: RawJsonField;
|
sniffing: RawJsonField;
|
||||||
|
forwarding: string;
|
||||||
clientStats: ClientStats[];
|
clientStats: ClientStats[];
|
||||||
nodeId: number | null;
|
nodeId: number | null;
|
||||||
shareAddrStrategy: string;
|
shareAddrStrategy: string;
|
||||||
@@ -88,6 +89,7 @@ export class DBInbound {
|
|||||||
streamSettings: RawJsonField;
|
streamSettings: RawJsonField;
|
||||||
tag: string;
|
tag: string;
|
||||||
sniffing: RawJsonField;
|
sniffing: RawJsonField;
|
||||||
|
forwarding: string;
|
||||||
clientStats: ClientStats[];
|
clientStats: ClientStats[];
|
||||||
nodeId: number | null;
|
nodeId: number | null;
|
||||||
shareAddrStrategy: string;
|
shareAddrStrategy: string;
|
||||||
@@ -119,6 +121,7 @@ export class DBInbound {
|
|||||||
this.streamSettings = '';
|
this.streamSettings = '';
|
||||||
this.tag = '';
|
this.tag = '';
|
||||||
this.sniffing = '';
|
this.sniffing = '';
|
||||||
|
this.forwarding = '';
|
||||||
this.clientStats = [];
|
this.clientStats = [];
|
||||||
this.nodeId = null;
|
this.nodeId = null;
|
||||||
this.shareAddrStrategy = 'node';
|
this.shareAddrStrategy = 'node';
|
||||||
|
|||||||
@@ -0,0 +1,78 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
|
import { rawInboundToFormValues, type RawInboundRow } from '@/lib/xray/inbound-form-adapter';
|
||||||
|
import { DBInbound, type DBInboundInit } from '@/models/dbinbound';
|
||||||
|
|
||||||
|
// Regression (P9.1): an inbound whose forwarding was saved must echo it
|
||||||
|
// back when the edit modal is seeded from the list row. The list page
|
||||||
|
// wraps API rows in DBInbound before handing them to the modal, so the
|
||||||
|
// row -> DBInbound -> form-values path is the exact chain that broke:
|
||||||
|
// DBInbound had no `forwarding` field and cloneProps only copies keys
|
||||||
|
// the instance already owns, silently dropping the stored spec.
|
||||||
|
|
||||||
|
const MANAGED = JSON.stringify({
|
||||||
|
mode: 'forward',
|
||||||
|
landing: 'managed',
|
||||||
|
targetInboundId: 42,
|
||||||
|
address: '',
|
||||||
|
port: 0,
|
||||||
|
protocol: 'socks5',
|
||||||
|
username: '',
|
||||||
|
password: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const MANUAL = JSON.stringify({
|
||||||
|
mode: 'forward',
|
||||||
|
landing: 'manual',
|
||||||
|
targetInboundId: 0,
|
||||||
|
address: '10.240.64.2',
|
||||||
|
port: 47814,
|
||||||
|
protocol: 'socks5',
|
||||||
|
username: 'ngs_user',
|
||||||
|
password: 's3cret',
|
||||||
|
});
|
||||||
|
|
||||||
|
function apiRow(forwarding: string): RawInboundRow & DBInboundInit {
|
||||||
|
return {
|
||||||
|
id: 7,
|
||||||
|
port: 40159,
|
||||||
|
listen: '',
|
||||||
|
protocol: 'vless',
|
||||||
|
remark: 'PPFL1-V4',
|
||||||
|
enable: true,
|
||||||
|
expiryTime: 0,
|
||||||
|
settings: JSON.stringify({ clients: [], decryption: 'none', fallbacks: [] }),
|
||||||
|
streamSettings: JSON.stringify({ network: 'tcp', security: 'reality', tcpSettings: {} }),
|
||||||
|
sniffing: JSON.stringify({ enabled: false }),
|
||||||
|
forwarding,
|
||||||
|
} as RawInboundRow & DBInboundInit;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('forwarding echo through DBInbound (edit modal seeding)', () => {
|
||||||
|
it('managed forwarding survives row -> DBInbound -> form values', () => {
|
||||||
|
const db = new DBInbound(apiRow(MANAGED));
|
||||||
|
const values = rawInboundToFormValues(db);
|
||||||
|
expect(values.forwarding.mode).toBe('forward');
|
||||||
|
expect(values.forwarding.landing).toBe('managed');
|
||||||
|
expect(values.forwarding.targetInboundId).toBe(42);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('manual forwarding survives with endpoint and credentials', () => {
|
||||||
|
const db = new DBInbound(apiRow(MANUAL));
|
||||||
|
const values = rawInboundToFormValues(db);
|
||||||
|
expect(values.forwarding.mode).toBe('forward');
|
||||||
|
expect(values.forwarding.landing).toBe('manual');
|
||||||
|
expect(values.forwarding.address).toBe('10.240.64.2');
|
||||||
|
expect(values.forwarding.port).toBe(47814);
|
||||||
|
expect(values.forwarding.protocol).toBe('socks5');
|
||||||
|
expect(values.forwarding.username).toBe('ngs_user');
|
||||||
|
expect(values.forwarding.password).toBe('s3cret');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('empty forwarding stays direct', () => {
|
||||||
|
const db = new DBInbound(apiRow(''));
|
||||||
|
const values = rawInboundToFormValues(db);
|
||||||
|
expect(values.forwarding.mode).toBe('direct');
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user