inserita la gestione degli utenti
This commit is contained in:
parent
bdb3145bd6
commit
b7fd15ffcd
29 changed files with 1145 additions and 216 deletions
|
|
@ -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 (
|
||||
<StabilimentoProvider>
|
||||
<div>
|
||||
<Navbar actions={<SignOutButton />} />
|
||||
<main>{children}</main>
|
||||
</div>
|
||||
</StabilimentoProvider>
|
||||
);
|
||||
export default function MainLayout({ children }: { children: React.ReactNode }) {
|
||||
return <AppShell>{children}</AppShell>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<StabilimentoProvider>
|
||||
<div>
|
||||
<Navbar actions={<SignOutButton />} />
|
||||
<main>{children}</main>
|
||||
</div>
|
||||
</StabilimentoProvider>
|
||||
);
|
||||
export default function ManagementLayout({ children }: { children: React.ReactNode }) {
|
||||
return <AppShell>{children}</AppShell>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<div className="min-h-screen bg-black -mx-4 -my-6 px-4 py-6">
|
||||
<StabilimentiManager />
|
||||
|
|
|
|||
|
|
@ -1,8 +1,23 @@
|
|||
import Link from "next/link";
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
export default function Utenti() {
|
||||
return <>
|
||||
<h1>Utenti</h1>
|
||||
<Link href="/utenti">Utenti</Link>
|
||||
</>
|
||||
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 (
|
||||
<div className="min-h-screen bg-black -mx-4 -my-6 px-4 py-6">
|
||||
<UtentiManager />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
|
|
|
|||
88
src/app/api/utenti/[id]/route.ts
Normal file
88
src/app/api/utenti/[id]/route.ts
Normal file
|
|
@ -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 },
|
||||
);
|
||||
}
|
||||
}
|
||||
36
src/app/api/utenti/route.ts
Normal file
36
src/app/api/utenti/route.ts
Normal file
|
|
@ -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 },
|
||||
);
|
||||
}
|
||||
}
|
||||
16
src/auth.ts
16
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<string, unk
|
|||
});
|
||||
}
|
||||
|
||||
function isUserRole(value: string): value is UserRole {
|
||||
return ["admin", "manager", "user", "sviluppo"].includes(value);
|
||||
}
|
||||
|
||||
async function findActiveUser(identifier: string): Promise<DbUser | null> {
|
||||
let rows: DbUserRow[];
|
||||
|
||||
|
|
@ -66,7 +63,8 @@ async function findActiveUser(identifier: string): Promise<DbUser | null> {
|
|||
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<DbUser | null> {
|
|||
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<void> {
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
38
src/components/AppShell.tsx
Normal file
38
src/components/AppShell.tsx
Normal file
|
|
@ -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 (
|
||||
<PermissionsProvider ruolo={ruolo} permissions={permissions}>
|
||||
<StabilimentoProvider>
|
||||
<div>
|
||||
<Navbar
|
||||
permissions={permissions}
|
||||
userLabel={session.user.utente}
|
||||
actions={<SignOutButton />}
|
||||
/>
|
||||
<main>{children}</main>
|
||||
</div>
|
||||
</StabilimentoProvider>
|
||||
</PermissionsProvider>
|
||||
);
|
||||
}
|
||||
|
|
@ -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({
|
|||
</button>
|
||||
<button
|
||||
onClick={() => onMovimento(article, "scarico")}
|
||||
className="p-1.5 rounded text-zinc-500 hover:text-red-400 hover:bg-red-500/10 transition-colors"
|
||||
title="Scarico"
|
||||
disabled={!allowScarico}
|
||||
className="p-1.5 rounded text-zinc-500 hover:text-red-400 hover:bg-red-500/10 transition-colors disabled:opacity-30 disabled:cursor-not-allowed disabled:hover:text-zinc-500 disabled:hover:bg-transparent"
|
||||
title={allowScarico ? "Scarico" : "Scarico non consentito per il tuo ruolo"}
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M20 12H4" />
|
||||
|
|
|
|||
|
|
@ -7,9 +7,12 @@ import type { TipoMovimento } from "@/src/types/movimento";
|
|||
import ArticleCard from "./ArticleCard";
|
||||
import ArticleModal from "./ArticleModal";
|
||||
import MovimentoModal from "./MovimentoModal";
|
||||
import { usePermissions } from "@/src/contexts/PermissionsContext";
|
||||
import { useStabilimento } from "@/src/contexts/StabilimentoContext";
|
||||
|
||||
export default function ArticlesList() {
|
||||
const { permissions } = usePermissions();
|
||||
|
||||
const [articles, setArticles] = useState<Article[]>([]);
|
||||
const [families, setFamilies] = useState<ArticleFamily[]>([]);
|
||||
const [giacenze, setGiacenze] = useState<GiacenzePerArticolo>({});
|
||||
|
|
@ -211,6 +214,8 @@ export default function ArticlesList() {
|
|||
onEdit={handleEdit}
|
||||
onDelete={handleDelete}
|
||||
onMovimento={handleMovimento}
|
||||
allowScarico={permissions.canScaricoArticoli}
|
||||
draggable={permissions.canAssegnaArticoliMacchina}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ interface ArticoliMacchinaModalProps {
|
|||
macchinaNome: string;
|
||||
macchinaStabilimentoId: number | null;
|
||||
articoli: ArticoloAssegnatoMacchina[];
|
||||
canRemove?: boolean;
|
||||
onClose: () => void;
|
||||
onArticoloRimosso: () => void;
|
||||
}
|
||||
|
|
@ -26,6 +27,7 @@ export default function ArticoliMacchinaModal({
|
|||
macchinaNome,
|
||||
macchinaStabilimentoId,
|
||||
articoli,
|
||||
canRemove = true,
|
||||
onClose,
|
||||
onArticoloRimosso,
|
||||
}: ArticoliMacchinaModalProps) {
|
||||
|
|
@ -192,8 +194,9 @@ export default function ArticoliMacchinaModal({
|
|||
</div>
|
||||
<button
|
||||
onClick={() => handleRimuoviClick(art)}
|
||||
className="p-2 rounded-lg text-zinc-500 hover:text-red-400 hover:bg-red-900/20 opacity-0 group-hover:opacity-100 transition-opacity"
|
||||
title="Rimuovi articolo"
|
||||
disabled={!canRemove}
|
||||
className="p-2 rounded-lg text-zinc-500 hover:text-red-400 hover:bg-red-900/20 opacity-0 group-hover:opacity-100 transition-opacity disabled:opacity-30 disabled:cursor-not-allowed disabled:hover:text-zinc-500 disabled:hover:bg-transparent"
|
||||
title={canRemove ? "Rimuovi articolo" : "Rimozione non consentita per il tuo ruolo"}
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import type { ArticoloAssegnatoMacchina } from "@/src/types/artMacchineParti";
|
|||
import TreeNode from "./TreeNode";
|
||||
import MacchinaModal from "./MacchinaModal";
|
||||
import ArticoliMacchinaModal from "./ArticoliMacchinaModal";
|
||||
import { usePermissions } from "@/src/contexts/PermissionsContext";
|
||||
import { useStabilimento } from "@/src/contexts/StabilimentoContext";
|
||||
|
||||
interface DroppedArticle {
|
||||
|
|
@ -24,6 +25,9 @@ interface DropModalState {
|
|||
}
|
||||
|
||||
export default function MacchineTree() {
|
||||
const { permissions } = usePermissions();
|
||||
const readOnly = !permissions.canEditMacchine;
|
||||
|
||||
const [nodes, setNodes] = useState<MacchinaWithDetails[]>([]);
|
||||
const [articlesByMacchina, setArticlesByMacchina] = useState<Record<number, ArticoloAssegnatoMacchina[]>>({});
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
|
@ -424,6 +428,7 @@ export default function MacchineTree() {
|
|||
{filteredCount} elementi in {selectedStabilimento.descrizione}
|
||||
</p>
|
||||
</div>
|
||||
{!readOnly && (
|
||||
<button
|
||||
onClick={handleAddMachine}
|
||||
disabled={isCloning}
|
||||
|
|
@ -434,6 +439,7 @@ export default function MacchineTree() {
|
|||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Indicatore clonazione */}
|
||||
|
|
@ -448,7 +454,7 @@ export default function MacchineTree() {
|
|||
)}
|
||||
|
||||
{/* Indicatore copia */}
|
||||
{copiedNodeId && (
|
||||
{!readOnly && copiedNodeId && (
|
||||
<div className="mb-3 px-3 py-2 rounded-lg bg-yellow-900/30 border border-yellow-700/50 flex items-center justify-between">
|
||||
<span className="text-sm text-yellow-300">
|
||||
Nodo copiato. Clicca su "Incolla" per inserirlo.
|
||||
|
|
@ -487,6 +493,8 @@ export default function MacchineTree() {
|
|||
<p className="text-zinc-500 text-sm mb-2">
|
||||
Nessuna macchina in "{selectedStabilimento.descrizione}"
|
||||
</p>
|
||||
{!readOnly && (
|
||||
<>
|
||||
<p className="text-zinc-600 text-xs mb-4">
|
||||
Clicca sul pulsante + per aggiungere la prima macchina
|
||||
</p>
|
||||
|
|
@ -496,6 +504,8 @@ export default function MacchineTree() {
|
|||
>
|
||||
Aggiungi macchina
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-1">
|
||||
|
|
@ -515,7 +525,8 @@ export default function MacchineTree() {
|
|||
onCopy={handleCopy}
|
||||
onPaste={handlePaste}
|
||||
onShowArticles={handleShowArticles}
|
||||
onDropArticle={handleDropArticle}
|
||||
onDropArticle={readOnly ? undefined : handleDropArticle}
|
||||
readOnly={readOnly}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
|
@ -609,6 +620,7 @@ export default function MacchineTree() {
|
|||
macchinaNome={showArticlesModal.macchinaNome}
|
||||
macchinaStabilimentoId={showArticlesModal.macchinaStabilimentoId}
|
||||
articoli={showArticlesModal.articoli}
|
||||
canRemove={permissions.canRemoveArticoloMacchina}
|
||||
onClose={() => setShowArticlesModal(null)}
|
||||
onArticoloRimosso={async () => {
|
||||
await fetchArticoliPerMacchina();
|
||||
|
|
|
|||
|
|
@ -2,34 +2,50 @@
|
|||
// File: Navbar.tsx
|
||||
// componente Navbar
|
||||
// @author: "villari.andrea@libero.it"
|
||||
// @version: "1.0.0 2026-05-07"
|
||||
// @version: "1.1.0 2026-05-31"
|
||||
//====================================
|
||||
"use client"
|
||||
"use client";
|
||||
|
||||
import { ReactNode } from "react";
|
||||
import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
import GlobalStabilimentoSelector from "./GlobalStabilimentoSelector";
|
||||
import type { AppPermissions } from "@/src/lib/permissions";
|
||||
|
||||
interface NavLink {
|
||||
href: string;
|
||||
label: string;
|
||||
visible: (p: AppPermissions) => boolean;
|
||||
}
|
||||
|
||||
const navLinks: NavLink[] = [
|
||||
{ href: "/", label: "Home"},
|
||||
{ href: "/movimenti", label: "Movimenti"},
|
||||
{ href: "/articoli", label: "Articoli"},
|
||||
{ href: "/macchine", label: "Macchine"},
|
||||
{ href: "/stabilimenti", label: "Stabilimenti"},
|
||||
{ href: "/utenti", label: "Utenti"},
|
||||
{ href: "/", label: "Home", visible: () => true },
|
||||
{ href: "/movimenti", label: "Movimenti", visible: () => true },
|
||||
{ href: "/articoli", label: "Articoli", visible: () => true },
|
||||
{ href: "/macchine", label: "Macchine", visible: () => true },
|
||||
{
|
||||
href: "/stabilimenti",
|
||||
label: "Stabilimenti",
|
||||
visible: (p) => p.canManageStabilimenti,
|
||||
},
|
||||
{ href: "/utenti", label: "Utenti", visible: (p) => p.canManageUtenti },
|
||||
];
|
||||
|
||||
export default function Navbar({ actions }: { actions?: ReactNode }) {
|
||||
export default function Navbar({
|
||||
actions,
|
||||
permissions,
|
||||
userLabel,
|
||||
}: {
|
||||
actions?: ReactNode;
|
||||
permissions: AppPermissions;
|
||||
userLabel?: string;
|
||||
}) {
|
||||
const visibleLinks = navLinks.filter((link) => link.visible(permissions));
|
||||
|
||||
return (
|
||||
<nav className="border-b border-zinc-800 bg-black">
|
||||
<div className="max-w-7xl mx-auto px-4">
|
||||
<div className="flex items-center justify-between h-32">
|
||||
{/* Logo */}
|
||||
<Link
|
||||
href="/"
|
||||
className="flex items-center hover:opacity-80 transition-opacity"
|
||||
|
|
@ -44,9 +60,8 @@ export default function Navbar({ actions }: { actions?: ReactNode }) {
|
|||
/>
|
||||
</Link>
|
||||
|
||||
{/* Navigation Links */}
|
||||
<div className="flex items-center space-x-6">
|
||||
{navLinks.map((link) => (
|
||||
{visibleLinks.map((link) => (
|
||||
<Link
|
||||
key={link.href}
|
||||
href={link.href}
|
||||
|
|
@ -57,17 +72,16 @@ export default function Navbar({ actions }: { actions?: ReactNode }) {
|
|||
))}
|
||||
</div>
|
||||
|
||||
{/* Stabilimento Selector */}
|
||||
<div className="flex-1 flex justify-center px-4">
|
||||
<GlobalStabilimentoSelector />
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
{actions ? (
|
||||
<div className="flex items-center">
|
||||
{actions}
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
{userLabel ? (
|
||||
<span className="text-xs text-zinc-500 hidden lg:inline">{userLabel}</span>
|
||||
) : null}
|
||||
{actions ? <div className="flex items-center">{actions}</div> : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
|
|
|||
|
|
@ -57,6 +57,7 @@ interface TreeNodeProps {
|
|||
onPaste: (targetId: number) => void;
|
||||
onShowArticles?: (nodeId: number) => void;
|
||||
onDropArticle?: (macchinaId: number, article: unknown) => void;
|
||||
readOnly?: boolean;
|
||||
}
|
||||
|
||||
export default function TreeNode({
|
||||
|
|
@ -74,6 +75,7 @@ export default function TreeNode({
|
|||
onPaste,
|
||||
onShowArticles,
|
||||
onDropArticle,
|
||||
readOnly = false,
|
||||
}: TreeNodeProps) {
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
const [isDragOver, setIsDragOver] = useState(false);
|
||||
|
|
@ -100,6 +102,7 @@ export default function TreeNode({
|
|||
const totalQuantity = getTotalQuantity(node);
|
||||
|
||||
const handleDrop = (e: DragEvent<HTMLDivElement>) => {
|
||||
if (readOnly) return;
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setIsDragOver(false);
|
||||
|
|
@ -136,6 +139,7 @@ export default function TreeNode({
|
|||
};
|
||||
|
||||
const handleDragEnter = (e: DragEvent<HTMLDivElement>) => {
|
||||
if (readOnly) return;
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setIsDragOver(true);
|
||||
|
|
@ -171,10 +175,10 @@ export default function TreeNode({
|
|||
${getNodeTypeBg()}
|
||||
${isDragOver ? "ring-2 ring-blue-500 bg-blue-900/50 scale-[1.02]" : ""}
|
||||
hover:bg-zinc-800/70`}
|
||||
onDragEnter={handleDragEnter}
|
||||
onDragOver={handleDragOver}
|
||||
onDragLeave={handleDragLeave}
|
||||
onDrop={handleDrop}
|
||||
onDragEnter={readOnly ? undefined : handleDragEnter}
|
||||
onDragOver={readOnly ? undefined : handleDragOver}
|
||||
onDragLeave={readOnly ? undefined : handleDragLeave}
|
||||
onDrop={readOnly ? undefined : handleDrop}
|
||||
>
|
||||
{children.length > 0 && (
|
||||
<button
|
||||
|
|
@ -214,6 +218,7 @@ export default function TreeNode({
|
|||
</button>
|
||||
)}
|
||||
|
||||
{!readOnly && (
|
||||
<div className="flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
<button
|
||||
onClick={() => onAdd(node.id)}
|
||||
|
|
@ -290,6 +295,7 @@ export default function TreeNode({
|
|||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{isExpanded && children.length > 0 && (
|
||||
|
|
@ -311,6 +317,7 @@ export default function TreeNode({
|
|||
onPaste={onPaste}
|
||||
onShowArticles={onShowArticles}
|
||||
onDropArticle={onDropArticle}
|
||||
readOnly={readOnly}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
|
|
|||
389
src/components/UtentiManager.tsx
Normal file
389
src/components/UtentiManager.tsx
Normal file
|
|
@ -0,0 +1,389 @@
|
|||
"use client";
|
||||
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import type { UserPublic, UserRole } from "@/src/types/user";
|
||||
import { ROLE_LABELS, USER_ROLES } from "@/src/lib/permissions";
|
||||
|
||||
function FormModal({
|
||||
title,
|
||||
onClose,
|
||||
onSubmit,
|
||||
children,
|
||||
submitLabel,
|
||||
}: {
|
||||
title: string;
|
||||
onClose: () => void;
|
||||
onSubmit: (e: React.FormEvent) => void;
|
||||
children: React.ReactNode;
|
||||
submitLabel: string;
|
||||
}) {
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
|
||||
<div className="absolute inset-0 bg-black/70 backdrop-blur-sm" onClick={onClose} />
|
||||
<div className="relative w-full max-w-md rounded-xl bg-zinc-900 border border-zinc-700 p-6 shadow-xl">
|
||||
<h3 className="text-lg font-semibold text-white mb-4">{title}</h3>
|
||||
<form onSubmit={onSubmit} className="space-y-4">
|
||||
{children}
|
||||
<div className="flex justify-end gap-3 pt-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className="px-4 py-2 rounded-lg border border-zinc-700 text-zinc-300 hover:bg-zinc-800"
|
||||
>
|
||||
Annulla
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="px-4 py-2 rounded-lg bg-blue-600 text-white hover:bg-blue-500"
|
||||
>
|
||||
{submitLabel}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function formatLastLogin(value?: Date | string | null): string {
|
||||
if (!value) return "Mai";
|
||||
const d = value instanceof Date ? value : new Date(value);
|
||||
if (Number.isNaN(d.getTime())) return "—";
|
||||
return d.toLocaleString("it-IT", {
|
||||
day: "2-digit",
|
||||
month: "2-digit",
|
||||
year: "numeric",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
});
|
||||
}
|
||||
|
||||
function roleBadgeClass(ruolo: UserRole): string {
|
||||
switch (ruolo) {
|
||||
case "amministratore":
|
||||
return "bg-purple-900/40 text-purple-300 border-purple-800";
|
||||
case "tecnico":
|
||||
return "bg-blue-900/40 text-blue-300 border-blue-800";
|
||||
case "operatore":
|
||||
return "bg-zinc-800 text-zinc-300 border-zinc-700";
|
||||
}
|
||||
}
|
||||
|
||||
export default function UtentiManager() {
|
||||
const [utenti, setUtenti] = useState<UserPublic[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [search, setSearch] = useState("");
|
||||
const [modal, setModal] = useState<"create" | "edit" | null>(null);
|
||||
const [editing, setEditing] = useState<UserPublic | null>(null);
|
||||
|
||||
const [formUtente, setFormUtente] = useState("");
|
||||
const [formMail, setFormMail] = useState("");
|
||||
const [formRuolo, setFormRuolo] = useState<UserRole>("operatore");
|
||||
const [formAttivo, setFormAttivo] = useState<0 | 1>(1);
|
||||
const [formPassword, setFormPassword] = useState("");
|
||||
|
||||
const loadData = useCallback(async () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const res = await fetch("/api/utenti");
|
||||
if (!res.ok) {
|
||||
const data = await res.json();
|
||||
throw new Error(data.error || "Errore nel caricamento");
|
||||
}
|
||||
const data = await res.json();
|
||||
setUtenti(data.utenti || []);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : "Errore sconosciuto");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
loadData();
|
||||
}, [loadData]);
|
||||
|
||||
const resetForm = () => {
|
||||
setFormUtente("");
|
||||
setFormMail("");
|
||||
setFormRuolo("operatore");
|
||||
setFormAttivo(1);
|
||||
setFormPassword("");
|
||||
};
|
||||
|
||||
const openCreate = () => {
|
||||
setEditing(null);
|
||||
resetForm();
|
||||
setModal("create");
|
||||
};
|
||||
|
||||
const openEdit = (user: UserPublic) => {
|
||||
setEditing(user);
|
||||
setFormUtente(user.utente);
|
||||
setFormMail(user.mail);
|
||||
setFormRuolo(user.ruolo);
|
||||
setFormAttivo(user.attivo);
|
||||
setFormPassword("");
|
||||
setModal("edit");
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setError(null);
|
||||
try {
|
||||
const url = modal === "edit" && editing ? `/api/utenti/${editing.id}` : "/api/utenti";
|
||||
const method = modal === "edit" ? "PUT" : "POST";
|
||||
const body: Record<string, unknown> = {
|
||||
utente: formUtente,
|
||||
mail: formMail,
|
||||
ruolo: formRuolo,
|
||||
attivo: formAttivo,
|
||||
};
|
||||
if (formPassword) {
|
||||
body.password = formPassword;
|
||||
}
|
||||
|
||||
const res = await fetch(url, {
|
||||
method,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
const data = await res.json();
|
||||
if (!res.ok) throw new Error(data.error || "Operazione fallita");
|
||||
setModal(null);
|
||||
await loadData();
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : "Errore");
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async (user: UserPublic) => {
|
||||
if (!confirm(`Eliminare l'utente "${user.utente}"?`)) return;
|
||||
setError(null);
|
||||
try {
|
||||
const res = await fetch(`/api/utenti/${user.id}`, { method: "DELETE" });
|
||||
const data = await res.json();
|
||||
if (!res.ok) throw new Error(data.error || "Eliminazione fallita");
|
||||
await loadData();
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : "Errore");
|
||||
}
|
||||
};
|
||||
|
||||
const filtered = utenti.filter(
|
||||
(u) =>
|
||||
u.utente.toLowerCase().includes(search.toLowerCase()) ||
|
||||
u.mail.toLowerCase().includes(search.toLowerCase()) ||
|
||||
ROLE_LABELS[u.ruolo].toLowerCase().includes(search.toLowerCase()),
|
||||
);
|
||||
|
||||
const attivi = utenti.filter((u) => u.attivo === 1).length;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="mb-6">
|
||||
<h1 className="text-2xl font-bold text-white">Gestione utenti</h1>
|
||||
<p className="text-sm text-zinc-500">
|
||||
Amministratori, tecnici e operatori con permessi differenziati
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4 mb-6">
|
||||
<div className="p-4 rounded-xl bg-zinc-900 border border-zinc-800">
|
||||
<p className="text-sm text-zinc-500">Utenti totali</p>
|
||||
<p className="text-2xl font-bold text-white">{utenti.length}</p>
|
||||
</div>
|
||||
<div className="p-4 rounded-xl bg-zinc-900 border border-zinc-800">
|
||||
<p className="text-sm text-zinc-500">Attivi</p>
|
||||
<p className="text-2xl font-bold text-green-400">{attivi}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="mb-4 p-4 rounded-lg bg-red-500/10 border border-red-500/30 text-red-400 text-sm flex justify-between items-center">
|
||||
<span>{error}</span>
|
||||
<button type="button" onClick={() => setError(null)} className="text-red-300 hover:text-white">
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex flex-col md:flex-row gap-4 items-start md:items-center justify-between mb-6">
|
||||
<input
|
||||
type="text"
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
placeholder="Cerca per nome, email o ruolo..."
|
||||
className="w-full md:max-w-md px-4 py-2 rounded-lg bg-zinc-900 border border-zinc-800 text-white placeholder-zinc-500 focus:outline-none focus:border-blue-500"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={openCreate}
|
||||
className="px-4 py-2 rounded-lg bg-blue-600 text-white font-medium hover:bg-blue-500"
|
||||
>
|
||||
Nuovo utente
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center py-20 text-zinc-500">
|
||||
<svg className="w-6 h-6 animate-spin mr-3" viewBox="0 0 24 24">
|
||||
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" fill="none" />
|
||||
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
|
||||
</svg>
|
||||
Caricamento...
|
||||
</div>
|
||||
) : filtered.length === 0 ? (
|
||||
<div className="p-12 rounded-xl bg-zinc-900 border border-zinc-800 text-center">
|
||||
<h3 className="text-lg font-medium text-white mb-2">Nessun utente</h3>
|
||||
<p className="text-zinc-500 mb-4">
|
||||
{search ? "Nessun risultato per la ricerca" : "Aggiungi il primo utente"}
|
||||
</p>
|
||||
{!search && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={openCreate}
|
||||
className="px-4 py-2 rounded-lg bg-blue-600 text-white hover:bg-blue-500"
|
||||
>
|
||||
Crea utente
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="rounded-xl bg-zinc-900 border border-zinc-800 overflow-hidden">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b border-zinc-800 text-zinc-500 text-left">
|
||||
<th className="px-4 py-3 font-medium">Utente</th>
|
||||
<th className="px-4 py-3 font-medium">Email</th>
|
||||
<th className="px-4 py-3 font-medium">Ruolo</th>
|
||||
<th className="px-4 py-3 font-medium">Stato</th>
|
||||
<th className="px-4 py-3 font-medium">Ultimo accesso</th>
|
||||
<th className="px-4 py-3 font-medium text-right">Azioni</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{filtered.map((user) => (
|
||||
<tr key={user.id} className="border-b border-zinc-800/80 hover:bg-zinc-800/30">
|
||||
<td className="px-4 py-3 text-white font-medium">{user.utente}</td>
|
||||
<td className="px-4 py-3 text-zinc-400">{user.mail}</td>
|
||||
<td className="px-4 py-3">
|
||||
<span
|
||||
className={`inline-block px-2 py-0.5 rounded text-xs border ${roleBadgeClass(user.ruolo)}`}
|
||||
>
|
||||
{ROLE_LABELS[user.ruolo]}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
{user.attivo === 1 ? (
|
||||
<span className="text-green-400">Attivo</span>
|
||||
) : (
|
||||
<span className="text-zinc-500">Disattivo</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-zinc-500 text-xs">
|
||||
{formatLastLogin(user.last_login)}
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
<div className="flex justify-end gap-1">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => openEdit(user)}
|
||||
className="p-2 rounded-lg text-zinc-400 hover:text-white hover:bg-zinc-800"
|
||||
title="Modifica"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleDelete(user)}
|
||||
className="p-2 rounded-lg text-red-400 hover:text-red-300 hover:bg-red-500/10"
|
||||
title="Elimina"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{modal && (
|
||||
<FormModal
|
||||
title={modal === "edit" ? "Modifica utente" : "Nuovo utente"}
|
||||
onClose={() => setModal(null)}
|
||||
onSubmit={handleSubmit}
|
||||
submitLabel={modal === "edit" ? "Salva" : "Crea"}
|
||||
>
|
||||
<div>
|
||||
<label className="block text-xs text-zinc-400 mb-1">Nome utente</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formUtente}
|
||||
onChange={(e) => setFormUtente(e.target.value)}
|
||||
required
|
||||
className="w-full px-3 py-2 rounded-lg bg-zinc-950 border border-zinc-700 text-white focus:outline-none focus:border-blue-500"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs text-zinc-400 mb-1">Email</label>
|
||||
<input
|
||||
type="email"
|
||||
value={formMail}
|
||||
onChange={(e) => setFormMail(e.target.value)}
|
||||
required
|
||||
className="w-full px-3 py-2 rounded-lg bg-zinc-950 border border-zinc-700 text-white focus:outline-none focus:border-blue-500"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs text-zinc-400 mb-1">Ruolo</label>
|
||||
<select
|
||||
value={formRuolo}
|
||||
onChange={(e) => setFormRuolo(e.target.value as UserRole)}
|
||||
className="w-full px-3 py-2 rounded-lg bg-zinc-950 border border-zinc-700 text-white focus:outline-none focus:border-blue-500"
|
||||
>
|
||||
{USER_ROLES.map((r) => (
|
||||
<option key={r} value={r}>
|
||||
{ROLE_LABELS[r]}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs text-zinc-400 mb-1">Stato</label>
|
||||
<select
|
||||
value={formAttivo}
|
||||
onChange={(e) => setFormAttivo(parseInt(e.target.value, 10) as 0 | 1)}
|
||||
className="w-full px-3 py-2 rounded-lg bg-zinc-950 border border-zinc-700 text-white focus:outline-none focus:border-blue-500"
|
||||
>
|
||||
<option value={1}>Attivo</option>
|
||||
<option value={0}>Disattivo</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs text-zinc-400 mb-1">
|
||||
Password {modal === "edit" && "(lascia vuoto per non cambiare)"}
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
value={formPassword}
|
||||
onChange={(e) => setFormPassword(e.target.value)}
|
||||
required={modal === "create"}
|
||||
minLength={6}
|
||||
className="w-full px-3 py-2 rounded-lg bg-zinc-950 border border-zinc-700 text-white focus:outline-none focus:border-blue-500"
|
||||
/>
|
||||
</div>
|
||||
</FormModal>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
37
src/contexts/PermissionsContext.tsx
Normal file
37
src/contexts/PermissionsContext.tsx
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
"use client";
|
||||
|
||||
import { createContext, useContext, type ReactNode } from "react";
|
||||
|
||||
import type { AppPermissions } from "@/src/lib/permissions";
|
||||
import type { UserRole } from "@/src/types/user";
|
||||
|
||||
interface PermissionsContextValue {
|
||||
ruolo: UserRole;
|
||||
permissions: AppPermissions;
|
||||
}
|
||||
|
||||
const PermissionsContext = createContext<PermissionsContextValue | null>(null);
|
||||
|
||||
export function PermissionsProvider({
|
||||
ruolo,
|
||||
permissions,
|
||||
children,
|
||||
}: {
|
||||
ruolo: UserRole;
|
||||
permissions: AppPermissions;
|
||||
children: ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<PermissionsContext.Provider value={{ ruolo, permissions }}>
|
||||
{children}
|
||||
</PermissionsContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function usePermissions(): PermissionsContextValue {
|
||||
const ctx = useContext(PermissionsContext);
|
||||
if (!ctx) {
|
||||
throw new Error("usePermissions deve essere usato dentro PermissionsProvider");
|
||||
}
|
||||
return ctx;
|
||||
}
|
||||
62
src/lib/auth/requireSession.ts
Normal file
62
src/lib/auth/requireSession.ts
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
import { NextResponse } from "next/server";
|
||||
|
||||
import { auth } from "@/src/auth";
|
||||
import {
|
||||
getPermissions,
|
||||
normalizeRole,
|
||||
type AppPermissions,
|
||||
} from "@/src/lib/permissions";
|
||||
import type { UserRole } from "@/src/types/user";
|
||||
|
||||
export type AuthSession = {
|
||||
user: {
|
||||
id: string;
|
||||
ruolo: UserRole;
|
||||
utente: string;
|
||||
name?: string | null;
|
||||
email?: string | null;
|
||||
};
|
||||
permissions: AppPermissions;
|
||||
};
|
||||
|
||||
export async function requireSession(): Promise<AuthSession | NextResponse> {
|
||||
const session = await auth();
|
||||
|
||||
if (!session?.user) {
|
||||
return NextResponse.json({ error: "Non autorizzato" }, { status: 401 });
|
||||
}
|
||||
|
||||
const ruolo = normalizeRole(session.user.ruolo);
|
||||
if (!ruolo) {
|
||||
return NextResponse.json({ error: "Ruolo utente non valido" }, { status: 403 });
|
||||
}
|
||||
|
||||
return {
|
||||
user: {
|
||||
id: session.user.id,
|
||||
ruolo,
|
||||
utente: session.user.utente,
|
||||
name: session.user.name,
|
||||
email: session.user.email,
|
||||
},
|
||||
permissions: getPermissions(ruolo),
|
||||
};
|
||||
}
|
||||
|
||||
export function forbid(message = "Accesso negato"): NextResponse {
|
||||
return NextResponse.json({ error: message }, { status: 403 });
|
||||
}
|
||||
|
||||
export async function requirePermission(
|
||||
check: (permissions: AppPermissions) => boolean,
|
||||
message = "Accesso negato",
|
||||
): Promise<AuthSession | NextResponse> {
|
||||
const result = await requireSession();
|
||||
if (result instanceof NextResponse) return result;
|
||||
|
||||
if (!check(result.permissions)) {
|
||||
return forbid(message);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
72
src/lib/permissions.ts
Normal file
72
src/lib/permissions.ts
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
import type { UserRole } from "@/src/types/user";
|
||||
|
||||
/** Ruoli canonici in italiano (valori consigliati in DB). */
|
||||
export const USER_ROLES: UserRole[] = ["amministratore", "tecnico", "operatore"];
|
||||
|
||||
/** Mappatura ruoli legacy → ruoli attuali. */
|
||||
const LEGACY_ROLE_MAP: Record<string, UserRole> = {
|
||||
admin: "amministratore",
|
||||
sviluppo: "amministratore",
|
||||
manager: "tecnico",
|
||||
user: "operatore",
|
||||
amministratore: "amministratore",
|
||||
tecnico: "tecnico",
|
||||
operatore: "operatore",
|
||||
};
|
||||
|
||||
export const ROLE_LABELS: Record<UserRole, string> = {
|
||||
amministratore: "Amministratore",
|
||||
tecnico: "Tecnico",
|
||||
operatore: "Operatore",
|
||||
};
|
||||
|
||||
export function isUserRole(value: string): value is UserRole {
|
||||
return USER_ROLES.includes(value as UserRole);
|
||||
}
|
||||
|
||||
export function normalizeRole(value: string): UserRole | null {
|
||||
const key = value.trim().toLowerCase();
|
||||
const mapped = LEGACY_ROLE_MAP[key];
|
||||
return mapped ?? null;
|
||||
}
|
||||
|
||||
export interface AppPermissions {
|
||||
canManageUtenti: boolean;
|
||||
canManageStabilimenti: boolean;
|
||||
canScaricoArticoli: boolean;
|
||||
canAssegnaArticoliMacchina: boolean;
|
||||
canEditMacchine: boolean;
|
||||
canRemoveArticoloMacchina: boolean;
|
||||
}
|
||||
|
||||
export function getPermissions(ruolo: UserRole): AppPermissions {
|
||||
switch (ruolo) {
|
||||
case "amministratore":
|
||||
return {
|
||||
canManageUtenti: true,
|
||||
canManageStabilimenti: true,
|
||||
canScaricoArticoli: true,
|
||||
canAssegnaArticoliMacchina: true,
|
||||
canEditMacchine: true,
|
||||
canRemoveArticoloMacchina: true,
|
||||
};
|
||||
case "tecnico":
|
||||
return {
|
||||
canManageUtenti: false,
|
||||
canManageStabilimenti: false,
|
||||
canScaricoArticoli: true,
|
||||
canAssegnaArticoliMacchina: true,
|
||||
canEditMacchine: true,
|
||||
canRemoveArticoloMacchina: true,
|
||||
};
|
||||
case "operatore":
|
||||
return {
|
||||
canManageUtenti: false,
|
||||
canManageStabilimenti: false,
|
||||
canScaricoArticoli: false,
|
||||
canAssegnaArticoliMacchina: false,
|
||||
canEditMacchine: false,
|
||||
canRemoveArticoloMacchina: false,
|
||||
};
|
||||
}
|
||||
}
|
||||
187
src/lib/users/index.ts
Normal file
187
src/lib/users/index.ts
Normal file
|
|
@ -0,0 +1,187 @@
|
|||
import bcrypt from "bcrypt";
|
||||
import type { ResultSetHeader, RowDataPacket } from "mysql2";
|
||||
|
||||
import { db } from "@/src/lib/db";
|
||||
import { isUserRole } from "@/src/lib/permissions";
|
||||
import type { DbUser, UserInput, UserPublic, UserRole } from "@/src/types/user";
|
||||
|
||||
type UserRow = DbUser & RowDataPacket;
|
||||
|
||||
const BCRYPT_ROUNDS = 10;
|
||||
|
||||
function toPublic(row: UserRow): UserPublic {
|
||||
return {
|
||||
id: row.id,
|
||||
utente: row.utente,
|
||||
mail: row.mail,
|
||||
ruolo: row.ruolo,
|
||||
attivo: row.attivo,
|
||||
last_login: row.last_login,
|
||||
};
|
||||
}
|
||||
|
||||
function cleanText(value: unknown): string {
|
||||
return String(value ?? "").trim();
|
||||
}
|
||||
|
||||
function parseUserInput(input: Partial<UserInput>, requirePassword: boolean): UserInput {
|
||||
const utente = cleanText(input.utente);
|
||||
const mail = cleanText(input.mail);
|
||||
const ruolo = cleanText(input.ruolo) as UserRole;
|
||||
const attivo = input.attivo === 0 || input.attivo === 1 ? input.attivo : 1;
|
||||
const password = input.password ? String(input.password) : "";
|
||||
|
||||
if (!utente) {
|
||||
throw new Error("Il nome utente è obbligatorio.");
|
||||
}
|
||||
if (!mail || !mail.includes("@")) {
|
||||
throw new Error("Inserire un indirizzo email valido.");
|
||||
}
|
||||
if (!isUserRole(ruolo)) {
|
||||
throw new Error("Ruolo non valido. Scegli Amministratore, Tecnico o Operatore.");
|
||||
}
|
||||
if (requirePassword && password.length < 6) {
|
||||
throw new Error("La password deve avere almeno 6 caratteri.");
|
||||
}
|
||||
if (!requirePassword && password && password.length < 6) {
|
||||
throw new Error("La password deve avere almeno 6 caratteri.");
|
||||
}
|
||||
|
||||
return { utente, mail, ruolo, attivo, password: password || undefined };
|
||||
}
|
||||
|
||||
export async function listUsers(): Promise<UserPublic[]> {
|
||||
const [rows] = await db.execute<UserRow[]>(
|
||||
`SELECT id, utente, mail, ruolo, attivo, last_login
|
||||
FROM utenti
|
||||
ORDER BY utente ASC`,
|
||||
);
|
||||
|
||||
return rows.map(toPublic);
|
||||
}
|
||||
|
||||
export async function getUserById(id: number): Promise<UserPublic | null> {
|
||||
const [rows] = await db.execute<UserRow[]>(
|
||||
`SELECT id, utente, mail, ruolo, attivo, last_login
|
||||
FROM utenti WHERE id = :id LIMIT 1`,
|
||||
{ id },
|
||||
);
|
||||
|
||||
const row = rows[0];
|
||||
return row ? toPublic(row) : null;
|
||||
}
|
||||
|
||||
async function assertUniqueUtenteMail(
|
||||
utente: string,
|
||||
mail: string,
|
||||
excludeId?: number,
|
||||
): Promise<void> {
|
||||
const params: Record<string, string | number> = { utente, mail };
|
||||
let query = `
|
||||
SELECT id FROM utenti
|
||||
WHERE (utente = :utente OR mail = :mail)
|
||||
`;
|
||||
if (excludeId) {
|
||||
query += " AND id != :excludeId";
|
||||
params.excludeId = excludeId;
|
||||
}
|
||||
query += " LIMIT 1";
|
||||
|
||||
const [rows] = await db.execute<RowDataPacket[]>(query, params);
|
||||
if (rows[0]) {
|
||||
throw new Error("Nome utente o email già in uso.");
|
||||
}
|
||||
}
|
||||
|
||||
export async function createUser(input: Partial<UserInput>): Promise<UserPublic> {
|
||||
const parsed = parseUserInput(input, true);
|
||||
await assertUniqueUtenteMail(parsed.utente, parsed.mail);
|
||||
|
||||
const hash = await bcrypt.hash(parsed.password!, BCRYPT_ROUNDS);
|
||||
|
||||
const [result] = await db.execute<ResultSetHeader>(
|
||||
`INSERT INTO utenti (utente, mail, password, ruolo, attivo)
|
||||
VALUES (:utente, :mail, :password, :ruolo, :attivo)`,
|
||||
{
|
||||
utente: parsed.utente,
|
||||
mail: parsed.mail,
|
||||
password: hash,
|
||||
ruolo: parsed.ruolo,
|
||||
attivo: parsed.attivo,
|
||||
},
|
||||
);
|
||||
|
||||
const created = await getUserById(result.insertId);
|
||||
if (!created) {
|
||||
throw new Error("Errore nella creazione dell'utente.");
|
||||
}
|
||||
return created;
|
||||
}
|
||||
|
||||
export async function updateUser(
|
||||
id: number,
|
||||
input: Partial<UserInput>,
|
||||
): Promise<UserPublic> {
|
||||
const existing = await getUserById(id);
|
||||
if (!existing) {
|
||||
throw new Error("Utente non trovato.");
|
||||
}
|
||||
|
||||
const parsed = parseUserInput(
|
||||
{
|
||||
utente: input.utente ?? existing.utente,
|
||||
mail: input.mail ?? existing.mail,
|
||||
ruolo: input.ruolo ?? existing.ruolo,
|
||||
attivo: input.attivo ?? existing.attivo,
|
||||
password: input.password,
|
||||
},
|
||||
false,
|
||||
);
|
||||
|
||||
await assertUniqueUtenteMail(parsed.utente, parsed.mail, id);
|
||||
|
||||
if (parsed.password) {
|
||||
const hash = await bcrypt.hash(parsed.password, BCRYPT_ROUNDS);
|
||||
await db.execute(
|
||||
`UPDATE utenti
|
||||
SET utente = :utente, mail = :mail, ruolo = :ruolo, attivo = :attivo, password = :password
|
||||
WHERE id = :id`,
|
||||
{
|
||||
utente: parsed.utente,
|
||||
mail: parsed.mail,
|
||||
ruolo: parsed.ruolo,
|
||||
attivo: parsed.attivo,
|
||||
password: hash,
|
||||
id,
|
||||
},
|
||||
);
|
||||
} else {
|
||||
await db.execute(
|
||||
`UPDATE utenti
|
||||
SET utente = :utente, mail = :mail, ruolo = :ruolo, attivo = :attivo
|
||||
WHERE id = :id`,
|
||||
{
|
||||
utente: parsed.utente,
|
||||
mail: parsed.mail,
|
||||
ruolo: parsed.ruolo,
|
||||
attivo: parsed.attivo,
|
||||
id,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
const updated = await getUserById(id);
|
||||
if (!updated) {
|
||||
throw new Error("Errore nell'aggiornamento dell'utente.");
|
||||
}
|
||||
return updated;
|
||||
}
|
||||
|
||||
export async function deleteUser(id: number): Promise<void> {
|
||||
const existing = await getUserById(id);
|
||||
if (!existing) {
|
||||
throw new Error("Utente non trovato.");
|
||||
}
|
||||
|
||||
await db.execute("DELETE FROM utenti WHERE id = :id", { id });
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
export type UserRole = "admin" | "manager" | "user" | "sviluppo";
|
||||
export type UserRole = "amministratore" | "tecnico" | "operatore";
|
||||
|
||||
export interface DbUser {
|
||||
id: number;
|
||||
|
|
@ -9,3 +9,21 @@ export interface DbUser {
|
|||
attivo: 0 | 1;
|
||||
last_login?: Date | string | null;
|
||||
}
|
||||
|
||||
/** Utente esposto dalle API (senza password). */
|
||||
export interface UserPublic {
|
||||
id: number;
|
||||
utente: string;
|
||||
mail: string;
|
||||
ruolo: UserRole;
|
||||
attivo: 0 | 1;
|
||||
last_login?: Date | string | null;
|
||||
}
|
||||
|
||||
export interface UserInput {
|
||||
utente: string;
|
||||
mail: string;
|
||||
ruolo: UserRole;
|
||||
attivo: 0 | 1;
|
||||
password?: string;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue