auth tests

This commit is contained in:
Giuseppe Naponiello
2026-06-22 14:31:44 +02:00
parent 34a8ebc6fb
commit cc4e174880
75 changed files with 4679 additions and 168 deletions

View File

@@ -0,0 +1,26 @@
{
"extends": "stylelint-config-standard",
"rules": {
"declaration-property-value-no-unknown": true,
"import-notation": null,
"at-rule-no-unknown": [
true,
{
"ignoreAtRules": [
"theme",
"apply",
"tailwind",
"config",
"plugin",
"source",
"utility",
"variant",
"custom-variant",
"reference"
]
}
],
"color-function-notation": null,
"alpha-value-notation": null
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -8,11 +8,15 @@
"build": "vite build",
"preview": "vite preview",
"type-check": "tsc --noEmit",
"lint:css": "stylelint \"src/**/*.css\"",
"lint:css:fix": "stylelint --fix \"src/**/*.css\"",
"test": "vitest run --coverage --passWithNoTests",
"test:watch": "vitest"
},
"devDependencies": {
"@tailwindcss/vite": "^4.3.1",
"stylelint": "^17.13.0",
"stylelint-config-standard": "^40.0.0",
"@types/leaflet": "^1.9.21",
"@types/node": "^25.9.3",
"@vitest/coverage-v8": "^4.1.8",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 99 KiB

After

Width:  |  Height:  |  Size: 87 KiB

View File

@@ -9,7 +9,15 @@ type IconName =
| 'log-out'
| 'user'
| 'settings'
| 'layout-dashboard';
| 'layout-dashboard'
| 'pickaxe'
| 'box'
| 'landmark'
| 'book-user'
| 'history'
| 'list'
| 'mail-plus'
| 'images';
type Visibility =
| 'always'
@@ -41,7 +49,7 @@ const menu: MenuItem[] = [
{ id: 'legalLink', href: '/policy', ico: 'shield', label: 'Legal', visibility: 'always' },
{ id: 'docsLink', href: 'https://lunddarklab.github.io/adc/', ico: 'book-open', label: 'Docs', visibility: 'always' },
{ id: 'loginLink', href: '/login', ico: 'log-in', label: 'Login', visibility: 'guest-only' },
{ id: 'userMenuToggle', href: '#', ico: 'menu', visibility: 'auth-only' },
{ id: 'user-menu-toggle', href: '#', ico: 'menu', visibility: 'auth-only' },
];
@@ -55,9 +63,15 @@ export function getVisibleItems(isLoggedIn: boolean) {
}
// --- Menu utente (pannello laterale) -------------------------------------
// Mostrato solo agli utenti autenticati. Le voci con `roles` sono visibili
// solo ai ruoli elencati; senza `roles` sono visibili a tutti i loggati.
// Per spostare/aggiungere una voce basta intervenire qui, senza toccare il DOM.
// Mostrato solo agli utenti autenticati. Le voci sono raggruppate in sezioni
// con titolo (come in dyncoll.v1/assets/menu.php). Sia i gruppi sia le singole
// voci possono dichiarare `roles`: se presente, sono visibili solo ai ruoli
// elencati; se assente, a tutti i loggati. Per spostare/aggiungere una voce
// basta intervenire qui, senza toccare il DOM.
//
// NB: le "main pages" di v1 (home/map/credits/legal/db model) non sono replicate
// qui: in v2 l'header-menu è sempre visibile, quindi quei link non servono nel
// pannello. Gli href sono rotte placeholder, da allineare al routing reale.
type PanelAction = 'logout';
@@ -70,35 +84,55 @@ export type UserPanelItem = {
roles?: RoleId[]; // assente = tutti gli autenticati
};
const userPanel: UserPanelItem[] = [
{ id: 'dashboardLink', label: 'Dashboard', ico: 'layout-dashboard', href: '/dashboard' },
{ id: 'profileLink', label: 'Profile', ico: 'user', href: '/profile' },
{ id: 'adminLink', label: 'Admin', ico: 'settings', href: '/admin', roles: [ROLE.ADMIN, ROLE.SUPERVISOR] },
{ id: 'logoutBtn', label: 'Logout', ico: 'log-out', action: 'logout' },
export type UserPanelGroup = {
title?: string; // titolo di sezione; assente nel primo gruppo
roles?: RoleId[]; // assente = tutti gli autenticati
items: UserPanelItem[];
};
const userPanel: UserPanelGroup[] = [
{
items: [
{ id: 'dashboardLink', label: 'Dashboard', ico: 'layout-dashboard', href: '/dashboard' },
],
},
{
title: 'Add resource',
items: [
{ id: 'artifactAddLink', label: 'Artifact', ico: 'pickaxe', href: '/artifacts/add' },
{ id: 'modelAddLink', label: 'Model', ico: 'box', href: '/models/add' },
{ id: 'institutionAddLink', label: 'Institution', ico: 'landmark', href: '/institutions/add' },
{ id: 'personAddLink', label: 'Person', ico: 'book-user', href: '/persons/add' },
],
},
{
title: 'Admin',
roles: [ROLE.ADMIN, ROLE.SUPERVISOR],
items: [
{ id: 'timelineLink', label: 'Timeline', ico: 'history', href: '/timeline' },
{ id: 'vocabulariesLink', label: 'Vocabularies', ico: 'list', href: '/vocabularies' },
{ id: 'mailComposerLink', label: 'Compose email', ico: 'mail-plus', href: '/mail/compose' },
],
},
{
title: 'My account',
items: [
{ id: 'settingsLink', label: 'Settings', ico: 'settings', href: '/settings' },
{ id: 'collectionsLink', label: 'My collections', ico: 'images', href: '#' },
{ id: 'logoutBtn', label: 'Logout', ico: 'log-out', action: 'logout' },
],
},
];
export function getUserPanelItems(roleId: number): UserPanelItem[] {
return userPanel.filter(item => !item.roles || item.roles.includes(roleId as RoleId));
}
// Tipi per sidebar
// type SidebarAction = 'logout';
// type SidebarItem = {
// id?: string;
// label: string;
// ico: IconName;
// href?: string; // se presente, la voce è un link
// action?: SidebarAction; // se presente, la voce è un pulsante con comportamento
// };
// Gruppo di voci del menu laterale. `roles` elenca i ruoli che vedono il gruppo:
// per spostare/aggiungere un link basta intervenire qui, senza toccare il DOM.
// type SidebarGroup = {
// title?: string; // titolo di sezione (header colorato); assente nel primo gruppo
// roles: RoleId[];
// items: SidebarItem[];
// };
// Menu laterale (utenti autenticati). I gruppi sono filtrati per ruolo:
// sposta una voce in un altro gruppo per cambiarne la visibilità.
// const ALL_AUTH: RoleId[] = [ROLE.ADMIN, ROLE.SUPERVISOR, ROLE.USER];
// Ritorna i gruppi visibili al ruolo: filtra prima i gruppi, poi le singole voci,
// scartando infine i gruppi rimasti vuoti.
export function getUserPanelGroups(roleId: number): UserPanelGroup[] {
const role = roleId as RoleId;
return userPanel
.filter(group => !group.roles || group.roles.includes(role))
.map(group => ({
...group,
items: group.items.filter(item => !item.roles || item.roles.includes(role)),
}))
.filter(group => group.items.length > 0);
}

View File

@@ -1,11 +1,12 @@
import type { AuthUser } from "@/shared/auth";
import { logoutUser } from "@/shared/auth";
import { getVisibleItems, getUserPanelItems, type UserPanelItem } from "./menuTypes";
import { getVisibleItems, getUserPanelGroups, type UserPanelItem } from "./menuTypes";
import { getCurrentDate } from "@/shared/utils";
import {
createIcons,
House, MapPin, Award, Shield, BookOpen, LogIn, Menu,
LogOut, User, Settings, LayoutDashboard,
Pickaxe, Box, Landmark, BookUser, History, List, MailPlus, Images,
} from 'lucide';
type Img = {
@@ -27,7 +28,11 @@ const footerImg: Img[] = [
];
// Set unico di icone Lucide usate nelle isole (header, pannello, footer).
const icons = { House, MapPin, Award, Shield, BookOpen, LogIn, Menu, LogOut, User, Settings, LayoutDashboard };
const icons = {
House, MapPin, Award, Shield, BookOpen, LogIn, Menu,
LogOut, User, Settings, LayoutDashboard,
Pickaxe, Box, Landmark, BookUser, History, List, MailPlus, Images,
};
export function renderUI(user: AuthUser | null): void {
setLogoHeader();
@@ -92,7 +97,7 @@ function setHeaderMenu(user: AuthUser | null): void {
function setFooterContent(user: AuthUser | null): void{
const footerMenu = document.getElementById('footer-menu');
if(footerMenu){
buildMenu(footerMenu, getVisibleItems(user !== null), { exclude: ['userMenuToggle'] });
buildMenu(footerMenu, getVisibleItems(user !== null), { exclude: ['user-menu-toggle'] });
}
addFooterLogo();
}
@@ -127,7 +132,7 @@ function buildMenu(dom: HTMLElement, links: MenuLink[], opts: { exclude?: string
// ---------------------------------------------------------------------------
// Pannello utente (solo loggati): pannello laterale da destra, sotto l'header,
// con backdrop leggero. L'hamburger (#userMenuToggle) resta sempre visibile e
// con backdrop leggero. L'hamburger (#user-menu-toggle) resta sempre visibile e
// fa da toggle. Replica il comportamento di v1 senza coprire l'header.
const PANEL_ID = 'user-panel';
const BACKDROP_ID = 'user-panel-backdrop';
@@ -139,7 +144,7 @@ function setUserPanel(user: AuthUser | null): void {
document.getElementById(BACKDROP_ID)?.remove();
if (escHandler) { document.removeEventListener('keydown', escHandler); escHandler = null; }
const toggle = document.getElementById('userMenuToggle') as HTMLAnchorElement | null;
const toggle = document.getElementById('user-menu-toggle') as HTMLAnchorElement | null;
if (!user || !toggle) return;
// backdrop (parte sotto l'header, vedi CSS)
@@ -156,8 +161,14 @@ function setUserPanel(user: AuthUser | null): void {
const list = document.createElement('ul');
list.className = 'menu w-full p-4';
getUserPanelItems(user.role_id).forEach(item => {
list.appendChild(buildPanelEntry(item));
getUserPanelGroups(user.role_id).forEach(group => {
if (group.title) {
const title = document.createElement('li');
title.className = 'menu-title';
title.textContent = group.title;
list.appendChild(title);
}
group.items.forEach(item => list.appendChild(buildPanelEntry(item)));
});
panel.appendChild(list);

View File

@@ -3,7 +3,9 @@
@import '@fontsource/rajdhani/500.css';
@import '@fontsource/rajdhani/700.css'; /* display/titoli, coerente col logo */
@import "tailwindcss";
@plugin "daisyui"; /* NOSONAR */
@theme {
--font-sans: "Titillium Web", ui-sans-serif, system-ui, sans-serif;
--font-secondary: "Rajdhani", ui-sans-serif, system-ui, sans-serif;
@@ -17,6 +19,7 @@
--dc-secondary-dark: rgb(241, 146, 4);
--dc-white: rgb(255, 255, 255);
--dc-dark-gray: rgb(52, 58, 64);
--dc-light-gray: rgb(77,76,68,1);
}
body{
@@ -48,6 +51,12 @@ header {
justify-content: center;
gap: 3px;
}
.user-panel li a,
.user-panel li button{
align-items: center;
}
#header-menu a{
width:70px;
height:100%;
@@ -57,7 +66,7 @@ header {
justify-content: center;
}
#header-menu a:not(#userMenuToggle){
#header-menu a:not(#user-menu-toggle){
border-bottom: 4px solid transparent;
transition: all 500ms ease;
}
@@ -90,22 +99,16 @@ header {
width:100%;
height:150px;
display: flex;
align-content: center;
justify-content: center;
}
@media (hover: hover) and (pointer: fine) {
#header-menu a:not(#userMenuToggle):hover{
border-bottom-color: var(--dc-white);
}
place-content: center center;
}
/* Sotto md (48rem): voci più strette e label più piccola, così il testo resta
sotto le icone senza mandare in overflow l'header su schermi piccoli. */
@media (max-width: 47.999rem) {
@media (width <= 47.999rem) {
#header-menu a{
width: 3rem;
}
#header-menu a span{
font-size: 0.6rem;
line-height: 1.1;
@@ -121,36 +124,72 @@ header {
top: var(--dc-header-h);
right: 0;
bottom: 0;
width: min(20rem, 85vw);
width: min(15rem, 85vw);
z-index: 910;
background-color: var(--dc-white);
color: var(--dc-dark-gray);
background-color: var(--dc-light-gray);
color: var(--dc-white);
box-shadow: -4px 0 16px rgb(0 0 0 / 0.18);
overflow-y: auto;
transform: translateX(100%);
transition: transform 300ms ease;
padding:0;
}
.user-panel.is-open{
transform: translateX(0);
}
ul.menu{ padding:0 }
ul.menu > li > *{
padding:10px 20px;
transition: all 300ms ease;
}
.menu .menu-title{
background-color: var(--dc-dark-gray);
color: var(--dc-white);
font-size: 1.2rem;
font-weight: normal;
}
.menu li:not(.menu-title){
border-bottom: 1px solid var(--dc-dark-gray);
}
/* Voci del pannello utente: l'icona Lucide (svg, 24px di default) viene
dimensionata in em così segue il testo, e allineata verticalmente ad esso. */
.user-panel li :is(a, button) svg{
width: 1.25em;
height: 1.25em;
flex-shrink: 0;
}
.user-panel-backdrop{
position: fixed;
top: var(--dc-header-h);
left: 0;
right: 0;
bottom: 0;
inset: var(--dc-header-h) 0 0 0;
z-index: 900;
background-color: rgb(0 0 0 / 0.25);
opacity: 0;
pointer-events: none;
transition: opacity 300ms ease;
}
.user-panel-backdrop.is-open{
opacity: 1;
pointer-events: auto;
}
@media (hover: hover) and (pointer: fine) {
#header-menu a:not(#user-menu-toggle):hover{
border-bottom-color: var(--dc-white);
}
ul.menu > li > *:hover{ padding-left: 35px;}
}
@media (prefers-reduced-motion: reduce) {
.user-panel,
.user-panel-backdrop{

View File

@@ -1,24 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=3.0">
<title>Dynamic Collection - Index</title>
</head>
<body>
<header>
<div id="header-brand"></div>
<nav aria-label="Menu principale"></nav>
</header>
<main id="main-container"></main>
<aside class="user-panel" aria-label="Area utente">
<div class="user-card"></div>
<nav aria-label="Menu utente"></nav>
</aside>
<footer id="main-footer"></footer>
<script type="module" src="./src/pages/index/index.ts"></script>
</body>
</html>

View File

@@ -3,7 +3,6 @@ import type { Plugin } from 'vite'
import { fileURLToPath } from 'node:url'
import { readdirSync } from 'node:fs'
import tailwindcss from '@tailwindcss/vite'
import { visualizer } from 'rollup-plugin-visualizer'
// MPA: raccoglie automaticamente ogni *.html nella root di frontend/ come entry di build.
// { 'index': '/app/index.html', 'scheda': '/app/scheda.html', ... }
@@ -44,7 +43,6 @@ function mpaRewritePlugin(): Plugin {
export default defineConfig({
plugins: [
tailwindcss(),
visualizer(),
mpaRewritePlugin()
],
resolve: {