feat(PROJ-30): Xapian → Manticore Search Migration
- internal/index/manticore.go: ManticoreTenantManager + manticoreIndex (RT-Indizes, CGO-frei) - internal/index/index.go: TenantIndexer Interface (Xapian + Manticore) - internal/index/tenant_worker.go: mgr-Typ auf TenantIndexer Interface - internal/api/server.go: idxMgr auf TenantIndexer Interface - config/config.go: IndexConfig.ManticoreDSN Feld - cmd/archivmail/cmd_reindex.go: reindex Subkommando - cmd/archivmail/main.go: Manticore-Branch + reindex Case - go.mod: github.com/go-sql-driver/mysql v1.8.1 - update.sh: Manticore auto-install, CGO_ENABLED=0, config.yml migration, auto-reindex fix(IMAP): TCP-Deadline-Wrapper für steckengebliebene Imports fix(auth): Email-Claim in JWT für User-Isolation fix(search): User-Isolation via sess.Email (fail-safe) fix(ui): Admin-Login Auth-Cache, Logout-Redirect, IMAP-Polling-Resilienz Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { login } from "@/lib/api";
|
||||
import { getCachedUser, setCachedUser } from "@/lib/auth-cache";
|
||||
import { getCachedUser, setCachedUser, clearAuthCache } from "@/lib/auth-cache";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
@@ -31,6 +31,7 @@ export default function AdminLoginPage() {
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
clearAuthCache();
|
||||
const res = await login(username, password);
|
||||
const role = res?.user?.role ?? "";
|
||||
if (!ADMIN_ROLES.includes(role)) {
|
||||
@@ -38,6 +39,7 @@ export default function AdminLoginPage() {
|
||||
setError("Kein Zugriff. Dieses Login ist nur für Admins und Auditoren.");
|
||||
return;
|
||||
}
|
||||
setCachedUser({ username: res.user.username, email: res.user.email, role });
|
||||
if (role === "auditor") {
|
||||
router.push("/search");
|
||||
} else {
|
||||
|
||||
+30
-4
@@ -79,8 +79,12 @@ export default function ImapPage() {
|
||||
// Saving state
|
||||
const [saving, setSaving] = useState(false);
|
||||
|
||||
// Import error state
|
||||
const [importError, setImportError] = useState<string>("");
|
||||
|
||||
// Polling refs
|
||||
const pollingRefs = useRef<Map<number, ReturnType<typeof setInterval>>>(new Map());
|
||||
const pollErrorCount = useRef<Map<number, number>>(new Map());
|
||||
|
||||
const loadAccounts = useCallback(async () => {
|
||||
try {
|
||||
@@ -102,19 +106,28 @@ export default function ImapPage() {
|
||||
for (const acc of accounts) {
|
||||
const isActive = acc.status === "running" || acc.sync_running;
|
||||
if (isActive && !pollingRefs.current.has(acc.id)) {
|
||||
pollErrorCount.current.set(acc.id, 0);
|
||||
const interval = setInterval(async () => {
|
||||
try {
|
||||
const updated = await getImapProgress(acc.id);
|
||||
pollErrorCount.current.set(acc.id, 0);
|
||||
setAccounts((prev) =>
|
||||
prev.map((a) => (a.id === updated.id ? updated : a))
|
||||
);
|
||||
if (updated.status !== "running" && !updated.sync_running) {
|
||||
clearInterval(pollingRefs.current.get(acc.id)!);
|
||||
pollingRefs.current.delete(acc.id);
|
||||
pollErrorCount.current.delete(acc.id);
|
||||
}
|
||||
} catch {
|
||||
clearInterval(pollingRefs.current.get(acc.id)!);
|
||||
pollingRefs.current.delete(acc.id);
|
||||
// Only stop polling after 5 consecutive failures (tolerates brief network hiccups)
|
||||
const errors = (pollErrorCount.current.get(acc.id) ?? 0) + 1;
|
||||
pollErrorCount.current.set(acc.id, errors);
|
||||
if (errors >= 5) {
|
||||
clearInterval(pollingRefs.current.get(acc.id)!);
|
||||
pollingRefs.current.delete(acc.id);
|
||||
pollErrorCount.current.delete(acc.id);
|
||||
}
|
||||
}
|
||||
}, 2000);
|
||||
pollingRefs.current.set(acc.id, interval);
|
||||
@@ -203,11 +216,12 @@ export default function ImapPage() {
|
||||
}
|
||||
|
||||
async function handleStartImport(id: number) {
|
||||
setImportError("");
|
||||
try {
|
||||
const updated = await startImapImport(id);
|
||||
setAccounts((prev) => prev.map((a) => (a.id === updated.id ? updated : a)));
|
||||
} catch {
|
||||
// ignore
|
||||
} catch (err) {
|
||||
setImportError(err instanceof Error ? err.message : "Import konnte nicht gestartet werden.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -336,6 +350,10 @@ export default function ImapPage() {
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{importError && (
|
||||
<p className="mb-4 text-sm text-destructive" role="alert">{importError}</p>
|
||||
)}
|
||||
|
||||
{loading ? (
|
||||
<div className="space-y-4">
|
||||
{[1, 2].map((i) => (
|
||||
@@ -363,6 +381,14 @@ export default function ImapPage() {
|
||||
{statusBadge(acc.status)}
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{acc.status === "running" && acc.progress_total === 0 && (
|
||||
<div className="mb-3 space-y-1">
|
||||
<Progress value={undefined} className="animate-pulse" />
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Zaehle E-Mails auf dem Server...
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{acc.status === "running" && acc.progress_total > 0 && (
|
||||
<div className="mb-3 space-y-1">
|
||||
<Progress
|
||||
|
||||
+14
-3
@@ -3,7 +3,7 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { login } from "@/lib/api";
|
||||
import { getCachedUser } from "@/lib/auth-cache";
|
||||
import { getCachedUser, setCachedUser, clearAuthCache } from "@/lib/auth-cache";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
@@ -32,12 +32,14 @@ export default function LoginPage() {
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
clearAuthCache();
|
||||
const res = await login(username, password);
|
||||
const role = res?.user?.role ?? "";
|
||||
if (ADMIN_ROLES.includes(role)) {
|
||||
setError("Admins und Auditoren bitte über /admin anmelden.");
|
||||
setError("ADMIN_REDIRECT");
|
||||
return;
|
||||
}
|
||||
setCachedUser({ username: res.user.username, email: res.user.email, role });
|
||||
router.push("/search");
|
||||
} catch {
|
||||
setError("Anmeldung fehlgeschlagen. Bitte Zugangsdaten pruefen.");
|
||||
@@ -83,11 +85,20 @@ export default function LoginPage() {
|
||||
aria-label="Passwort"
|
||||
/>
|
||||
</div>
|
||||
{error && (
|
||||
{error && error !== "ADMIN_REDIRECT" && (
|
||||
<p className="text-sm text-destructive" role="alert">
|
||||
{error}
|
||||
</p>
|
||||
)}
|
||||
{error === "ADMIN_REDIRECT" && (
|
||||
<p className="text-sm text-destructive" role="alert">
|
||||
Admins und Auditoren bitte{" "}
|
||||
<a href="/admin/login" className="underline font-medium">
|
||||
hier anmelden
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
)}
|
||||
<Button type="submit" className="w-full" disabled={loading}>
|
||||
{loading ? "Anmelden..." : "Anmelden"}
|
||||
</Button>
|
||||
|
||||
@@ -21,13 +21,15 @@ interface UserNavProps {
|
||||
export function UserNav({ username, role }: UserNavProps) {
|
||||
const router = useRouter();
|
||||
|
||||
const ADMIN_ROLES = ["auditor", "admin", "domain_admin", "superadmin"];
|
||||
|
||||
async function handleLogout() {
|
||||
try {
|
||||
await logout();
|
||||
} catch {
|
||||
// ignore logout errors
|
||||
}
|
||||
router.push("/");
|
||||
router.push(ADMIN_ROLES.includes(role) ? "/admin/login" : "/");
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
Reference in New Issue
Block a user