mirror of
https://github.com/anotherhadi/iknowyou.git
synced 2026-05-20 17:22:33 +02:00
302166c87d
Signed-off-by: Hadi <112569860+anotherhadi@users.noreply.github.com>
202 lines
6.1 KiB
Svelte
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>
|