diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 9ade835..215f023 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -41,6 +41,13 @@ jobs: type=semver,pattern={{version}} type=sha,enable=false + - name: Create env file + run: | + echo "NEXT_PUBLIC_OIDC_CLIENT_ID=${{ vars.NEXT_PUBLIC_OIDC_CLIENT_ID }}" >> .env.production + echo "NEXT_PUBLIC_OIDC_PROVIDER_URL=${{ vars.NEXT_PUBLIC_OIDC_PROVIDER_URL }}" >> .env.production + echo "NEXT_PUBLIC_OIDC_REDIRECT_URI=${{ vars.NEXT_PUBLIC_OIDC_REDIRECT_URI }}" >> .env.production + echo "SHARING_API=${{ vars.SHARING_API }}" >> .env.production + - name: Build and push Docker image id: push uses: docker/build-push-action@v5 @@ -82,4 +89,4 @@ jobs: git config --global user.email "github-actions[bot]@users.noreply.github.com" git config --global user.name "github-actions[bot]" git commit -m "Update app version to ${{ github.event.release.tag_name }}" - git push origin main \ No newline at end of file + git push origin main diff --git a/app/actions/sharing.tsx b/app/actions/sharing.tsx index ab5cd15..0abc0cb 100644 --- a/app/actions/sharing.tsx +++ b/app/actions/sharing.tsx @@ -1,5 +1,7 @@ 'use server' +import { apiClient } from '@/lib/api-client' + // Types pour l'API export interface PostSecretRequest { content: Record @@ -41,7 +43,7 @@ export async function createSecret(request: PostSecretRequest): Promise { console.log(`Récupération du secret ${id} depuis ${apiUrl}/${id}`) try { - const response = await fetch(`${apiUrl}/${id}`, { + const response = await apiClient.fetch(`${apiUrl}/${id}`, { method: 'GET', headers: { 'Content-Type': 'application/json', @@ -114,7 +116,7 @@ export async function decryptSecret(id: string, passphrase: string): Promise { console.log(`Récupération de l'historique depuis ${apiUrl}/history`) try { - const response = await fetch(`${apiUrl}/history`, { + const response = await apiClient.fetch(`${apiUrl}/history`, { method: 'GET', headers: { 'Content-Type': 'application/json', diff --git a/app/auth/callback/loading.tsx b/app/auth/callback/loading.tsx new file mode 100644 index 0000000..f15322a --- /dev/null +++ b/app/auth/callback/loading.tsx @@ -0,0 +1,3 @@ +export default function Loading() { + return null +} diff --git a/app/auth/callback/page.tsx b/app/auth/callback/page.tsx new file mode 100644 index 0000000..14ca62a --- /dev/null +++ b/app/auth/callback/page.tsx @@ -0,0 +1,94 @@ +'use client' + +import { useEffect, useState } from 'react' +import { useRouter, useSearchParams } from 'next/navigation' +import { exchangeCodeForTokens } from '@/lib/auth' +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' +import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert' +import { AlertCircle, CheckCircle2 } from 'lucide-react' + +export default function AuthCallbackPage() { + const router = useRouter() + const searchParams = useSearchParams() + const [status, setStatus] = useState<'loading' | 'success' | 'error'>('loading') + const [error, setError] = useState(null) + + useEffect(() => { + const handleCallback = async () => { + try { + const code = searchParams.get('code') + const state = searchParams.get('state') + const errorParam = searchParams.get('error') + + if (errorParam) { + setError(`Erreur d'authentification: ${errorParam}`) + setStatus('error') + return + } + + if (!code || !state) { + setError('Paramètres de callback manquants') + setStatus('error') + return + } + + const success = await exchangeCodeForTokens(code, state) + + if (success) { + setStatus('success') + // Rediriger vers la page d'accueil après un court délai + setTimeout(() => { + router.push('/') + }, 2000) + } else { + setError("Échec de l'échange du code d'autorisation") + setStatus('error') + } + } catch (error) { + console.error('Erreur dans le callback:', error) + setError("Une erreur inattendue s'est produite") + setStatus('error') + } + } + + handleCallback() + }, [searchParams, router]) + + return ( +
+ + + + {status === 'loading' && 'Authentification en cours...'} + {status === 'success' && 'Connexion réussie'} + {status === 'error' && "Erreur d'authentification"} + + + {status === 'loading' && 'Veuillez patienter pendant que nous finalisons votre connexion.'} + {status === 'success' && "Vous allez être redirigé vers l'application."} + {status === 'error' && "Une erreur s'est produite lors de l'authentification."} + + + + {status === 'loading' &&
} + + {status === 'success' && ( + + + Succès + Vous êtes maintenant connecté. Redirection en cours... + + )} + + {status === 'error' && ( + + + Erreur + {error || "Une erreur inconnue s'est produite."} + + )} +
+
+
+ ) +} diff --git a/app/layout.tsx b/app/layout.tsx index 17b2ce8..76ff3c3 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,5 +1,8 @@ -import type { Metadata } from 'next' +import type React from 'react' +import { Toaster } from '@/components/ui/toaster' import './globals.css' +import { Metadata } from 'next' +import { AuthGuard } from '@/components/auth-guard' export const metadata: Metadata = { title: 'v0 App', @@ -7,14 +10,13 @@ export const metadata: Metadata = { generator: 'v0.dev', } -export default function RootLayout({ - children, -}: Readonly<{ - children: React.ReactNode -}>) { +export default function RootLayout({ children }: { children: React.ReactNode }) { return ( - - {children} + + + {children} + + ) } diff --git a/components/auth-guard.tsx b/components/auth-guard.tsx new file mode 100644 index 0000000..75b1c88 --- /dev/null +++ b/components/auth-guard.tsx @@ -0,0 +1,49 @@ +'use client' + +import type React from 'react' + +import { useEffect } from 'react' +import { useAuthStore } from '@/lib/auth' +import { initiateOIDCLogin } from '@/lib/auth' +import { usePathname } from 'next/navigation' + +interface AuthGuardProps { + children: React.ReactNode +} + +export function AuthGuard({ children }: AuthGuardProps) { + const { isAuthenticated, accessToken } = useAuthStore() + + const pathname = usePathname() // Get current path + + useEffect(() => { + // Skip guard for /auth/callback + if (pathname === '/auth/callback') { + return + } + // Si pas d'access token, rediriger vers l'identity provider + if (!accessToken) { + initiateOIDCLogin() + } + }, [accessToken, pathname]) + + // Skip guard for /auth/callback + if (pathname === '/auth/callback') { + return <>{children} + } + + // Afficher un loader pendant la redirection + if (!isAuthenticated || !accessToken) { + return ( +
+
+
+

Connexion en cours...

+

Redirection vers le fournisseur d'identité...

+
+
+ ) + } + + return <>{children} +} diff --git a/components/password-list.tsx b/components/password-list.tsx index c81d4bc..2dee489 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,7 +27,7 @@ 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" interface PasswordListProps { passwords: PasswordEntry[] folders: FolderItem[] @@ -37,16 +37,40 @@ export function PasswordList({ passwords, folders }: PasswordListProps) { const [revealedPasswords, setRevealedPasswords] = useState>({}) const [selectedPassword, setSelectedPassword] = useState(null) const [showShareDialog, setShowShareDialog] = useState(false) + const [passwordUsages, setPasswordUsages] = useState>({}) + const [isLoadingUsages, setIsLoadingUsages] = useState(false) + const [isMounted, setIsMounted] = useState(false) + - const togglePasswordVisibility = (id: string) => { + 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 +105,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 (
@@ -143,10 +198,10 @@ export function PasswordList({ passwords, folders }: PasswordListProps) { Actions - copyToClipboard(password.username)}> + copyToClipboard(password.username, password.id)}> Copier l'identifiant - copyToClipboard(password.password)}> + copyToClipboard(password.password,password.id)}> Copier le mot de passe @@ -212,7 +267,7 @@ export function PasswordList({ passwords, folders }: PasswordListProps) { variant="ghost" size="icon" className="h-8 w-8" - onClick={() => copyToClipboard("ExamplePass123")} + onClick={() => copyToClipboard("ExamplePass123", password.id)} > Copier le mot de passe @@ -225,6 +280,12 @@ export function PasswordList({ passwords, folders }: PasswordListProps) { {password.strength === "strong" ? "Fort" : password.strength === "medium" ? "Moyen" : "Faible"}
+
+ Nombre de vues : + + {!isMounted || isLoadingUsages ? '...' : passwordUsages[password.id] ?? 0} + +
{password.notes && (
@@ -268,7 +329,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 +363,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 +383,7 @@ export function PasswordList({ passwords, folders }: PasswordListProps) { readOnly className="flex-1" /> -
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/components/user-nav.tsx b/components/user-nav.tsx index 93b8c78..5aaf575 100644 --- a/components/user-nav.tsx +++ b/components/user-nav.tsx @@ -1,19 +1,16 @@ -"use client" +'use client' -import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar" -import { Button } from "@/components/ui/button" -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuGroup, - DropdownMenuItem, - DropdownMenuLabel, - DropdownMenuSeparator, - DropdownMenuTrigger, -} from "@/components/ui/dropdown-menu" -import { Bell, Settings, LogOut, User } from "lucide-react" +import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar' +import { Button } from '@/components/ui/button' +import { DropdownMenu, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger } from '@/components/ui/dropdown-menu' +import { Bell, Settings, LogOut, User } from 'lucide-react' +import { logout } from '@/lib/auth' export function UserNav() { + const handleLogout = () => { + logout() + } + return (