implementato modulo Auth.js e pagina Login
This commit is contained in:
parent
882d7b122d
commit
c5db715e4c
6 changed files with 251 additions and 13 deletions
|
|
@ -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>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
3
src/app/api/auth/[...nextauth]/route.ts
Normal file
3
src/app/api/auth/[...nextauth]/route.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
import { handlers } from "@/src/auth";
|
||||||
|
|
||||||
|
export const { GET, POST } = handlers;
|
||||||
89
src/app/login/page.tsx
Normal file
89
src/app/login/page.tsx
Normal 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
105
src/auth.ts
Normal 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;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
19
src/components/auth/SignOutButton.tsx
Normal file
19
src/components/auth/SignOutButton.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
10
src/types/next-auth.d.ts
vendored
10
src/types/next-auth.d.ts
vendored
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue