feat(password-vault): add company scope and password strength meter
This commit is contained in:
@@ -1,61 +1,109 @@
|
|||||||
'use client';
|
"use client";
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useState } from "react";
|
||||||
import {
|
import {
|
||||||
Plus, Pencil, Trash2, Search, Eye, EyeOff, Copy, ExternalLink,
|
Plus,
|
||||||
KeyRound, X,
|
Pencil,
|
||||||
} from 'lucide-react';
|
Trash2,
|
||||||
import { Button } from '@/shared/components/ui/button';
|
Search,
|
||||||
import { Input } from '@/shared/components/ui/input';
|
Eye,
|
||||||
import { Label } from '@/shared/components/ui/label';
|
EyeOff,
|
||||||
import { Textarea } from '@/shared/components/ui/textarea';
|
Copy,
|
||||||
import { Badge } from '@/shared/components/ui/badge';
|
ExternalLink,
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/shared/components/ui/card';
|
KeyRound,
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/shared/components/ui/select';
|
X,
|
||||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@/shared/components/ui/dialog';
|
} from "lucide-react";
|
||||||
import { Switch } from '@/shared/components/ui/switch';
|
import { Button } from "@/shared/components/ui/button";
|
||||||
import type { CompanyId } from '@/core/auth/types';
|
import { Input } from "@/shared/components/ui/input";
|
||||||
import type { VaultEntry, VaultEntryCategory, CustomField } from '../types';
|
import { Label } from "@/shared/components/ui/label";
|
||||||
import { useVault } from '../hooks/use-vault';
|
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<VaultEntryCategory, string> = {
|
const CATEGORY_LABELS: Record<VaultEntryCategory, string> = {
|
||||||
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<CompanyId, string> = {
|
const COMPANY_LABELS: Record<CompanyId, string> = {
|
||||||
'beletage': 'Beletage',
|
beletage: "Beletage",
|
||||||
'urban-switch': 'Urban Switch',
|
"urban-switch": "Urban Switch",
|
||||||
'studii-de-teren': 'Studii de Teren',
|
"studii-de-teren": "Studii de Teren",
|
||||||
'group': 'Grup',
|
group: "Grup",
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Calculate password strength: 0-3 (weak, medium, strong, very strong) */
|
/** Calculate password strength: 0-3 (weak, medium, strong, very strong) */
|
||||||
function getPasswordStrength(pwd: string): { level: 0 | 1 | 2 | 3; label: string; color: string } {
|
function getPasswordStrength(pwd: string): {
|
||||||
if (!pwd) return { level: 0, label: 'Nicio parolă', color: 'bg-gray-300' };
|
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 len = pwd.length;
|
||||||
const hasUpper = /[A-Z]/.test(pwd);
|
const hasUpper = /[A-Z]/.test(pwd);
|
||||||
const hasLower = /[a-z]/.test(pwd);
|
const hasLower = /[a-z]/.test(pwd);
|
||||||
const hasDigit = /\d/.test(pwd);
|
const hasDigit = /\d/.test(pwd);
|
||||||
const hasSymbol = /[!@#$%^&*()\-_=+\[\]{}|;:,.<>?]/.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;
|
const score = len + varietyScore * 2;
|
||||||
if (score < 8) return { level: 0, label: 'Slabă', color: 'bg-red-500' };
|
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 < 16) return { level: 1, label: "Medie", color: "bg-yellow-500" };
|
||||||
if (score < 24) return { level: 2, label: 'Puternică', color: 'bg-green-500' };
|
if (score < 24)
|
||||||
return { level: 3, label: 'Foarte puternică', color: 'bg-emerald-600' };
|
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 */
|
/** Generate a random password */
|
||||||
function generatePassword(length: number, options: { upper: boolean; lower: boolean; digits: boolean; symbols: boolean }): string {
|
function generatePassword(
|
||||||
let chars = '';
|
length: number,
|
||||||
if (options.upper) chars += 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
options: {
|
||||||
if (options.lower) chars += 'abcdefghijklmnopqrstuvwxyz';
|
upper: boolean;
|
||||||
if (options.digits) chars += '0123456789';
|
lower: boolean;
|
||||||
if (options.symbols) chars += '!@#$%^&*()-_=+[]{}|;:,.<>?';
|
digits: boolean;
|
||||||
if (!chars) chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
|
symbols: boolean;
|
||||||
let result = '';
|
},
|
||||||
|
): 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++) {
|
for (let i = 0; i < length; i++) {
|
||||||
result += chars.charAt(Math.floor(Math.random() * chars.length));
|
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() {
|
export function PasswordVaultModule() {
|
||||||
const { entries, allEntries, loading, filters, updateFilter, addEntry, updateEntry, removeEntry } = useVault();
|
const {
|
||||||
const [viewMode, setViewMode] = useState<ViewMode>('list');
|
entries,
|
||||||
|
allEntries,
|
||||||
|
loading,
|
||||||
|
filters,
|
||||||
|
updateFilter,
|
||||||
|
addEntry,
|
||||||
|
updateEntry,
|
||||||
|
removeEntry,
|
||||||
|
} = useVault();
|
||||||
|
const [viewMode, setViewMode] = useState<ViewMode>("list");
|
||||||
const [editingEntry, setEditingEntry] = useState<VaultEntry | null>(null);
|
const [editingEntry, setEditingEntry] = useState<VaultEntry | null>(null);
|
||||||
const [visiblePasswords, setVisiblePasswords] = useState<Set<string>>(new Set());
|
const [visiblePasswords, setVisiblePasswords] = useState<Set<string>>(
|
||||||
|
new Set(),
|
||||||
|
);
|
||||||
const [copiedId, setCopiedId] = useState<string | null>(null);
|
const [copiedId, setCopiedId] = useState<string | null>(null);
|
||||||
const [deletingId, setDeletingId] = useState<string | null>(null);
|
const [deletingId, setDeletingId] = useState<string | null>(null);
|
||||||
|
|
||||||
const togglePassword = (id: string) => {
|
const togglePassword = (id: string) => {
|
||||||
setVisiblePasswords((prev) => {
|
setVisiblePasswords((prev) => {
|
||||||
const next = new Set(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;
|
return next;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -83,16 +143,20 @@ export function PasswordVaultModule() {
|
|||||||
await navigator.clipboard.writeText(text);
|
await navigator.clipboard.writeText(text);
|
||||||
setCopiedId(id);
|
setCopiedId(id);
|
||||||
setTimeout(() => setCopiedId(null), 2000);
|
setTimeout(() => setCopiedId(null), 2000);
|
||||||
} catch { /* silent */ }
|
} catch {
|
||||||
|
/* silent */
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmit = async (data: Omit<VaultEntry, 'id' | 'createdAt' | 'updatedAt'>) => {
|
const handleSubmit = async (
|
||||||
if (viewMode === 'edit' && editingEntry) {
|
data: Omit<VaultEntry, "id" | "createdAt" | "updatedAt">,
|
||||||
|
) => {
|
||||||
|
if (viewMode === "edit" && editingEntry) {
|
||||||
await updateEntry(editingEntry.id, data);
|
await updateEntry(editingEntry.id, data);
|
||||||
} else {
|
} else {
|
||||||
await addEntry(data);
|
await addEntry(data);
|
||||||
}
|
}
|
||||||
setViewMode('list');
|
setViewMode("list");
|
||||||
setEditingEntry(null);
|
setEditingEntry(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -106,42 +170,89 @@ export function PasswordVaultModule() {
|
|||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<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">
|
<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.
|
Atenție: Parolele sunt stocate local (localStorage). Nu sunt criptate.
|
||||||
|
Folosiți un manager de parole dedicat pentru date sensibile.
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Stats */}
|
{/* Stats */}
|
||||||
<div className="grid grid-cols-2 gap-3 sm:grid-cols-4">
|
<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>
|
||||||
<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>
|
<CardContent className="p-4">
|
||||||
<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>
|
<p className="text-xs text-muted-foreground">Total</p>
|
||||||
<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>
|
<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>
|
</div>
|
||||||
|
|
||||||
{viewMode === 'list' && (
|
{viewMode === "list" && (
|
||||||
<>
|
<>
|
||||||
<div className="flex flex-wrap items-center gap-3">
|
<div className="flex flex-wrap items-center gap-3">
|
||||||
<div className="relative min-w-[200px] flex-1">
|
<div className="relative min-w-[200px] flex-1">
|
||||||
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
|
<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" />
|
<Input
|
||||||
|
placeholder="Caută..."
|
||||||
|
value={filters.search}
|
||||||
|
onChange={(e) => updateFilter("search", e.target.value)}
|
||||||
|
className="pl-9"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Select value={filters.category} onValueChange={(v) => updateFilter('category', v as VaultEntryCategory | 'all')}>
|
<Select
|
||||||
<SelectTrigger className="w-[150px]"><SelectValue /></SelectTrigger>
|
value={filters.category}
|
||||||
|
onValueChange={(v) =>
|
||||||
|
updateFilter("category", v as VaultEntryCategory | "all")
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="w-[150px]">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="all">Toate</SelectItem>
|
<SelectItem value="all">Toate</SelectItem>
|
||||||
{(Object.keys(CATEGORY_LABELS) as VaultEntryCategory[]).map((c) => (
|
{(Object.keys(CATEGORY_LABELS) as VaultEntryCategory[]).map(
|
||||||
<SelectItem key={c} value={c}>{CATEGORY_LABELS[c]}</SelectItem>
|
(c) => (
|
||||||
))}
|
<SelectItem key={c} value={c}>
|
||||||
|
{CATEGORY_LABELS[c]}
|
||||||
|
</SelectItem>
|
||||||
|
),
|
||||||
|
)}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
<Button onClick={() => setViewMode('add')} className="shrink-0">
|
<Button onClick={() => setViewMode("add")} className="shrink-0">
|
||||||
<Plus className="mr-1.5 h-4 w-4" /> Adaugă
|
<Plus className="mr-1.5 h-4 w-4" /> Adaugă
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<p className="py-8 text-center text-sm text-muted-foreground">Se încarcă...</p>
|
<p className="py-8 text-center text-sm text-muted-foreground">
|
||||||
|
Se încarcă...
|
||||||
|
</p>
|
||||||
) : entries.length === 0 ? (
|
) : entries.length === 0 ? (
|
||||||
<p className="py-8 text-center text-sm text-muted-foreground">Nicio intrare găsită.</p>
|
<p className="py-8 text-center text-sm text-muted-foreground">
|
||||||
|
Nicio intrare găsită.
|
||||||
|
</p>
|
||||||
) : (
|
) : (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{entries.map((entry) => (
|
{entries.map((entry) => (
|
||||||
@@ -150,20 +261,44 @@ export function PasswordVaultModule() {
|
|||||||
<div className="min-w-0 flex-1 space-y-1">
|
<div className="min-w-0 flex-1 space-y-1">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<p className="font-medium">{entry.label}</p>
|
<p className="font-medium">{entry.label}</p>
|
||||||
<Badge variant="outline" className="text-[10px]">{CATEGORY_LABELS[entry.category]}</Badge>
|
<Badge variant="outline" className="text-[10px]">
|
||||||
|
{CATEGORY_LABELS[entry.category]}
|
||||||
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-muted-foreground">{entry.username}</p>
|
<p className="text-xs text-muted-foreground">
|
||||||
|
{entry.username}
|
||||||
|
</p>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<code className="text-xs">
|
<code className="text-xs">
|
||||||
{visiblePasswords.has(entry.id) ? entry.password : '••••••••••'}
|
{visiblePasswords.has(entry.id)
|
||||||
|
? entry.password
|
||||||
|
: "••••••••••"}
|
||||||
</code>
|
</code>
|
||||||
<Button variant="ghost" size="icon" className="h-5 w-5" onClick={() => togglePassword(entry.id)}>
|
<Button
|
||||||
{visiblePasswords.has(entry.id) ? <EyeOff className="h-3 w-3" /> : <Eye className="h-3 w-3" />}
|
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>
|
||||||
<Button variant="ghost" size="icon" className="h-5 w-5" onClick={() => handleCopy(entry.password, entry.id)}>
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="h-5 w-5"
|
||||||
|
onClick={() => handleCopy(entry.password, entry.id)}
|
||||||
|
>
|
||||||
<Copy className="h-3 w-3" />
|
<Copy className="h-3 w-3" />
|
||||||
</Button>
|
</Button>
|
||||||
{copiedId === entry.id && <span className="text-[10px] text-green-500">Copiat!</span>}
|
{copiedId === entry.id && (
|
||||||
|
<span className="text-[10px] text-green-500">
|
||||||
|
Copiat!
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
{entry.url && (
|
{entry.url && (
|
||||||
<p className="flex items-center gap-1 text-xs text-muted-foreground">
|
<p className="flex items-center gap-1 text-xs text-muted-foreground">
|
||||||
@@ -173,7 +308,11 @@ export function PasswordVaultModule() {
|
|||||||
{entry.customFields && entry.customFields.length > 0 && (
|
{entry.customFields && entry.customFields.length > 0 && (
|
||||||
<div className="mt-1 flex flex-wrap gap-1">
|
<div className="mt-1 flex flex-wrap gap-1">
|
||||||
{entry.customFields.map((cf, i) => (
|
{entry.customFields.map((cf, i) => (
|
||||||
<Badge key={i} variant="secondary" className="text-[10px]">
|
<Badge
|
||||||
|
key={i}
|
||||||
|
variant="secondary"
|
||||||
|
className="text-[10px]"
|
||||||
|
>
|
||||||
{cf.key}: {cf.value}
|
{cf.key}: {cf.value}
|
||||||
</Badge>
|
</Badge>
|
||||||
))}
|
))}
|
||||||
@@ -181,10 +320,23 @@ export function PasswordVaultModule() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-1 opacity-0 transition-opacity group-hover:opacity-100">
|
<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'); }}>
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="h-7 w-7"
|
||||||
|
onClick={() => {
|
||||||
|
setEditingEntry(entry);
|
||||||
|
setViewMode("edit");
|
||||||
|
}}
|
||||||
|
>
|
||||||
<Pencil className="h-3.5 w-3.5" />
|
<Pencil className="h-3.5 w-3.5" />
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="ghost" size="icon" className="h-7 w-7 text-destructive" onClick={() => setDeletingId(entry.id)}>
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="h-7 w-7 text-destructive"
|
||||||
|
onClick={() => setDeletingId(entry.id)}
|
||||||
|
>
|
||||||
<Trash2 className="h-3.5 w-3.5" />
|
<Trash2 className="h-3.5 w-3.5" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -196,23 +348,48 @@ export function PasswordVaultModule() {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{(viewMode === 'add' || viewMode === 'edit') && (
|
{(viewMode === "add" || viewMode === "edit") && (
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader><CardTitle>{viewMode === 'edit' ? 'Editare' : 'Intrare nouă'}</CardTitle></CardHeader>
|
<CardHeader>
|
||||||
|
<CardTitle>
|
||||||
|
{viewMode === "edit" ? "Editare" : "Intrare nouă"}
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<VaultForm initial={editingEntry ?? undefined} onSubmit={handleSubmit} onCancel={() => { setViewMode('list'); setEditingEntry(null); }} />
|
<VaultForm
|
||||||
|
initial={editingEntry ?? undefined}
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
onCancel={() => {
|
||||||
|
setViewMode("list");
|
||||||
|
setEditingEntry(null);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Delete confirmation */}
|
{/* Delete confirmation */}
|
||||||
<Dialog open={deletingId !== null} onOpenChange={(open) => { if (!open) setDeletingId(null); }}>
|
<Dialog
|
||||||
|
open={deletingId !== null}
|
||||||
|
onOpenChange={(open) => {
|
||||||
|
if (!open) setDeletingId(null);
|
||||||
|
}}
|
||||||
|
>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<DialogHeader><DialogTitle>Confirmare ștergere</DialogTitle></DialogHeader>
|
<DialogHeader>
|
||||||
<p className="text-sm">Ești sigur că vrei să ștergi această intrare? Acțiunea este ireversibilă.</p>
|
<DialogTitle>Confirmare ștergere</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
<p className="text-sm">
|
||||||
|
Ești sigur că vrei să ștergi această intrare? Acțiunea este
|
||||||
|
ireversibilă.
|
||||||
|
</p>
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button variant="outline" onClick={() => setDeletingId(null)}>Anulează</Button>
|
<Button variant="outline" onClick={() => setDeletingId(null)}>
|
||||||
<Button variant="destructive" onClick={handleDeleteConfirm}>Șterge</Button>
|
Anulează
|
||||||
|
</Button>
|
||||||
|
<Button variant="destructive" onClick={handleDeleteConfirm}>
|
||||||
|
Șterge
|
||||||
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
@@ -220,19 +397,29 @@ export function PasswordVaultModule() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function VaultForm({ initial, onSubmit, onCancel }: {
|
function VaultForm({
|
||||||
|
initial,
|
||||||
|
onSubmit,
|
||||||
|
onCancel,
|
||||||
|
}: {
|
||||||
initial?: VaultEntry;
|
initial?: VaultEntry;
|
||||||
onSubmit: (data: Omit<VaultEntry, 'id' | 'createdAt' | 'updatedAt'>) => void;
|
onSubmit: (data: Omit<VaultEntry, "id" | "createdAt" | "updatedAt">) => void;
|
||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
}) {
|
}) {
|
||||||
const [label, setLabel] = useState(initial?.label ?? '');
|
const [label, setLabel] = useState(initial?.label ?? "");
|
||||||
const [username, setUsername] = useState(initial?.username ?? '');
|
const [username, setUsername] = useState(initial?.username ?? "");
|
||||||
const [password, setPassword] = useState(initial?.password ?? '');
|
const [password, setPassword] = useState(initial?.password ?? "");
|
||||||
const [url, setUrl] = useState(initial?.url ?? '');
|
const [url, setUrl] = useState(initial?.url ?? "");
|
||||||
const [category, setCategory] = useState<VaultEntryCategory>(initial?.category ?? 'web');
|
const [category, setCategory] = useState<VaultEntryCategory>(
|
||||||
const [company, setCompany] = useState<CompanyId>(initial?.company ?? 'beletage');
|
initial?.category ?? "web",
|
||||||
const [notes, setNotes] = useState(initial?.notes ?? '');
|
);
|
||||||
const [customFields, setCustomFields] = useState<CustomField[]>(initial?.customFields ?? []);
|
const [company, setCompany] = useState<CompanyId>(
|
||||||
|
initial?.company ?? "beletage",
|
||||||
|
);
|
||||||
|
const [notes, setNotes] = useState(initial?.notes ?? "");
|
||||||
|
const [customFields, setCustomFields] = useState<CustomField[]>(
|
||||||
|
initial?.customFields ?? [],
|
||||||
|
);
|
||||||
|
|
||||||
// Password generator state
|
// Password generator state
|
||||||
const [genLength, setGenLength] = useState(16);
|
const [genLength, setGenLength] = useState(16);
|
||||||
@@ -244,15 +431,30 @@ function VaultForm({ initial, onSubmit, onCancel }: {
|
|||||||
const strength = getPasswordStrength(password);
|
const strength = getPasswordStrength(password);
|
||||||
|
|
||||||
const handleGenerate = () => {
|
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 = () => {
|
const addCustomField = () => {
|
||||||
setCustomFields([...customFields, { key: '', value: '' }]);
|
setCustomFields([...customFields, { key: "", value: "" }]);
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateCustomField = (index: number, field: keyof CustomField, value: string) => {
|
const updateCustomField = (
|
||||||
setCustomFields(customFields.map((cf, i) => i === index ? { ...cf, [field]: value } : cf));
|
index: number,
|
||||||
|
field: keyof CustomField,
|
||||||
|
value: string,
|
||||||
|
) => {
|
||||||
|
setCustomFields(
|
||||||
|
customFields.map((cf, i) =>
|
||||||
|
i === index ? { ...cf, [field]: value } : cf,
|
||||||
|
),
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const removeCustomField = (index: number) => {
|
const removeCustomField = (index: number) => {
|
||||||
@@ -260,39 +462,99 @@ function VaultForm({ initial, onSubmit, onCancel }: {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={(e) => {
|
<form
|
||||||
e.preventDefault();
|
onSubmit={(e) => {
|
||||||
onSubmit({
|
e.preventDefault();
|
||||||
label, username, password, url, category, company, notes,
|
onSubmit({
|
||||||
customFields: customFields.filter((cf) => cf.key.trim()),
|
label,
|
||||||
tags: initial?.tags ?? [], visibility: initial?.visibility ?? 'admin',
|
username,
|
||||||
});
|
password,
|
||||||
}} className="space-y-4">
|
url,
|
||||||
|
category,
|
||||||
|
company,
|
||||||
|
notes,
|
||||||
|
customFields: customFields.filter((cf) => cf.key.trim()),
|
||||||
|
tags: initial?.tags ?? [],
|
||||||
|
visibility: initial?.visibility ?? "admin",
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
className="space-y-4"
|
||||||
|
>
|
||||||
<div className="grid gap-4 sm:grid-cols-2">
|
<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>
|
||||||
<div><Label>Categorie</Label>
|
<Label>Nume/Etichetă *</Label>
|
||||||
<Select value={category} onValueChange={(v) => setCategory(v as VaultEntryCategory)}>
|
<Input
|
||||||
<SelectTrigger className="mt-1"><SelectValue /></SelectTrigger>
|
value={label}
|
||||||
<SelectContent>{(Object.keys(CATEGORY_LABELS) as VaultEntryCategory[]).map((c) => (<SelectItem key={c} value={c}>{CATEGORY_LABELS[c]}</SelectItem>))}</SelectContent>
|
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>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid gap-4 sm:grid-cols-2">
|
<div className="grid gap-4 sm:grid-cols-2">
|
||||||
<div><Label>Companie</Label>
|
<div>
|
||||||
<Select value={company} onValueChange={(v) => setCompany(v as CompanyId)}>
|
<Label>Companie</Label>
|
||||||
<SelectTrigger className="mt-1"><SelectValue /></SelectTrigger>
|
<Select
|
||||||
|
value={company}
|
||||||
|
onValueChange={(v) => setCompany(v as CompanyId)}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="mt-1">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{(Object.keys(COMPANY_LABELS) as CompanyId[]).map((c) => (<SelectItem key={c} value={c}>{COMPANY_LABELS[c]}</SelectItem>))}
|
{(Object.keys(COMPANY_LABELS) as CompanyId[]).map((c) => (
|
||||||
|
<SelectItem key={c} value={c}>
|
||||||
|
{COMPANY_LABELS[c]}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
<div><Label>Utilizator</Label><Input value={username} onChange={(e) => setUsername(e.target.value)} className="mt-1" /></div>
|
<div>
|
||||||
|
<Label>Utilizator</Label>
|
||||||
|
<Input
|
||||||
|
value={username}
|
||||||
|
onChange={(e) => setUsername(e.target.value)}
|
||||||
|
className="mt-1"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Label>Parolă</Label>
|
<Label>Parolă</Label>
|
||||||
<div className="mt-1 flex gap-1.5">
|
<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" />
|
<Input
|
||||||
<Button type="button" variant="outline" size="icon" onClick={handleGenerate} title="Generează parolă">
|
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" />
|
<KeyRound className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -300,12 +562,25 @@ function VaultForm({ initial, onSubmit, onCancel }: {
|
|||||||
<div className="mt-2 space-y-1">
|
<div className="mt-2 space-y-1">
|
||||||
<div className="flex items-center justify-between text-xs">
|
<div className="flex items-center justify-between text-xs">
|
||||||
<span className="text-muted-foreground">Forță:</span>
|
<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'}>
|
<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}
|
{strength.label}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="h-1.5 w-full overflow-hidden rounded-full bg-muted">
|
<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
|
||||||
|
className={`h-full ${strength.color} transition-all`}
|
||||||
|
style={{ width: `${(strength.level + 1) * 25}%` }}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -313,29 +588,92 @@ function VaultForm({ initial, onSubmit, onCancel }: {
|
|||||||
|
|
||||||
{/* Password generator options */}
|
{/* Password generator options */}
|
||||||
<div className="rounded border p-3 space-y-2">
|
<div className="rounded border p-3 space-y-2">
|
||||||
<p className="text-xs font-medium text-muted-foreground">Generator parolă</p>
|
<p className="text-xs font-medium text-muted-foreground">
|
||||||
|
Generator parolă
|
||||||
|
</p>
|
||||||
<div className="flex flex-wrap items-center gap-4">
|
<div className="flex flex-wrap items-center gap-4">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Label className="text-xs">Lungime:</Label>
|
<Label className="text-xs">Lungime:</Label>
|
||||||
<Input type="number" value={genLength} onChange={(e) => setGenLength(parseInt(e.target.value, 10) || 8)} className="w-16 text-sm" min={4} max={64} />
|
<Input
|
||||||
|
type="number"
|
||||||
|
value={genLength}
|
||||||
|
onChange={(e) => setGenLength(parseInt(e.target.value, 10) || 8)}
|
||||||
|
className="w-16 text-sm"
|
||||||
|
min={4}
|
||||||
|
max={64}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-1.5"><Switch checked={genUpper} onCheckedChange={setGenUpper} id="gen-upper" /><Label htmlFor="gen-upper" className="text-xs cursor-pointer">A-Z</Label></div>
|
<div className="flex items-center gap-1.5">
|
||||||
<div className="flex items-center gap-1.5"><Switch checked={genLower} onCheckedChange={setGenLower} id="gen-lower" /><Label htmlFor="gen-lower" className="text-xs cursor-pointer">a-z</Label></div>
|
<Switch
|
||||||
<div className="flex items-center gap-1.5"><Switch checked={genDigits} onCheckedChange={setGenDigits} id="gen-digits" /><Label htmlFor="gen-digits" className="text-xs cursor-pointer">0-9</Label></div>
|
checked={genUpper}
|
||||||
<div className="flex items-center gap-1.5"><Switch checked={genSymbols} onCheckedChange={setGenSymbols} id="gen-symbols" /><Label htmlFor="gen-symbols" className="text-xs cursor-pointer">!@#$</Label></div>
|
onCheckedChange={setGenUpper}
|
||||||
<Button type="button" variant="secondary" size="sm" onClick={handleGenerate}>
|
id="gen-upper"
|
||||||
|
/>
|
||||||
|
<Label htmlFor="gen-upper" className="text-xs cursor-pointer">
|
||||||
|
A-Z
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
|
<Switch
|
||||||
|
checked={genLower}
|
||||||
|
onCheckedChange={setGenLower}
|
||||||
|
id="gen-lower"
|
||||||
|
/>
|
||||||
|
<Label htmlFor="gen-lower" className="text-xs cursor-pointer">
|
||||||
|
a-z
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
|
<Switch
|
||||||
|
checked={genDigits}
|
||||||
|
onCheckedChange={setGenDigits}
|
||||||
|
id="gen-digits"
|
||||||
|
/>
|
||||||
|
<Label htmlFor="gen-digits" className="text-xs cursor-pointer">
|
||||||
|
0-9
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
|
<Switch
|
||||||
|
checked={genSymbols}
|
||||||
|
onCheckedChange={setGenSymbols}
|
||||||
|
id="gen-symbols"
|
||||||
|
/>
|
||||||
|
<Label htmlFor="gen-symbols" className="text-xs cursor-pointer">
|
||||||
|
!@#$
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="secondary"
|
||||||
|
size="sm"
|
||||||
|
onClick={handleGenerate}
|
||||||
|
>
|
||||||
<KeyRound className="mr-1 h-3 w-3" /> Generează
|
<KeyRound className="mr-1 h-3 w-3" /> Generează
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div><Label>URL</Label><Input value={url} onChange={(e) => setUrl(e.target.value)} className="mt-1" placeholder="https://..." /></div>
|
<div>
|
||||||
|
<Label>URL</Label>
|
||||||
|
<Input
|
||||||
|
value={url}
|
||||||
|
onChange={(e) => setUrl(e.target.value)}
|
||||||
|
className="mt-1"
|
||||||
|
placeholder="https://..."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Custom fields */}
|
{/* Custom fields */}
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<Label>Câmpuri personalizate</Label>
|
<Label>Câmpuri personalizate</Label>
|
||||||
<Button type="button" variant="outline" size="sm" onClick={addCustomField}>
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={addCustomField}
|
||||||
|
>
|
||||||
<Plus className="mr-1 h-3 w-3" /> Adaugă câmp
|
<Plus className="mr-1 h-3 w-3" /> Adaugă câmp
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -343,9 +681,27 @@ function VaultForm({ initial, onSubmit, onCancel }: {
|
|||||||
<div className="mt-2 space-y-1.5">
|
<div className="mt-2 space-y-1.5">
|
||||||
{customFields.map((cf, i) => (
|
{customFields.map((cf, i) => (
|
||||||
<div key={i} className="flex items-center gap-2">
|
<div key={i} className="flex items-center gap-2">
|
||||||
<Input placeholder="Cheie" value={cf.key} onChange={(e) => updateCustomField(i, 'key', e.target.value)} className="w-[140px] text-sm" />
|
<Input
|
||||||
<Input placeholder="Valoare" value={cf.value} onChange={(e) => updateCustomField(i, 'value', e.target.value)} className="flex-1 text-sm" />
|
placeholder="Cheie"
|
||||||
<Button type="button" variant="ghost" size="icon" className="h-7 w-7 text-destructive" onClick={() => removeCustomField(i)}>
|
value={cf.key}
|
||||||
|
onChange={(e) => updateCustomField(i, "key", e.target.value)}
|
||||||
|
className="w-[140px] text-sm"
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
placeholder="Valoare"
|
||||||
|
value={cf.value}
|
||||||
|
onChange={(e) =>
|
||||||
|
updateCustomField(i, "value", e.target.value)
|
||||||
|
}
|
||||||
|
className="flex-1 text-sm"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="h-7 w-7 text-destructive"
|
||||||
|
onClick={() => removeCustomField(i)}
|
||||||
|
>
|
||||||
<X className="h-3.5 w-3.5" />
|
<X className="h-3.5 w-3.5" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -354,10 +710,20 @@ function VaultForm({ initial, onSubmit, onCancel }: {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div><Label>Note</Label><Textarea value={notes} onChange={(e) => setNotes(e.target.value)} rows={2} className="mt-1" /></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">
|
<div className="flex justify-end gap-2 pt-2">
|
||||||
<Button type="button" variant="outline" onClick={onCancel}>Anulează</Button>
|
<Button type="button" variant="outline" onClick={onCancel}>
|
||||||
<Button type="submit">{initial ? 'Actualizează' : 'Adaugă'}</Button>
|
Anulează
|
||||||
|
</Button>
|
||||||
|
<Button type="submit">{initial ? "Actualizează" : "Adaugă"}</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import type { Visibility } from '@/core/module-registry/types';
|
import type { Visibility } from "@/core/module-registry/types";
|
||||||
import type { CompanyId } from '@/core/auth/types';
|
import type { CompanyId } from "@/core/auth/types";
|
||||||
|
|
||||||
export type VaultEntryCategory =
|
export type VaultEntryCategory =
|
||||||
| 'web'
|
| "web"
|
||||||
| 'email'
|
| "email"
|
||||||
| 'server'
|
| "server"
|
||||||
| 'database'
|
| "database"
|
||||||
| 'api'
|
| "api"
|
||||||
| 'other';
|
| "other";
|
||||||
|
|
||||||
/** Custom key-value field */
|
/** Custom key-value field */
|
||||||
export interface CustomField {
|
export interface CustomField {
|
||||||
|
|||||||
Reference in New Issue
Block a user