feat(password-vault): add company scope + password strength meter + rename encryptedPassword to password (task 1.07)
This commit is contained in:
@@ -14,6 +14,7 @@ import { Card, CardContent, CardHeader, CardTitle } from '@/shared/components/ui
|
||||
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';
|
||||
|
||||
@@ -21,6 +22,29 @@ const CATEGORY_LABELS: Record<VaultEntryCategory, string> = {
|
||||
web: 'Web', email: 'Email', server: 'Server', database: 'Bază de date', api: 'API', other: 'Altele',
|
||||
};
|
||||
|
||||
const COMPANY_LABELS: Record<CompanyId, string> = {
|
||||
'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' };
|
||||
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 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' };
|
||||
}
|
||||
|
||||
type ViewMode = 'list' | 'add' | 'edit';
|
||||
|
||||
/** Generate a random password */
|
||||
@@ -131,12 +155,12 @@ export function PasswordVaultModule() {
|
||||
<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 : '••••••••••'}
|
||||
{visiblePasswords.has(entry.id) ? entry.password : '••••••••••'}
|
||||
</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)}>
|
||||
<Button variant="ghost" size="icon" className="h-5 w-5" onClick={() => handleCopy(entry.password, entry.id)}>
|
||||
<Copy className="h-3 w-3" />
|
||||
</Button>
|
||||
{copiedId === entry.id && <span className="text-[10px] text-green-500">Copiat!</span>}
|
||||
@@ -203,9 +227,10 @@ function VaultForm({ initial, onSubmit, onCancel }: {
|
||||
}) {
|
||||
const [label, setLabel] = useState(initial?.label ?? '');
|
||||
const [username, setUsername] = useState(initial?.username ?? '');
|
||||
const [password, setPassword] = useState(initial?.encryptedPassword ?? '');
|
||||
const [password, setPassword] = useState(initial?.password ?? '');
|
||||
const [url, setUrl] = useState(initial?.url ?? '');
|
||||
const [category, setCategory] = useState<VaultEntryCategory>(initial?.category ?? 'web');
|
||||
const [company, setCompany] = useState<CompanyId>(initial?.company ?? 'beletage');
|
||||
const [notes, setNotes] = useState(initial?.notes ?? '');
|
||||
const [customFields, setCustomFields] = useState<CustomField[]>(initial?.customFields ?? []);
|
||||
|
||||
@@ -216,6 +241,8 @@ function VaultForm({ initial, onSubmit, onCancel }: {
|
||||
const [genDigits, setGenDigits] = useState(true);
|
||||
const [genSymbols, setGenSymbols] = useState(true);
|
||||
|
||||
const strength = getPasswordStrength(password);
|
||||
|
||||
const handleGenerate = () => {
|
||||
setPassword(generatePassword(genLength, { upper: genUpper, lower: genLower, digits: genDigits, symbols: genSymbols }));
|
||||
};
|
||||
@@ -236,7 +263,7 @@ function VaultForm({ initial, onSubmit, onCancel }: {
|
||||
<form onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
onSubmit({
|
||||
label, username, encryptedPassword: password, url, category, notes,
|
||||
label, username, password, url, category, company, notes,
|
||||
customFields: customFields.filter((cf) => cf.key.trim()),
|
||||
tags: initial?.tags ?? [], visibility: initial?.visibility ?? 'admin',
|
||||
});
|
||||
@@ -251,16 +278,37 @@ function VaultForm({ initial, onSubmit, onCancel }: {
|
||||
</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>
|
||||
<div className="mt-1 flex gap-1.5">
|
||||
<Input type="text" value={password} onChange={(e) => setPassword(e.target.value)} className="flex-1 font-mono text-sm" />
|
||||
<Button type="button" variant="outline" size="icon" onClick={handleGenerate} title="Generează parolă">
|
||||
<KeyRound className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<div><Label>Companie</Label>
|
||||
<Select value={company} onValueChange={(v) => setCompany(v as CompanyId)}>
|
||||
<SelectTrigger className="mt-1"><SelectValue /></SelectTrigger>
|
||||
<SelectContent>
|
||||
{(Object.keys(COMPANY_LABELS) as CompanyId[]).map((c) => (<SelectItem key={c} value={c}>{COMPANY_LABELS[c]}</SelectItem>))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div><Label>Utilizator</Label><Input value={username} onChange={(e) => setUsername(e.target.value)} className="mt-1" /></div>
|
||||
</div>
|
||||
<div>
|
||||
<Label>Parolă</Label>
|
||||
<div className="mt-1 flex gap-1.5">
|
||||
<Input type="text" value={password} onChange={(e) => setPassword(e.target.value)} className="flex-1 font-mono text-sm" />
|
||||
<Button type="button" variant="outline" size="icon" onClick={handleGenerate} title="Generează parolă">
|
||||
<KeyRound className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
{password && (
|
||||
<div className="mt-2 space-y-1">
|
||||
<div className="flex items-center justify-between text-xs">
|
||||
<span className="text-muted-foreground">Forță:</span>
|
||||
<span className={strength.level === 3 ? 'text-emerald-600 font-medium' : strength.level === 2 ? 'text-green-600 font-medium' : strength.level === 1 ? 'text-yellow-600 font-medium' : 'text-red-600 font-medium'}>
|
||||
{strength.label}
|
||||
</span>
|
||||
</div>
|
||||
<div className="h-1.5 w-full overflow-hidden rounded-full bg-muted">
|
||||
<div className={`h-full ${strength.color} transition-all`} style={{ width: `${(strength.level + 1) * 25}%` }} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Password generator options */}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { Visibility } from '@/core/module-registry/types';
|
||||
import type { CompanyId } from '@/core/auth/types';
|
||||
|
||||
export type VaultEntryCategory =
|
||||
| 'web'
|
||||
@@ -18,9 +19,10 @@ export interface VaultEntry {
|
||||
id: string;
|
||||
label: string;
|
||||
username: string;
|
||||
encryptedPassword: string;
|
||||
password: string;
|
||||
url: string;
|
||||
category: VaultEntryCategory;
|
||||
company: CompanyId;
|
||||
/** Custom key-value fields */
|
||||
customFields: CustomField[];
|
||||
notes: string;
|
||||
|
||||
Reference in New Issue
Block a user