Add SSH inbound, log level. Update MTPROXY. Fixes

This commit is contained in:
Shtorm
2026-06-07 07:59:43 +03:00
parent 6f6af8e902
commit 9f5ccf43d4
115 changed files with 2742 additions and 527 deletions

View File

@@ -42,6 +42,7 @@ export type UserType =
| "mtproxy"
| "naive"
| "socks"
| "ssh"
| "trojan"
| "trusttunnel"
| "tuic"
@@ -57,6 +58,7 @@ export interface User {
uuid: string;
password: string;
secret: string;
authorized_keys: string[];
flow: string;
alter_id: number;
created_at: string;
@@ -70,6 +72,7 @@ export interface UserCreate {
uuid?: string;
password?: string;
secret?: string;
authorized_keys?: string[];
flow?: string;
alter_id?: number;
}
@@ -77,6 +80,7 @@ export interface UserUpdate {
uuid?: string;
password?: string;
secret?: string;
authorized_keys?: string[];
flow?: string;
alter_id?: number;
}

View File

@@ -64,7 +64,7 @@ import type { Listable } from "../api/types";
import { notifyApiError, useNotify } from "../notifications/NotificationsProvider";
import { PageHeader } from "./PageHeader";
export type FieldType = "text" | "number" | "select" | "multiselect" | "ids" | "uuid";
export type FieldType = "text" | "number" | "select" | "multiselect" | "ids" | "uuid" | "string-list";
// FILTER_WIDTH is the fixed CSS width (px) of a single filter cell in the
// filter panel. FILTER_GAP is the flex gap between cells (matches the
@@ -126,7 +126,7 @@ export interface FieldSpec<TValue = unknown> {
// spec, matching what `emptyForm` would produce.
function emptyValueForField(f: FieldSpec): unknown {
if (f.defaultValue !== undefined) return f.defaultValue;
if (f.type === "multiselect") return [];
if (f.type === "multiselect" || f.type === "string-list") return [];
return "";
}
@@ -575,7 +575,7 @@ function fieldVisible(
// strings, missing selections, and empty arrays for multi-select / ids.
function isFieldEmpty(f: FieldSpec, value: unknown): boolean {
if (value === undefined || value === null) return true;
if (f.type === "multiselect" || f.type === "ids") {
if (f.type === "multiselect" || f.type === "ids" || f.type === "string-list") {
if (Array.isArray(value)) return value.length === 0;
if (typeof value === "string") return value.trim() === "";
return true;
@@ -4764,6 +4764,40 @@ function CrudDialog({
/>
);
}
if (f.type === "string-list") {
const arr = Array.isArray(value) ? (value as string[]) : [];
return (
<Box key={f.name} sx={{ mt: -1 }}>
<Typography variant="caption" color={errored ? "error" : "textSecondary"} sx={{ mb: 0.5, ml: "14px", display: "block" }}>
{f.label}{f.required ? " *" : ""}
</Typography>
<Stack spacing={1}>
{arr.map((item, idx) => (
<Stack key={idx} direction="row" spacing={1} alignItems="center">
<TextField
fullWidth
size="small"
value={item}
placeholder={`Key ${idx + 1}`}
onChange={(e) => {
const next = [...arr];
next[idx] = e.target.value;
set(f.name, next);
}}
/>
<IconButton size="small" color="error" onClick={() => set(f.name, arr.filter((_, i) => i !== idx))}>
<DeleteIcon fontSize="small" />
</IconButton>
</Stack>
))}
</Stack>
<Button size="small" startIcon={<AddIcon />} sx={{ mt: 1 }} onClick={() => set(f.name, [...arr, ""])}>
Add
</Button>
{fieldErr && <Typography variant="caption" color="error" sx={{ mt: 0.5, display: "block" }}>{fieldErr}</Typography>}
</Box>
);
}
const isNumber = f.type === "number";
// Numeric value of the current cell. Falls back to 0 for the
// empty state so the up-arrow always has a sensible base to

View File

@@ -25,6 +25,7 @@ const USER_TYPES: { value: UserType; label: string }[] = [
{ value: "mtproxy", label: "MTProxy" },
{ value: "naive", label: "Naive" },
{ value: "socks", label: "SOCKS" },
{ value: "ssh", label: "SSH" },
{ value: "trojan", label: "Trojan" },
{ value: "trusttunnel", label: "TrustTunnel" },
{ value: "tuic", label: "TUIC" },
@@ -44,8 +45,9 @@ const FLOW_OPTIONS: { value: string; label: string }[] = [
// same rule up-front (required fields invisible for the current type
// are filtered out before validateRequired runs).
const SHOW_UUID = new Set<UserType>(["vless", "vmess", "tuic"]);
const SHOW_PASSWORD = new Set<UserType>(["anytls", "http", "hysteria", "hysteria2", "mixed", "naive", "socks", "trojan", "trusttunnel", "tuic"]);
const SHOW_PASSWORD = new Set<UserType>(["anytls", "http", "hysteria", "hysteria2", "mixed", "naive", "socks", "ssh", "trojan", "trusttunnel", "tuic"]);
const SHOW_SECRET = new Set<UserType>(["mtproxy"]);
const SHOW_AUTHORIZED_KEYS = new Set<UserType>(["ssh"]);
const SHOW_FLOW = new Set<UserType>(["vless"]);
const SHOW_ALTER_ID = new Set<UserType>(["vmess"]);
@@ -103,7 +105,7 @@ export function UsersPage() {
options: USER_TYPES,
// Switching the user type wipes every credential field so the form
// matches the legacy admin's behaviour of starting fresh.
clears: ["uuid", "password", "secret", "flow", "alter_id"],
clears: ["uuid", "password", "secret", "authorized_keys", "flow", "alter_id"],
},
// Credential fields: the Go struct validator reports "required" for
// whichever of these is missing once the type is chosen, so each one
@@ -111,8 +113,9 @@ export function UsersPage() {
// are filtered out before validateRequired runs, so e.g. Password is
// only enforced for hysteria/hysteria2/trojan/tuic and not for vless.
{ name: "uuid", label: "UUID", type: "uuid", required: true, visibleWhen: showFor(SHOW_UUID) },
{ name: "password", label: "Password", type: "text", required: true, visibleWhen: showFor(SHOW_PASSWORD) },
{ name: "password", label: "Password", type: "text", visibleWhen: showFor(SHOW_PASSWORD) },
{ name: "secret", label: "Secret", type: "text", required: true, visibleWhen: showFor(SHOW_SECRET) },
{ name: "authorized_keys", label: "Authorized Keys", type: "string-list", visibleWhen: showFor(SHOW_AUTHORIZED_KEYS) },
{
name: "flow",
label: "Flow",
@@ -135,6 +138,7 @@ export function UsersPage() {
uuid: u.uuid,
password: u.password,
secret: u.secret,
authorized_keys: u.authorized_keys ?? [],
flow: u.flow,
alter_id: u.alter_id,
}),
@@ -146,6 +150,7 @@ export function UsersPage() {
uuid: f.uuid ? String(f.uuid).trim() : undefined,
password: f.password ? String(f.password) : undefined,
secret: f.secret ? String(f.secret) : undefined,
authorized_keys: Array.isArray(f.authorized_keys) ? (f.authorized_keys as string[]).filter(Boolean) : undefined,
flow: f.flow ? String(f.flow) : undefined,
alter_id: f.alter_id !== undefined && f.alter_id !== "" ? Number(f.alter_id) : undefined,
}),
@@ -154,6 +159,7 @@ export function UsersPage() {
if (f.uuid && String(f.uuid).trim() !== "") out.uuid = String(f.uuid).trim();
if (f.password !== undefined && f.password !== "") out.password = String(f.password);
if (f.secret !== undefined && f.secret !== "") out.secret = String(f.secret);
if (Array.isArray(f.authorized_keys) && (f.authorized_keys as string[]).filter(Boolean).length > 0) out.authorized_keys = (f.authorized_keys as string[]).filter(Boolean);
if (f.flow !== undefined && f.flow !== "") out.flow = String(f.flow);
if (f.alter_id !== undefined && f.alter_id !== "") out.alter_id = Number(f.alter_id);
return out;