Compare commits
10 commits
71ed95251c
...
319d8c95e9
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
319d8c95e9 | ||
|
|
76095ffff0 | ||
|
|
adf745e28b | ||
|
|
68b8ab31b7 | ||
|
|
1a81ef0865 | ||
|
|
85e6aad3f1 | ||
|
|
487983bf3e | ||
|
|
cb75c19b8c | ||
|
|
b006ac7f00 | ||
|
|
2de9d601f5 |
16 changed files with 308 additions and 68 deletions
|
|
@ -7,6 +7,8 @@ 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:
|
||||||
|
|
@ -23,6 +25,8 @@ 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
|
||||||
|
|
|
||||||
BIN
immagini/Home.jpg
Normal file
BIN
immagini/Home.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 227 KiB |
BIN
public/Home.jpg
Normal file
BIN
public/Home.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 227 KiB |
BIN
public/images/Home.jpg
Normal file
BIN
public/images/Home.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 227 KiB |
22
src/app/(main)/articoli/page.tsx
Normal file
22
src/app/(main)/articoli/page.tsx
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,11 +1,24 @@
|
||||||
export default function MainLayout({
|
import { redirect } from "next/navigation";
|
||||||
|
|
||||||
|
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>
|
||||||
|
|
|
||||||
22
src/app/(main)/macchine/page.tsx
Normal file
22
src/app/(main)/macchine/page.tsx
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -2,6 +2,7 @@ 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,
|
||||||
|
|
@ -16,6 +17,7 @@ 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>
|
||||||
|
|
@ -24,7 +26,6 @@ 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>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
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. */
|
||||||
|
|
@ -19,7 +18,7 @@ export const metadata: Metadata = {
|
||||||
description: "Generated by create next app",
|
description: "Generated by create next app",
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function RootLayout({
|
export default async function RootLayout({
|
||||||
children,
|
children,
|
||||||
}: Readonly<{
|
}: Readonly<{
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
|
|
@ -30,7 +29,6 @@ export default 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>
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
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";
|
||||||
|
|
||||||
|
|
@ -5,24 +6,55 @@ 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: formData.get("identifier"),
|
identifier,
|
||||||
password: formData.get("password"),
|
password: formData.get("password"),
|
||||||
redirectTo: "/",
|
redirectTo: getSafeRedirectTo(formData.get("callbackUrl")),
|
||||||
});
|
});
|
||||||
} 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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -38,51 +70,64 @@ export default async function LoginPage({ searchParams }: LoginPageProps) {
|
||||||
const hasError = params?.error === "CredentialsSignin";
|
const hasError = params?.error === "CredentialsSignin";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="flex min-h-[calc(100vh-4rem)] items-center justify-center bg-zinc-50 px-4 py-12">
|
<main className="flex min-h-screen items-center justify-center bg-zinc-50 px-4 py-12">
|
||||||
<section className="w-full max-w-sm rounded-lg border border-zinc-200 bg-white p-6 shadow-sm">
|
<section className="w-full max-w-sm overflow-hidden rounded-lg border border-zinc-200 bg-white shadow-sm">
|
||||||
<div className="mb-6">
|
<div className="relative aspect-[3/2] w-full bg-zinc-100">
|
||||||
|
<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>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
123
src/auth.ts
123
src/auth.ts
|
|
@ -8,34 +8,92 @@ 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> {
|
||||||
const [rows] = await db.execute<DbUserRow[]>(
|
let rows: DbUserRow[];
|
||||||
`SELECT id, utente, mail, password, ruolo, attivo, last_login
|
|
||||||
FROM utenti
|
try {
|
||||||
WHERE attivo = 1
|
[rows] = await db.execute<DbUserRow[]>(
|
||||||
AND (utente = :identifier OR mail = :identifier)
|
`SELECT id, utente, mail, password, ruolo, attivo, last_login
|
||||||
LIMIT 1`,
|
FROM utenti
|
||||||
{ identifier },
|
WHERE attivo = 1
|
||||||
);
|
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 || !isUserRole(user.ruolo)) {
|
if (!user) {
|
||||||
|
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> {
|
||||||
await db.execute<ResultSetHeader>(
|
try {
|
||||||
"UPDATE utenti SET last_login = NOW() WHERE id = :userId",
|
await db.execute<ResultSetHeader>(
|
||||||
{ userId },
|
"UPDATE utenti SET last_login = NOW() WHERE id = :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({
|
||||||
|
|
@ -53,27 +111,64 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
const passwordMatches = await bcrypt.compare(password, user.password);
|
let passwordMatches = false;
|
||||||
|
|
||||||
|
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,
|
||||||
|
|
|
||||||
|
|
@ -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} from "react";
|
import { useState , MouseEvent, ReactNode} 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() {
|
export default function Navbar({ actions }: { actions?: ReactNode }) {
|
||||||
|
|
||||||
const [ isServicesOpen, setIsServicesOpen] = useState<boolean>(false);
|
const [ isServicesOpen, setIsServicesOpen] = useState<boolean>(false);
|
||||||
|
|
||||||
|
|
@ -56,7 +56,7 @@ export default function Navbar() {
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
{/* Navigation */}
|
{/* Navigation */}
|
||||||
<div className="flex space-x-8">
|
<div className="flex items-center space-x-8">
|
||||||
|
|
||||||
{/* Link normali */}
|
{/* Link normali */}
|
||||||
{navLinks.map((link) => (
|
{navLinks.map((link) => (
|
||||||
|
|
@ -117,6 +117,12 @@ export default function Navbar() {
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{actions ? (
|
||||||
|
<div className="flex items-center">
|
||||||
|
{actions}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
|
||||||
|
|
@ -58,8 +58,15 @@ function getPool(): Pool {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const db = new Proxy({} as Pool, {
|
export const db = new Proxy({} as Pool, {
|
||||||
get(_target, property, receiver) {
|
get(_target, property) {
|
||||||
return Reflect.get(getPool(), property, receiver);
|
const pool = getPool();
|
||||||
|
const value = Reflect.get(pool, property);
|
||||||
|
|
||||||
|
if (typeof value === "function") {
|
||||||
|
return value.bind(pool);
|
||||||
|
}
|
||||||
|
|
||||||
|
return value;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
27
src/proxy.ts
Normal file
27
src/proxy.ts
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
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|.*\\..*).*)",
|
||||||
|
],
|
||||||
|
};
|
||||||
Loading…
Add table
Reference in a new issue