Files
iknowyou/front/src/components/ProxySettings.svelte
T

202 lines
6.1 KiB
Svelte

<script>
import { onMount } from "svelte";
import { Plus, Trash2, Save, Shield, AlertTriangle, Lock } from "@lucide/svelte";
let proxies = $state([]);
let loading = $state(true);
let saving = $state(false);
let msg = $state(null);
let configReadonly = $state(false);
let newUrl = $state("");
let newUrlError = $state("");
onMount(async () => {
try {
const res = await fetch("/api/config");
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const cfg = await res.json();
proxies = cfg.proxies ?? [];
configReadonly = cfg.readonly ?? false;
} catch (e) {
msg = { ok: false, text: e.message };
} finally {
loading = false;
}
});
function validateUrl(url) {
if (!url) return "URL is required";
try {
const u = new URL(url);
if (!["socks4:", "socks5:", "http:"].includes(u.protocol)) {
return "Protocol must be socks4, socks5, or http";
}
if (!u.hostname) return "Missing hostname";
} catch {
return "Invalid URL format";
}
return "";
}
function addProxy() {
const url = newUrl.trim();
const err = validateUrl(url);
if (err) { newUrlError = err; return; }
if (proxies.some((p) => p.url === url)) {
newUrlError = "This proxy is already in the list";
return;
}
proxies = [...proxies, { url }];
newUrl = "";
newUrlError = "";
}
function removeProxy(index) {
proxies = proxies.filter((_, i) => i !== index);
}
async function save() {
saving = true;
msg = null;
try {
const res = await fetch("/api/config/proxies", {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(proxies),
});
if (!res.ok)
throw new Error((await res.json().catch(() => ({}))).error || `HTTP ${res.status}`);
msg = { ok: true, text: "Saved" };
setTimeout(() => (msg = null), 3000);
} catch (e) {
msg = { ok: false, text: e.message };
} finally {
saving = false;
}
}
function proxyLabel(url) {
try {
const u = new URL(url);
const proto = u.protocol.replace(":", "").toUpperCase();
const auth = u.username ? `${u.username}@` : "";
return { proto, host: `${auth}${u.hostname}:${u.port || defaultPort(u.protocol)}` };
} catch {
return { proto: "?", host: url };
}
}
function defaultPort(protocol) {
if (protocol === "http:") return "8080";
return "1080";
}
const PROTO_COLORS = {
SOCKS5: "badge-primary",
SOCKS4: "badge-secondary",
HTTP: "badge-neutral",
};
</script>
<div class="card bg-base-200 shadow">
<div class="card-body gap-4 p-4">
<div class="flex items-center justify-between gap-2 flex-wrap">
<div class="flex items-center gap-2">
<Shield size={15} class="text-base-content/50 shrink-0" />
<h3 class="text-xs uppercase tracking-widest text-base-content/50">Proxies</h3>
</div>
<div class="flex items-center gap-2">
{#if msg}
<span class="text-xs {msg.ok ? 'text-success' : 'text-error'}">{msg.text}</span>
{/if}
{#if !configReadonly}
<button
class="btn btn-primary btn-sm gap-1"
onclick={save}
disabled={saving || loading}
>
{#if saving}
<span class="loading loading-spinner loading-xs"></span>
{:else}
<Save size={13} />
{/if}
Save
</button>
{/if}
</div>
</div>
{#if configReadonly}
<div class="flex items-center gap-2 text-xs text-base-content/50">
<Lock size={12} />
Proxy config is read-only.
</div>
{/if}
{#if loading}
<div class="flex justify-center py-4">
<span class="loading loading-spinner loading-sm"></span>
</div>
{:else}
<div class="flex flex-col gap-2">
{#if proxies.length === 0}
<p class="text-sm text-base-content/40">
No proxies configured, tools will connect directly.
</p>
{:else}
{#each proxies as proxy, i}
{@const lbl = proxyLabel(proxy.url)}
<div class="flex items-center gap-2 bg-base-300 rounded-box px-3 py-2">
<span class="badge badge-xs {PROTO_COLORS[lbl.proto] ?? 'badge-ghost'} shrink-0">
{lbl.proto}
</span>
<span class="font-mono text-sm flex-1 truncate">{lbl.host}</span>
{#if !configReadonly}
<button
class="btn btn-ghost btn-xs text-error shrink-0"
onclick={() => removeProxy(i)}
title="Remove proxy"
>
<Trash2 size={13} />
</button>
{/if}
</div>
{/each}
{/if}
{#if !configReadonly}
<div class="flex flex-col gap-1 mt-1">
<div class="flex gap-2">
<input
type="text"
class="input input-bordered input-sm font-mono flex-1 {newUrlError ? 'input-error' : ''}"
placeholder="socks5://user:pass@host:1080"
bind:value={newUrl}
onkeydown={(e) => e.key === "Enter" && addProxy()}
/>
<button
class="btn btn-neutral btn-sm gap-1 shrink-0"
onclick={addProxy}
disabled={!newUrl.trim()}
>
<Plus size={14} />
Add
</button>
</div>
{#if newUrlError}
<p class="text-xs text-error flex items-center gap-1">
<AlertTriangle size={11} />{newUrlError}
</p>
{/if}
<p class="text-xs text-base-content/40">
Supported: <span class="font-mono">socks5://</span>,
<span class="font-mono">socks4://</span>,
<span class="font-mono">http://</span>. On failure, the next proxy is tried automatically.
</p>
</div>
{/if}
</div>
{/if}
</div>
</div>