Initial commit: ArchiTools modular dashboard platform

Complete Next.js 16 application with 13 fully implemented modules:
Email Signature, Word XML Generator, Registratura, Dashboard,
Tag Manager, IT Inventory, Address Book, Password Vault,
Mini Utilities, Prompt Generator, Digital Signatures,
Word Templates, and AI Chat.

Includes core platform systems (module registry, feature flags,
storage abstraction, i18n, theming, auth stub, tagging),
16 technical documentation files, Docker deployment config,
and legacy HTML tool reference.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Marius Tarau
2026-02-17 12:50:25 +02:00
commit 4c46e8bcdd
189 changed files with 33780 additions and 0 deletions

View File

@@ -0,0 +1,185 @@
'use client';
import { useState } from 'react';
import { Plus, Pencil, Trash2, Search, Eye, EyeOff, Copy, ExternalLink } from 'lucide-react';
import { Button } from '@/shared/components/ui/button';
import { Input } from '@/shared/components/ui/input';
import { Label } from '@/shared/components/ui/label';
import { Textarea } from '@/shared/components/ui/textarea';
import { Badge } from '@/shared/components/ui/badge';
import { Card, CardContent, CardHeader, CardTitle } from '@/shared/components/ui/card';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/shared/components/ui/select';
import type { VaultEntry, VaultEntryCategory } from '../types';
import { useVault } from '../hooks/use-vault';
const CATEGORY_LABELS: Record<VaultEntryCategory, string> = {
web: 'Web', email: 'Email', server: 'Server', database: 'Bază de date', api: 'API', other: 'Altele',
};
type ViewMode = 'list' | 'add' | 'edit';
export function PasswordVaultModule() {
const { entries, allEntries, loading, filters, updateFilter, addEntry, updateEntry, removeEntry } = useVault();
const [viewMode, setViewMode] = useState<ViewMode>('list');
const [editingEntry, setEditingEntry] = useState<VaultEntry | null>(null);
const [visiblePasswords, setVisiblePasswords] = useState<Set<string>>(new Set());
const [copiedId, setCopiedId] = useState<string | null>(null);
const togglePassword = (id: string) => {
setVisiblePasswords((prev) => {
const next = new Set(prev);
if (next.has(id)) next.delete(id); else next.add(id);
return next;
});
};
const handleCopy = async (text: string, id: string) => {
try {
await navigator.clipboard.writeText(text);
setCopiedId(id);
setTimeout(() => setCopiedId(null), 2000);
} catch { /* silent */ }
};
const handleSubmit = async (data: Omit<VaultEntry, 'id' | 'createdAt' | 'updatedAt'>) => {
if (viewMode === 'edit' && editingEntry) {
await updateEntry(editingEntry.id, data);
} else {
await addEntry(data);
}
setViewMode('list');
setEditingEntry(null);
};
return (
<div className="space-y-6">
<div className="rounded-md border border-amber-500/30 bg-amber-500/5 px-4 py-2 text-xs text-amber-700 dark:text-amber-400">
Atenție: Parolele sunt stocate local (localStorage). Nu sunt criptate. Folosiți un manager de parole dedicat pentru date sensibile.
</div>
{/* Stats */}
<div className="grid grid-cols-2 gap-3 sm:grid-cols-4">
<Card><CardContent className="p-4"><p className="text-xs text-muted-foreground">Total</p><p className="text-2xl font-bold">{allEntries.length}</p></CardContent></Card>
<Card><CardContent className="p-4"><p className="text-xs text-muted-foreground">Web</p><p className="text-2xl font-bold">{allEntries.filter((e) => e.category === 'web').length}</p></CardContent></Card>
<Card><CardContent className="p-4"><p className="text-xs text-muted-foreground">Server</p><p className="text-2xl font-bold">{allEntries.filter((e) => e.category === 'server').length}</p></CardContent></Card>
<Card><CardContent className="p-4"><p className="text-xs text-muted-foreground">API</p><p className="text-2xl font-bold">{allEntries.filter((e) => e.category === 'api').length}</p></CardContent></Card>
</div>
{viewMode === 'list' && (
<>
<div className="flex flex-wrap items-center gap-3">
<div className="relative min-w-[200px] flex-1">
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
<Input placeholder="Caută..." value={filters.search} onChange={(e) => updateFilter('search', e.target.value)} className="pl-9" />
</div>
<Select value={filters.category} onValueChange={(v) => updateFilter('category', v as VaultEntryCategory | 'all')}>
<SelectTrigger className="w-[150px]"><SelectValue /></SelectTrigger>
<SelectContent>
<SelectItem value="all">Toate</SelectItem>
{(Object.keys(CATEGORY_LABELS) as VaultEntryCategory[]).map((c) => (
<SelectItem key={c} value={c}>{CATEGORY_LABELS[c]}</SelectItem>
))}
</SelectContent>
</Select>
<Button onClick={() => setViewMode('add')} className="shrink-0">
<Plus className="mr-1.5 h-4 w-4" /> Adaugă
</Button>
</div>
{loading ? (
<p className="py-8 text-center text-sm text-muted-foreground">Se încarcă...</p>
) : entries.length === 0 ? (
<p className="py-8 text-center text-sm text-muted-foreground">Nicio intrare găsită.</p>
) : (
<div className="space-y-2">
{entries.map((entry) => (
<Card key={entry.id} className="group">
<CardContent className="flex items-center gap-4 p-4">
<div className="min-w-0 flex-1 space-y-1">
<div className="flex items-center gap-2">
<p className="font-medium">{entry.label}</p>
<Badge variant="outline" className="text-[10px]">{CATEGORY_LABELS[entry.category]}</Badge>
</div>
<p className="text-xs text-muted-foreground">{entry.username}</p>
<div className="flex items-center gap-2">
<code className="text-xs">
{visiblePasswords.has(entry.id) ? entry.encryptedPassword : '••••••••••'}
</code>
<Button variant="ghost" size="icon" className="h-5 w-5" onClick={() => togglePassword(entry.id)}>
{visiblePasswords.has(entry.id) ? <EyeOff className="h-3 w-3" /> : <Eye className="h-3 w-3" />}
</Button>
<Button variant="ghost" size="icon" className="h-5 w-5" onClick={() => handleCopy(entry.encryptedPassword, entry.id)}>
<Copy className="h-3 w-3" />
</Button>
{copiedId === entry.id && <span className="text-[10px] text-green-500">Copiat!</span>}
</div>
{entry.url && (
<p className="flex items-center gap-1 text-xs text-muted-foreground">
<ExternalLink className="h-3 w-3" /> {entry.url}
</p>
)}
</div>
<div className="flex gap-1 opacity-0 transition-opacity group-hover:opacity-100">
<Button variant="ghost" size="icon" className="h-7 w-7" onClick={() => { setEditingEntry(entry); setViewMode('edit'); }}>
<Pencil className="h-3.5 w-3.5" />
</Button>
<Button variant="ghost" size="icon" className="h-7 w-7 text-destructive" onClick={() => removeEntry(entry.id)}>
<Trash2 className="h-3.5 w-3.5" />
</Button>
</div>
</CardContent>
</Card>
))}
</div>
)}
</>
)}
{(viewMode === 'add' || viewMode === 'edit') && (
<Card>
<CardHeader><CardTitle>{viewMode === 'edit' ? 'Editare' : 'Intrare nouă'}</CardTitle></CardHeader>
<CardContent>
<VaultForm initial={editingEntry ?? undefined} onSubmit={handleSubmit} onCancel={() => { setViewMode('list'); setEditingEntry(null); }} />
</CardContent>
</Card>
)}
</div>
);
}
function VaultForm({ initial, onSubmit, onCancel }: {
initial?: VaultEntry;
onSubmit: (data: Omit<VaultEntry, 'id' | 'createdAt' | 'updatedAt'>) => void;
onCancel: () => void;
}) {
const [label, setLabel] = useState(initial?.label ?? '');
const [username, setUsername] = useState(initial?.username ?? '');
const [password, setPassword] = useState(initial?.encryptedPassword ?? '');
const [url, setUrl] = useState(initial?.url ?? '');
const [category, setCategory] = useState<VaultEntryCategory>(initial?.category ?? 'web');
const [notes, setNotes] = useState(initial?.notes ?? '');
return (
<form onSubmit={(e) => { e.preventDefault(); onSubmit({ label, username, encryptedPassword: password, url, category, notes, tags: initial?.tags ?? [], visibility: initial?.visibility ?? 'admin' }); }} className="space-y-4">
<div className="grid gap-4 sm:grid-cols-2">
<div><Label>Nume/Etichetă</Label><Input value={label} onChange={(e) => setLabel(e.target.value)} className="mt-1" required /></div>
<div><Label>Categorie</Label>
<Select value={category} onValueChange={(v) => setCategory(v as VaultEntryCategory)}>
<SelectTrigger className="mt-1"><SelectValue /></SelectTrigger>
<SelectContent>{(Object.keys(CATEGORY_LABELS) as VaultEntryCategory[]).map((c) => (<SelectItem key={c} value={c}>{CATEGORY_LABELS[c]}</SelectItem>))}</SelectContent>
</Select>
</div>
</div>
<div className="grid gap-4 sm:grid-cols-2">
<div><Label>Utilizator</Label><Input value={username} onChange={(e) => setUsername(e.target.value)} className="mt-1" /></div>
<div><Label>Parolă</Label><Input type="password" value={password} onChange={(e) => setPassword(e.target.value)} className="mt-1" /></div>
</div>
<div><Label>URL</Label><Input value={url} onChange={(e) => setUrl(e.target.value)} className="mt-1" placeholder="https://..." /></div>
<div><Label>Note</Label><Textarea value={notes} onChange={(e) => setNotes(e.target.value)} rows={2} className="mt-1" /></div>
<div className="flex justify-end gap-2 pt-2">
<Button type="button" variant="outline" onClick={onCancel}>Anulează</Button>
<Button type="submit">{initial ? 'Actualizează' : 'Adaugă'}</Button>
</div>
</form>
);
}

View File

@@ -0,0 +1,17 @@
import type { ModuleConfig } from '@/core/module-registry/types';
export const passwordVaultConfig: ModuleConfig = {
id: 'password-vault',
name: 'Seif Parole',
description: 'Manager securizat de parole și credențiale cu criptare locală',
icon: 'lock',
route: '/password-vault',
category: 'operations',
featureFlag: 'module.password-vault',
visibility: 'admin',
version: '0.1.0',
dependencies: [],
storageNamespace: 'password-vault',
navOrder: 11,
tags: ['parole', 'securitate', 'credențiale'],
};

View File

@@ -0,0 +1,74 @@
'use client';
import { useState, useEffect, useCallback } from 'react';
import { useStorage } from '@/core/storage';
import { v4 as uuid } from 'uuid';
import type { VaultEntry, VaultEntryCategory } from '../types';
const PREFIX = 'vault:';
export interface VaultFilters {
search: string;
category: VaultEntryCategory | 'all';
}
export function useVault() {
const storage = useStorage('password-vault');
const [entries, setEntries] = useState<VaultEntry[]>([]);
const [loading, setLoading] = useState(true);
const [filters, setFilters] = useState<VaultFilters>({ search: '', category: 'all' });
const refresh = useCallback(async () => {
setLoading(true);
const keys = await storage.list();
const results: VaultEntry[] = [];
for (const key of keys) {
if (key.startsWith(PREFIX)) {
const item = await storage.get<VaultEntry>(key);
if (item) results.push(item);
}
}
results.sort((a, b) => a.label.localeCompare(b.label));
setEntries(results);
setLoading(false);
}, [storage]);
// eslint-disable-next-line react-hooks/set-state-in-effect
useEffect(() => { refresh(); }, [refresh]);
const addEntry = useCallback(async (data: Omit<VaultEntry, 'id' | 'createdAt' | 'updatedAt'>) => {
const now = new Date().toISOString();
const entry: VaultEntry = { ...data, id: uuid(), createdAt: now, updatedAt: now };
await storage.set(`${PREFIX}${entry.id}`, entry);
await refresh();
return entry;
}, [storage, refresh]);
const updateEntry = useCallback(async (id: string, updates: Partial<VaultEntry>) => {
const existing = entries.find((e) => e.id === id);
if (!existing) return;
const updated = { ...existing, ...updates, id: existing.id, createdAt: existing.createdAt, updatedAt: new Date().toISOString() };
await storage.set(`${PREFIX}${id}`, updated);
await refresh();
}, [storage, refresh, entries]);
const removeEntry = useCallback(async (id: string) => {
await storage.delete(`${PREFIX}${id}`);
await refresh();
}, [storage, refresh]);
const updateFilter = useCallback(<K extends keyof VaultFilters>(key: K, value: VaultFilters[K]) => {
setFilters((prev) => ({ ...prev, [key]: value }));
}, []);
const filteredEntries = entries.filter((e) => {
if (filters.category !== 'all' && e.category !== filters.category) return false;
if (filters.search) {
const q = filters.search.toLowerCase();
return e.label.toLowerCase().includes(q) || e.username.toLowerCase().includes(q) || e.url.toLowerCase().includes(q);
}
return true;
});
return { entries: filteredEntries, allEntries: entries, loading, filters, updateFilter, addEntry, updateEntry, removeEntry, refresh };
}

View File

@@ -0,0 +1,3 @@
export { passwordVaultConfig } from './config';
export { PasswordVaultModule } from './components/password-vault-module';
export type { VaultEntry, VaultEntryCategory } from './types';

View File

@@ -0,0 +1,23 @@
import type { Visibility } from '@/core/module-registry/types';
export type VaultEntryCategory =
| 'web'
| 'email'
| 'server'
| 'database'
| 'api'
| 'other';
export interface VaultEntry {
id: string;
label: string;
username: string;
encryptedPassword: string;
url: string;
category: VaultEntryCategory;
notes: string;
tags: string[];
visibility: Visibility;
createdAt: string;
updatedAt: string;
}