security: Zufallspasswörter beim Erststart, kryptographisch sichere JTI-Generierung
- seedDefaultUsers: generiert kryptographisch zufällige Passwörter (crypto/rand) statt hartkodiertes "archivmailrockz" — Passwörter werden einmalig im Terminal angezeigt und können danach nicht wiederhergestellt werden - generateJTI: verwendet crypto/rand (16 Byte, hex) statt time.UnixNano XOR deadbeef Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+64
-10
@@ -17,6 +17,13 @@ import {
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
@@ -31,6 +38,12 @@ import { Switch } from "@/components/ui/switch";
|
||||
|
||||
const PAGE_SIZE = 25;
|
||||
|
||||
function formatBytes(bytes: number): string {
|
||||
if (bytes < 1024) return `${bytes} B`;
|
||||
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(0)} KB`;
|
||||
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
||||
}
|
||||
|
||||
export default function SearchPage() {
|
||||
const { user, loading: authLoading } = useAuth();
|
||||
const router = useRouter();
|
||||
@@ -40,6 +53,8 @@ export default function SearchPage() {
|
||||
const [toFilter, setToFilter] = useState("");
|
||||
const [dateFrom, setDateFrom] = useState("");
|
||||
const [dateTo, setDateTo] = useState("");
|
||||
const [sort, setSort] = useState("date_desc");
|
||||
const [hasAttachment, setHasAttachment] = useState<boolean | undefined>(undefined);
|
||||
|
||||
const [results, setResults] = useState<SearchHit[]>([]);
|
||||
const [total, setTotal] = useState(0);
|
||||
@@ -68,6 +83,8 @@ export default function SearchPage() {
|
||||
to: toFilter || undefined,
|
||||
date_from: dateFrom || undefined,
|
||||
date_to: dateTo || undefined,
|
||||
sort: sort !== "date_desc" ? sort : undefined,
|
||||
has_attachment: hasAttachment,
|
||||
page: p,
|
||||
page_size: PAGE_SIZE,
|
||||
});
|
||||
@@ -82,7 +99,7 @@ export default function SearchPage() {
|
||||
setSearching(false);
|
||||
}
|
||||
},
|
||||
[query, fromFilter, toFilter, dateFrom, dateTo]
|
||||
[query, fromFilter, toFilter, dateFrom, dateTo, sort, hasAttachment]
|
||||
);
|
||||
|
||||
// Alle Mails beim Öffnen der Seite laden — direkt, ohne useCallback-Closure
|
||||
@@ -130,18 +147,18 @@ export default function SearchPage() {
|
||||
const totalPages = Math.ceil(total / PAGE_SIZE);
|
||||
const allSelected = results.length > 0 && results.every((h) => selected.has(h.id));
|
||||
|
||||
if (authLoading || !user) {
|
||||
return (
|
||||
<div className="flex min-h-screen items-center justify-center">
|
||||
<Skeleton className="h-8 w-48" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen">
|
||||
<Navbar username={user.username} role={user.role} />
|
||||
<Navbar username={user?.username ?? ""} role={user?.role ?? ""} />
|
||||
<main className="mx-auto max-w-7xl px-4 py-6">
|
||||
{(authLoading || !user) && (
|
||||
<div className="space-y-4">
|
||||
<Skeleton className="h-10 w-full" />
|
||||
<Skeleton className="h-4 w-2/3" />
|
||||
<Skeleton className="h-64 w-full" />
|
||||
</div>
|
||||
)}
|
||||
{!authLoading && user && (<>
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
@@ -206,6 +223,34 @@ export default function SearchPage() {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap items-center gap-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<Label htmlFor="sort-select" className="text-xs whitespace-nowrap">Sortierung</Label>
|
||||
<Select value={sort} onValueChange={setSort}>
|
||||
<SelectTrigger id="sort-select" className="h-8 w-40 text-xs">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="date_desc">Datum (neu → alt)</SelectItem>
|
||||
<SelectItem value="date_asc">Datum (alt → neu)</SelectItem>
|
||||
<SelectItem value="relevance">Relevanz</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Switch
|
||||
id="attach-toggle"
|
||||
checked={hasAttachment === true}
|
||||
onCheckedChange={(checked) =>
|
||||
setHasAttachment(checked ? true : undefined)
|
||||
}
|
||||
/>
|
||||
<Label htmlFor="attach-toggle" className="text-xs cursor-pointer">
|
||||
Nur mit Anhang
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div className="mt-6">
|
||||
@@ -257,6 +302,8 @@ export default function SearchPage() {
|
||||
<TableHead className="w-56">Von</TableHead>
|
||||
<TableHead>Betreff</TableHead>
|
||||
<TableHead className="w-48">An</TableHead>
|
||||
<TableHead className="w-8 text-center" title="Anhang">📎</TableHead>
|
||||
<TableHead className="w-20 text-right">Größe</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
@@ -294,6 +341,12 @@ export default function SearchPage() {
|
||||
<TableCell className="max-w-[14rem] truncate text-sm">{hit.from || "-"}</TableCell>
|
||||
<TableCell className="font-medium">{hit.subject || "(kein Betreff)"}</TableCell>
|
||||
<TableCell className="max-w-[12rem] truncate text-sm text-muted-foreground">{hit.to || "-"}</TableCell>
|
||||
<TableCell className="text-center text-sm">
|
||||
{hit.has_attachments ? "📎" : ""}
|
||||
</TableCell>
|
||||
<TableCell className="text-right text-xs text-muted-foreground whitespace-nowrap">
|
||||
{hit.size ? formatBytes(hit.size) : ""}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
@@ -353,6 +406,7 @@ export default function SearchPage() {
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>)}
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user