diff --git a/src/modules/password-vault/components/password-vault-module.tsx b/src/modules/password-vault/components/password-vault-module.tsx index ac9fd2f..e9eaf75 100644 --- a/src/modules/password-vault/components/password-vault-module.tsx +++ b/src/modules/password-vault/components/password-vault-module.tsx @@ -1,61 +1,109 @@ -'use client'; +"use client"; -import { useState } from 'react'; +import { useState } from "react"; import { - Plus, Pencil, Trash2, Search, Eye, EyeOff, Copy, ExternalLink, - KeyRound, X, -} 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 { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@/shared/components/ui/dialog'; -import { Switch } from '@/shared/components/ui/switch'; -import type { CompanyId } from '@/core/auth/types'; -import type { VaultEntry, VaultEntryCategory, CustomField } from '../types'; -import { useVault } from '../hooks/use-vault'; + Plus, + Pencil, + Trash2, + Search, + Eye, + EyeOff, + Copy, + ExternalLink, + KeyRound, + X, +} 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 { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogFooter, +} from "@/shared/components/ui/dialog"; +import { Switch } from "@/shared/components/ui/switch"; +import type { CompanyId } from "@/core/auth/types"; +import type { VaultEntry, VaultEntryCategory, CustomField } from "../types"; +import { useVault } from "../hooks/use-vault"; const CATEGORY_LABELS: Record = { - web: 'Web', email: 'Email', server: 'Server', database: 'Bază de date', api: 'API', other: 'Altele', + web: "Web", + email: "Email", + server: "Server", + database: "Bază de date", + api: "API", + other: "Altele", }; const COMPANY_LABELS: Record = { - 'beletage': 'Beletage', - 'urban-switch': 'Urban Switch', - 'studii-de-teren': 'Studii de Teren', - 'group': 'Grup', + beletage: "Beletage", + "urban-switch": "Urban Switch", + "studii-de-teren": "Studii de Teren", + group: "Grup", }; /** Calculate password strength: 0-3 (weak, medium, strong, very strong) */ -function getPasswordStrength(pwd: string): { level: 0 | 1 | 2 | 3; label: string; color: string } { - if (!pwd) return { level: 0, label: 'Nicio parolă', color: 'bg-gray-300' }; +function getPasswordStrength(pwd: string): { + level: 0 | 1 | 2 | 3; + label: string; + color: string; +} { + if (!pwd) return { level: 0, label: "Nicio parolă", color: "bg-gray-300" }; const len = pwd.length; const hasUpper = /[A-Z]/.test(pwd); const hasLower = /[a-z]/.test(pwd); const hasDigit = /\d/.test(pwd); const hasSymbol = /[!@#$%^&*()\-_=+\[\]{}|;:,.<>?]/.test(pwd); - const varietyScore = (hasUpper ? 1 : 0) + (hasLower ? 1 : 0) + (hasDigit ? 1 : 0) + (hasSymbol ? 1 : 0); + const varietyScore = + (hasUpper ? 1 : 0) + + (hasLower ? 1 : 0) + + (hasDigit ? 1 : 0) + + (hasSymbol ? 1 : 0); const score = len + varietyScore * 2; - if (score < 8) return { level: 0, label: 'Slabă', color: 'bg-red-500' }; - if (score < 16) return { level: 1, label: 'Medie', color: 'bg-yellow-500' }; - if (score < 24) return { level: 2, label: 'Puternică', color: 'bg-green-500' }; - return { level: 3, label: 'Foarte puternică', color: 'bg-emerald-600' }; + if (score < 8) return { level: 0, label: "Slabă", color: "bg-red-500" }; + if (score < 16) return { level: 1, label: "Medie", color: "bg-yellow-500" }; + if (score < 24) + return { level: 2, label: "Puternică", color: "bg-green-500" }; + return { level: 3, label: "Foarte puternică", color: "bg-emerald-600" }; } -type ViewMode = 'list' | 'add' | 'edit'; +type ViewMode = "list" | "add" | "edit"; /** Generate a random password */ -function generatePassword(length: number, options: { upper: boolean; lower: boolean; digits: boolean; symbols: boolean }): string { - let chars = ''; - if (options.upper) chars += 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; - if (options.lower) chars += 'abcdefghijklmnopqrstuvwxyz'; - if (options.digits) chars += '0123456789'; - if (options.symbols) chars += '!@#$%^&*()-_=+[]{}|;:,.<>?'; - if (!chars) chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; - let result = ''; +function generatePassword( + length: number, + options: { + upper: boolean; + lower: boolean; + digits: boolean; + symbols: boolean; + }, +): string { + let chars = ""; + if (options.upper) chars += "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; + if (options.lower) chars += "abcdefghijklmnopqrstuvwxyz"; + if (options.digits) chars += "0123456789"; + if (options.symbols) chars += "!@#$%^&*()-_=+[]{}|;:,.<>?"; + if (!chars) + chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; + let result = ""; for (let i = 0; i < length; i++) { result += chars.charAt(Math.floor(Math.random() * chars.length)); } @@ -63,17 +111,29 @@ function generatePassword(length: number, options: { upper: boolean; lower: bool } export function PasswordVaultModule() { - const { entries, allEntries, loading, filters, updateFilter, addEntry, updateEntry, removeEntry } = useVault(); - const [viewMode, setViewMode] = useState('list'); + const { + entries, + allEntries, + loading, + filters, + updateFilter, + addEntry, + updateEntry, + removeEntry, + } = useVault(); + const [viewMode, setViewMode] = useState("list"); const [editingEntry, setEditingEntry] = useState(null); - const [visiblePasswords, setVisiblePasswords] = useState>(new Set()); + const [visiblePasswords, setVisiblePasswords] = useState>( + new Set(), + ); const [copiedId, setCopiedId] = useState(null); const [deletingId, setDeletingId] = useState(null); const togglePassword = (id: string) => { setVisiblePasswords((prev) => { const next = new Set(prev); - if (next.has(id)) next.delete(id); else next.add(id); + if (next.has(id)) next.delete(id); + else next.add(id); return next; }); }; @@ -83,16 +143,20 @@ export function PasswordVaultModule() { await navigator.clipboard.writeText(text); setCopiedId(id); setTimeout(() => setCopiedId(null), 2000); - } catch { /* silent */ } + } catch { + /* silent */ + } }; - const handleSubmit = async (data: Omit) => { - if (viewMode === 'edit' && editingEntry) { + const handleSubmit = async ( + data: Omit, + ) => { + if (viewMode === "edit" && editingEntry) { await updateEntry(editingEntry.id, data); } else { await addEntry(data); } - setViewMode('list'); + setViewMode("list"); setEditingEntry(null); }; @@ -106,42 +170,89 @@ export function PasswordVaultModule() { return (
- Atenție: Parolele sunt stocate local (localStorage). Nu sunt criptate. Folosiți un manager de parole dedicat pentru date sensibile. + Atenție: Parolele sunt stocate local (localStorage). Nu sunt criptate. + Folosiți un manager de parole dedicat pentru date sensibile.
{/* Stats */}
-

Total

{allEntries.length}

-

Web

{allEntries.filter((e) => e.category === 'web').length}

-

Server

{allEntries.filter((e) => e.category === 'server').length}

-

API

{allEntries.filter((e) => e.category === 'api').length}

+ + +

Total

+

{allEntries.length}

+
+
+ + +

Web

+

+ {allEntries.filter((e) => e.category === "web").length} +

+
+
+ + +

Server

+

+ {allEntries.filter((e) => e.category === "server").length} +

+
+
+ + +

API

+

+ {allEntries.filter((e) => e.category === "api").length} +

+
+
- {viewMode === 'list' && ( + {viewMode === "list" && ( <>
- updateFilter('search', e.target.value)} className="pl-9" /> + updateFilter("search", e.target.value)} + className="pl-9" + />
- + updateFilter("category", v as VaultEntryCategory | "all") + } + > + + + Toate - {(Object.keys(CATEGORY_LABELS) as VaultEntryCategory[]).map((c) => ( - {CATEGORY_LABELS[c]} - ))} + {(Object.keys(CATEGORY_LABELS) as VaultEntryCategory[]).map( + (c) => ( + + {CATEGORY_LABELS[c]} + + ), + )} -
{loading ? ( -

Se încarcă...

+

+ Se încarcă... +

) : entries.length === 0 ? ( -

Nicio intrare găsită.

+

+ Nicio intrare găsită. +

) : (
{entries.map((entry) => ( @@ -150,20 +261,44 @@ export function PasswordVaultModule() {

{entry.label}

- {CATEGORY_LABELS[entry.category]} + + {CATEGORY_LABELS[entry.category]} +
-

{entry.username}

+

+ {entry.username} +

- {visiblePasswords.has(entry.id) ? entry.password : '••••••••••'} + {visiblePasswords.has(entry.id) + ? entry.password + : "••••••••••"} - - - {copiedId === entry.id && Copiat!} + {copiedId === entry.id && ( + + Copiat! + + )}
{entry.url && (

@@ -173,7 +308,11 @@ export function PasswordVaultModule() { {entry.customFields && entry.customFields.length > 0 && (

{entry.customFields.map((cf, i) => ( - + {cf.key}: {cf.value} ))} @@ -181,10 +320,23 @@ export function PasswordVaultModule() { )}
- -
@@ -196,23 +348,48 @@ export function PasswordVaultModule() { )} - {(viewMode === 'add' || viewMode === 'edit') && ( + {(viewMode === "add" || viewMode === "edit") && ( - {viewMode === 'edit' ? 'Editare' : 'Intrare nouă'} + + + {viewMode === "edit" ? "Editare" : "Intrare nouă"} + + - { setViewMode('list'); setEditingEntry(null); }} /> + { + setViewMode("list"); + setEditingEntry(null); + }} + /> )} {/* Delete confirmation */} - { if (!open) setDeletingId(null); }}> + { + if (!open) setDeletingId(null); + }} + > - Confirmare ștergere -

Ești sigur că vrei să ștergi această intrare? Acțiunea este ireversibilă.

+ + Confirmare ștergere + +

+ Ești sigur că vrei să ștergi această intrare? Acțiunea este + ireversibilă. +

- - + +
@@ -220,19 +397,29 @@ export function PasswordVaultModule() { ); } -function VaultForm({ initial, onSubmit, onCancel }: { +function VaultForm({ + initial, + onSubmit, + onCancel, +}: { initial?: VaultEntry; - onSubmit: (data: Omit) => void; + onSubmit: (data: Omit) => void; onCancel: () => void; }) { - const [label, setLabel] = useState(initial?.label ?? ''); - const [username, setUsername] = useState(initial?.username ?? ''); - const [password, setPassword] = useState(initial?.password ?? ''); - const [url, setUrl] = useState(initial?.url ?? ''); - const [category, setCategory] = useState(initial?.category ?? 'web'); - const [company, setCompany] = useState(initial?.company ?? 'beletage'); - const [notes, setNotes] = useState(initial?.notes ?? ''); - const [customFields, setCustomFields] = useState(initial?.customFields ?? []); + const [label, setLabel] = useState(initial?.label ?? ""); + const [username, setUsername] = useState(initial?.username ?? ""); + const [password, setPassword] = useState(initial?.password ?? ""); + const [url, setUrl] = useState(initial?.url ?? ""); + const [category, setCategory] = useState( + initial?.category ?? "web", + ); + const [company, setCompany] = useState( + initial?.company ?? "beletage", + ); + const [notes, setNotes] = useState(initial?.notes ?? ""); + const [customFields, setCustomFields] = useState( + initial?.customFields ?? [], + ); // Password generator state const [genLength, setGenLength] = useState(16); @@ -244,15 +431,30 @@ function VaultForm({ initial, onSubmit, onCancel }: { const strength = getPasswordStrength(password); const handleGenerate = () => { - setPassword(generatePassword(genLength, { upper: genUpper, lower: genLower, digits: genDigits, symbols: genSymbols })); + setPassword( + generatePassword(genLength, { + upper: genUpper, + lower: genLower, + digits: genDigits, + symbols: genSymbols, + }), + ); }; const addCustomField = () => { - setCustomFields([...customFields, { key: '', value: '' }]); + setCustomFields([...customFields, { key: "", value: "" }]); }; - const updateCustomField = (index: number, field: keyof CustomField, value: string) => { - setCustomFields(customFields.map((cf, i) => i === index ? { ...cf, [field]: value } : cf)); + const updateCustomField = ( + index: number, + field: keyof CustomField, + value: string, + ) => { + setCustomFields( + customFields.map((cf, i) => + i === index ? { ...cf, [field]: value } : cf, + ), + ); }; const removeCustomField = (index: number) => { @@ -260,39 +462,99 @@ function VaultForm({ initial, onSubmit, onCancel }: { }; return ( -
{ - e.preventDefault(); - onSubmit({ - label, username, password, url, category, company, notes, - customFields: customFields.filter((cf) => cf.key.trim()), - tags: initial?.tags ?? [], visibility: initial?.visibility ?? 'admin', - }); - }} className="space-y-4"> + { + e.preventDefault(); + onSubmit({ + label, + username, + password, + url, + category, + company, + notes, + customFields: customFields.filter((cf) => cf.key.trim()), + tags: initial?.tags ?? [], + visibility: initial?.visibility ?? "admin", + }); + }} + className="space-y-4" + >
-
setLabel(e.target.value)} className="mt-1" required />
-
- setLabel(e.target.value)} + className="mt-1" + required + /> +
+
+ +
-
- setCompany(v as CompanyId)} + > + + + - {(Object.keys(COMPANY_LABELS) as CompanyId[]).map((c) => ({COMPANY_LABELS[c]}))} + {(Object.keys(COMPANY_LABELS) as CompanyId[]).map((c) => ( + + {COMPANY_LABELS[c]} + + ))}
-
setUsername(e.target.value)} className="mt-1" />
+
+ + setUsername(e.target.value)} + className="mt-1" + /> +
- setPassword(e.target.value)} className="flex-1 font-mono text-sm" /> -
@@ -300,12 +562,25 @@ function VaultForm({ initial, onSubmit, onCancel }: {
Forță: - + {strength.label}
-
+
)} @@ -313,29 +588,92 @@ function VaultForm({ initial, onSubmit, onCancel }: { {/* Password generator options */}
-

Generator parolă

+

+ Generator parolă +

- setGenLength(parseInt(e.target.value, 10) || 8)} className="w-16 text-sm" min={4} max={64} /> + setGenLength(parseInt(e.target.value, 10) || 8)} + className="w-16 text-sm" + min={4} + max={64} + />
-
-
-
-
-
-
setUrl(e.target.value)} className="mt-1" placeholder="https://..." />
+
+ + setUrl(e.target.value)} + className="mt-1" + placeholder="https://..." + /> +
{/* Custom fields */}
-
@@ -343,9 +681,27 @@ function VaultForm({ initial, onSubmit, onCancel }: {
{customFields.map((cf, i) => (
- updateCustomField(i, 'key', e.target.value)} className="w-[140px] text-sm" /> - updateCustomField(i, 'value', e.target.value)} className="flex-1 text-sm" /> -
@@ -354,10 +710,20 @@ function VaultForm({ initial, onSubmit, onCancel }: { )}
-