diff --git a/src/App.tsx b/src/App.tsx index 40f596c..cbdd5a4 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -8,13 +8,14 @@ import { useEffect, useState } from "react"; import Index from "./pages/Index"; import Login from "./pages/Login"; import NotFound from "./pages/NotFound"; +import AuthService from "./services/AuthService"; const queryClient = new QueryClient(); // Protected Route Component const ProtectedRoute = ({ children }: { children: React.ReactNode }) => { - const token = localStorage.getItem("token"); - if (!token) { + const isAuthenticated = AuthService.isAuthenticated(); + if (!isAuthenticated) { return ; } return <>{children}; @@ -25,24 +26,12 @@ const App = () => { useEffect(() => { // Vérifier le token au démarrage - const token = localStorage.getItem("token"); - if (token) { - // Optionnel: Vérifier la validité du token avec le serveur - fetch("/api/auth/me", { - headers: { - Authorization: `Bearer ${token}`, - }, - }) - .catch(() => { - localStorage.removeItem("token"); - localStorage.removeItem("user"); - }) - .finally(() => { - setIsLoading(false); - }); - } else { + const checkAuth = async () => { + const isValid = await AuthService.checkTokenValidity(); setIsLoading(false); - } + }; + + checkAuth(); }, []); if (isLoading) { diff --git a/src/pages/Index.tsx b/src/pages/Index.tsx index a13d76b..531ac2d 100644 --- a/src/pages/Index.tsx +++ b/src/pages/Index.tsx @@ -6,30 +6,140 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from "@/components/ui/dialog"; import { Avatar, AvatarFallback } from "@/components/ui/avatar"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"; -import { showSuccess } from "@/utils/toast"; import { MadeWithDyad } from "@/components/made-with-dyad"; import { PlusCircle, Search, User, LogOut, Settings, ChevronDown, Plus, Archive, Trash2, Image, CheckSquare } from "lucide-react"; +import AuthService from "@/services/AuthService"; +import AdminService from "@/services/AdminService"; +import NotesService, { Note } from "@/services/NotesService"; +import { useNavigate } from "react-router-dom"; const Index = () => { const [user, setUser] = useState(null); const [showAdmin, setShowAdmin] = useState(false); const [activeTab, setActiveTab] = useState("all"); const [searchQuery, setSearchQuery] = useState(""); + const [notes, setNotes] = useState([]); + const [currentNote, setCurrentNote] = useState(null); + const [users, setUsers] = useState([]); + const navigate = useNavigate(); useEffect(() => { - const userStr = localStorage.getItem("user"); - if (userStr) { - setUser(JSON.parse(userStr)); + const user = AuthService.getUser(); + if (user) { + setUser(user); + loadNotes(); } }, []); - const handleLogout = () => { - localStorage.removeItem("token"); - localStorage.removeItem("user"); - showSuccess("Déconnexion réussie"); - window.location.href = "/login"; + const loadNotes = async () => { + const notes = await NotesService.getNotes(); + setNotes(notes); + if (notes.length > 0) { + setCurrentNote(notes[0]); + } }; + const loadUsers = async () => { + if (user?.is_admin) { + const users = await AdminService.getUsers(); + setUsers(users); + } + }; + + const handleLogout = () => { + AuthService.logout(); + navigate("/login"); + }; + + const handleCreateNote = async () => { + const newNote = await NotesService.createNote("Nouvelle note"); + if (newNote) { + setNotes([newNote, ...notes]); + setCurrentNote(newNote); + } + }; + + const handleUpdateNote = async (updatedFields: Partial) => { + if (!currentNote) return; + + const updatedNote = { ...currentNote, ...updatedFields }; + const success = await NotesService.updateNote(updatedNote); + + if (success) { + setCurrentNote(updatedNote); + setNotes(notes.map(note => note.id === updatedNote.id ? updatedNote : note)); + } + }; + + const handleDeleteNote = async () => { + if (!currentNote?.id) return; + + if (window.confirm("Êtes-vous sûr de vouloir supprimer cette note ?")) { + const success = await NotesService.deleteNote(currentNote.id); + + if (success) { + const updatedNotes = notes.filter(note => note.id !== currentNote.id); + setNotes(updatedNotes); + setCurrentNote(updatedNotes.length > 0 ? updatedNotes[0] : null); + } + } + }; + + const handleAddUser = async () => { + const username = prompt("Nom d'utilisateur:"); + if (!username) return; + + const password = prompt("Mot de passe:"); + if (!password) return; + + const isAdmin = window.confirm("Donner les droits administrateur ?"); + + const newUser = await AdminService.createUser(username, password, isAdmin); + if (newUser) { + loadUsers(); + } + }; + + const handleChangePassword = async (userId: number) => { + const password = prompt("Nouveau mot de passe:"); + if (!password) return; + + const success = await AdminService.updateUserPassword(userId, password); + if (success) { + loadUsers(); + } + }; + + const handleToggleAdmin = async (userId: number, currentStatus: boolean) => { + const success = await AdminService.toggleAdminStatus(userId, currentStatus); + if (success) { + loadUsers(); + } + }; + + const handleDeleteUser = async (userId: number) => { + if (window.confirm("Êtes-vous sûr de vouloir supprimer cet utilisateur ?")) { + const success = await AdminService.deleteUser(userId); + if (success) { + loadUsers(); + } + } + }; + + // Charger les utilisateurs quand le panel admin s'ouvre + useEffect(() => { + if (showAdmin) { + loadUsers(); + } + }, [showAdmin]); + + const filteredNotes = notes.filter(note => + (activeTab === "all" || (activeTab === "archived" && note.archived)) && + (searchQuery === "" || + note.title.toLowerCase().includes(searchQuery.toLowerCase()) || + note.content.toLowerCase().includes(searchQuery.toLowerCase())) + ); + return (
{/* Top Navigation */} @@ -103,7 +213,11 @@ const Index = () => { />
- @@ -115,96 +229,221 @@ const Index = () => { -
- {/* Notes list placeholder */} - - -

Note d'exemple

-

- Ceci est un exemple de contenu de note. Cliquez pour éditer. -

-
-
+
+ {filteredNotes.length === 0 ? ( +

Aucune note trouvée

+ ) : ( + filteredNotes.map(note => ( + setCurrentNote(note)} + > + +

{note.title || "Sans titre"}

+

+ {note.content ? note.content.replace(/<[^>]*>/g, '') : ""} +

+
+
+ )) + )}
{/* Note Editor */} -
-
- + {currentNote ? ( +
+
+ handleUpdateNote({ title: e.target.value })} + /> + +
+ + +
+
-
- - +
+ +
+ {currentNote.todos && currentNote.todos.length > 0 ? ( + currentNote.todos.map((todo, index) => ( +
+ { + const updatedTodos = [...currentNote.todos]; + updatedTodos[index].completed = !updatedTodos[index].completed; + handleUpdateNote({ todos: updatedTodos }); + }} + className="h-4 w-4" + /> + + {todo.text} + + +
+ )) + ) : ( +

Aucune tâche

+ )} +
+ + + + + + + +
+ +
+ +
+ {currentNote.images && currentNote.images.length > 0 ? ( + currentNote.images.map((image, index) => ( +
+ {`Note + +
+ )) + ) : ( +

Aucune image

+ )} +
+
+
+
+ +
+ ) : ( +
+
+

Aucune note sélectionnée

+
- - - - Contenu - Tâches - Images - - - - - -
- - - - - - - -
- -
- -
-
- - Exemple de tâche -
-
-
-
-
- - - - -
- -
- -
- {/* Image placeholders */} -
-
-
-
- -
+ )}
@@ -225,28 +464,63 @@ const Index = () => { -
- - -
- - AD - -
-

admin

-

Administrateur

-
-
-
- -
-
-
+ {users.length > 0 ? ( + users.map(user => ( + + +
+ + {user.username.charAt(0).toUpperCase()} + +
+

{user.username}

+

+ {user.is_admin ? "Administrateur" : "Utilisateur"} +

+
+
+
+ {user.id !== 1 && ( + <> + + + + + )} +
+
+
+ )) + ) : ( +

Chargement des utilisateurs...

+ )}
diff --git a/src/pages/Login.tsx b/src/pages/Login.tsx index 362209d..0dc95cf 100644 --- a/src/pages/Login.tsx +++ b/src/pages/Login.tsx @@ -2,40 +2,26 @@ import { useState } from "react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Card, CardContent, CardHeader, CardTitle, CardDescription, CardFooter } from "@/components/ui/card"; -import { showError, showSuccess } from "@/utils/toast"; import { MadeWithDyad } from "@/components/made-with-dyad"; +import AuthService from "@/services/AuthService"; +import { useNavigate } from "react-router-dom"; const Login = () => { const [username, setUsername] = useState(""); const [password, setPassword] = useState(""); const [isLoading, setIsLoading] = useState(false); + const navigate = useNavigate(); const handleLogin = async (e: React.FormEvent) => { e.preventDefault(); setIsLoading(true); - try { - const response = await fetch("/api/auth/login", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ username, password }), - }); - - const data = await response.json(); - - if (!response.ok) { - throw new Error(data.message || "Erreur de connexion"); - } - - localStorage.setItem("token", data.token); - localStorage.setItem("user", JSON.stringify(data.user)); - showSuccess("Connexion réussie"); - window.location.href = "/"; - } catch (error) { - showError(error instanceof Error ? error.message : "Erreur de connexion"); - } finally { - setIsLoading(false); + const success = await AuthService.login(username, password); + if (success) { + navigate("/"); } + + setIsLoading(false); }; return ( diff --git a/src/services/AdminService.ts b/src/services/AdminService.ts new file mode 100644 index 0000000..d66cfe4 --- /dev/null +++ b/src/services/AdminService.ts @@ -0,0 +1,110 @@ +import AuthService from "./AuthService"; +import { showError, showSuccess } from "@/utils/toast"; + +interface User { + id: number; + username: string; + is_admin: boolean; + created_at?: string; +} + +class AdminService { + async getUsers(): Promise { + try { + const response = await fetch("/api/users", { + headers: AuthService.getHeaders() + }); + + if (!response.ok) { + throw new Error("Erreur lors de la récupération des utilisateurs"); + } + + return await response.json(); + } catch (error) { + showError(error instanceof Error ? error.message : "Erreur serveur"); + return []; + } + } + + async createUser(username: string, password: string, isAdmin: boolean): Promise { + try { + const response = await fetch("/api/users", { + method: "POST", + headers: AuthService.getHeaders(), + body: JSON.stringify({ username, password, is_admin: isAdmin }) + }); + + if (!response.ok) { + const error = await response.json(); + throw new Error(error.message || "Erreur lors de la création de l'utilisateur"); + } + + showSuccess("Utilisateur créé avec succès"); + return await response.json(); + } catch (error) { + showError(error instanceof Error ? error.message : "Erreur serveur"); + return null; + } + } + + async updateUserPassword(userId: number, password: string): Promise { + try { + const response = await fetch(`/api/users/${userId}`, { + method: "PUT", + headers: AuthService.getHeaders(), + body: JSON.stringify({ password }) + }); + + if (!response.ok) { + throw new Error("Erreur lors de la modification du mot de passe"); + } + + showSuccess("Mot de passe modifié avec succès"); + return true; + } catch (error) { + showError(error instanceof Error ? error.message : "Erreur serveur"); + return false; + } + } + + async toggleAdminStatus(userId: number, currentStatus: boolean): Promise { + try { + const response = await fetch(`/api/users/${userId}`, { + method: "PUT", + headers: AuthService.getHeaders(), + body: JSON.stringify({ is_admin: !currentStatus }) + }); + + if (!response.ok) { + throw new Error("Erreur lors de la modification des droits"); + } + + showSuccess("Droits modifiés avec succès"); + return true; + } catch (error) { + showError(error instanceof Error ? error.message : "Erreur serveur"); + return false; + } + } + + async deleteUser(userId: number): Promise { + try { + const response = await fetch(`/api/users/${userId}`, { + method: "DELETE", + headers: AuthService.getHeaders() + }); + + if (!response.ok) { + throw new Error("Erreur lors de la suppression de l'utilisateur"); + } + + showSuccess("Utilisateur supprimé avec succès"); + return true; + } catch (error) { + showError(error instanceof Error ? error.message : "Erreur serveur"); + return false; + } + } +} + +export default new AdminService(); \ No newline at end of file diff --git a/src/services/AuthService.ts b/src/services/AuthService.ts new file mode 100644 index 0000000..adc52bf --- /dev/null +++ b/src/services/AuthService.ts @@ -0,0 +1,94 @@ +import { showError, showSuccess } from "@/utils/toast"; + +interface User { + id: number; + username: string; + is_admin: boolean; +} + +class AuthService { + private token: string | null; + private user: User | null; + + constructor() { + this.token = localStorage.getItem("token"); + const userStr = localStorage.getItem("user"); + this.user = userStr ? JSON.parse(userStr) : null; + } + + isAuthenticated(): boolean { + return !!this.token; + } + + getUser(): User | null { + return this.user; + } + + getToken(): string | null { + return this.token; + } + + getHeaders(): Record { + return { + "Authorization": `Bearer ${this.token}`, + "Content-Type": "application/json" + }; + } + + async login(username: string, password: string): Promise { + try { + const response = await fetch("/api/auth/login", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ username, password }) + }); + + const data = await response.json(); + + if (!response.ok) { + throw new Error(data.message || "Erreur de connexion"); + } + + this.token = data.token; + this.user = data.user; + localStorage.setItem("token", this.token); + localStorage.setItem("user", JSON.stringify(this.user)); + + showSuccess("Connexion réussie"); + return true; + } catch (error) { + showError(error instanceof Error ? error.message : "Erreur de connexion"); + return false; + } + } + + logout(): void { + localStorage.removeItem("token"); + localStorage.removeItem("user"); + this.token = null; + this.user = null; + showSuccess("Déconnexion réussie"); + } + + async checkTokenValidity(): Promise { + if (!this.token) return false; + + try { + const response = await fetch("/api/auth/me", { + headers: this.getHeaders() + }); + + if (!response.ok) { + this.logout(); + return false; + } + + return true; + } catch (error) { + this.logout(); + return false; + } + } +} + +export default new AuthService(); \ No newline at end of file diff --git a/src/services/NotesService.ts b/src/services/NotesService.ts new file mode 100644 index 0000000..fbde5a6 --- /dev/null +++ b/src/services/NotesService.ts @@ -0,0 +1,148 @@ +import AuthService from "./AuthService"; +import { showError, showSuccess } from "@/utils/toast"; + +interface Todo { + id?: number; + text: string; + completed: boolean; + position?: number; +} + +interface Image { + id?: number; + filename: string; +} + +export interface Note { + id?: number; + title: string; + content: string; + archived?: boolean; + created_at?: string; + updated_at?: string; + todos: Todo[]; + images: Image[]; +} + +class NotesService { + async getNotes(): Promise { + try { + const response = await fetch("/api/notes", { + headers: AuthService.getHeaders() + }); + + if (!response.ok) { + throw new Error("Erreur lors de la récupération des notes"); + } + + return await response.json(); + } catch (error) { + showError(error instanceof Error ? error.message : "Erreur serveur"); + return []; + } + } + + async createNote(title: string, content: string = ""): Promise { + try { + const response = await fetch("/api/notes", { + method: "POST", + headers: AuthService.getHeaders(), + body: JSON.stringify({ title, content, todos: [] }) + }); + + if (!response.ok) { + throw new Error("Erreur lors de la création de la note"); + } + + showSuccess("Note créée avec succès"); + return await response.json(); + } catch (error) { + showError(error instanceof Error ? error.message : "Erreur serveur"); + return null; + } + } + + async updateNote(note: Note): Promise { + if (!note.id) return false; + + try { + const response = await fetch(`/api/notes/${note.id}`, { + method: "PUT", + headers: AuthService.getHeaders(), + body: JSON.stringify(note) + }); + + if (!response.ok) { + throw new Error("Erreur lors de la mise à jour de la note"); + } + + return true; + } catch (error) { + showError(error instanceof Error ? error.message : "Erreur serveur"); + return false; + } + } + + async deleteNote(noteId: number): Promise { + try { + const response = await fetch(`/api/notes/${noteId}`, { + method: "DELETE", + headers: AuthService.getHeaders() + }); + + if (!response.ok) { + throw new Error("Erreur lors de la suppression de la note"); + } + + showSuccess("Note supprimée avec succès"); + return true; + } catch (error) { + showError(error instanceof Error ? error.message : "Erreur serveur"); + return false; + } + } + + async uploadImage(noteId: number, file: File): Promise { + const formData = new FormData(); + formData.append("image", file); + + try { + const response = await fetch(`/api/notes/${noteId}/images`, { + method: "POST", + headers: { + "Authorization": AuthService.getHeaders().Authorization + }, + body: formData + }); + + if (!response.ok) { + throw new Error("Erreur lors de l'upload de l'image"); + } + + return await response.json(); + } catch (error) { + showError(error instanceof Error ? error.message : "Erreur serveur"); + return null; + } + } + + async deleteImage(noteId: number, imageId: number): Promise { + try { + const response = await fetch(`/api/notes/${noteId}/images/${imageId}`, { + method: "DELETE", + headers: AuthService.getHeaders() + }); + + if (!response.ok) { + throw new Error("Erreur lors de la suppression de l'image"); + } + + return true; + } catch (error) { + showError(error instanceof Error ? error.message : "Erreur serveur"); + return false; + } + } +} + +export default new NotesService(); \ No newline at end of file