Ajout du système d'administration avec token unique et QR code
- Implémentation complète du système d'administration (/admin) - Gestion des clients avec base de données JSON - Génération de token unique et QR code pour chaque client - Intégration des données client dans l'application (bungalow, WiFi, message) - Amélioration du composant WifiCard avec fallback de copie - Optimisation du hook useClientData pour chargement immédiat - Ajout de la variable d'environnement ADMIN_PASSWORD
This commit is contained in:
@ -1,8 +1,10 @@
|
||||
"use client";
|
||||
|
||||
import dynamic from "next/dynamic";
|
||||
import Layout from "@/components/layout/Layout";
|
||||
import { config } from "@/lib/config";
|
||||
import WifiCard from "@/components/accueil/WifiCard";
|
||||
import Logo from "@/components/Logo";
|
||||
import { useClientData } from "@/lib/hooks/useClientData";
|
||||
|
||||
const WeatherWidget = dynamic(() => import("@/components/accueil/WeatherWidget"), {
|
||||
loading: () => <div className="h-32 bg-gray-100 rounded-2xl animate-pulse" />,
|
||||
@ -10,6 +12,18 @@ const WeatherWidget = dynamic(() => import("@/components/accueil/WeatherWidget")
|
||||
});
|
||||
|
||||
export default function AccueilPage() {
|
||||
const { bungalowNumber, gerantMessage, loading } = useClientData();
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Layout>
|
||||
<div className="px-4 py-6 space-y-6">
|
||||
<div className="h-32 bg-gray-100 rounded-2xl animate-pulse" />
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
<div className="px-4 py-6 space-y-6">
|
||||
@ -19,7 +33,7 @@ export default function AccueilPage() {
|
||||
Ia Ora Na
|
||||
</h1>
|
||||
<p className="text-lg text-gray-700">
|
||||
Bienvenue au Bungalow {config.bungalowNumber}
|
||||
Bienvenue au Bungalow {bungalowNumber}
|
||||
</p>
|
||||
</header>
|
||||
|
||||
@ -32,7 +46,7 @@ export default function AccueilPage() {
|
||||
Le mot du gérant
|
||||
</h2>
|
||||
<p className="text-gray-700 leading-relaxed">
|
||||
{config.gerantMessage}
|
||||
{gerantMessage}
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
85
app/admin/login/page.tsx
Normal file
85
app/admin/login/page.tsx
Normal file
@ -0,0 +1,85 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
|
||||
import Logo from "@/components/Logo";
|
||||
|
||||
export default function AdminLoginPage() {
|
||||
const [password, setPassword] = useState("");
|
||||
const [error, setError] = useState("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
const router = useRouter();
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setError("");
|
||||
setLoading(true);
|
||||
|
||||
// Vérification simple côté client (la vraie vérification se fait côté serveur)
|
||||
// Pour l'instant, on stocke le mot de passe dans localStorage
|
||||
localStorage.setItem("adminPassword", password);
|
||||
|
||||
// Test avec une requête API
|
||||
try {
|
||||
const response = await fetch("/api/admin/clients", {
|
||||
headers: {
|
||||
Authorization: `Bearer ${password}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
router.push("/admin");
|
||||
} else {
|
||||
setError("Mot de passe incorrect");
|
||||
localStorage.removeItem("adminPassword");
|
||||
}
|
||||
} catch (err) {
|
||||
setError("Erreur de connexion");
|
||||
localStorage.removeItem("adminPassword");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background flex items-center justify-center px-4">
|
||||
<Card className="w-full max-w-md">
|
||||
<CardHeader className="text-center">
|
||||
<div className="flex justify-center mb-4">
|
||||
<Logo size={100} />
|
||||
</div>
|
||||
<CardTitle>Administration</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Mot de passe
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
className="w-full px-4 py-2 border border-gray-300 rounded-xl focus:ring-2 focus:ring-primary focus:border-transparent"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded-xl text-sm">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Button type="submit" disabled={loading} className="w-full">
|
||||
{loading ? "Connexion..." : "Se connecter"}
|
||||
</Button>
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
73
app/admin/page.tsx
Normal file
73
app/admin/page.tsx
Normal file
@ -0,0 +1,73 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import AdminLayout from "@/components/admin/AdminLayout";
|
||||
import ClientForm from "@/components/admin/ClientForm";
|
||||
import ClientList from "@/components/admin/ClientList";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Plus } from "lucide-react";
|
||||
import { Client } from "@/lib/types/client";
|
||||
|
||||
export default function AdminPage() {
|
||||
const [showForm, setShowForm] = useState(false);
|
||||
const [editingClient, setEditingClient] = useState<Client | undefined>();
|
||||
const [refreshKey, setRefreshKey] = useState(0);
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
// Vérifier si l'admin est connecté
|
||||
const adminPassword = localStorage.getItem("adminPassword");
|
||||
if (!adminPassword) {
|
||||
router.push("/admin/login");
|
||||
}
|
||||
}, [router]);
|
||||
|
||||
const handleNewClient = () => {
|
||||
setEditingClient(undefined);
|
||||
setShowForm(true);
|
||||
};
|
||||
|
||||
const handleEdit = (client: Client) => {
|
||||
setEditingClient(client);
|
||||
setShowForm(true);
|
||||
};
|
||||
|
||||
const handleSuccess = () => {
|
||||
setShowForm(false);
|
||||
setEditingClient(undefined);
|
||||
setRefreshKey((k) => k + 1);
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
setShowForm(false);
|
||||
setEditingClient(undefined);
|
||||
};
|
||||
|
||||
return (
|
||||
<AdminLayout>
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-2xl font-bold text-primary">Gestion des clients</h2>
|
||||
{!showForm && (
|
||||
<Button onClick={handleNewClient}>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
Nouveau client
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{showForm ? (
|
||||
<ClientForm
|
||||
client={editingClient}
|
||||
onSuccess={handleSuccess}
|
||||
onCancel={handleCancel}
|
||||
/>
|
||||
) : (
|
||||
<ClientList onEdit={handleEdit} onRefresh={() => setRefreshKey((k) => k + 1)} />
|
||||
)}
|
||||
</div>
|
||||
</AdminLayout>
|
||||
);
|
||||
}
|
||||
|
||||
55
app/api/admin/clients/[id]/route.ts
Normal file
55
app/api/admin/clients/[id]/route.ts
Normal file
@ -0,0 +1,55 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { updateClient, deleteClient, loadClients } from "@/lib/admin/client-utils";
|
||||
import { requireAdminAuth } from "@/lib/admin/auth";
|
||||
import { ClientInput } from "@/lib/types/client";
|
||||
|
||||
export async function PUT(
|
||||
request: Request,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
if (!requireAdminAuth(request)) {
|
||||
return NextResponse.json({ error: "Non autorisé" }, { status: 401 });
|
||||
}
|
||||
|
||||
try {
|
||||
const { id } = await params;
|
||||
const body: Partial<ClientInput> = await request.json();
|
||||
const client = updateClient(id, body);
|
||||
|
||||
if (!client) {
|
||||
return NextResponse.json(
|
||||
{ error: "Client non trouvé" },
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json(client);
|
||||
} catch (error: any) {
|
||||
return NextResponse.json(
|
||||
{ error: error.message || "Erreur lors de la mise à jour" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function DELETE(
|
||||
request: Request,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
if (!requireAdminAuth(request)) {
|
||||
return NextResponse.json({ error: "Non autorisé" }, { status: 401 });
|
||||
}
|
||||
|
||||
const { id } = await params;
|
||||
const deleted = deleteClient(id);
|
||||
|
||||
if (!deleted) {
|
||||
return NextResponse.json(
|
||||
{ error: "Client non trouvé" },
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json({ success: true });
|
||||
}
|
||||
|
||||
60
app/api/admin/clients/route.ts
Normal file
60
app/api/admin/clients/route.ts
Normal file
@ -0,0 +1,60 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import {
|
||||
createClient,
|
||||
loadClients,
|
||||
updateClient,
|
||||
deleteClient,
|
||||
validateEmail,
|
||||
} from "@/lib/admin/client-utils";
|
||||
import { requireAdminAuth } from "@/lib/admin/auth";
|
||||
import { ClientInput } from "@/lib/types/client";
|
||||
|
||||
export async function GET(request: Request) {
|
||||
if (!requireAdminAuth(request)) {
|
||||
return NextResponse.json({ error: "Non autorisé" }, { status: 401 });
|
||||
}
|
||||
|
||||
const clients = loadClients();
|
||||
return NextResponse.json(clients);
|
||||
}
|
||||
|
||||
export async function POST(request: Request) {
|
||||
if (!requireAdminAuth(request)) {
|
||||
return NextResponse.json({ error: "Non autorisé" }, { status: 401 });
|
||||
}
|
||||
|
||||
try {
|
||||
const body: ClientInput = await request.json();
|
||||
|
||||
// Validation
|
||||
if (!body.email || !validateEmail(body.email)) {
|
||||
return NextResponse.json(
|
||||
{ error: "Email invalide" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
if (!body.bungalowNumber) {
|
||||
return NextResponse.json(
|
||||
{ error: "Numéro de bungalow requis" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const client = createClient({
|
||||
email: body.email,
|
||||
bungalowNumber: body.bungalowNumber,
|
||||
wifiName: body.wifiName || "Lagon-WiFi",
|
||||
wifiPassword: body.wifiPassword || "",
|
||||
gerantMessage: body.gerantMessage || "Bienvenue dans notre pension de famille !",
|
||||
});
|
||||
|
||||
return NextResponse.json(client, { status: 201 });
|
||||
} catch (error: any) {
|
||||
return NextResponse.json(
|
||||
{ error: error.message || "Erreur lors de la création du client" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
26
app/api/client/[token]/route.ts
Normal file
26
app/api/client/[token]/route.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { getClientByToken } from "@/lib/admin/client-utils";
|
||||
|
||||
export async function GET(
|
||||
request: Request,
|
||||
{ params }: { params: Promise<{ token: string }> }
|
||||
) {
|
||||
const { token } = await params;
|
||||
const client = getClientByToken(token);
|
||||
|
||||
if (!client) {
|
||||
return NextResponse.json(
|
||||
{ error: "Token invalide ou expiré" },
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
// Retourner uniquement les informations nécessaires (sans le token)
|
||||
return NextResponse.json({
|
||||
bungalowNumber: client.bungalowNumber,
|
||||
wifiName: client.wifiName,
|
||||
wifiPassword: client.wifiPassword,
|
||||
gerantMessage: client.gerantMessage,
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user