Initial commit: thbd-portal, duplicated from elmi-portal

Rebranded for Thunderbird Portal (THBD): numbering prefix EL->TH, all
"ELMI"/"ЭЛМИ" UI strings genericized, ELMI's real contract templates and
company legal/bank details removed (not applicable to this company), fresh
VAPID keypair and encryption key, fresh single-migration schema history.
Same architecture as elmi-portal: Web Push notifications, unread-chat app
badge, cookie-session auth.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gh2UXUQUVBWroWEnn1FLFG
This commit is contained in:
ogrechkoandClaude Sonnet 5 committed 2026-08-31 16:33:30 +00:00
commit 579528edf7
197 files changed
+25022

No files matched your search

@@ -0,0 +1,127 @@
"use client";
import { useState } from "react";
import { useRouter } from "next/navigation";
interface Status {
configured: boolean;
enabled: boolean;
user: string | null;
imapHost: string | null;
verifiedAt: number | null;
}
export function MailboxForm({ status }: { status: Status }) {
const router = useRouter();
const [imapHost, setImapHost] = useState(status.imapHost ?? "");
const [imapPort, setImapPort] = useState("993");
const [user, setUser] = useState(status.user ?? "");
const [password, setPassword] = useState("");
const [allowInsecureTls, setAllowInsecureTls] = useState(false);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
setLoading(true);
setError(null);
const res = await fetch("/api/settings/mailbox", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ imapHost, imapPort: Number(imapPort), user, password, allowInsecureTls }),
});
setLoading(false);
if (!res.ok) {
const data = await res.json().catch(() => null);
setError(data?.error ?? "Не удалось подключиться к почтовому ящику");
return;
}
setPassword("");
router.refresh();
}
async function handleDisable() {
setLoading(true);
await fetch("/api/settings/mailbox", { method: "DELETE" });
setLoading(false);
router.refresh();
}
return (
<div className="card p-4">
<h2 className="mb-3 text-sm font-semibold text-text-muted">Почтовый ящик для запросов (IMAP)</h2>
<div className="mb-4 text-sm">
{status.configured ? (
<p className="text-text-muted">
{status.enabled ? "Активен" : "Отключен"}: {status.user} @ {status.imapHost}
{status.verifiedAt && ` — проверен ${new Date(status.verifiedAt).toLocaleString("ru-RU")}`}
</p>
) : (
<p className="text-text-faint">Ещё не настроен.</p>
)}
</div>
<form onSubmit={handleSubmit} className="grid grid-cols-1 gap-3 sm:grid-cols-2">
<label className="text-sm">
<span className="mb-1 block font-medium text-text-muted">IMAP-сервер</span>
<input
required
value={imapHost}
onChange={(e) => setImapHost(e.target.value)}
placeholder="imap.yandex.ru"
className="w-full rounded-md border border-border bg-surface px-3 py-2 outline-none focus:border-accent"
/>
</label>
<label className="text-sm">
<span className="mb-1 block font-medium text-text-muted">Порт</span>
<input
type="number"
required
value={imapPort}
onChange={(e) => setImapPort(e.target.value)}
className="w-full rounded-md border border-border bg-surface px-3 py-2 outline-none focus:border-accent"
/>
</label>
<label className="text-sm">
<span className="mb-1 block font-medium text-text-muted">Логин</span>
<input
required
value={user}
onChange={(e) => setUser(e.target.value)}
className="w-full rounded-md border border-border bg-surface px-3 py-2 outline-none focus:border-accent"
/>
</label>
<label className="text-sm">
<span className="mb-1 block font-medium text-text-muted">Пароль</span>
<input
type="password"
required
value={password}
onChange={(e) => setPassword(e.target.value)}
className="w-full rounded-md border border-border bg-surface px-3 py-2 outline-none focus:border-accent"
/>
</label>
<label className="flex items-center gap-2 text-sm sm:col-span-2">
<input type="checkbox" checked={allowInsecureTls} onChange={(e) => setAllowInsecureTls(e.target.checked)} />
<span className="text-text-muted">Разрешить недоверенный TLS-сертификат</span>
</label>
{error && <p className="sm:col-span-2 rounded-md bg-danger-soft px-3 py-2 text-sm text-danger-soft-text">{error}</p>}
<div className="flex gap-2 sm:col-span-2">
<button type="submit" disabled={loading} className="btn btn-primary">
{loading ? "Проверяем…" : "Проверить и сохранить"}
</button>
{status.configured && status.enabled && (
<button type="button" onClick={handleDisable} disabled={loading} className="btn btn-ghost">
Отключить
</button>
)}
</div>
</form>
</div>
);
}