implementato modulo Auth.js e pagina Login

This commit is contained in:
AV77web 2026-05-27 13:05:32 +02:00
parent 882d7b122d
commit c5db715e4c
6 changed files with 251 additions and 13 deletions

View file

@ -1,15 +1,33 @@
export default function MarketingLayout({ import { redirect } from "next/navigation";
children,
import { auth } from "@/src/auth";
import SignOutButton from "@/src/components/auth/SignOutButton";
export default async function ManagementLayout({
children,
}: { }: {
children: React.ReactNode; children: React.ReactNode;
}) { }) {
return ( const session = await auth();
<div>
<p className="bg-violet-700 text-white w-full text-center py-2"> if (!session?.user) {
Sono del layout (marketing) redirect("/login");
}
return (
<div>
<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>
<p className="text-sm font-medium text-zinc-500">Area gestionale</p>
<p className="text-base font-semibold text-zinc-950">
{session.user.utente}
</p> </p>
<main>{children}</main> </div>
<SignOutButton />
</div> </div>
); </header>
} <main className="mx-auto max-w-6xl px-4 py-6">{children}</main>
</div>
);
}

View file

@ -0,0 +1,3 @@
import { handlers } from "@/src/auth";
export const { GET, POST } = handlers;

89
src/app/login/page.tsx Normal file
View file

@ -0,0 +1,89 @@
import { AuthError } from "next-auth";
import { redirect } from "next/navigation";
import { auth, signIn } from "@/src/auth";
type LoginPageProps = {
searchParams?: Promise<{
error?: string;
}>;
};
async function login(formData: FormData) {
"use server";
try {
await signIn("credentials", {
identifier: formData.get("identifier"),
password: formData.get("password"),
redirectTo: "/",
});
} catch (error) {
if (error instanceof AuthError) {
redirect("/login?error=CredentialsSignin");
}
throw error;
}
}
export default async function LoginPage({ searchParams }: LoginPageProps) {
const session = await auth();
if (session?.user) {
redirect("/");
}
const params = await searchParams;
const hasError = params?.error === "CredentialsSignin";
return (
<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 rounded-lg border border-zinc-200 bg-white p-6 shadow-sm">
<div className="mb-6">
<h1 className="text-2xl font-semibold text-zinc-950">Accesso</h1>
<p className="mt-2 text-sm text-zinc-600">
Entra in MagRicambi con utente o email.
</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>
</section>
</main>
);
}

105
src/auth.ts Normal file
View file

@ -0,0 +1,105 @@
import bcrypt from "bcrypt";
import NextAuth from "next-auth";
import Credentials from "next-auth/providers/credentials";
import type { ResultSetHeader, RowDataPacket } from "mysql2";
import { db } from "@/src/lib/db";
import type { DbUser, UserRole } from "@/src/types/user";
type DbUserRow = DbUser & RowDataPacket;
function isUserRole(value: string): value is UserRole {
return ["admin", "manager", "user", "sviluppo"].includes(value);
}
async function findActiveUser(identifier: string): Promise<DbUser | null> {
const [rows] = await db.execute<DbUserRow[]>(
`SELECT id, utente, mail, password, ruolo, attivo, last_login
FROM utenti
WHERE attivo = 1
AND (utente = :identifier OR mail = :identifier)
LIMIT 1`,
{ identifier },
);
const user = rows[0];
if (!user || !isUserRole(user.ruolo)) {
return null;
}
return user;
}
async function updateLastLogin(userId: number): Promise<void> {
await db.execute<ResultSetHeader>(
"UPDATE utenti SET last_login = NOW() WHERE id = :userId",
{ userId },
);
}
export const { handlers, auth, signIn, signOut } = NextAuth({
session: {
strategy: "jwt",
},
pages: {
signIn: "/login",
},
providers: [
Credentials({
name: "Credenziali",
credentials: {
identifier: { label: "Utente o email", type: "text" },
password: { label: "Password", type: "password" },
},
async authorize(credentials) {
const identifier = String(credentials?.identifier ?? "").trim();
const password = String(credentials?.password ?? "");
if (!identifier || !password) {
return null;
}
const user = await findActiveUser(identifier);
if (!user) {
return null;
}
const passwordMatches = await bcrypt.compare(password, user.password);
if (!passwordMatches) {
return null;
}
await updateLastLogin(user.id);
return {
id: String(user.id),
name: user.utente,
email: user.mail,
ruolo: user.ruolo,
utente: user.utente,
};
},
}),
],
callbacks: {
jwt({ token, user }) {
if (user) {
token.id = user.id;
token.ruolo = user.ruolo;
token.utente = user.utente;
}
return token;
},
session({ session, token }) {
session.user.id = token.id ?? "";
session.user.ruolo = token.ruolo ?? "user";
session.user.utente = token.utente ?? session.user.name ?? "";
return session;
},
},
});

View file

@ -0,0 +1,19 @@
import { signOut } from "@/src/auth";
export default function SignOutButton() {
return (
<form
action={async () => {
"use server";
await signOut({ redirectTo: "/login" });
}}
>
<button
type="submit"
className="rounded-md border border-zinc-300 px-3 py-2 text-sm font-medium text-zinc-700 transition-colors hover:bg-zinc-100"
>
Esci
</button>
</form>
);
}

View file

@ -4,19 +4,23 @@ import type { UserRole } from "./user";
declare module "next-auth" { declare module "next-auth" {
interface Session { interface Session {
user: { user: {
id: number; id: string;
ruolo: UserRole; ruolo: UserRole;
utente: string;
} & DefaultSession["user"]; } & DefaultSession["user"];
} }
interface User { interface User {
id: string;
ruolo: UserRole; ruolo: UserRole;
utente: string;
} }
} }
declare module "next-auth/jwt" { declare module "@auth/core/jwt" {
interface JWT { interface JWT {
id?: number; id?: string;
ruolo?: UserRole; ruolo?: UserRole;
utente?: string;
} }
} }