diff --git a/app/layout.tsx b/app/layout.tsx index 17b2ce8..df94818 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,5 +1,6 @@ import type { Metadata } from 'next' import './globals.css' +import { OrganizationProvider } from '@/providers/organization-provider' export const metadata: Metadata = { title: 'v0 App', @@ -14,7 +15,11 @@ export default function RootLayout({ }>) { return ( - {children} + + + {children} + + ) } diff --git a/components/folder-tree.tsx b/components/folder-tree.tsx index 3d87e19..e6569ff 100644 --- a/components/folder-tree.tsx +++ b/components/folder-tree.tsx @@ -1,27 +1,29 @@ "use client" -import { useState } from "react" +import { useEffect, useState } from "react" import { ChevronRight, ChevronDown, Folder, FolderOpen, Plus, Users } from "lucide-react" import { Button } from "@/components/ui/button" import { Input } from "@/components/ui/input" import { cn } from "@/lib/utils" import type { FolderItem } from "@/components/password-manager" +import useOrganization from "@/hooks/use-organization" -interface FolderTreeProps { - folders: FolderItem[] - selectedFolderId: string | null - onSelectFolder: (folderId: string | null) => void - onAddFolder: (name: string, parentId: string | null) => void - className?: string -} +export function FolderTree() { + const { folders, selectedFolderId, loadings, onCreateFolder, updateSelectedFolderId } = useOrganization() -export function FolderTree({ folders, selectedFolderId, onSelectFolder, onAddFolder, className }: FolderTreeProps) { const [expandedFolders, setExpandedFolders] = useState>({}) const [newFolderParentId, setNewFolderParentId] = useState(null) const [newFolderName, setNewFolderName] = useState("") - // Get root level folders - const rootFolders = folders.filter((folder) => folder.parentId === null) + const getRootFolders = () => { + return folders.filter((folder) => !folders.map(f => f.id).includes(folder.parentId ?? '')) + } + + const [rootFolders, setRootFolders] = useState(getRootFolders()) + + useEffect(() => { + setRootFolders(getRootFolders()) + }, [folders]) const toggleExpand = (folderId: string) => { setExpandedFolders((prev) => ({ @@ -32,9 +34,8 @@ export function FolderTree({ folders, selectedFolderId, onSelectFolder, onAddFol const handleAddFolder = () => { if (newFolderName.trim()) { - onAddFolder(newFolderName.trim(), newFolderParentId) + onCreateFolder(newFolderName.trim(), newFolderParentId) setNewFolderName("") - setNewFolderParentId(null) } } @@ -60,7 +61,7 @@ export function FolderTree({ folders, selectedFolderId, onSelectFolder, onAddFol > {isExpanded ? : } - - + { + level < 2 && ( + + ) + } {isExpanded && hasChildren && ( @@ -87,7 +92,7 @@ export function FolderTree({ folders, selectedFolderId, onSelectFolder, onAddFol )} {newFolderParentId === folder.id && ( -
+
setNewFolderName(e.target.value)} @@ -99,12 +104,14 @@ export function FolderTree({ folders, selectedFolderId, onSelectFolder, onAddFol if (e.key === "Escape") setNewFolderParentId(null) }} /> - - +
+ + +
)}
@@ -112,14 +119,14 @@ export function FolderTree({ folders, selectedFolderId, onSelectFolder, onAddFol } return ( -
+
+ + + Actions + copyToClipboard(credential.user_identifier, credential.id)}> + Copier l'identifiant + + copyToClipboard(credential.password, credential.id)}> + Copier le mot de passe + + + {/* { + setSelectedPassword(password) + }} + > + Voir les détails + + { + setSelectedPassword(password) + setShowShareDialog(true) + }} + > + Partager + */} + + Modifier + Supprimer + + +
+ {/*
+ + {getFolderName(password.folderId)} + + {password.tags.slice(0, 2).map((tag, index) => ( + + {tag} + + ))} + {password.tags.length > 2 && ( + + +{password.tags.length - 2} + + )} +
*/} + + +
+
+
+ Mot de passe: + + {revealedPasswords[credential.password] ? "ExamplePass123" : credential.password} + +
+
+ {/* */} + +
+
+ {/*
+ Mis à jour: {password.lastUpdated} + + {password.strength === "strong" ? "Fort" : password.strength === "medium" ? "Moyen" : "Faible"} + +
+
+ Nombre de vues : + + {!isMounted || isLoadingUsages ? '...' : passwordUsages[password.id] ?? 0} + +
*/} + {credential.note && ( +
+ + Notes +
+ )} +
+
+ + ) +} \ No newline at end of file diff --git a/components/password-form.tsx b/components/password-form.tsx index aef7f7b..abd8a5b 100644 --- a/components/password-form.tsx +++ b/components/password-form.tsx @@ -26,14 +26,15 @@ import type { FolderItem } from "@/components/password-manager" import { Switch } from "@/components/ui/switch" interface PasswordFormProps { - onAddPassword: (password: any) => void + onAddPassword: (title: string, domaine: string | null, userIdentifier: string, password: string, folderId: string) => void folders: FolderItem[] + selectedFolderId?: string onAddFolder: (name: string, parentId: string | null) => void onCancel: () => void allTags: string[] } -export function PasswordForm({ onAddPassword, folders, onAddFolder, onCancel, allTags }: PasswordFormProps) { +export function PasswordForm({ onAddPassword, folders, selectedFolderId, onAddFolder, onCancel, allTags }: PasswordFormProps) { const [title, setTitle] = useState("") const [website, setWebsite] = useState("") const [username, setUsername] = useState("") @@ -45,9 +46,8 @@ export function PasswordForm({ onAddPassword, folders, onAddFolder, onCancel, al const [passwordStrength, setPasswordStrength] = useState<"weak" | "medium" | "strong">("medium") const [copied, setCopied] = useState(false) const [folderId, setFolderId] = useState("") - const [showNewFolderInput, setShowNewFolderInput] = useState(false) const [newFolderName, setNewFolderName] = useState("") - const [newFolderParentId, setNewFolderParentId] = useState(null) + const [newFolderParentId, setNewFolderParentId] = useState(selectedFolderId ?? null) const [tags, setTags] = useState([]) const [newTag, setNewTag] = useState("") const [notes, setNotes] = useState("") @@ -159,26 +159,25 @@ export function PasswordForm({ onAddPassword, folders, onAddFolder, onCancel, al setPasswordStrength(strength) } - const addTag = () => { - if (newTag && !tags.includes(newTag)) { - setTags([...tags, newTag]) - setNewTag("") - } - } - - const removeTag = (tagToRemove: string) => { - setTags(tags.filter((tag) => tag !== tagToRemove)) - } - - const handleAddFolder = () => { - if (newFolderName) { - onAddFolder(newFolderName, newFolderParentId) - const newId = newFolderName.toLowerCase().replace(/\s+/g, "-") + "-" + Date.now().toString(36) - setFolderId(newId) - setNewFolderName("") - setShowNewFolderInput(false) - } - } + // const addTag = () => { + // if (newTag && !tags.includes(newTag)) { + // setTags([...tags, newTag]) + // setNewTag("") + // } + // } + + // const removeTag = (tagToRemove: string) => { + // setTags(tags.filter((tag) => tag !== tagToRemove)) + // } + + // const handleAddFolder = () => { + // if (newFolderName) { + // onAddFolder(newFolderName, newFolderParentId) + // const newId = newFolderName.toLowerCase().replace(/\s+/g, "-") + "-" + Date.now().toString(36) + // setFolderId(newId) + // setNewFolderName("") + // } + // } const addCustomField = () => { if (newFieldLabel) { @@ -208,33 +207,21 @@ export function PasswordForm({ onAddPassword, folders, onAddFolder, onCancel, al const handleSubmit = (e: React.FormEvent) => { e.preventDefault() if (title && username && password) { - onAddPassword({ - title, - website, - username, - password, - strength: passwordStrength, - folderId, - tags, - notes, - customFields: customFields.length > 0 ? customFields : undefined, - }) + // onAddPassword({ + // title, + // website, + // username, + // password, + // strength: passwordStrength, + // folderId, + // tags, + // notes, + // customFields: customFields.length > 0 ? customFields : undefined, + // }) + onAddPassword(title, website, username, password, newFolderParentId || selectedFolderId || "") } } - // Group folders by parent for the select dropdown - const getFolderGroups = () => { - const rootFolders = folders.filter((f) => f.parentId === null) - - return rootFolders.map((rootFolder) => { - const children = folders.filter((f) => f.parentId === rootFolder.id) - return { - parent: rootFolder, - children, - } - }) - } - // Get folder path for display const getFolderPath = (folder: FolderItem): string => { if (!folder.parentId) return folder.name @@ -328,80 +315,25 @@ export function PasswordForm({ onAddPassword, folders, onAddFolder, onCancel, al
- - {showNewFolderInput ? ( -
-
- setNewFolderName(e.target.value)} - className="flex-1" - /> - - -
- -
- - -
-
- ) : ( -
- - -
- )} + +
-
+ {/*
)} -
+
*/} @@ -613,7 +545,7 @@ export function PasswordForm({ onAddPassword, folders, onAddFolder, onCancel, al passwordStrength === "strong" ? "default" : passwordStrength === "medium" - ? "warning" + ? "default" : "destructive" } > diff --git a/components/password-list.tsx b/components/password-list.tsx index c81d4bc..2107bb4 100644 --- a/components/password-list.tsx +++ b/components/password-list.tsx @@ -1,7 +1,7 @@ "use client" -import { useState } from "react" -import { Copy, Eye, EyeOff, MoreHorizontal, FileText, AlertTriangle, RefreshCw, Link2 } from "lucide-react" +import { useState, useEffect } from "react" +import { Copy, Eye, EyeOff, MoreHorizontal, FileText, AlertTriangle, RefreshCw, Link2, User } from "lucide-react" import { Button } from "@/components/ui/button" import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" import { @@ -27,9 +27,12 @@ import { Label } from "@/components/ui/label" import { Input } from "@/components/ui/input" import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" import { Textarea } from "@/components/ui/textarea" +import { getPasswordUsage, usePassword } from "@/lib/api/security" +import { CardCredential, PasswordCredential, SSHKeyCredential } from "@/types/credential" +import { PasswordCredentialCard } from "./password-credential-card" interface PasswordListProps { - passwords: PasswordEntry[] + passwords: (CardCredential | PasswordCredential | SSHKeyCredential)[] folders: FolderItem[] } @@ -37,16 +40,39 @@ export function PasswordList({ passwords, folders }: PasswordListProps) { const [revealedPasswords, setRevealedPasswords] = useState>({}) const [selectedPassword, setSelectedPassword] = useState(null) const [showShareDialog, setShowShareDialog] = useState(false) - - const togglePasswordVisibility = (id: string) => { + const [passwordUsages, setPasswordUsages] = useState>({}) + const [isLoadingUsages, setIsLoadingUsages] = useState(false) + const [isMounted, setIsMounted] = useState(false) + + const togglePasswordVisibility = async (id: string) => { setRevealedPasswords((prev) => ({ ...prev, [id]: !prev[id], })) + try { + await usePassword(id) + // Mettre à jour le compteur local + setPasswordUsages(prev => ({ + ...prev, + [id]: (prev[id] || 0) + 1 + })) + } catch (error) { + console.error('Erreur lors de la mise à jour du compteur:', error) + } } - const copyToClipboard = (text: string) => { - navigator.clipboard.writeText(text) + const copyToClipboard = async (text: string, idPassword: string) => { + try { + await navigator.clipboard.writeText(text) + await usePassword(idPassword) + // Mettre à jour le compteur local + setPasswordUsages(prev => ({ + ...prev, + [idPassword]: (prev[idPassword] || 0) + 1 + })) + } catch (error) { + console.error('Erreur lors de la copie:', error) + } } const getStrengthColor = (strength: string) => { @@ -81,6 +107,37 @@ export function PasswordList({ passwords, folders }: PasswordListProps) { return folder.name } + useEffect(() => { + setIsMounted(true) + }, []) + + useEffect(() => { + if (!isMounted) return + + const fetchPasswordUsages = async () => { + setIsLoadingUsages(true) + try { + const usages: Record = {} + for (const password of passwords) { + try { + const usage = await getPasswordUsage(password.id) + usages[password.id] = usage + } catch (error) { + console.error(`Erreur lors de la récupération de l'utilisation pour ${password.id}:`, error) + usages[password.id] = 0 + } + } + setPasswordUsages(usages) + } catch (error) { + console.error('Erreur lors de la récupération des utilisations:', error) + } finally { + setIsLoadingUsages(false) + } + } + + fetchPasswordUsages() + }, [passwords, isMounted]) + if (passwords.length === 0) { return (
@@ -92,149 +149,162 @@ export function PasswordList({ passwords, folders }: PasswordListProps) { return ( <>
- {passwords.map((password) => ( - - -
-
- - {password.title} - {password.breached && ( - - - - - - -

Identifiants compromis

-
-
-
- )} - {password.reused && ( - - - - - - -

Mot de passe réutilisé

-
-
-
- )} -
- {password.username} -
- - - - - - Actions - copyToClipboard(password.username)}> - Copier l'identifiant - - copyToClipboard(password.password)}> - Copier le mot de passe - - - { - setSelectedPassword(password) - }} - > - Voir les détails - - { - setSelectedPassword(password) - setShowShareDialog(true) - }} - > - Partager - - - Modifier - Supprimer - - -
-
- - {getFolderName(password.folderId)} - - {password.tags.slice(0, 2).map((tag, index) => ( - - {tag} - - ))} - {password.tags.length > 2 && ( - - +{password.tags.length - 2} - - )} -
-
- -
-
-
- Mot de passe: - - {revealedPasswords[password.id] ? "ExamplePass123" : password.password} - -
-
- - -
-
-
- Mis à jour: {password.lastUpdated} - - {password.strength === "strong" ? "Fort" : password.strength === "medium" ? "Moyen" : "Faible"} - -
- {password.notes && ( -
- - Notes -
- )} -
-
-
- ))} + {passwords.map((password) => { + if ("password" in password) { + return + } else if ("card_number" in password) { + return

Card

+ } else { + return

SSH key

+ } + // + // + //
+ //
+ // + // {password.title} + // {password.breached && ( + // + // + // + // + // + // + //

Identifiants compromis

+ //
+ //
+ //
+ // )} + // {password.reused && ( + // + // + // + // + // + // + //

Mot de passe réutilisé

+ //
+ //
+ //
+ // )} + //
+ // {password.username} + //
+ // + // + // + // + // + // Actions + // copyToClipboard(password.username, password.id)}> + // Copier l'identifiant + // + // copyToClipboard(password.password,password.id)}> + // Copier le mot de passe + // + // + // { + // setSelectedPassword(password) + // }} + // > + // Voir les détails + // + // { + // setSelectedPassword(password) + // setShowShareDialog(true) + // }} + // > + // Partager + // + // + // Modifier + // Supprimer + // + // + //
+ //
+ // + // {getFolderName(password.folderId)} + // + // {password.tags.slice(0, 2).map((tag, index) => ( + // + // {tag} + // + // ))} + // {password.tags.length > 2 && ( + // + // +{password.tags.length - 2} + // + // )} + //
+ //
+ // + //
+ //
+ //
+ // Mot de passe: + // + // {revealedPasswords[password.id] ? "ExamplePass123" : password.password} + // + //
+ //
+ // + // + //
+ //
+ //
+ // Mis à jour: {password.lastUpdated} + // + // {password.strength === "strong" ? "Fort" : password.strength === "medium" ? "Moyen" : "Faible"} + // + //
+ //
+ // Nombre de vues : + // + // {!isMounted || isLoadingUsages ? '...' : passwordUsages[password.id] ?? 0} + // + //
+ // {password.notes && ( + //
+ // + // Notes + //
+ // )} + //
+ //
+ //
+})}
{/* Password Details Dialog */} @@ -268,7 +338,7 @@ export function PasswordList({ passwords, folders }: PasswordListProps) { variant="ghost" size="icon" className="ml-2" - onClick={() => selectedPassword && copyToClipboard(selectedPassword.username)} + onClick={() => selectedPassword && copyToClipboard(selectedPassword.username, selectedPassword.id)} > @@ -302,7 +372,7 @@ export function PasswordList({ passwords, folders }: PasswordListProps) { variant="ghost" size="icon" className="ml-2" - onClick={() => copyToClipboard("ExamplePass123")} + onClick={() => selectedPassword && copyToClipboard("ExamplePass123", selectedPassword.id)} > @@ -322,7 +392,7 @@ export function PasswordList({ passwords, folders }: PasswordListProps) { readOnly className="flex-1" /> -
diff --git a/components/password-manager.tsx b/components/password-manager.tsx index c0809da..4ddf82e 100644 --- a/components/password-manager.tsx +++ b/components/password-manager.tsx @@ -11,6 +11,7 @@ import { PasswordForm } from "@/components/password-form" import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" import { Badge } from "@/components/ui/badge" import { ScrollArea } from "@/components/ui/scroll-area" +import useOrganization from "@/hooks/use-organization" // Types for our data model export interface FolderItem { @@ -38,108 +39,15 @@ export interface PasswordEntry { old?: boolean } -// Sample data -const initialFolders: FolderItem[] = [ - { id: "personal", name: "Personnel", parentId: null }, - { id: "work", name: "Travail", parentId: null }, - { id: "work-dev", name: "Développement", parentId: "work" }, - { id: "entertainment", name: "Divertissement", parentId: null }, - { id: "shopping", name: "Shopping", parentId: null }, - { id: "finance", name: "Finance", parentId: null }, - { id: "finance-banking", name: "Banque", parentId: "finance" }, - { id: "shared", name: "Partagé", parentId: null, shared: true }, - { id: "shared-team", name: "Équipe", parentId: "shared", shared: true }, -] - -const initialPasswords: PasswordEntry[] = [ - { - id: "1", - title: "Gmail", - website: "gmail.com", - username: "user@example.com", - password: "••••••••••••", - strength: "strong", - lastUpdated: "2023-12-01", - folderId: "personal", - tags: ["Important", "Email"], - notes: "Compte email principal", - customFields: [{ label: "Téléphone de récupération", value: "+33612345678", type: "text" }], - }, - { - id: "2", - title: "GitHub", - website: "github.com", - username: "devuser", - password: "••••••••", - strength: "medium", - lastUpdated: "2023-11-15", - folderId: "work-dev", - tags: ["Développement"], - reused: true, - }, - { - id: "3", - title: "Netflix", - website: "netflix.com", - username: "moviefan", - password: "•••••••••", - strength: "weak", - lastUpdated: "2023-10-20", - folderId: "entertainment", - tags: ["Streaming", "Partagé"], - breached: true, - }, - { - id: "4", - title: "Amazon", - website: "amazon.com", - username: "shopper123", - password: "•••••••••••", - strength: "strong", - lastUpdated: "2023-09-05", - folderId: "shopping", - tags: ["Shopping"], - old: true, - }, - { - id: "5", - title: "Banque Populaire", - website: "banquepopulaire.fr", - username: "client123", - password: "••••••••••••••", - strength: "strong", - lastUpdated: "2023-11-28", - folderId: "finance-banking", - tags: ["Banque", "Important"], - customFields: [ - { label: "Numéro de compte", value: "FR76 1234 5678 9012 3456 7890 123", type: "text" }, - { label: "Code secret", value: "••••", type: "password" }, - ], - }, - { - id: "6", - title: "Projet X", - website: "projet-x.com", - username: "team-member", - password: "•••••••••••", - strength: "strong", - lastUpdated: "2023-12-10", - folderId: "shared-team", - tags: ["Projet", "Équipe"], - shared: true, - }, -] - -const allTags = Array.from(new Set(initialPasswords.flatMap((p) => p.tags))) - export function PasswordManager() { - const [passwords, setPasswords] = useState(initialPasswords) - const [folders, setFolders] = useState(initialFolders) + const { folders, tags, credentials, selectedFolderId, credentialsFilter, updateSelectedFolderId, onCreateCredential, setCredentialsFilter } = useOrganization() + + const [passwords, setPasswords] = useState([]) + // const [folders, setFolders] = useState(initialFolders) const [searchQuery, setSearchQuery] = useState("") - const [selectedFolderId, setSelectedFolderId] = useState(null) + // const [selectedFolderId, setSelectedFolderId] = useState(null) const [selectedTags, setSelectedTags] = useState([]) const [showAddForm, setShowAddForm] = useState(false) - const [searchFilter, setSearchFilter] = useState<"all" | "login" | "website" | "tags">("all") // Function to get all child folder IDs recursively const getAllChildFolderIds = (folderId: string): string[] => { @@ -166,73 +74,96 @@ export function PasswordManager() { } // Filter passwords based on search query, selected folder and tags - const filteredPasswords = passwords.filter((password) => { - // Search filter - let matchesSearch = true - if (searchQuery) { - switch (searchFilter) { - case "login": - matchesSearch = password.username.toLowerCase().includes(searchQuery.toLowerCase()) - break - case "website": - matchesSearch = - password.website.toLowerCase().includes(searchQuery.toLowerCase()) || - password.title.toLowerCase().includes(searchQuery.toLowerCase()) - break - case "tags": - matchesSearch = password.tags.some((tag) => tag.toLowerCase().includes(searchQuery.toLowerCase())) - break - case "all": - default: - matchesSearch = - password.title.toLowerCase().includes(searchQuery.toLowerCase()) || - password.website.toLowerCase().includes(searchQuery.toLowerCase()) || - password.username.toLowerCase().includes(searchQuery.toLowerCase()) || - password.tags.some((tag) => tag.toLowerCase().includes(searchQuery.toLowerCase())) - } - } + // const filteredPasswords = credentials.filter((password) => { + // // Search filter + // let matchesSearch = true + // if (searchQuery) { + // switch (searchFilter) { + // case "login": + // if ("password" in password && typeof password.password === "string") { + // matchesSearch = password.user_identifier.toLowerCase().includes(searchQuery.toLowerCase()) + // } else { + // matchesSearch = false + // } + // break + // case "website": + // if ("website" in password && typeof password.website === "string") { + // matchesSearch = password.website.toLowerCase().includes(searchQuery.toLowerCase()) + // } else { + // matchesSearch = false + // } + // // matchesSearch = + // // password.website.toLowerCase().includes(searchQuery.toLowerCase()) || + // // password.title.toLowerCase().includes(searchQuery.toLowerCase()) + // break + // case "tags": + + // matchesSearch = password.tags.some((tag) => tag.toLowerCase().includes(searchQuery.toLowerCase())) + // break + // case "all": + // default: + // matchesSearch = + // password.title.toLowerCase().includes(searchQuery.toLowerCase()) || + // password.website.toLowerCase().includes(searchQuery.toLowerCase()) || + // password.username.toLowerCase().includes(searchQuery.toLowerCase()) || + // password.tags.some((tag) => tag.toLowerCase().includes(searchQuery.toLowerCase())) + // } + // } - // Folder filter - let matchesFolder = true - if (selectedFolderId) { - const folderIds = [selectedFolderId, ...getAllChildFolderIds(selectedFolderId)] - matchesFolder = folderIds.includes(password.folderId) - } + // // Folder filter + // let matchesFolder = true + // if (selectedFolderId) { + // const folderIds = [selectedFolderId, ...getAllChildFolderIds(selectedFolderId)] + // matchesFolder = folderIds.includes(password.folderId) + // } - // Tags filter - let matchesTags = true - if (selectedTags.length > 0) { - matchesTags = selectedTags.every((tag) => password.tags.includes(tag)) - } + // // Tags filter + // let matchesTags = true + // if (selectedTags.length > 0) { + // matchesTags = selectedTags.every((tag) => password.tags.includes(tag)) + // } - return matchesSearch && matchesFolder && matchesTags - }) + // return matchesSearch && matchesFolder && matchesTags + // }) // Add a new password - const handleAddPassword = (newPassword: Partial) => { - const passwordEntry: PasswordEntry = { - id: (passwords.length + 1).toString(), - title: newPassword.title || "", - website: newPassword.website || "", - username: newPassword.username || "", - password: newPassword.password || "", - strength: newPassword.strength || "medium", - lastUpdated: new Date().toISOString().split("T")[0], - folderId: newPassword.folderId || "", - tags: newPassword.tags || [], - notes: newPassword.notes, - customFields: newPassword.customFields, - files: newPassword.files, - } + const handleAddPassword = (title: string, domaine: string | null, userIdentifier: string, password: string, folderId: string) => { + // const passwordEntry: PasswordEntry = { + // id: (passwords.length + 1).toString(), + // title: password.title || "", + // website: password.website || "", + // username: newPassword.username || "", + // password: newPassword.password || "", + // strength: newPassword.strength || "medium", + // lastUpdated: new Date().toISOString().split("T")[0], + // folderId: newPassword.folderId || "", + // tags: newPassword.tags || [], + // notes: newPassword.notes, + // customFields: newPassword.customFields, + // files: newPassword.files, + // } + + // setPasswords([...passwords, passwordEntry]) - setPasswords([...passwords, passwordEntry]) + onCreateCredential(folderId || "", "password", { + title, + domain_name: domaine, + user_identifier: userIdentifier, + password, + custom_fields: { + additionalProp1: "", + additionalProp2: "", + additionalProp3: "", + }, + note: "" + }) setShowAddForm(false) } // Add a new folder const handleAddFolder = (name: string, parentId: string | null) => { - const newId = name.toLowerCase().replace(/\s+/g, "-") + "-" + Date.now().toString(36) - setFolders([...folders, { id: newId, name, parentId }]) + // const newId = name.toLowerCase().replace(/\s+/g, "-") + "-" + Date.now().toString(36) + // setFolders([...folders, { id: newId, name, parentId }]) } // Toggle tag selection @@ -267,12 +198,7 @@ export function PasswordManager() { - + @@ -283,14 +209,14 @@ export function PasswordManager() {
- {allTags.map((tag) => ( + {tags.map((tag) => ( toggleTag(tag)} + // onClick={() => toggleTag(tag)} > - {tag} + {tag.name} ))}
@@ -346,19 +272,19 @@ export function PasswordManager() { onChange={(e) => setSearchQuery(e.target.value)} />
- setSearchFilter(v as any)}> + setCredentialsFilter(v as any)}> Tout - - Login + + Mots de passe - - Site + + Cartes de crédit - - Tags + + Clés SSH @@ -375,7 +301,7 @@ export function PasswordManager() { ))} - @@ -422,12 +348,13 @@ export function PasswordManager() { setShowAddForm(false)} - allTags={allTags} + allTags={tags.map((tag) => tag.name)} /> ) : ( - + )} diff --git a/components/security-dashboard.tsx b/components/security-dashboard.tsx index 89d613a..9650f80 100644 --- a/components/security-dashboard.tsx +++ b/components/security-dashboard.tsx @@ -1,50 +1,151 @@ "use client" -import { useState } from "react" +import { useState, useEffect } from "react" import { AlertTriangle, Shield, RefreshCw, Clock, CheckCircle2 } from "lucide-react" import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card" import { Button } from "@/components/ui/button" import { Progress } from "@/components/ui/progress" import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" import { Badge } from "@/components/ui/badge" +import { analyzePasswords, SecurityMetrics } from "@/lib/api/security" +import { useCredentials } from "@/hooks/use-credentials" +import { toast } from "sonner" export function SecurityDashboard() { - const [securityScore, setSecurityScore] = useState(68) - const [lastScan, setLastScan] = useState("2023-12-15") + const [securityScore, setSecurityScore] = useState(0) + const [lastScan, setLastScan] = useState(null) + const [isAnalyzing, setIsAnalyzing] = useState(false) + const [metrics, setMetrics] = useState(() => { + // Récupérer les métriques du localStorage au chargement initial + if (typeof window !== 'undefined') { + const savedMetrics = localStorage.getItem('securityMetrics') + const savedLastScan = localStorage.getItem('lastSecurityScan') + if (savedMetrics && savedLastScan) { + setLastScan(savedLastScan) + return JSON.parse(savedMetrics) + } + } + return { + weakPasswords: {}, + strongPasswords: {}, + reusedPasswords: {}, + oldPasswords: {}, + breachedPasswords: {}, + } + }) - const weakPasswords = [ - { id: "1", title: "Netflix", username: "moviefan", website: "netflix.com", lastUpdated: "2023-10-20" }, - { id: "2", title: "Twitter", username: "socialuser", website: "twitter.com", lastUpdated: "2023-08-12" }, - ] + const { credentials, isLoading: isLoadingCredentials } = useCredentials() - const reusedPasswords = [ - { id: "3", title: "GitHub", username: "devuser", website: "github.com", lastUpdated: "2023-11-15" }, - { id: "4", title: "GitLab", username: "devuser", website: "gitlab.com", lastUpdated: "2023-09-22" }, - ] + const analyzeSecurity = async () => { + if (credentials.length === 0) { + toast.error("Aucun credential à analyser") + return + } - const oldPasswords = [ - { id: "5", title: "Amazon", username: "shopper123", website: "amazon.com", lastUpdated: "2023-09-05" }, - { id: "6", title: "eBay", username: "bidder42", website: "ebay.com", lastUpdated: "2023-07-18" }, - ] + try { + setIsAnalyzing(true) + const result = await analyzePasswords(credentials) + setMetrics(result) + const scanTime = new Date().toISOString() + setLastScan(scanTime) + + // Sauvegarder les métriques et le timestamp dans le localStorage + localStorage.setItem('securityMetrics', JSON.stringify(result)) + localStorage.setItem('lastSecurityScan', scanTime) + + toast.success("Analyse de sécurité terminée") + } catch (error) { + console.error("Erreur lors de l'analyse:", error) + toast.error("Erreur lors de l'analyse de sécurité") + } finally { + setIsAnalyzing(false) + } + } - const breachedAccounts = [ - { - id: "7", - title: "LinkedIn", - username: "professional", - website: "linkedin.com", - breachDate: "2023-11-30", - severity: "high", - }, - { - id: "8", - title: "Dropbox", - username: "fileuser", - website: "dropbox.com", - breachDate: "2023-10-05", - severity: "medium", - }, - ] + // Calculer le score de sécurité + useEffect(() => { + if (credentials.length === 0) return + + const totalPasswords = credentials.length + const weakCount = Object.keys(metrics.weakPasswords).length + const reusedCount = Object.keys(metrics.reusedPasswords).length + const oldCount = Object.keys(metrics.oldPasswords).length + const breachedCount = Object.keys(metrics.breachedPasswords).length + + // Calcul basé sur la proportion de chaque type de problème + const weakPenalty = (weakCount / totalPasswords) * 30 // 30% de pénalité max pour les mots de passe faibles + const reusedPenalty = (reusedCount / totalPasswords) * 25 // 25% de pénalité max pour les réutilisations + const oldPenalty = (oldCount / totalPasswords) * 20 // 20% de pénalité max pour les mots de passe anciens + const breachedPenalty = (breachedCount / totalPasswords) * 25 // 25% de pénalité max pour les compromissions + + // Score final en soustrayant les pénalités de 100 + const score = Math.max( + 0, + Math.min( + 100, + Math.round(100 - (weakPenalty + reusedPenalty + oldPenalty + breachedPenalty)) + ) + ) + + setSecurityScore(score) + }, [metrics, credentials]) + + // Transformer les données pour l'affichage + const weakPasswordsList = Object.entries(metrics.weakPasswords).flatMap(([password, ids]) => + ids.map(id => { + const credential = credentials.find(c => c.id === id) + return { + id, + title: credential?.title || "Inconnu", + username: credential?.username || "Inconnu", + website: credential?.website || "Inconnu", + lastUpdated: credential?.lastUpdated || "Inconnu", + } + }) + ) + + const reusedPasswordsList = Object.entries(metrics.reusedPasswords).flatMap(([password, ids]) => + ids.map(id => { + const credential = credentials.find(c => c.id === id) + return { + id, + title: credential?.title || "Inconnu", + username: credential?.username || "Inconnu", + website: credential?.website || "Inconnu", + lastUpdated: credential?.lastUpdated || "Inconnu", + } + }) + ) + + const oldPasswordsList = Object.entries(metrics.oldPasswords).flatMap(([password, ids]) => + ids.map(id => { + const credential = credentials.find(c => c.id === id) + return { + id, + title: credential?.title || "Inconnu", + username: credential?.username || "Inconnu", + website: credential?.website || "Inconnu", + lastUpdated: credential?.lastUpdated || "Inconnu", + } + }) + ) + + const breachedPasswordsList = Object.entries(metrics.breachedPasswords).flatMap(([password, ids]) => + ids.map(id => { + const credential = credentials.find(c => c.id === id) + return { + id, + title: credential?.title || "Inconnu", + username: credential?.username || "Inconnu", + website: credential?.website || "Inconnu", + lastUpdated: credential?.lastUpdated || "Inconnu", + } + }) + ) + + if (isLoadingCredentials) { + return
Chargement...
+ } return (
@@ -77,9 +178,9 @@ export function SecurityDashboard() { -
{weakPasswords.length}
+
{weakPasswordsList.length}

- {weakPasswords.length === 0 + {weakPasswordsList.length === 0 ? "Tous vos mots de passe sont forts" : "Mots de passe nécessitant une amélioration"}

@@ -91,9 +192,9 @@ export function SecurityDashboard() { -
{reusedPasswords.length}
+
{reusedPasswordsList.length}

- {reusedPasswords.length === 0 + {reusedPasswordsList.length === 0 ? "Aucun mot de passe n'est réutilisé" : "Mots de passe utilisés sur plusieurs sites"}

@@ -101,15 +202,15 @@ export function SecurityDashboard() { - Comptes compromis + Mots de passe compromis -
{breachedAccounts.length}
+
{breachedPasswordsList.length}

- {breachedAccounts.length === 0 - ? "Aucun compte compromis détecté" - : "Comptes détectés dans des fuites de données"} + {breachedPasswordsList.length === 0 + ? "Aucun mot de passe compromis" + : "Mots de passe trouvés dans des fuites de données"}

@@ -119,7 +220,11 @@ export function SecurityDashboard() { Analyse de sécurité - Dernière analyse: {lastScan} + + {lastScan + ? `Dernière analyse: ${new Date(lastScan).toLocaleString('fr-FR')}` + : "Aucune analyse effectuée"} + @@ -130,7 +235,7 @@ export function SecurityDashboard() { Compromis - {weakPasswords.length === 0 ? ( + {weakPasswordsList.length === 0 ? (
@@ -139,7 +244,7 @@ export function SecurityDashboard() {
) : ( - weakPasswords.map((password) => ( + weakPasswordsList.map((password) => (
{password.title}
@@ -151,7 +256,7 @@ export function SecurityDashboard() { )} - {reusedPasswords.length === 0 ? ( + {reusedPasswordsList.length === 0 ? (
@@ -160,7 +265,7 @@ export function SecurityDashboard() {
) : ( - reusedPasswords.map((password) => ( + reusedPasswordsList.map((password) => (
{password.title}
@@ -172,22 +277,25 @@ export function SecurityDashboard() { )} - {oldPasswords.length === 0 ? ( + {oldPasswordsList.length === 0 ? (
-

Tous vos mots de passe sont récents

+

Aucun mot de passe ancien

- Vous mettez régulièrement à jour vos mots de passe. + Tous vos mots de passe ont été mis à jour récemment.

) : ( - oldPasswords.map((password) => ( + oldPasswordsList.map((password) => (
{password.title}
-
Mis à jour: {password.lastUpdated}
+
{password.username}
+
+ Dernière mise à jour: {password.lastUpdated} +
@@ -195,29 +303,27 @@ export function SecurityDashboard() { )}
- {breachedAccounts.length === 0 ? ( + {breachedPasswordsList.length === 0 ? (
-

Aucun compte compromis

+

Aucun mot de passe compromis

- Vos comptes n'ont pas été détectés dans des fuites de données. + Aucun de vos mots de passe n'a été trouvé dans les fuites de données connues.

) : ( - breachedAccounts.map((account) => ( -
+ breachedPasswordsList.map((password) => ( +
-
{account.title}
-
Fuite détectée: {account.breachDate}
-
-
- - {account.severity === "high" ? "Critique" : "Moyen"} - - +
{password.title}
+
{password.username}
+
+ Ce mot de passe a été compromis dans une fuite de données +
+
)) )} @@ -225,9 +331,14 @@ export function SecurityDashboard() { - diff --git a/hooks/use-credentials.ts b/hooks/use-credentials.ts new file mode 100644 index 0000000..e0b1378 --- /dev/null +++ b/hooks/use-credentials.ts @@ -0,0 +1,37 @@ +import { useState, useEffect } from 'react' +import { Credential } from '@/types/credential' +import { mockCredentials } from '@/lib/mocks/credentials' + +export function useCredentials() { + const [credentials, setCredentials] = useState([]) + const [isLoading, setIsLoading] = useState(true) + + useEffect(() => { + const fetchCredentials = async () => { + try { + // En développement, utiliser les données mockées + if (process.env.NODE_ENV === 'development') { + setCredentials(mockCredentials) + setIsLoading(false) + return + } + + // En production, faire l'appel API + const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/credentials`) + if (!response.ok) { + throw new Error('Erreur lors de la récupération des credentials') + } + const data = await response.json() + setCredentials(data) + } catch (error) { + console.error('Erreur:', error) + } finally { + setIsLoading(false) + } + } + + fetchCredentials() + }, []) + + return { credentials, isLoading } +} \ No newline at end of file diff --git a/hooks/use-organization.ts b/hooks/use-organization.ts new file mode 100644 index 0000000..1b47e42 --- /dev/null +++ b/hooks/use-organization.ts @@ -0,0 +1,8 @@ +import OrganizationContext, { OrganizationProviderProps } from "@/providers/organization-provider"; +import { use } from "react"; + +const useOrganization = (): OrganizationProviderProps => { + return use(OrganizationContext); +} + +export default useOrganization; \ No newline at end of file diff --git a/lib/api/credentials.ts b/lib/api/credentials.ts new file mode 100644 index 0000000..5d94199 --- /dev/null +++ b/lib/api/credentials.ts @@ -0,0 +1,183 @@ +import { CardCredential, GetCredentialsResponse, PasswordCredential, SSHKeyCredential } from "@/types/credential"; + +export async function getCredentials(folderId: string, type: 'password' | 'card' | 'sshkey'): Promise<(CardCredential | PasswordCredential | SSHKeyCredential)[]> { + const baseUrl = process.env.NEXT_PUBLIC_ORGANIZATION_URL || 'http://localhost:3001'; + + try { + const response = await fetch(`${baseUrl}/folders/${folderId}/credentials/${type}?page=1&limit=100`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + body: null, + }); + + if (!response.ok) { + throw new Error('Erreur lors de la récupération des identifiants'); + } + + const data: GetCredentialsResponse = await response.json(); + + return data.credentials.map((cred: any) => { + if (type === 'password') { + return { + id: cred.id, + title: cred.title, + note: cred.note || null, + created_at: cred.created_at, + updated_at: cred.updated_at, + expires_at: cred.expires_at || null, + last_read_at: cred.last_read_at || null, + custom_fields: cred.custom_fields || null, + username: cred.username, + password: cred.password, + url: cred.url || null, + user_identifier: cred.user_identifier || null, + domain_name: cred.domain_name || null, + } as PasswordCredential; + } else if (type === 'card') { + return { + id: cred.id, + title: cred.title, + note: cred.note || null, + created_at: cred.created_at, + updated_at: cred.updated_at, + expires_at: cred.expires_at || null, + last_read_at: cred.last_read_at || null, + custom_fields: cred.custom_fields || null, + card_number: cred.card_number, + card_holder: cred.card_holder, + expiration_date: cred.expiration_date, + cvv: cred.cvv, + owner_name: cred.owner_name, + cvc: cred.cvc, + } as CardCredential; + } else if (type === 'sshkey') { + return { + id: cred.id, + title: cred.title, + note: cred.note || null, + created_at: cred.created_at, + updated_at: cred.updated_at, + expires_at: cred.expires_at || null, + last_read_at: cred.last_read_at || null, + custom_fields: cred.custom_fields || null, + private_key: cred.private_key, + public_key: cred.public_key, + hostname: cred.hostname, + user_identifier: cred.user_identifier, + } as SSHKeyCredential; + } else { + throw new Error('Unknown credential type'); + } + }); + } catch (error) { + console.error('Erreur lors de la récupération des identifiants:', error); + throw error; + } +} + +export async function getUserCredentials(userId: string, type?: 'all' | 'password' | 'card' | 'sshkey'): Promise<(CardCredential | PasswordCredential | SSHKeyCredential)[]> { + const baseUrl = process.env.NEXT_PUBLIC_ORGANIZATION_URL || 'http://localhost:3001'; + + try { + const url = `${baseUrl}/users/credentials?user_id=${userId}` + (type ? `&type=${type}` : ''); + const response = await fetch(url, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + }); + + if (!response.ok) { + throw new Error('Erreur lors de la récupération des identifiants de l\'utilisateur'); + } + + const data: (CardCredential | PasswordCredential | SSHKeyCredential)[] = await response.json(); + + return data.map((cred: any) => { + if (type === 'password') { + return { + id: cred.id, + title: cred.title, + note: cred.note || null, + created_at: cred.created_at, + updated_at: cred.updated_at, + expires_at: cred.expires_at || null, + last_read_at: cred.last_read_at || null, + custom_fields: cred.custom_fields || null, + username: cred.username, + password: cred.password, + url: cred.url || null, + user_identifier: cred.user_identifier || null, + domain_name: cred.domain_name || null, + } as PasswordCredential; + } else if (type === 'card') { + return { + id: cred.id, + title: cred.title, + note: cred.note || null, + created_at: cred.created_at, + updated_at: cred.updated_at, + expires_at: cred.expires_at || null, + last_read_at: cred.last_read_at || null, + custom_fields: cred.custom_fields || null, + card_number: cred.card_number, + card_holder: cred.card_holder, + expiration_date: cred.expiration_date, + cvv: cred.cvv, + owner_name: cred.owner_name, + cvc: cred.cvc, + } as CardCredential; + } else if (type === 'sshkey') { + return { + id: cred.id, + title: cred.title, + note: cred.note || null, + created_at: cred.created_at, + updated_at: cred.updated_at, + expires_at: cred.expires_at || null, + last_read_at: cred.last_read_at || null, + custom_fields: cred.custom_fields || null, + private_key: cred.private_key, + public_key: cred.public_key, + hostname: cred.hostname, + user_identifier: cred.user_identifier, + } as SSHKeyCredential; + } else if (type === 'all') { + throw new Error('Type "all" is not supported for user credentials. Please specify a type.'); + } else if (type === undefined) { + return cred as (CardCredential | PasswordCredential | SSHKeyCredential); + } else { + throw new Error(`Unknown credential type '${type}'`); + } + }); + } catch (error) { + console.error('Erreur lors de la récupération des identifiants de l\'utilisateur:', error); + throw error; + } +} + +export async function createCredential(folderId: string, type: string, credentialData: any): Promise { + const baseUrl = process.env.NEXT_PUBLIC_ORGANIZATION_URL || 'http://localhost:3001'; + + try { + const response = await fetch(`${baseUrl}/folders/${folderId}/credentials/${type}`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(credentialData), + }); + + if (!response.ok) { + throw new Error('Erreur lors de la création de l\'identifiant'); + } + + const data: CardCredential | PasswordCredential | SSHKeyCredential = await response.json(); + return data; + } catch (error) { + console.error('Erreur lors de la création de l\'identifiant:', error); + throw error; + } +} \ No newline at end of file diff --git a/lib/api/folders.ts b/lib/api/folders.ts new file mode 100644 index 0000000..0d14e7d --- /dev/null +++ b/lib/api/folders.ts @@ -0,0 +1,81 @@ +import { Folder, GetFoldersResponse, PostFolderRequest, PostFolderResponse } from '@/types/folder' + +export async function getFolders(userId?: string, page?: number, limit?: number): Promise { + const baseUrl = process.env.NEXT_PUBLIC_ORGANIZATION_URL || 'http://localhost:8000' + const url = new URL(`${baseUrl}/folders`) + if (userId) url.searchParams.append('user_id', userId) + if (page) url.searchParams.append('page', String(page)) + if (limit) url.searchParams.append('limit', String(limit)) + + try { + const response = await fetch(url.toString(), { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + body: null, + }) + if (!response.ok) { + throw new Error('Erreur lors de la récupération des dossiers') + } + + const data: GetFoldersResponse = await response.json() + + return data.folders.map(folder => ({ + id: folder.Id, + name: folder.Name, + description: folder.Description || null, + icon: folder.Icon || null, + parentId: folder.ParentID || null, + createdBy: folder.CreatedBy || undefined, + })) + } catch (error) { + console.error('Erreur lors de la récupération des dossiers:', error) + throw error + } +} + +export async function createFolder(data: PostFolderRequest): Promise { + const baseUrl = process.env.NEXT_PUBLIC_ORGANIZATION_URL || 'http://localhost:3001' + const response = await fetch(`${baseUrl}/folders`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(data), + }) + + if (!response.ok) { + throw new Error('Erreur lors de la création du dossier') + } + + const responseData: PostFolderResponse = await response.json() + + return { + id: responseData.Id, + name: responseData.Name, + description: responseData.Description || null, + icon: responseData.Icon || null, + parentId: responseData.ParentID || null, + createdBy: responseData.CreatedBy || undefined, + } +} + +export async function updateFolder(id: string, data: Partial): Promise { + const baseUrl = process.env.NEXT_PUBLIC_ORGANIZATION_URL || 'http://localhost:3001' + const response = await fetch(`${baseUrl}/folders/${id}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(data), + }) + if (!response.ok) { + throw new Error('Erreur lors de la mise à jour du dossier') + } + return await response.json() +} + +export async function deleteFolder(id: string): Promise { + const baseUrl = process.env.NEXT_PUBLIC_ORGANIZATION_URL || 'http://localhost:3001' + const response = await fetch(`${baseUrl}/folders/${id}`, { method: 'DELETE' }) + if (!response.ok) { + throw new Error('Erreur lors de la suppression du dossier') + } +} \ No newline at end of file diff --git a/lib/api/security.ts b/lib/api/security.ts new file mode 100644 index 0000000..3f9efac --- /dev/null +++ b/lib/api/security.ts @@ -0,0 +1,90 @@ +import { Credential } from '@/types/credential' + +export interface SecurityMetrics { + weakPasswords: { + [password: string]: string[] + } + strongPasswords: { + [password: string]: string[] + } + reusedPasswords: { + [password: string]: string[] + } + oldPasswords: { + [password: string]: string[] + } + breachedPasswords: { + [password: string]: string[] + } +} + +export async function analyzePasswords(credentials: Credential[]): Promise { + try { + const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/statistics/analyze`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(credentials), + }) + + if (!response.ok) { + throw new Error('Erreur lors de l\'analyse des mots de passe') + } + + return await response.json() + } catch (error) { + console.error('Erreur lors de l\'analyse des mots de passe:', error) + throw error + } +} + +export async function getPasswordUsage(passwordId: string): Promise { + // Récupérer les métriques d'analyse + const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/statistics/analyze`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify([{ + id: passwordId, + password: 'dummy-password', // Le mot de passe n'est pas utilisé pour le comptage + lastUpdated: new Date().toISOString().split('T')[0] + }]) + }) + + if (!response.ok) { + throw new Error('Erreur lors de la récupération de l\'utilisation du mot de passe') + } + + const data = await response.json() + // Le nombre d'utilisations est stocké dans les métriques de réutilisation + const reusedCount = Object.values(data.reusedPasswords || {}).flat().length + return reusedCount +} + +export async function usePassword(passwordId: string): Promise { + try { + const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/statistics/analyze`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify([{ + id: passwordId, + password: 'dummy-password', // Le mot de passe n'est pas utilisé pour le comptage + lastUpdated: new Date().toISOString().split('T')[0] + }]) + }) + + if (!response.ok) { + throw new Error('Erreur lors de l\'utilisation des mots de passe') + } + + await response.json() + } catch (error) { + console.error('Erreur lors de l\'utilisation des mots de passe:', error) + throw error + } +} + diff --git a/lib/api/tags.ts b/lib/api/tags.ts new file mode 100644 index 0000000..845c5e9 --- /dev/null +++ b/lib/api/tags.ts @@ -0,0 +1,72 @@ +import { GetTagsResponse, PostTagRequest, Tag } from '@/types/tag' + +export async function getTags(): Promise { + const baseUrl = process.env.NEXT_PUBLIC_ORGANIZATION_URL || 'http://localhost:3001' + + try { + const response = await fetch(`${baseUrl}/tags`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + body: null + }) + if (!response.ok) { + throw new Error('Erreur lors de la récupération des tags') + } + + const data: GetTagsResponse[] = await response.json() + + return data.map(tag => ({ + id: tag.id, + name: tag.name, + color: tag.color, + folderId: tag.folder_id, + createdBy: tag.created_by, + })) + } catch (error) { + console.error('Erreur lors de la récupération des tags:', error) + throw error + } +} + +export async function createTag(data: PostTagRequest): Promise { + const baseUrl = process.env.NEXT_PUBLIC_ORGANIZATION_URL || 'http://localhost:3001' + + try { + const response = await fetch(`${baseUrl}/tags`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(data), + }) + + if (!response.ok) { + throw new Error('Erreur lors de la création du tag') + } + + } catch (error) { + console.error('Erreur lors de la création du tag:', error) + throw error + } +} + +export async function updateTag(id: string, data: Partial): Promise { + const baseUrl = process.env.NEXT_PUBLIC_ORGANIZATION_URL || 'http://localhost:3001' + const response = await fetch(`${baseUrl}/tags/${id}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(data), + }) + if (!response.ok) { + throw new Error('Erreur lors de la mise à jour du tag') + } + return await response.json() +} + +export async function deleteTag(id: string): Promise { + const baseUrl = process.env.NEXT_PUBLIC_ORGANIZATION_URL || 'http://localhost:3001' + const response = await fetch(`${baseUrl}/tags/${id}`, { method: 'DELETE' }) + if (!response.ok) { + throw new Error('Erreur lors de la suppression du tag') + } +} \ No newline at end of file diff --git a/lib/mocks/credentials.ts b/lib/mocks/credentials.ts new file mode 100644 index 0000000..ce7af29 --- /dev/null +++ b/lib/mocks/credentials.ts @@ -0,0 +1,89 @@ +import { Credential } from '@/types/credential' + +export const mockCredentials: Credential[] = [ + // Mots de passe faibles (3) + { + id: '1', + title: 'Compte Gmail', + username: 'john.doe@gmail.com', + website: 'gmail.com', + password: 'password123', + lastUpdated: '2025-01-15' + }, + { + id: '2', + title: 'Compte Facebook', + username: 'john.doe', + website: 'facebook.com', + password: '12345678', + lastUpdated: '2025-01-20' + }, + { + id: '3', + title: 'Compte Twitter', + username: '@johndoe', + website: 'twitter.com', + password: 'qwerty123', + lastUpdated: '2025-01-25' + }, + + // Mots de passe identiques (2) + { + id: '4', + title: 'Compte GitHub', + username: 'johndoe', + website: 'github.com', + password: 'SecurePass123!', + lastUpdated: '2025-01-10' + }, + { + id: '5', + title: 'Compte GitLab', + username: 'john.doe', + website: 'gitlab.com', + password: 'SecurePass123!', + lastUpdated: '2025-01-12' + }, + + // Mots de passe forts (5) + { + id: '6', + title: 'Compte AWS', + username: 'john.doe@company.com', + website: 'aws.amazon.com', + password: 'Kj#9mP$2vL@5nX', + lastUpdated: '2025-01-05' + }, + { + id: '7', + title: 'Compte Azure', + username: 'john.doe@company.com', + website: 'portal.azure.com', + password: 'P@ssw0rd!2024#Secure', + lastUpdated: '2025-01-08' + }, + { + id: '8', + title: 'Compte Google Cloud', + username: 'john.doe@company.com', + website: 'cloud.google.com', + password: 'Str0ng!P@ssw0rd2024', + lastUpdated: '2020-12-15' // Ancien mot de passe + }, + { + id: '9', + title: 'Compte Digital Ocean', + username: 'john.doe@company.com', + website: 'digitalocean.com', + password: 'C0mpl3x!P@ssw0rd#2024', + lastUpdated: '2025-01-18' + }, + { + id: '10', + title: 'Compte Heroku', + username: 'john.doe@company.com', + website: 'heroku.com', + password: 'S3cur3!P@ssw0rd@2024', + lastUpdated: '2025-01-22' + } +] \ No newline at end of file diff --git a/providers/organization-provider.tsx b/providers/organization-provider.tsx new file mode 100644 index 0000000..3c45075 --- /dev/null +++ b/providers/organization-provider.tsx @@ -0,0 +1,216 @@ +'use client' + +import { createCredential, getCredentials, getUserCredentials } from "@/lib/api/credentials"; +import { createFolder, getFolders } from "@/lib/api/folders"; +import { createTag, getTags } from "@/lib/api/tags"; +import { CardCredential, PasswordCredential, SSHKeyCredential } from "@/types/credential"; +import { Folder } from "@/types/folder"; +import { Tag } from "@/types/tag"; +import { createContext, useEffect, useState } from "react"; + +export interface OrganizationProviderProps { + folders: Folder[]; + tags: Tag[]; + credentials: (CardCredential | PasswordCredential | SSHKeyCredential)[]; + selectedFolderId?: string; + credentialsFilter: 'all' | 'password' | 'card' | 'sshkey', + loadings: OrganizationLoadings; + onCreateFolder: (folderName: string, parentId: string | null) => void; + // onUpdateFolder: (id: string, data: Partial) => void; + // onDeleteFolder: (id: string) => void; + onCreateTag: (tagName: string, color: string, folderId: string) => void; + // onUpdateTag: (id: string, data: Partial) => void; + // onDeleteTag: (id: string) => void; + onCreateCredential: (folderId: string, type: string, credentialData: any) => void; + updateSelectedFolderId: (folderId: string | null) => void; + setCredentialsFilter: (filter: 'all' | 'password' | 'card' | 'sshkey') => void; + reloadData: (credentialType: 'all' | 'password' | 'card' | 'sshkey', folderId?: string) => void; +} + +export interface OrganizationLoadings { + foldersLoading: boolean; + tagsLoading: boolean; + credentialsLoading: boolean; +} + +const OrganizationContext = createContext({ + folders: [], + tags: [], + credentials: [], + selectedFolderId: undefined, + credentialsFilter: 'all', + loadings: { + foldersLoading: false, + tagsLoading: false, + credentialsLoading: false, + }, + onCreateFolder: (folderName: string, parentId: string | null) => {}, + // onUpdateFolder: (id: string, data: Partial) => {}, + // onDeleteFolder: (id: string) => {}, + onCreateTag: (tagName: string, color: string, folderId: string) => {}, + onCreateCredential: (folderId: string, type: string, credentialData: any) => {}, + updateSelectedFolderId: (folderId: string | null) => {}, + setCredentialsFilter: (filter: 'all' | 'password' | 'card' | 'sshkey') => {}, + reloadData: (credentialType: 'all' | 'password' | 'card' | 'sshkey', folderId?: string) => {}, +}); + +export const OrganizationProvider = ({ children }: any) => { + const [folders, setFolders] = useState([]); + const [tags, setTags] = useState([]); + const [credentials, setCredentials] = useState<(CardCredential | PasswordCredential | SSHKeyCredential)[]>([]); + const [selectedFolderId, setSelectedFolderId] = useState(undefined); + const [credentialsFilter, setCredentialsFilter] = useState<'all' | 'password' | 'card' | 'sshkey'>('all'); + const [loadings, setLoadings] = useState({ + foldersLoading: false, + tagsLoading: false, + credentialsLoading: false, + }); + + const loadFolders = async () => { + setLoadings((prev) => ({ ...prev, foldersLoading: true })); + try { + const folders = await getFolders("Baptiste", 1, 100); + setFolders(folders); + } catch (error) { + console.error("Error loading folders:", error); + } finally { + setLoadings((prev) => ({ ...prev, foldersLoading: false })); + } + } + + const loadTags = async () => { + setLoadings((prev) => ({ ...prev, tagsLoading: true })); + try { + const tags = await getTags(); + setTags(tags); + } catch (error) { + console.error("Error loading tags:", error); + } finally { + setLoadings((prev) => ({ ...prev, tagsLoading: false })); + } + } + + const loadCredentials = async (type: 'all' | 'password' | 'card' | 'sshkey', folderId?: string) => { + setLoadings((prev) => ({ ...prev, credentialsLoading: true })); + try { + if (type === 'all') { + if (!folderId) { + const credentials = await getUserCredentials("Baptiste"); + setCredentials(credentials); + } else { + const credentials1 = await getCredentials(folderId, 'password'); + setCredentials(credentials1); + const credentials2 = await getCredentials(folderId, 'card'); + setCredentials((prev) => [...prev, ...credentials2]); + const credentials3 = await getCredentials(folderId, 'sshkey'); + setCredentials((prev) => [...prev, ...credentials3]); + } + } + else { + if (!folderId) { + const credentials = await getUserCredentials("Baptiste", type); + setCredentials(credentials); + } else { + const credentials = await getCredentials(folderId, type); + setCredentials(credentials); + } + } + } catch (error) { + console.error("Error loading credentials:", error); + console.log("credntial type:", type); + } finally { + setLoadings((prev) => ({ ...prev, credentialsLoading: false })); + } + } + + const loadData = async (credentialType: 'all' | 'password' | 'card' | 'sshkey', folderId?: string) => { + loadFolders(); + + loadTags(); + + loadCredentials(credentialType, folderId); + } + + const updateSelectedFolderId = (folderId: string | null) => { + setSelectedFolderId(folderId ?? undefined); + console.log(`Selected folder ID updated to: ${folderId}`); + } + + useEffect(() => { + loadData(credentialsFilter, selectedFolderId) + }, []) + + useEffect(() => { + loadCredentials(credentialsFilter, selectedFolderId); + }, [selectedFolderId, credentialsFilter]); + + const onCreateFolder = async (folderName: string, parentId: string | null) => { + try { + const newFolder = await createFolder({ name: folderName, parent_id: parentId, description: null, icon: null, created_by: "Baptiste" }); + setFolders((prev) => [...prev, newFolder]); + } catch (error) { + console.error("Error creating folder:", error); + } + }; + // const onUpdateFolder = async (id: string, data: Partial) => { + // try { + // // const updatedFolder = await updateFolder(id, data); + // // setFolders((prev) => + // // prev.map((folder) => (folder.id === id ? updatedFolder : folder)) + // // ); + // } catch (error) { + // console.error("Error updating folder:", error); + // } + // }; + // const onDeleteFolder = async (id: string) => { + // try { + // // await deleteFolder(id); + // // setFolders((prev) => prev.filter((folder) => folder.id !== id)); + // } catch (error) { + // console.error("Error deleting folder:", error); + // } + // }; + + const onCreateTag = async (tagName: string, color: string, folderId: string) => { + try { + const newTag = await createTag({ name: tagName, color, folder_id: folderId, created_by: "Baptiste" }); + loadTags(); + } catch (error) { + console.error("Error creating tag:", error); + } + }; + + const onCreateCredential = async (folderId: string, type: string, credentialData: any) => { + try { + const newCredential = await createCredential(folderId, type, credentialData); + setCredentials((prev) => [...prev, newCredential]); + } catch (error) { + console.error("Error creating credential:", error); + } + } + + return ( + + {children} + + ); +} + +export default OrganizationContext; \ No newline at end of file diff --git a/types/credential.ts b/types/credential.ts new file mode 100644 index 0000000..70844fe --- /dev/null +++ b/types/credential.ts @@ -0,0 +1,37 @@ +export interface Credential { + id: string; + title: string; + note: string | null; + created_at: string; + updated_at: string; + expires_at: string | null; + last_read_at: string | null; + custom_fields: Record | null; +} + +export interface CardCredential extends Credential { + owner_name: string; + cvc: number; + expiration_date: string; + card_number: string; +} + +export interface PasswordCredential extends Credential { + user_identifier: string; + password: string; + domain_name: string; +} + +export interface SSHKeyCredential extends Credential { + private_key: string; + public_key: string; + hostname: string | null; + user_identifier: string | null; +} + +export interface GetCredentialsResponse { + credentials: (CardCredential | PasswordCredential | SSHKeyCredential)[], + total: number, + page: number, + limit: number +} \ No newline at end of file diff --git a/types/folder.ts b/types/folder.ts new file mode 100644 index 0000000..f3b9f11 --- /dev/null +++ b/types/folder.ts @@ -0,0 +1,42 @@ +export interface Folder { + id: string + name: string + description?: string | null + icon?: string | null + parentId: string | null + createdBy?: string +} + +export interface GetFoldersResponse { + folders: { + Id: string + Name: string + Description: string | null + Icon: string | null + ParentID: string | null + CreatedBy?: string + }[] + total: number + page: number + limit: number +} + +export interface PostFolderRequest { + name: string + description: string | null + icon: string | null + parent_id: string | null + created_by: string +} + +export interface PostFolderResponse { + Id: string, + Name: string, + Description: string | null, + Icon: string | null, + CreatedAt: string, + UpdatedAt: string, + ParentID: string | null, + CreatedBy: string, + user: { id: string }[] +} \ No newline at end of file diff --git a/types/tag.ts b/types/tag.ts new file mode 100644 index 0000000..11510a1 --- /dev/null +++ b/types/tag.ts @@ -0,0 +1,24 @@ +export interface Tag { + id: string + name: string + color: string + folderId: string + createdBy: string +} + +export interface GetTagsResponse { + id: string + name: string + color: string + created_at: string + updated_at: string + folder_id: string + created_by: string +} + +export interface PostTagRequest { + name: string + color: string + folder_id: string + created_by: string +} \ No newline at end of file