feat(PROJ-53): Konfigurierbare Listenanzahl pro Seite
- users.list_page_size (Default 25), PATCH /api/auth/preferences, Whitelist 25/50/100/200, Wert in login/me-Response - Settings-UI mit Select, /search nutzt gespeicherte Seitengröße - /api/search page_size serverseitig auf max. 500 gecappt fix(PROJ-46): login_attempts-Migration nutzte s.db statt s.pool (Backend kompilierte nicht) feat(PROJ-50): DSGVO-Löschersuchen Backend (dsgvo_requests, Handler, cc_addr/bcc_addr Indexerweiterung) — noch nicht QA'd/deployed
This commit is contained in:
@@ -44,7 +44,7 @@ import { Label } from "@/components/ui/label";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { Bookmark, BookmarkPlus, Trash2 } from "lucide-react";
|
||||
|
||||
const PAGE_SIZE = 25;
|
||||
const DEFAULT_PAGE_SIZE = 25;
|
||||
|
||||
function formatBytes(bytes: number): string {
|
||||
if (bytes < 1024) return `${bytes} B`;
|
||||
@@ -86,6 +86,8 @@ export default function SearchPage() {
|
||||
const { user, loading: authLoading } = useAuth();
|
||||
const router = useRouter();
|
||||
|
||||
const pageSize = user?.list_page_size ?? DEFAULT_PAGE_SIZE;
|
||||
|
||||
const [query, setQuery] = useState("");
|
||||
const [fromFilter, setFromFilter] = useState("");
|
||||
const [toFilter, setToFilter] = useState("");
|
||||
@@ -143,7 +145,7 @@ export default function SearchPage() {
|
||||
sort: sort !== "date_desc" ? sort : undefined,
|
||||
has_attachment: hasAttachment,
|
||||
page: p,
|
||||
page_size: PAGE_SIZE,
|
||||
page_size: pageSize,
|
||||
});
|
||||
setResults(res.hits || []);
|
||||
setTotal(res.total);
|
||||
@@ -156,7 +158,7 @@ export default function SearchPage() {
|
||||
setSearching(false);
|
||||
}
|
||||
},
|
||||
[query, fromFilter, toFilter, dateFrom, dateTo, sort, hasAttachment]
|
||||
[query, fromFilter, toFilter, dateFrom, dateTo, sort, hasAttachment, pageSize]
|
||||
);
|
||||
|
||||
// Superadmin has no mail access — redirect to admin dashboard
|
||||
@@ -170,7 +172,7 @@ export default function SearchPage() {
|
||||
useEffect(() => {
|
||||
if (!user || user.role === "superadmin") return;
|
||||
setSearching(true);
|
||||
searchEmails({ page: 1, page_size: PAGE_SIZE })
|
||||
searchEmails({ page: 1, page_size: pageSize })
|
||||
.then((res) => {
|
||||
setResults(res.hits || []);
|
||||
setTotal(res.total);
|
||||
@@ -182,7 +184,7 @@ export default function SearchPage() {
|
||||
setTotal(0);
|
||||
})
|
||||
.finally(() => setSearching(false));
|
||||
}, [user]);
|
||||
}, [user, pageSize]);
|
||||
|
||||
function handleSubmit(e: React.FormEvent) {
|
||||
e.preventDefault();
|
||||
@@ -339,7 +341,7 @@ export default function SearchPage() {
|
||||
date_to: s.query.date_to || undefined,
|
||||
has_attachment: s.query.has_attachment === "true" ? true : undefined,
|
||||
page: 1,
|
||||
page_size: PAGE_SIZE,
|
||||
page_size: pageSize,
|
||||
})
|
||||
.then((res) => {
|
||||
setResults(res.hits || []);
|
||||
@@ -364,7 +366,7 @@ export default function SearchPage() {
|
||||
}
|
||||
}
|
||||
|
||||
const totalPages = Math.ceil(total / PAGE_SIZE);
|
||||
const totalPages = Math.ceil(total / pageSize);
|
||||
const allSelected = results.length > 0 && results.every((h) => selected.has(h.id));
|
||||
|
||||
return (
|
||||
|
||||
@@ -16,10 +16,18 @@ import {
|
||||
DialogFooter,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Server, Lock, Info } from "lucide-react";
|
||||
import {
|
||||
changePassword,
|
||||
changeEmail,
|
||||
updatePreferences,
|
||||
getTOTPSetup,
|
||||
confirmTOTPSetup,
|
||||
disableTOTP,
|
||||
@@ -28,7 +36,7 @@ import {
|
||||
} from "@/lib/api";
|
||||
|
||||
export default function SettingsPage() {
|
||||
const { user, loading } = useAuth();
|
||||
const { user, loading, refresh } = useAuth();
|
||||
|
||||
// ── Password state ────────────────────────────────────────────────────
|
||||
const [currentPw, setCurrentPw] = useState("");
|
||||
@@ -59,6 +67,13 @@ export default function SettingsPage() {
|
||||
const [disableError, setDisableError] = useState("");
|
||||
const [disableLoading, setDisableLoading] = useState(false);
|
||||
|
||||
// ── Listenanzahl (Eintraege pro Seite) ────────────────────────────────
|
||||
const [listPageSize, setListPageSize] = useState("25");
|
||||
const [listPageSizeInitialized, setListPageSizeInitialized] = useState(false);
|
||||
const [listPageSizeError, setListPageSizeError] = useState("");
|
||||
const [listPageSizeSuccess, setListPageSizeSuccess] = useState("");
|
||||
const [listPageSizeLoading, setListPageSizeLoading] = useState(false);
|
||||
|
||||
// ── System info (FQDN + IMAP-Ports) ───────────────────────────────────
|
||||
const [systemInfo, setSystemInfo] = useState<SystemInfo | null>(null);
|
||||
const [systemInfoLoading, setSystemInfoLoading] = useState(true);
|
||||
@@ -87,6 +102,12 @@ export default function SettingsPage() {
|
||||
setEmailInitialized(true);
|
||||
}
|
||||
|
||||
// Initialize list page size from user data once available
|
||||
if (user && !listPageSizeInitialized) {
|
||||
setListPageSize(String(user.list_page_size || 25));
|
||||
setListPageSizeInitialized(true);
|
||||
}
|
||||
|
||||
// ── Handlers ──────────────────────────────────────────────────────────
|
||||
|
||||
async function handleChangePassword(e: React.FormEvent) {
|
||||
@@ -139,6 +160,24 @@ export default function SettingsPage() {
|
||||
}
|
||||
}
|
||||
|
||||
async function handleChangeListPageSize(value: string) {
|
||||
setListPageSizeError("");
|
||||
setListPageSizeSuccess("");
|
||||
const parsed = Number(value);
|
||||
|
||||
setListPageSizeLoading(true);
|
||||
try {
|
||||
const result = await updatePreferences(parsed);
|
||||
setListPageSize(String(result.list_page_size));
|
||||
setListPageSizeSuccess("Eintraege pro Seite wurden gespeichert.");
|
||||
await refresh();
|
||||
} catch (err) {
|
||||
setListPageSizeError(err instanceof Error ? err.message : "Fehler beim Speichern");
|
||||
} finally {
|
||||
setListPageSizeLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSetupTOTP() {
|
||||
setTotpError("");
|
||||
setTotpSuccess("");
|
||||
@@ -312,6 +351,43 @@ export default function SettingsPage() {
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* ── Card 2b: Listenanzahl ─────────────────────────────────────── */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Listenansicht</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="list-page-size">Eintraege pro Seite</Label>
|
||||
<Select
|
||||
value={listPageSize}
|
||||
onValueChange={handleChangeListPageSize}
|
||||
disabled={listPageSizeLoading}
|
||||
>
|
||||
<SelectTrigger id="list-page-size" className="w-40">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="25">25</SelectItem>
|
||||
<SelectItem value="50">50</SelectItem>
|
||||
<SelectItem value="100">100</SelectItem>
|
||||
<SelectItem value="200">200</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
{listPageSizeError && (
|
||||
<Alert variant="destructive">
|
||||
<AlertDescription>{listPageSizeError}</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
{listPageSizeSuccess && (
|
||||
<Alert>
|
||||
<AlertDescription>{listPageSizeSuccess}</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* ── Card 3: Zwei-Faktor-Authentifizierung ────────────────────── */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
|
||||
+11
-1
@@ -58,5 +58,15 @@ export function useAuth(requireRole?: "admin" | "domain_admin" | "superadmin" |
|
||||
checkAuth();
|
||||
}, [checkAuth]);
|
||||
|
||||
return { user, loading };
|
||||
const refresh = useCallback(async () => {
|
||||
try {
|
||||
const me = await getMe();
|
||||
setCachedUser(me);
|
||||
setUser(me);
|
||||
} catch {
|
||||
// ignore refresh errors, keep existing cached user
|
||||
}
|
||||
}, []);
|
||||
|
||||
return { user, loading, refresh };
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ export {
|
||||
logout,
|
||||
changePassword,
|
||||
changeEmail,
|
||||
updatePreferences,
|
||||
getUsers,
|
||||
createUser,
|
||||
updateUser,
|
||||
|
||||
@@ -26,6 +26,7 @@ export interface MeResponse {
|
||||
username: string;
|
||||
role: string;
|
||||
email: string;
|
||||
list_page_size: number;
|
||||
}
|
||||
|
||||
export interface CreateUserRequest {
|
||||
@@ -82,6 +83,15 @@ export async function changeEmail(
|
||||
});
|
||||
}
|
||||
|
||||
export async function updatePreferences(
|
||||
listPageSize: number
|
||||
): Promise<{ ok: boolean; list_page_size: number }> {
|
||||
return request<{ ok: boolean; list_page_size: number }>("/api/auth/preferences", {
|
||||
method: "PATCH",
|
||||
body: JSON.stringify({ list_page_size: listPageSize }),
|
||||
});
|
||||
}
|
||||
|
||||
// ── User management functions ─────────────────────────────────────────────────
|
||||
|
||||
export async function getUsers(): Promise<User[]> {
|
||||
|
||||
Reference in New Issue
Block a user