commit b9a9d687b617cba0883a3a4e172b371e461674a4 Author: jessy-david-dev Date: Sun Feb 8 17:24:48 2026 +0100 feat: mise à jour sécurité CVE-2025-55182 (Next.js 15.5.7, React 19.1.2) diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5ef6a52 --- /dev/null +++ b/.gitignore @@ -0,0 +1,41 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/versions + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# env files (can opt-in for committing if needed) +.env* + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..32e07b4 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "WillLuke.nextjs.hasPrompted": true +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..4828f25 --- /dev/null +++ b/README.md @@ -0,0 +1,83 @@ + +``` +aystone +├─ app +│ ├─ a-propos +│ │ └─ page.tsx +│ ├─ ads +│ │ └─ page.tsx +│ ├─ api +│ │ ├─ auth +│ │ │ └─ [...nextauth] +│ │ │ └─ route.ts +│ │ └─ planning +│ │ └─ route.ts +│ ├─ dashboard +│ │ ├─ Sidebar.tsx +│ │ ├─ StatCard.tsx +│ │ ├─ actions.ts +│ │ ├─ ajouter +│ │ │ ├─ AddProjectForm.tsx +│ │ │ └─ page.tsx +│ │ ├─ layout.tsx +│ │ ├─ page.tsx +│ │ ├─ profil +│ │ │ └─ page.tsx +│ │ └─ projets +│ │ ├─ [id] +│ │ │ ├─ EditProjectForm.tsx +│ │ │ └─ page.tsx +│ │ └─ page.tsx +│ ├─ globals.css +│ ├─ layout.tsx +│ ├─ login +│ │ └─ page.tsx +│ ├─ maps +│ │ └─ page.tsx +│ ├─ page.tsx +│ └─ providers.tsx +├─ auth.ts +├─ components +│ ├─ AdsGalleryClient.tsx +│ ├─ AdsSideBanners.tsx +│ ├─ AvailabilityPlanner.tsx +│ ├─ Banner.tsx +│ ├─ Footer.tsx +│ ├─ MapsClient.tsx +│ ├─ McSplash.tsx +│ ├─ MotionHeading.tsx +│ ├─ Navbar.tsx +│ ├─ ProjectsTable.tsx +│ └─ RedstoneParticles.tsx +├─ eslint.config.mjs +├─ lib +│ ├─ getAds.ts +│ └─ prisma.ts +├─ middleware.ts +├─ next.config.ts +├─ package-lock.json +├─ package.json +├─ postcss.config.mjs +├─ prisma +│ ├─ migrations +│ │ ├─ 20250727013821_init +│ │ │ └─ migration.sql +│ │ ├─ 20250727025700_add_mcname +│ │ │ └─ migration.sql +│ │ ├─ 20250727031005_project +│ │ │ └─ migration.sql +│ │ ├─ 20250727203115_add_availability_model +│ │ │ └─ migration.sql +│ │ └─ migration_lock.toml +│ └─ schema.prisma +├─ public +│ ├─ ads +│ │ ├─ riseofkinkdom.jpeg +│ │ └─ voat.png +│ ├─ aystone-saison2.png +│ └─ aystone.png +├─ tsconfig.json +└─ types + └─ next-auth.d.ts + +``` \ No newline at end of file diff --git a/app/a-propos/page.tsx b/app/a-propos/page.tsx new file mode 100644 index 0000000..d879b1f --- /dev/null +++ b/app/a-propos/page.tsx @@ -0,0 +1,87 @@ +'use client'; + +import { motion } from 'framer-motion'; + +const TAGS = [ + { id: 'spawn', label: 'Spawn', cls: 'bg-purple-700/30 text-purple-300' }, + { id: 'build', label: 'Build', cls: 'bg-emerald-600/25 text-emerald-300' }, + { id: 'usine', label: 'Usine', cls: 'bg-orange-600/20 text-orange-300' }, +]; + +export default function AProposPage() { + return ( +
+
+ + À propos + + + +

+ Cette application web permet à la communauté du serveur  + Aystone de référencer, suivre + et partager ses projets Minecraft dans les différents mondes : + Overworld, + Nether, + End. +

+ +

+ Le tableau liste : joueur, état, monde, nom, description, + coordonnées et tags personnalisés. L’interface responsive et + épurée garantit une expérience fluide sur mobile comme sur desktop. +

+ +

+ Sous le capot : Next 15, React 19, + Tailwind 4 et Framer Motion pour les + animations. Aucune donnée sensible n’est stockée : on se connecte + simplement via discord . +
+ Sur le dashboard, vous pouvez ajouter votre pseudo Minecraft dans la section Infos du compte +

+ +
+

Légende des tags

+
+ {TAGS.map(t => ( + + {t.label} + + ))} +
+
+ +
+

Code source

+

+ Le projet est entièrement open-source :{" "} + + dépôt GitHub + . + Déployé sur Coolify. +

+
+
+
+
+ ); +} diff --git a/app/ads/page.tsx b/app/ads/page.tsx new file mode 100644 index 0000000..e3e45a5 --- /dev/null +++ b/app/ads/page.tsx @@ -0,0 +1,11 @@ +import AdsGalleryClient from "@/components/AdsGalleryClient"; +import { getAdFiles } from "@/lib/getAds"; + +export const metadata = { + title: "Galerie des publicités – Aystone", +}; + +export default async function AdsGalleryPage() { + const files = await getAdFiles(); + return ; +} \ No newline at end of file diff --git a/app/api/auth/[...nextauth]/route.ts b/app/api/auth/[...nextauth]/route.ts new file mode 100644 index 0000000..020852e --- /dev/null +++ b/app/api/auth/[...nextauth]/route.ts @@ -0,0 +1,3 @@ +import { handlers } from "@/auth"; + +export const { GET, POST } = handlers; \ No newline at end of file diff --git a/app/couleurs/page.tsx b/app/couleurs/page.tsx new file mode 100644 index 0000000..8267121 --- /dev/null +++ b/app/couleurs/page.tsx @@ -0,0 +1,26 @@ +import { Palette } from "lucide-react"; +import Image from "next/image"; + +export const metadata = { + title: "Charte des couleurs Minecraft", +}; + +export default function CouleursPage() { + return ( +
+

+ + Charte des couleurs Minecraft +

+
+ Charte des couleurs Minecraft +
+
+ ); +} diff --git a/app/dashboard/Sidebar.tsx b/app/dashboard/Sidebar.tsx new file mode 100644 index 0000000..975843d --- /dev/null +++ b/app/dashboard/Sidebar.tsx @@ -0,0 +1,63 @@ +"use client"; + +import { + LayoutDashboard, + PlusCircle, + TableProperties, + User +} from "lucide-react"; +import Link from "next/link"; +import { usePathname } from "next/navigation"; + +const LINKS = [ + { name: "Tableau", href: "/dashboard", icon: LayoutDashboard }, + { name: "Profil", href: "/dashboard/profil", icon: User }, + { name: "Ajouter", href: "/dashboard/ajouter", icon: PlusCircle }, + { name: "Mes Projets", href: "/dashboard/projets", icon: TableProperties }, +]; + +export default function Sidebar() { + const pathname = usePathname(); + + return ( + + ); +} \ No newline at end of file diff --git a/app/dashboard/StatCard.tsx b/app/dashboard/StatCard.tsx new file mode 100644 index 0000000..e8eedf2 --- /dev/null +++ b/app/dashboard/StatCard.tsx @@ -0,0 +1,28 @@ +"use client"; + +import { motion } from "framer-motion"; +import { ReactNode } from "react"; + +type Props = { + title: string; + value: string | number; + icon?: ReactNode; +}; + +export default function StatCard({ title, value, icon }: Props) { + return ( + + {icon && ( +
+ {icon} +
+ )} +

{title}

+

{value}

+
+ ); +} \ No newline at end of file diff --git a/app/dashboard/actions.ts b/app/dashboard/actions.ts new file mode 100644 index 0000000..e37ef5f --- /dev/null +++ b/app/dashboard/actions.ts @@ -0,0 +1,103 @@ +"use server"; + +import { auth } from "@/auth"; +import { prisma } from "@/lib/prisma"; +import { revalidatePath } from "next/cache"; +import { redirect } from "next/navigation"; + +export async function createProject(form: FormData) { + const session = await auth(); + if (!session) throw new Error("unauth"); + + const currentUser = await prisma.user.findUnique({ + where: { email: session.user!.email! }, + }); + + const joueur = + (form.get("joueur") as string | null)?.trim() || + currentUser?.mcName || + session.user!.name!; + + const coords = form.get("coords")?.toString().trim(); + const tagsRaw = form.get("tags")?.toString() ?? ""; + + const data = { + joueur, + etat: (form.get("etat") as string) ?? "En cours", + monde: (form.get("monde") as string) ?? "Overworld", + projet: (form.get("projet") as string).trim(), + description: (form.get("description") as string).trim(), + coords: coords && coords.length > 0 ? coords : null, + tags: tagsRaw + .split(",") + .map((t) => t.trim()) + .filter(Boolean), + }; + + await prisma.project.create({ data }); + revalidatePath("/dashboard"); + redirect("/dashboard"); +} + +export async function updateProject(projectId: string, form: FormData) { + const session = await auth(); + if (!session) throw new Error("unauth"); + + const coords = form.get("coords")?.toString().trim(); + const tagsRaw = form.get("tags")?.toString() ?? ""; + + const data = { + etat: (form.get("etat") as string), + monde: (form.get("monde") as string), + projet: (form.get("projet") as string).trim(), + description: (form.get("description") as string).trim(), + coords: coords && coords.length > 0 ? coords : null, + tags: tagsRaw + .split(",") + .map((t) => t.trim()) + .filter(Boolean), + }; + + await prisma.project.update({ + where: { id: projectId }, + data, + }); + + revalidatePath("/dashboard"); + redirect("/dashboard"); +} + +export async function updateMinecraftName(form: FormData) { + const session = await auth(); + if (!session) throw new Error("unauth"); + + const mcName = (form.get("mcName") as string).trim(); + if (!/^[A-Za-z0-9_]{3,16}$/.test(mcName)) throw new Error("invalid"); + + const user = await prisma.user.findUnique({ + where: { email: session.user!.email! }, + }); + + if (!user) { + await prisma.user.create({ + data: { + email: session.user!.email!, + name: session.user!.name, + image: session.user!.image, + mcName, + }, + }); + } else { + if (user.mcName) { + throw new Error("Le pseudo Minecraft ne peut pas être modifié après avoir été défini."); + } + + await prisma.user.update({ + where: { email: session.user!.email! }, + data: { mcName }, + }); + } + + revalidatePath("/dashboard"); + revalidatePath("/"); +} diff --git a/app/dashboard/ajouter/AddProjectForm.tsx b/app/dashboard/ajouter/AddProjectForm.tsx new file mode 100644 index 0000000..8879d57 --- /dev/null +++ b/app/dashboard/ajouter/AddProjectForm.tsx @@ -0,0 +1,192 @@ +"use client"; + +import { motion } from "framer-motion"; +import { Layers3, MapPin, Plus, Tags, User, X } from "lucide-react"; +import { useSession } from "next-auth/react"; +import { useState } from "react"; +import { createProject } from "../actions"; + +const PREDEF = ["build", "usine", "spawn"] as const; +type TagColor = (typeof PREDEF)[number]; + +const palette: Record = { + build: "bg-emerald-600/90", + usine: "bg-orange-600/90", + spawn: "bg-fuchsia-700/90", +}; + +const isValidCoord = (value: string) => /^-?\d*$/.test(value); + +export default function AddProjectForm() { + const { data: session } = useSession(); + const [tags, setTags] = useState([]); + const [tagInput, setInput] = useState(""); + + const [x, setX] = useState(""); + const [y, setY] = useState(""); + const [z, setZ] = useState(""); + + const addTag = (t: string) => { + const val = t.trim().toLowerCase(); + if (!val || tags.includes(val) || tags.length >= 5) return; + setTags([...tags, val]); + }; + + const del = (t: string) => setTags(tags.filter((x) => x !== t)); + + const getTagClass = (t: string) => { + return palette[t as TagColor] ?? "bg-gray-600"; + }; + + return ( + + {/* nom */} +
+ + +
+ + {/* joueur */} + {session?.user?.email === "ultralionfr@gmail.com" && ( +
+ + +
+ )} + + {/* coords */} +
+ {[ + { name: "x", value: x, setValue: setX }, + { name: "y", value: y, setValue: setY }, + { name: "z", value: z, setValue: setZ }, + ].map(({ name, value, setValue }) => ( +
+ + { + if (isValidCoord(e.target.value)) setValue(e.target.value); + }} + className="w-full rounded-lg bg-gray-700 py-2 pl-9 pr-3 text-amber-50 placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-indigo-500" + /> +
+ ))} +
+ + {/* Tags */} +
+ + +
+ {tags.map((t) => ( + + ))} +
+ +
+ + +
+ setInput(e.target.value)} + onKeyDown={(e) => + e.key === "Enter" && (e.preventDefault(), addTag(tagInput), setInput("")) + } + placeholder="Ajouter un tag personnalisé" + className="w-full rounded-lg bg-gray-700 py-2 pl-3 pr-12 text-amber-50 placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-indigo-500" + /> + +
+
+
+ + {/* description */} +