diff --git a/src/app/(main)/layout.tsx b/src/app/(main)/layout.tsx
index 73f7760..67475a9 100644
--- a/src/app/(main)/layout.tsx
+++ b/src/app/(main)/layout.tsx
@@ -1,28 +1,5 @@
-import { redirect } from "next/navigation";
+import AppShell from "@/src/components/AppShell";
-import { auth } from "@/src/auth";
-import SignOutButton from "@/src/components/auth/SignOutButton";
-import Navbar from "@/src/components/Navbar";
-import { StabilimentoProvider } from "@/src/contexts/StabilimentoContext";
-
-export default async function MainLayout({
- children,
-
-}: {
- children: React.ReactNode;
-}) {
- const session = await auth();
-
- if (!session?.user) {
- redirect("/login");
- }
-
- return (
-
-
- } />
- {children}
-
-
- );
+export default function MainLayout({ children }: { children: React.ReactNode }) {
+ return {children};
}
diff --git a/src/app/(management)/layout.tsx b/src/app/(management)/layout.tsx
index 1573611..263a174 100644
--- a/src/app/(management)/layout.tsx
+++ b/src/app/(management)/layout.tsx
@@ -1,27 +1,5 @@
-import { redirect } from "next/navigation";
+import AppShell from "@/src/components/AppShell";
-import { auth } from "@/src/auth";
-import SignOutButton from "@/src/components/auth/SignOutButton";
-import Navbar from "@/src/components/Navbar";
-import { StabilimentoProvider } from "@/src/contexts/StabilimentoContext";
-
-export default async function ManagementLayout({
- children,
-}: {
- children: React.ReactNode;
-}) {
- const session = await auth();
-
- if (!session?.user) {
- redirect("/login");
- }
-
- return (
-
-
- } />
- {children}
-
-
- );
+export default function ManagementLayout({ children }: { children: React.ReactNode }) {
+ return {children};
}
diff --git a/src/app/(management)/stabilimenti/page.tsx b/src/app/(management)/stabilimenti/page.tsx
index 4afb86e..2a1a12a 100644
--- a/src/app/(management)/stabilimenti/page.tsx
+++ b/src/app/(management)/stabilimenti/page.tsx
@@ -1,8 +1,20 @@
-"use client";
+import { redirect } from "next/navigation";
+import { auth } from "@/src/auth";
import StabilimentiManager from "@/src/components/StabilimentiManager";
+import { getPermissions, normalizeRole } from "@/src/lib/permissions";
+
+export default async function StabilimentiPage() {
+ const session = await auth();
+ if (!session?.user) {
+ redirect("/login");
+ }
+
+ const ruolo = normalizeRole(session.user.ruolo);
+ if (!ruolo || !getPermissions(ruolo).canManageStabilimenti) {
+ redirect("/");
+ }
-export default function StabilimentiPage() {
return (
diff --git a/src/app/(management)/utenti/page.tsx b/src/app/(management)/utenti/page.tsx
index 1cf70ad..b5d074f 100644
--- a/src/app/(management)/utenti/page.tsx
+++ b/src/app/(management)/utenti/page.tsx
@@ -1,8 +1,23 @@
-import Link from "next/link";
+import { redirect } from "next/navigation";
-export default function Utenti() {
- return <>
-
Utenti
-
Utenti
- >
-}
\ No newline at end of file
+import { auth } from "@/src/auth";
+import UtentiManager from "@/src/components/UtentiManager";
+import { getPermissions, normalizeRole } from "@/src/lib/permissions";
+
+export default async function UtentiPage() {
+ const session = await auth();
+ if (!session?.user) {
+ redirect("/login");
+ }
+
+ const ruolo = normalizeRole(session.user.ruolo);
+ if (!ruolo || !getPermissions(ruolo).canManageUtenti) {
+ redirect("/");
+ }
+
+ return (
+
+
+
+ );
+}
diff --git a/src/app/api/art-macchine-parti/[idmacchina]/[idart]/route.ts b/src/app/api/art-macchine-parti/[idmacchina]/[idart]/route.ts
index 5e36ebd..bdac23b 100644
--- a/src/app/api/art-macchine-parti/[idmacchina]/[idart]/route.ts
+++ b/src/app/api/art-macchine-parti/[idmacchina]/[idart]/route.ts
@@ -1,6 +1,6 @@
import { NextResponse } from "next/server";
-import { auth } from "@/src/auth";
+import { requirePermission } from "@/src/lib/auth/requireSession";
import { rimuoviArticoloDaMacchina } from "@/src/lib/artMacchineParti";
interface RouteParams {
@@ -8,11 +8,8 @@ interface RouteParams {
}
export async function DELETE(request: Request, { params }: RouteParams) {
- const session = await auth();
-
- if (!session?.user) {
- return NextResponse.json({ error: "Non autorizzato" }, { status: 401 });
- }
+ const session = await requirePermission((p) => p.canRemoveArticoloMacchina);
+ if (session instanceof NextResponse) return session;
const { idmacchina, idart } = await params;
const macchinaId = parseInt(idmacchina, 10);
diff --git a/src/app/api/art-macchine-parti/assegna/route.ts b/src/app/api/art-macchine-parti/assegna/route.ts
index bc51065..f888593 100644
--- a/src/app/api/art-macchine-parti/assegna/route.ts
+++ b/src/app/api/art-macchine-parti/assegna/route.ts
@@ -1,14 +1,11 @@
import { NextResponse } from "next/server";
-import { auth } from "@/src/auth";
+import { requirePermission } from "@/src/lib/auth/requireSession";
import { assegnaArticoloMacchina } from "@/src/lib/artMacchineParti";
export async function POST(request: Request) {
- const session = await auth();
-
- if (!session?.user) {
- return NextResponse.json({ error: "Non autorizzato" }, { status: 401 });
- }
+ const session = await requirePermission((p) => p.canAssegnaArticoliMacchina);
+ if (session instanceof NextResponse) return session;
try {
const body = await request.json();
diff --git a/src/app/api/macchine/[id]/route.ts b/src/app/api/macchine/[id]/route.ts
index 8c9dd77..1eba494 100644
--- a/src/app/api/macchine/[id]/route.ts
+++ b/src/app/api/macchine/[id]/route.ts
@@ -1,6 +1,7 @@
import { NextResponse } from "next/server";
import { auth } from "@/src/auth";
+import { requirePermission } from "@/src/lib/auth/requireSession";
import {
getMacchinaById,
updateMacchina,
@@ -47,11 +48,8 @@ export async function GET(request: Request, { params }: RouteParams) {
}
export async function PUT(request: Request, { params }: RouteParams) {
- const session = await auth();
-
- if (!session?.user) {
- return NextResponse.json({ error: "Non autorizzato" }, { status: 401 });
- }
+ const session = await requirePermission((p) => p.canEditMacchine);
+ if (session instanceof NextResponse) return session;
const { id } = await params;
const macchinaId = parseInt(id, 10);
@@ -116,7 +114,7 @@ export async function DELETE(request: Request, { params }: RouteParams) {
// Body vuoto o non JSON, continua senza magazzino
}
- const user = session.user.email || session.user.name || "unknown";
+ const user = session.user.email || session.user.utente || "unknown";
const deleted = await deleteMacchina(macchinaId, idMagazzinoRientro, user);
if (!deleted) {
diff --git a/src/app/api/macchine/route.ts b/src/app/api/macchine/route.ts
index afad7f9..17b3e99 100644
--- a/src/app/api/macchine/route.ts
+++ b/src/app/api/macchine/route.ts
@@ -1,6 +1,7 @@
import { NextResponse } from "next/server";
import { auth } from "@/src/auth";
+import { requirePermission } from "@/src/lib/auth/requireSession";
import {
listMacchine,
createMacchina,
@@ -28,11 +29,8 @@ export async function GET() {
}
export async function POST(request: Request) {
- const session = await auth();
-
- if (!session?.user) {
- return NextResponse.json({ error: "Non autorizzato" }, { status: 401 });
- }
+ const session = await requirePermission((p) => p.canEditMacchine);
+ if (session instanceof NextResponse) return session;
try {
const body = await request.json();
diff --git a/src/app/api/magazzini/[id]/route.ts b/src/app/api/magazzini/[id]/route.ts
index d473712..1d44779 100644
--- a/src/app/api/magazzini/[id]/route.ts
+++ b/src/app/api/magazzini/[id]/route.ts
@@ -1,6 +1,7 @@
import { NextResponse } from "next/server";
import { auth } from "@/src/auth";
+import { requirePermission } from "@/src/lib/auth/requireSession";
import { getMagazzinoById, updateMagazzino, deleteMagazzino } from "@/src/lib/stabilimenti";
interface RouteContext {
@@ -40,11 +41,8 @@ export async function GET(_request: Request, context: RouteContext) {
}
export async function PUT(request: Request, context: RouteContext) {
- const session = await auth();
-
- if (!session?.user) {
- return NextResponse.json({ error: "Non autorizzato" }, { status: 401 });
- }
+ const session = await requirePermission((p) => p.canManageStabilimenti);
+ if (session instanceof NextResponse) return session;
try {
const magazzino = await updateMagazzino(
@@ -61,11 +59,8 @@ export async function PUT(request: Request, context: RouteContext) {
}
export async function DELETE(_request: Request, context: RouteContext) {
- const session = await auth();
-
- if (!session?.user) {
- return NextResponse.json({ error: "Non autorizzato" }, { status: 401 });
- }
+ const session = await requirePermission((p) => p.canManageStabilimenti);
+ if (session instanceof NextResponse) return session;
try {
await deleteMagazzino(await readId(context.params));
diff --git a/src/app/api/magazzini/route.ts b/src/app/api/magazzini/route.ts
index 599e6be..e063f03 100644
--- a/src/app/api/magazzini/route.ts
+++ b/src/app/api/magazzini/route.ts
@@ -1,6 +1,7 @@
import { NextResponse } from "next/server";
import { auth } from "@/src/auth";
+import { requirePermission } from "@/src/lib/auth/requireSession";
import { listMagazzini, createMagazzino } from "@/src/lib/stabilimenti";
export async function GET(request: Request) {
@@ -29,11 +30,8 @@ export async function GET(request: Request) {
}
export async function POST(request: Request) {
- const session = await auth();
-
- if (!session?.user) {
- return NextResponse.json({ error: "Non autorizzato" }, { status: 401 });
- }
+ const session = await requirePermission((p) => p.canManageStabilimenti);
+ if (session instanceof NextResponse) return session;
try {
const magazzino = await createMagazzino(await request.json());
diff --git a/src/app/api/movimenti/route.ts b/src/app/api/movimenti/route.ts
index 16fac46..2a2decd 100644
--- a/src/app/api/movimenti/route.ts
+++ b/src/app/api/movimenti/route.ts
@@ -1,6 +1,7 @@
import { NextResponse } from "next/server";
import { auth } from "@/src/auth";
+import { forbid, requireSession } from "@/src/lib/auth/requireSession";
import { listMovimenti, createMovimento } from "@/src/lib/movimenti";
import { getArticleById } from "@/src/lib/articles";
import type { TipoMovimento } from "@/src/types/movimento";
@@ -35,11 +36,8 @@ export async function GET(request: Request) {
}
export async function POST(request: Request) {
- const session = await auth();
-
- if (!session?.user) {
- return NextResponse.json({ error: "Non autorizzato" }, { status: 401 });
- }
+ const session = await requireSession();
+ if (session instanceof NextResponse) return session;
try {
const body = await request.json();
@@ -72,6 +70,10 @@ export async function POST(request: Request) {
);
}
+ if (tipo === "scarico" && !session.permissions.canScaricoArticoli) {
+ return forbid("Scarico non consentito per il tuo ruolo");
+ }
+
const articolo = await getArticleById(id_articolo);
if (!articolo) {
return NextResponse.json(
@@ -81,7 +83,7 @@ export async function POST(request: Request) {
}
const tipoMovimentoDB = tipo === "carico" ? 1 : 2;
- const utente = session.user.name || session.user.email || "Sistema";
+ const utente = session.user.utente || session.user.name || session.user.email || "Sistema";
const movimentoId = await createMovimento({
id_articolo,
diff --git a/src/app/api/stabilimenti/[id]/route.ts b/src/app/api/stabilimenti/[id]/route.ts
index 3bff566..cef75aa 100644
--- a/src/app/api/stabilimenti/[id]/route.ts
+++ b/src/app/api/stabilimenti/[id]/route.ts
@@ -1,6 +1,7 @@
import { NextResponse } from "next/server";
import { auth } from "@/src/auth";
+import { requirePermission } from "@/src/lib/auth/requireSession";
import {
getStabilimentoById,
updateStabilimento,
@@ -44,11 +45,8 @@ export async function GET(_request: Request, context: RouteContext) {
}
export async function PUT(request: Request, context: RouteContext) {
- const session = await auth();
-
- if (!session?.user) {
- return NextResponse.json({ error: "Non autorizzato" }, { status: 401 });
- }
+ const session = await requirePermission((p) => p.canManageStabilimenti);
+ if (session instanceof NextResponse) return session;
try {
const stabilimento = await updateStabilimento(
@@ -65,11 +63,8 @@ export async function PUT(request: Request, context: RouteContext) {
}
export async function DELETE(_request: Request, context: RouteContext) {
- const session = await auth();
-
- if (!session?.user) {
- return NextResponse.json({ error: "Non autorizzato" }, { status: 401 });
- }
+ const session = await requirePermission((p) => p.canManageStabilimenti);
+ if (session instanceof NextResponse) return session;
try {
await deleteStabilimento(await readId(context.params));
diff --git a/src/app/api/stabilimenti/route.ts b/src/app/api/stabilimenti/route.ts
index 926f05d..f5b286a 100644
--- a/src/app/api/stabilimenti/route.ts
+++ b/src/app/api/stabilimenti/route.ts
@@ -1,6 +1,7 @@
import { NextResponse } from "next/server";
import { auth } from "@/src/auth";
+import { requirePermission } from "@/src/lib/auth/requireSession";
import {
listStabilimenti,
getStabilimentiWithMagazzini,
@@ -35,11 +36,8 @@ export async function GET(request: Request) {
}
export async function POST(request: Request) {
- const session = await auth();
-
- if (!session?.user) {
- return NextResponse.json({ error: "Non autorizzato" }, { status: 401 });
- }
+ const session = await requirePermission((p) => p.canManageStabilimenti);
+ if (session instanceof NextResponse) return session;
try {
const stabilimento = await createStabilimento(await request.json());
diff --git a/src/app/api/utenti/[id]/route.ts b/src/app/api/utenti/[id]/route.ts
new file mode 100644
index 0000000..18e017f
--- /dev/null
+++ b/src/app/api/utenti/[id]/route.ts
@@ -0,0 +1,88 @@
+import { NextResponse } from "next/server";
+
+import { forbid, requirePermission } from "@/src/lib/auth/requireSession";
+import { deleteUser, getUserById, updateUser } from "@/src/lib/users";
+
+type RouteContext = { params: Promise<{ id: string }> };
+
+export async function GET(_request: Request, context: RouteContext) {
+ const session = await requirePermission((p) => p.canManageUtenti);
+ if (session instanceof NextResponse) return session;
+
+ const { id } = await context.params;
+ const userId = parseInt(id, 10);
+
+ if (!Number.isInteger(userId) || userId <= 0) {
+ return NextResponse.json({ error: "ID non valido" }, { status: 400 });
+ }
+
+ try {
+ const utente = await getUserById(userId);
+ if (!utente) {
+ return NextResponse.json({ error: "Utente non trovato" }, { status: 404 });
+ }
+ return NextResponse.json({ utente });
+ } catch (error) {
+ console.error("[API utenti/id] Errore GET:", error);
+ return NextResponse.json({ error: "Errore nel recupero" }, { status: 500 });
+ }
+}
+
+export async function PUT(request: Request, context: RouteContext) {
+ const session = await requirePermission((p) => p.canManageUtenti);
+ if (session instanceof NextResponse) return session;
+
+ const { id } = await context.params;
+ const userId = parseInt(id, 10);
+ const currentId = parseInt(session.user.id, 10);
+
+ if (!Number.isInteger(userId) || userId <= 0) {
+ return NextResponse.json({ error: "ID non valido" }, { status: 400 });
+ }
+
+ try {
+ const body = await request.json();
+
+ if (userId === currentId && body.ruolo && body.ruolo !== session.user.ruolo) {
+ return forbid("Non puoi modificare il tuo ruolo.");
+ }
+ if (userId === currentId && body.attivo === 0) {
+ return forbid("Non puoi disattivare il tuo account.");
+ }
+
+ const utente = await updateUser(userId, body);
+ return NextResponse.json({ utente });
+ } catch (error) {
+ return NextResponse.json(
+ { error: error instanceof Error ? error.message : "Errore aggiornamento" },
+ { status: 400 },
+ );
+ }
+}
+
+export async function DELETE(_request: Request, context: RouteContext) {
+ const session = await requirePermission((p) => p.canManageUtenti);
+ if (session instanceof NextResponse) return session;
+
+ const { id } = await context.params;
+ const userId = parseInt(id, 10);
+ const currentId = parseInt(session.user.id, 10);
+
+ if (!Number.isInteger(userId) || userId <= 0) {
+ return NextResponse.json({ error: "ID non valido" }, { status: 400 });
+ }
+
+ if (userId === currentId) {
+ return forbid("Non puoi eliminare il tuo account.");
+ }
+
+ try {
+ await deleteUser(userId);
+ return NextResponse.json({ success: true });
+ } catch (error) {
+ return NextResponse.json(
+ { error: error instanceof Error ? error.message : "Errore eliminazione" },
+ { status: 400 },
+ );
+ }
+}
diff --git a/src/app/api/utenti/route.ts b/src/app/api/utenti/route.ts
new file mode 100644
index 0000000..06d3cea
--- /dev/null
+++ b/src/app/api/utenti/route.ts
@@ -0,0 +1,36 @@
+import { NextResponse } from "next/server";
+
+import { requirePermission } from "@/src/lib/auth/requireSession";
+import { createUser, listUsers } from "@/src/lib/users";
+
+export async function GET() {
+ const session = await requirePermission((p) => p.canManageUtenti);
+ if (session instanceof NextResponse) return session;
+
+ try {
+ const utenti = await listUsers();
+ return NextResponse.json({ utenti });
+ } catch (error) {
+ console.error("[API utenti] Errore GET:", error);
+ return NextResponse.json(
+ { error: "Errore nel recupero degli utenti" },
+ { status: 500 },
+ );
+ }
+}
+
+export async function POST(request: Request) {
+ const session = await requirePermission((p) => p.canManageUtenti);
+ if (session instanceof NextResponse) return session;
+
+ try {
+ const body = await request.json();
+ const utente = await createUser(body);
+ return NextResponse.json({ utente }, { status: 201 });
+ } catch (error) {
+ return NextResponse.json(
+ { error: error instanceof Error ? error.message : "Errore creazione utente" },
+ { status: 400 },
+ );
+ }
+}
diff --git a/src/auth.ts b/src/auth.ts
index a0aa73b..5fff024 100644
--- a/src/auth.ts
+++ b/src/auth.ts
@@ -4,7 +4,8 @@ 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";
+import { normalizeRole } from "@/src/lib/permissions";
+import type { DbUser } from "@/src/types/user";
type DbUserRow = DbUser & RowDataPacket;
@@ -34,10 +35,6 @@ function authError(message: string, error: unknown, details?: Record
{
let rows: DbUserRow[];
@@ -66,7 +63,8 @@ async function findActiveUser(identifier: string): Promise {
return null;
}
- if (!isUserRole(user.ruolo)) {
+ const ruolo = normalizeRole(user.ruolo);
+ if (!ruolo) {
authLog("Utente trovato con ruolo non valido", {
userId: user.id,
ruolo: user.ruolo,
@@ -77,10 +75,10 @@ async function findActiveUser(identifier: string): Promise {
authLog("Utente attivo trovato", {
userId: user.id,
utente: user.utente,
- ruolo: user.ruolo,
+ ruolo,
});
- return user;
+ return { ...user, ruolo };
}
async function updateLastLogin(userId: number): Promise {
@@ -191,7 +189,7 @@ export const { handlers, auth, signIn, signOut } = NextAuth({
},
session({ session, token }) {
session.user.id = token.id ?? "";
- session.user.ruolo = token.ruolo ?? "user";
+ session.user.ruolo = token.ruolo ?? "operatore";
session.user.utente = token.utente ?? session.user.name ?? "";
return session;
diff --git a/src/components/AppShell.tsx b/src/components/AppShell.tsx
new file mode 100644
index 0000000..dfaffdb
--- /dev/null
+++ b/src/components/AppShell.tsx
@@ -0,0 +1,38 @@
+import { redirect } from "next/navigation";
+
+import { auth } from "@/src/auth";
+import SignOutButton from "@/src/components/auth/SignOutButton";
+import Navbar from "@/src/components/Navbar";
+import { PermissionsProvider } from "@/src/contexts/PermissionsContext";
+import { StabilimentoProvider } from "@/src/contexts/StabilimentoContext";
+import { getPermissions, normalizeRole } from "@/src/lib/permissions";
+
+export default async function AppShell({ children }: { children: React.ReactNode }) {
+ const session = await auth();
+
+ if (!session?.user) {
+ redirect("/login");
+ }
+
+ const ruolo = normalizeRole(session.user.ruolo);
+ if (!ruolo) {
+ redirect("/login");
+ }
+
+ const permissions = getPermissions(ruolo);
+
+ return (
+
+
+
+ }
+ />
+ {children}
+
+
+
+ );
+}
diff --git a/src/components/ArticleCard.tsx b/src/components/ArticleCard.tsx
index 02fa1b5..2f466a0 100644
--- a/src/components/ArticleCard.tsx
+++ b/src/components/ArticleCard.tsx
@@ -11,6 +11,7 @@ interface ArticleCardProps {
onEdit: (article: Article) => void;
onDelete: (article: Article) => void;
onMovimento?: (article: Article, tipo: TipoMovimento) => void;
+ allowScarico?: boolean;
draggable?: boolean;
}
@@ -21,6 +22,7 @@ export default function ArticleCard({
onEdit,
onDelete,
onMovimento,
+ allowScarico = true,
draggable = true,
}: ArticleCardProps) {
const [deleting, setDeleting] = useState(false);
@@ -166,8 +168,9 @@ export default function ArticleCard({