3.14 Password Vault encryption AES-256-GCM server-side

- Created src/core/crypto/ with AES-256-GCM encrypt/decrypt (PBKDF2 key derivation)
- Created /api/vault route: CRUD with server-side password encryption
- PATCH /api/vault migration endpoint to re-encrypt legacy plaintext passwords
- Rewrote use-vault hook to use dedicated /api/vault instead of generic storage
- Updated UI: amber 'not encrypted' warning  green 'encrypted' badge
- Added ENCRYPTION_SECRET env var to docker-compose.yml and stack.env
- Module version bumped to 0.2.0
This commit is contained in:
AI Assistant
2026-02-28 04:12:44 +02:00
parent f0b3659247
commit 85bdb59da4
10 changed files with 366 additions and 59 deletions
@@ -122,8 +122,9 @@ export function WordTemplatesModule() {
null,
);
const [deletingId, setDeletingId] = useState<string | null>(null);
const [revisingTemplate, setRevisingTemplate] =
useState<WordTemplate | null>(null);
const [revisingTemplate, setRevisingTemplate] = useState<WordTemplate | null>(
null,
);
const [revisionUrl, setRevisionUrl] = useState("");
const [revisionNotes, setRevisionNotes] = useState("");
const [historyTemplate, setHistoryTemplate] = useState<WordTemplate | null>(
@@ -172,7 +173,12 @@ export function WordTemplatesModule() {
<CardContent className="p-4">
<p className="text-xs text-muted-foreground">Word/Excel</p>
<p className="text-2xl font-bold">
{allTemplates.filter((t) => (t.fileType ?? "docx") === "docx" || t.fileType === "xlsx").length}
{
allTemplates.filter(
(t) =>
(t.fileType ?? "docx") === "docx" || t.fileType === "xlsx",
).length
}
</p>
</CardContent>
</Card>
@@ -180,7 +186,11 @@ export function WordTemplatesModule() {
<CardContent className="p-4">
<p className="text-xs text-muted-foreground">DWG/Archicad</p>
<p className="text-2xl font-bold">
{allTemplates.filter((t) => t.fileType === "dwg" || t.fileType === "archicad").length}
{
allTemplates.filter(
(t) => t.fileType === "dwg" || t.fileType === "archicad",
).length
}
</p>
</CardContent>
</Card>
@@ -188,7 +198,10 @@ export function WordTemplatesModule() {
<CardContent className="p-4">
<p className="text-xs text-muted-foreground">Cu versiuni</p>
<p className="text-2xl font-bold">
{allTemplates.filter((t) => (t.versionHistory ?? []).length > 0).length}
{
allTemplates.filter((t) => (t.versionHistory ?? []).length > 0)
.length
}
</p>
</CardContent>
</Card>
@@ -431,9 +444,7 @@ export function WordTemplatesModule() {
>
<DialogContent>
<DialogHeader>
<DialogTitle>
Revizie nouă {revisingTemplate?.name}
</DialogTitle>
<DialogTitle>Revizie nouă {revisingTemplate?.name}</DialogTitle>
</DialogHeader>
<div className="space-y-3">
<p className="text-sm text-muted-foreground">
@@ -546,10 +557,7 @@ export function WordTemplatesModule() {
)}
</div>
<DialogFooter>
<Button
variant="outline"
onClick={() => setHistoryTemplate(null)}
>
<Button variant="outline" onClick={() => setHistoryTemplate(null)}>
Închide
</Button>
</DialogFooter>
@@ -726,13 +734,13 @@ function TemplateForm({
<SelectValue />
</SelectTrigger>
<SelectContent>
{(
Object.keys(FILE_TYPE_LABELS) as TemplateFileType[]
).map((ft) => (
<SelectItem key={ft} value={ft}>
{FILE_TYPE_LABELS[ft]}
</SelectItem>
))}
{(Object.keys(FILE_TYPE_LABELS) as TemplateFileType[]).map(
(ft) => (
<SelectItem key={ft} value={ft}>
{FILE_TYPE_LABELS[ft]}
</SelectItem>
),
)}
</SelectContent>
</Select>
</div>
@@ -112,7 +112,8 @@ export function useTemplates() {
updatedAt: now,
};
if (notes) {
updated.versionHistory[updated.versionHistory.length - 1]!.notes = notes;
updated.versionHistory[updated.versionHistory.length - 1]!.notes =
notes;
}
await storage.set(`${PREFIX}${id}`, updated);
await refresh();