Compare commits

..

No commits in common. "319d8c95e98e4f5bd9b2dfebe695625e4f46b329" and "71ed95251c0ddebfaa650b90572e1bf6447469d9" have entirely different histories.

16 changed files with 68 additions and 308 deletions

View file

@ -7,8 +7,6 @@ services:
restart: always restart: always
env_file: env_file:
- .env - .env
environment:
MYSQL_HOST: mysql
ports: ports:
- "127.0.0.1:3001:3000" - "127.0.0.1:3001:3000"
depends_on: depends_on:
@ -25,8 +23,6 @@ services:
MYSQL_PASSWORD: ${MYSQL_PASSWORD} MYSQL_PASSWORD: ${MYSQL_PASSWORD}
volumes: volumes:
- dbdata:/var/lib/mysql - dbdata:/var/lib/mysql
ports:
- "127.0.0.1:3306:3306"
phpmyadmin: phpmyadmin:
image: phpmyadmin:latest image: phpmyadmin:latest

Binary file not shown.

Before

Width:  |  Height:  |  Size: 227 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 227 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 227 KiB

View file

@ -1,22 +0,0 @@
export default function ArticoliPage() {
return (
<div className="flex flex-1 flex-col bg-zinc-50 p-6 dark:bg-black">
<div className="mx-auto w-full max-w-6xl">
<header className="mb-8">
<h1 className="text-3xl font-semibold text-zinc-950 dark:text-zinc-50">
Articoli
</h1>
<p className="mt-2 text-zinc-600 dark:text-zinc-400">
Catalogo ricambi e componenti
</p>
</header>
<div className="rounded-lg border border-zinc-200 bg-white p-6 shadow-sm dark:border-zinc-800 dark:bg-zinc-900">
<p className="text-zinc-600 dark:text-zinc-400">
Contenuto della pagina articoli in arrivo...
</p>
</div>
</div>
</div>
);
}

View file

View file

@ -1,24 +1,11 @@
import { redirect } from "next/navigation"; export default function MainLayout({
import { auth } from "@/src/auth";
import SignOutButton from "@/src/components/auth/SignOutButton";
import Navbar from "@/src/components/Navbar";
export default async function MainLayout({
children, children,
}: { }: {
children: React.ReactNode; children: React.ReactNode;
}) { }) {
const session = await auth();
if (!session?.user) {
redirect("/login");
}
return ( return (
<div> <div>
<Navbar actions={<SignOutButton />} />
<p className="bg-violet-700 text-white w-full text-center py-2"> <p className="bg-violet-700 text-white w-full text-center py-2">
Sono del layout (main) Sono del layout (main)
</p> </p>

View file

@ -1,22 +0,0 @@
export default function MachinePage() {
return (
<div className="flex flex-1 flex-col bg-zinc-50 p-6 dark:bg-black">
<div className="mx-auto w-full max-w-6xl">
<header className="mb-8">
<h1 className="text-3xl font-semibold text-zinc-950 dark:text-zinc-50">
Macchine
</h1>
<p className="mt-2 text-zinc-600 dark:text-zinc-400">
Gestione del parco macchine e attrezzature
</p>
</header>
<div className="rounded-lg border border-zinc-200 bg-white p-6 shadow-sm dark:border-zinc-800 dark:bg-zinc-900">
<p className="text-zinc-600 dark:text-zinc-400">
Contenuto della pagina macchine in arrivo...
</p>
</div>
</div>
</div>
);
}

View file

View file

@ -2,7 +2,6 @@ import { redirect } from "next/navigation";
import { auth } from "@/src/auth"; import { auth } from "@/src/auth";
import SignOutButton from "@/src/components/auth/SignOutButton"; import SignOutButton from "@/src/components/auth/SignOutButton";
import Navbar from "@/src/components/Navbar";
export default async function ManagementLayout({ export default async function ManagementLayout({
children, children,
@ -17,7 +16,6 @@ export default async function ManagementLayout({
return ( return (
<div> <div>
<Navbar actions={<SignOutButton />} />
<header className="border-b border-zinc-200 bg-white"> <header className="border-b border-zinc-200 bg-white">
<div className="mx-auto flex max-w-6xl items-center justify-between px-4 py-3"> <div className="mx-auto flex max-w-6xl items-center justify-between px-4 py-3">
<div> <div>
@ -26,6 +24,7 @@ export default async function ManagementLayout({
{session.user.utente} {session.user.utente}
</p> </p>
</div> </div>
<SignOutButton />
</div> </div>
</header> </header>
<main className="mx-auto max-w-6xl px-4 py-6">{children}</main> <main className="mx-auto max-w-6xl px-4 py-6">{children}</main>

View file

@ -1,5 +1,6 @@
import type { Metadata } from "next"; import type { Metadata } from "next";
import { Inter, JetBrains_Mono } from "next/font/google"; import { Inter, JetBrains_Mono } from "next/font/google";
import Navbar from "../components/Navbar";
import "./globals.css"; import "./globals.css";
/** Stesse variabili CSS di globals.css; Geist non è disponibile in next/font su Next 14. */ /** Stesse variabili CSS di globals.css; Geist non è disponibile in next/font su Next 14. */
@ -18,7 +19,7 @@ export const metadata: Metadata = {
description: "Generated by create next app", description: "Generated by create next app",
}; };
export default async function RootLayout({ export default function RootLayout({
children, children,
}: Readonly<{ }: Readonly<{
children: React.ReactNode; children: React.ReactNode;
@ -29,6 +30,7 @@ export default async function RootLayout({
<p className="bg-purple-700 text-white w-full text-center py-2"> <p className="bg-purple-700 text-white w-full text-center py-2">
Sono del layout root Sono del layout root
</p> </p>
<Navbar />
{children} {children}
</body> </body>
</html> </html>

View file

@ -1,4 +1,3 @@
import Image from "next/image";
import { AuthError } from "next-auth"; import { AuthError } from "next-auth";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
@ -6,55 +5,24 @@ import { auth, signIn } from "@/src/auth";
type LoginPageProps = { type LoginPageProps = {
searchParams?: Promise<{ searchParams?: Promise<{
callbackUrl?: string;
error?: string; error?: string;
}>; }>;
}; };
function getSafeRedirectTo(value: FormDataEntryValue | null): string {
const raw = String(value ?? "/");
if (raw.startsWith("/") && !raw.startsWith("//")) {
return raw;
}
try {
const parsed = new URL(raw);
return `${parsed.pathname}${parsed.search}${parsed.hash}`;
} catch {
return "/";
}
}
async function login(formData: FormData) { async function login(formData: FormData) {
"use server"; "use server";
const identifier = String(formData.get("identifier") ?? "").trim();
console.log("[login-page] Submit login ricevuto", {
hasIdentifier: Boolean(identifier),
hasPassword: Boolean(formData.get("password")),
});
try { try {
await signIn("credentials", { await signIn("credentials", {
identifier, identifier: formData.get("identifier"),
password: formData.get("password"), password: formData.get("password"),
redirectTo: getSafeRedirectTo(formData.get("callbackUrl")), redirectTo: "/",
}); });
} catch (error) { } catch (error) {
if (error instanceof AuthError) { if (error instanceof AuthError) {
console.log("[login-page] AuthError durante login", {
type: error.type,
cause: error.cause,
});
redirect("/login?error=CredentialsSignin"); redirect("/login?error=CredentialsSignin");
} }
console.error("[login-page] Errore inatteso durante login", {
error: error instanceof Error ? error.message : String(error),
});
throw error; throw error;
} }
} }
@ -70,64 +38,51 @@ export default async function LoginPage({ searchParams }: LoginPageProps) {
const hasError = params?.error === "CredentialsSignin"; const hasError = params?.error === "CredentialsSignin";
return ( return (
<main className="flex min-h-screen items-center justify-center bg-zinc-50 px-4 py-12"> <main className="flex min-h-[calc(100vh-4rem)] items-center justify-center bg-zinc-50 px-4 py-12">
<section className="w-full max-w-sm overflow-hidden rounded-lg border border-zinc-200 bg-white shadow-sm"> <section className="w-full max-w-sm rounded-lg border border-zinc-200 bg-white p-6 shadow-sm">
<div className="relative aspect-[3/2] w-full bg-zinc-100"> <div className="mb-6">
<Image
src="/images/Home.jpg"
alt="MagRicambi"
fill
priority
className="object-contain"
sizes="(max-width: 640px) 100vw, 384px"
/>
</div>
<div className="p-6">
<div className="mb-6">
<h1 className="text-2xl font-semibold text-zinc-950">Accesso</h1> <h1 className="text-2xl font-semibold text-zinc-950">Accesso</h1>
<p className="mt-2 text-sm text-zinc-600"> <p className="mt-2 text-sm text-zinc-600">
Entra in MagRicambi con utente o email. Entra in MagRicambi con utente o email.
</p> </p>
</div>
{hasError ? (
<p className="mb-4 rounded-md border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-700">
Credenziali non valide oppure utente non attivo.
</p>
) : null}
<form action={login} className="space-y-4">
<label className="block">
<span className="text-sm font-medium text-zinc-700">Utente o email</span>
<input
required
autoComplete="username"
name="identifier"
type="text"
className="mt-1 w-full rounded-md border border-zinc-300 px-3 py-2 text-zinc-950 outline-none transition-colors focus:border-zinc-900"
/>
</label>
<label className="block">
<span className="text-sm font-medium text-zinc-700">Password</span>
<input
required
autoComplete="current-password"
name="password"
type="password"
className="mt-1 w-full rounded-md border border-zinc-300 px-3 py-2 text-zinc-950 outline-none transition-colors focus:border-zinc-900"
/>
</label>
<button
type="submit"
className="w-full rounded-md bg-zinc-950 px-4 py-2 font-medium text-white transition-colors hover:bg-zinc-800"
>
Accedi
</button>
</form>
</div> </div>
{hasError ? (
<p className="mb-4 rounded-md border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-700">
Credenziali non valide oppure utente non attivo.
</p>
) : null}
<form action={login} className="space-y-4">
<label className="block">
<span className="text-sm font-medium text-zinc-700">Utente o email</span>
<input
required
autoComplete="username"
name="identifier"
type="text"
className="mt-1 w-full rounded-md border border-zinc-300 px-3 py-2 text-zinc-950 outline-none transition-colors focus:border-zinc-900"
/>
</label>
<label className="block">
<span className="text-sm font-medium text-zinc-700">Password</span>
<input
required
autoComplete="current-password"
name="password"
type="password"
className="mt-1 w-full rounded-md border border-zinc-300 px-3 py-2 text-zinc-950 outline-none transition-colors focus:border-zinc-900"
/>
</label>
<button
type="submit"
className="w-full rounded-md bg-zinc-950 px-4 py-2 font-medium text-white transition-colors hover:bg-zinc-800"
>
Accedi
</button>
</form>
</section> </section>
</main> </main>
); );

View file

@ -8,92 +8,34 @@ import type { DbUser, UserRole } from "@/src/types/user";
type DbUserRow = DbUser & RowDataPacket; type DbUserRow = DbUser & RowDataPacket;
function maskIdentifier(identifier: string): string {
if (identifier.includes("@")) {
const [name = "", domain = ""] = identifier.split("@");
const visibleName = name.slice(0, 2);
return `${visibleName}${"*".repeat(Math.max(name.length - 2, 1))}@${domain}`;
}
if (identifier.length <= 2) {
return "*".repeat(identifier.length);
}
return `${identifier.slice(0, 2)}${"*".repeat(identifier.length - 2)}`;
}
function authLog(message: string, details?: Record<string, unknown>): void {
console.log(`[auth] ${message}`, details ?? "");
}
function authError(message: string, error: unknown, details?: Record<string, unknown>): void {
console.error(`[auth] ${message}`, {
...details,
error: error instanceof Error ? error.message : String(error),
});
}
function isUserRole(value: string): value is UserRole { function isUserRole(value: string): value is UserRole {
return ["admin", "manager", "user", "sviluppo"].includes(value); return ["admin", "manager", "user", "sviluppo"].includes(value);
} }
async function findActiveUser(identifier: string): Promise<DbUser | null> { async function findActiveUser(identifier: string): Promise<DbUser | null> {
let rows: DbUserRow[]; const [rows] = await db.execute<DbUserRow[]>(
`SELECT id, utente, mail, password, ruolo, attivo, last_login
try { FROM utenti
[rows] = await db.execute<DbUserRow[]>( WHERE attivo = 1
`SELECT id, utente, mail, password, ruolo, attivo, last_login AND (utente = :identifier OR mail = :identifier)
FROM utenti LIMIT 1`,
WHERE attivo = 1 { identifier },
AND (utente = :identifier OR mail = :identifier) );
LIMIT 1`,
{ identifier },
);
} catch (error) {
authError("Errore durante la query utente attivo", error, {
identifier: maskIdentifier(identifier),
});
throw error;
}
const user = rows[0]; const user = rows[0];
if (!user) { if (!user || !isUserRole(user.ruolo)) {
authLog("Nessun utente attivo trovato", {
identifier: maskIdentifier(identifier),
});
return null; return null;
} }
if (!isUserRole(user.ruolo)) {
authLog("Utente trovato con ruolo non valido", {
userId: user.id,
ruolo: user.ruolo,
});
return null;
}
authLog("Utente attivo trovato", {
userId: user.id,
utente: user.utente,
ruolo: user.ruolo,
});
return user; return user;
} }
async function updateLastLogin(userId: number): Promise<void> { async function updateLastLogin(userId: number): Promise<void> {
try { await db.execute<ResultSetHeader>(
await db.execute<ResultSetHeader>( "UPDATE utenti SET last_login = NOW() WHERE id = :userId",
"UPDATE utenti SET last_login = NOW() WHERE id = :userId", { userId },
{ userId }, );
);
authLog("last_login aggiornato", { userId });
} catch (error) {
authError("Errore durante aggiornamento last_login", error, { userId });
throw error;
}
} }
export const { handlers, auth, signIn, signOut } = NextAuth({ export const { handlers, auth, signIn, signOut } = NextAuth({
@ -111,64 +53,27 @@ export const { handlers, auth, signIn, signOut } = NextAuth({
password: { label: "Password", type: "password" }, password: { label: "Password", type: "password" },
}, },
async authorize(credentials) { async authorize(credentials) {
const startedAt = Date.now();
const identifier = String(credentials?.identifier ?? "").trim(); const identifier = String(credentials?.identifier ?? "").trim();
const password = String(credentials?.password ?? ""); const password = String(credentials?.password ?? "");
const maskedIdentifier = maskIdentifier(identifier);
authLog("Tentativo login ricevuto", {
identifier: maskedIdentifier,
hasIdentifier: Boolean(identifier),
hasPassword: Boolean(password),
});
if (!identifier || !password) { if (!identifier || !password) {
authLog("Login respinto: campi mancanti", {
identifier: maskedIdentifier,
hasIdentifier: Boolean(identifier),
hasPassword: Boolean(password),
});
return null; return null;
} }
const user = await findActiveUser(identifier); const user = await findActiveUser(identifier);
if (!user) { if (!user) {
authLog("Login respinto: utente non trovato o non valido", {
identifier: maskedIdentifier,
durationMs: Date.now() - startedAt,
});
return null; return null;
} }
let passwordMatches = false; const passwordMatches = await bcrypt.compare(password, user.password);
try {
passwordMatches = await bcrypt.compare(password, user.password);
} catch (error) {
authError("Errore durante confronto password bcrypt", error, {
userId: user.id,
durationMs: Date.now() - startedAt,
});
throw error;
}
if (!passwordMatches) { if (!passwordMatches) {
authLog("Login respinto: password non valida", {
userId: user.id,
durationMs: Date.now() - startedAt,
});
return null; return null;
} }
await updateLastLogin(user.id); await updateLastLogin(user.id);
authLog("Login riuscito", {
userId: user.id,
ruolo: user.ruolo,
durationMs: Date.now() - startedAt,
});
return { return {
id: String(user.id), id: String(user.id),
name: user.utente, name: user.utente,

View file

@ -5,7 +5,7 @@
// @version: "1.0.0 2026-05-07" // @version: "1.0.0 2026-05-07"
//==================================== //====================================
"use client" "use client"
import { useState , MouseEvent, ReactNode} from "react"; import { useState , MouseEvent} from "react";
import Link from "next/link"; import Link from "next/link";
interface NavLink { interface NavLink {
@ -34,7 +34,7 @@ const serviceLinks: ServiceLink[] = [
] ]
export default function Navbar({ actions }: { actions?: ReactNode }) { export default function Navbar() {
const [ isServicesOpen, setIsServicesOpen] = useState<boolean>(false); const [ isServicesOpen, setIsServicesOpen] = useState<boolean>(false);
@ -56,7 +56,7 @@ export default function Navbar({ actions }: { actions?: ReactNode }) {
</Link> </Link>
{/* Navigation */} {/* Navigation */}
<div className="flex items-center space-x-8"> <div className="flex space-x-8">
{/* Link normali */} {/* Link normali */}
{navLinks.map((link) => ( {navLinks.map((link) => (
@ -117,12 +117,6 @@ export default function Navbar({ actions }: { actions?: ReactNode }) {
</div> </div>
</div> </div>
{actions ? (
<div className="flex items-center">
{actions}
</div>
) : null}
</div> </div>
</div> </div>
</nav> </nav>

View file

@ -58,15 +58,8 @@ function getPool(): Pool {
} }
export const db = new Proxy({} as Pool, { export const db = new Proxy({} as Pool, {
get(_target, property) { get(_target, property, receiver) {
const pool = getPool(); return Reflect.get(getPool(), property, receiver);
const value = Reflect.get(pool, property);
if (typeof value === "function") {
return value.bind(pool);
}
return value;
}, },
}); });

View file

@ -1,27 +0,0 @@
import { NextResponse } from "next/server";
import { auth } from "@/src/auth";
export default auth((request) => {
const isLoggedIn = Boolean(request.auth);
const isLoginPage = request.nextUrl.pathname === "/login";
if (!isLoggedIn && !isLoginPage) {
const loginUrl = new URL("/login", request.nextUrl);
loginUrl.searchParams.set("callbackUrl", request.nextUrl.href);
return NextResponse.redirect(loginUrl);
}
if (isLoggedIn && isLoginPage) {
return NextResponse.redirect(new URL("/", request.nextUrl));
}
return NextResponse.next();
});
export const config = {
matcher: [
"/((?!api/auth|_next/static|_next/image|favicon.ico|images|.*\\..*).*)",
],
};