added cache system
This commit is contained in:
214
README.md
214
README.md
@@ -1,214 +0,0 @@
|
|||||||
# Org-Parking
|
|
||||||
|
|
||||||
Un'applicazione leggera gestionale per i parcheggi aziendali, progettata per le organizzazioni. Offre un algoritmo di assegnazione equa, tracciamento delle presenze ed è ottimizzata per basse risorse.
|
|
||||||
|
|
||||||
## Funzionalità
|
|
||||||
|
|
||||||
- **Modello Centrato sull'Ufficio**: I posti auto appartengono agli Uffici, non ai Manager o agli Utenti.
|
|
||||||
- **Algoritmo di Assegnazione Equa**: Gli utenti con il rapporto parcheggi/presenze più basso hanno la priorità.
|
|
||||||
- **Tracciamento Presenze**: Marcatura delle presenze basata su calendario (presente/remoto/assente).
|
|
||||||
- **Regole di Chiusura Flessibili**: Supporto per chiusure in date specifiche e chiusure settimanali ricorrenti per ufficio.
|
|
||||||
- **Garanzie ed Esclusioni**: Regole di parcheggio per utente gestite dagli amministratori dell'ufficio.
|
|
||||||
- **Accesso Basato sui Ruoli**:
|
|
||||||
- **Admin**: Controllo completo del sistema, creazione uffici, gestione utenti.
|
|
||||||
- **Manager**: Gestisce le impostazioni del proprio ufficio e il team.
|
|
||||||
- **Impiegato**: Segna presenza, visualizza calendario, controlla stato parcheggio.
|
|
||||||
- **Basso Consumo di Memoria**: Ottimizzato per piccoli server (<500MB RAM).
|
|
||||||
- **Autenticazione**: Auth JWT integrata o integrazione SSO con Authelia.
|
|
||||||
|
|
||||||
## Architettura
|
|
||||||
|
|
||||||
```
|
|
||||||
app/
|
|
||||||
├── routes/ # API endpoints
|
|
||||||
│ ├── auth.py # Autenticazione
|
|
||||||
│ ├── users.py # Gestione utenti
|
|
||||||
│ ├── offices.py # Gestione uffici (quote, regole)
|
|
||||||
│ ├── presence.py # Marcatura presenze
|
|
||||||
│ └── parking.py # Logica di assegnazione
|
|
||||||
└── config.py # Configurazione
|
|
||||||
database/
|
|
||||||
├── models.py # Modelli SQLAlchemy ORM
|
|
||||||
└── connection.py # Setup Database
|
|
||||||
frontend/ # Frontend Vanilla JS pulito
|
|
||||||
├── pages/ # Viste HTML
|
|
||||||
├── js/ # Moduli logici
|
|
||||||
└── css/ # Stili
|
|
||||||
```
|
|
||||||
|
|
||||||
## Guida Rapida
|
|
||||||
|
|
||||||
### Sviluppo Locale
|
|
||||||
|
|
||||||
1. **Setup Ambiente**:
|
|
||||||
```bash
|
|
||||||
python3 -m venv .venv
|
|
||||||
source .venv/bin/activate
|
|
||||||
pip install -r requirements.txt
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Avvio Server**:
|
|
||||||
```bash
|
|
||||||
python main.py
|
|
||||||
```
|
|
||||||
Accedi a `http://localhost:8000`
|
|
||||||
|
|
||||||
### Deployment Docker (Consigliato)
|
|
||||||
|
|
||||||
Questa applicazione è ottimizzata per ambienti con risorse limitate (es. Raspberry Pi, piccoli VPS).
|
|
||||||
|
|
||||||
1. **Build**:
|
|
||||||
```bash
|
|
||||||
docker compose build
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Run**:
|
|
||||||
```bash
|
|
||||||
docker compose up -d
|
|
||||||
```
|
|
||||||
|
|
||||||
**Nota sull'Uso della Memoria**:
|
|
||||||
Il `Dockerfile` è configurato per eseguire `uvicorn` con `--workers 1` per minimizzare l'uso della memoria (~50MB in idle). Se in esecuzione su un server più grande, puoi rimuovere questo flag nel `Dockerfile`.
|
|
||||||
|
|
||||||
## Configurazione
|
|
||||||
|
|
||||||
Copia `.env.example` in `.env` e configura:
|
|
||||||
|
|
||||||
| Variabile | Descrizione | Default |
|
|
||||||
|-----------|-------------|---------|
|
|
||||||
| `SECRET_KEY` | **Richiesto**. Chiave cripto per JWT. | `""` (Esce se mancante) |
|
|
||||||
| `DATABASE_URL` | Vedi docs SQLAlchemy. | `sqlite:///data/parking.db` |
|
|
||||||
| `AUTHELIA_ENABLED`| Abilita supporto header SSO. | `false` |
|
|
||||||
| `SMTP_ENABLED` | Abilita notifiche email. | `false` |
|
|
||||||
| `LOG_LEVEL` | Verbosità log. | `INFO` |
|
|
||||||
|
|
||||||
## Algoritmo di Equità
|
|
||||||
|
|
||||||
I posti auto vengono assegnati giornalmente basandosi su un rapporto di equità:
|
|
||||||
```
|
|
||||||
Rapporto = (Giorni Parcheggiati con successo) / (Giorni di Presenza in ufficio)
|
|
||||||
```
|
|
||||||
- Gli utenti **Garantiti** vengono assegnati per primi.
|
|
||||||
- I posti **Rimanenti** sono distribuiti agli utenti presenti iniziando da chi ha il rapporto più **basso**.
|
|
||||||
- Gli utenti **Esclusi** non ricevono mai un posto.
|
|
||||||
|
|
||||||
## API Endpoints
|
|
||||||
|
|
||||||
Di seguito la lista delle chiamate API disponibili suddivise per modulo.
|
|
||||||
|
|
||||||
### Auth (`/api/auth`)
|
|
||||||
Gestione autenticazione e sessione.
|
|
||||||
|
|
||||||
- `POST /register`: Registrazione nuovo utente (Disabilitato se Authelia è attivo).
|
|
||||||
- `POST /login`: Login con email e password (ritorna token JWT/cookie).
|
|
||||||
- `POST /logout`: Logout e invalidazione sessione.
|
|
||||||
- `GET /me`: Ritorna informazioni sull'utente corrente.
|
|
||||||
- `GET /config`: Ritorna la configurazione pubblica di autenticazione.
|
|
||||||
- `GET /holidays/{year}`: Ritorna i giorni festivi per l'anno specificato.
|
|
||||||
|
|
||||||
### Users (`/api/users`)
|
|
||||||
Gestione utenti e profili.
|
|
||||||
|
|
||||||
- `GET /`: Lista di tutti gli utenti (Solo Admin).
|
|
||||||
- `POST /`: Crea un nuovo utente (Solo Admin).
|
|
||||||
- `GET /{user_id}`: Dettaglio di un utente specifico (Solo Admin).
|
|
||||||
- `PUT /{user_id}`: Aggiorna dati di un utente (Solo Admin).
|
|
||||||
- `DELETE /{user_id}`: Elimina un utente (Solo Admin).
|
|
||||||
- `GET /me/profile`: Ottieni il proprio profilo.
|
|
||||||
- `PUT /me/profile`: Aggiorna il proprio profilo.
|
|
||||||
- `GET /me/settings`: Ottieni le proprie impostazioni.
|
|
||||||
- `PUT /me/settings`: Aggiorna le proprie impostazioni.
|
|
||||||
- `POST /me/change-password`: Modifica la propria password.
|
|
||||||
|
|
||||||
### Offices (`/api/offices`)
|
|
||||||
Gestione uffici, regole di chiusura e quote.
|
|
||||||
|
|
||||||
- `GET /`: Lista di tutti gli uffici.
|
|
||||||
- `POST /`: Crea un nuovo ufficio (Solo Admin).
|
|
||||||
- `GET /{office_id}`: Dettagli di un ufficio.
|
|
||||||
- `PUT /{office_id}`: Aggiorna configurazione ufficio (Solo Admin).
|
|
||||||
- `DELETE /{office_id}`: Elimina un ufficio (Solo Admin).
|
|
||||||
- `GET /{office_id}/users`: Lista utenti assegnati all'ufficio.
|
|
||||||
- `GET /{office_id}/closing-days`: Lista giorni di chiusura specifici.
|
|
||||||
- `POST /{office_id}/closing-days`: Aggiungi giorno di chiusura.
|
|
||||||
- `DELETE /{office_id}/closing-days/{id}`: Rimuovi giorno di chiusura.
|
|
||||||
- `GET /{office_id}/weekly-closing-days`: Lista giorni di chiusura settimanali (es. Sabato/Domenica).
|
|
||||||
- `POST /{office_id}/weekly-closing-days`: Aggiungi giorno di chiusura settimanale.
|
|
||||||
- `DELETE /{office_id}/weekly-closing-days/{id}`: Rimuovi giorno di chiusura settimanale.
|
|
||||||
- `GET /{office_id}/guarantees`: Lista utenti con posto garantito.
|
|
||||||
- `POST /{office_id}/guarantees`: Aggiungi garanzia posto.
|
|
||||||
- `DELETE /{office_id}/guarantees/{id}`: Rimuovi garanzia.
|
|
||||||
- `GET /{office_id}/exclusions`: Lista utenti esclusi dal parcheggio.
|
|
||||||
- `POST /{office_id}/exclusions`: Aggiungi esclusione.
|
|
||||||
- `DELETE /{office_id}/exclusions/{id}`: Rimuovi esclusione.
|
|
||||||
|
|
||||||
### Presence (`/api/presence`)
|
|
||||||
Gestione presenze giornaliere.
|
|
||||||
|
|
||||||
- `POST /mark`: Segna la propria presenza per una data (Presente/Remoto/Assente).
|
|
||||||
- `GET /my-presences`: Lista delle proprie presenze.
|
|
||||||
- `DELETE /{date}`: Rimuovi la propria presenza per una data.
|
|
||||||
- `POST /admin/mark`: Segna presenza per un altro utente (Manager/Admin).
|
|
||||||
- `DELETE /admin/{user_id}/{date}`: Rimuovi presenza di un altro utente (Manager/Admin).
|
|
||||||
- `GET /team`: Visualizza presenze e stato parcheggio del team.
|
|
||||||
- `GET /admin/{user_id}`: Storico presenze di un utente.
|
|
||||||
|
|
||||||
### Parking (`/api/parking`)
|
|
||||||
Gestione assegnazioni posti auto.
|
|
||||||
|
|
||||||
- `POST /init-office-pool`: Inizializza i posti disponibili per un giorno.
|
|
||||||
- `GET /assignments/{date}`: Lista assegnazioni per una data.
|
|
||||||
- `GET /my-assignments`: Le mie assegnazioni parcheggio.
|
|
||||||
- `POST /run-allocation`: Esegui manualmente l'algoritmo di assegnazione per una data.
|
|
||||||
- `POST /clear-assignments`: Cancella tutte le assegnazioni per una data.
|
|
||||||
- `POST /manual-assign`: Assegna manualmente un posto a un utente.
|
|
||||||
- `POST /reassign-spot`: Riassegna o libera un posto già assegnato.
|
|
||||||
- `POST /release-my-spot/{id}`: Rilascia il proprio posto assegnato.
|
|
||||||
- `GET /eligible-users/{id}`: Lista utenti idonei a ricevere un posto riassegnato.
|
|
||||||
|
|
||||||
## Utilizzo con AUTHELIA
|
|
||||||
|
|
||||||
Org-Parking supporta l'integrazione nativa con **Authelia** (o altri provider SSO compatibili con Forward Auth). Questo permette il Single Sign-On (SSO) e la gestione centralizzata degli utenti.
|
|
||||||
|
|
||||||
### Configurazione
|
|
||||||
|
|
||||||
1. **Abilita Authelia**:
|
|
||||||
Nel file `.env`, imposta `AUTHELIA_ENABLED=true`.
|
|
||||||
|
|
||||||
2. **Configura gli Header del Proxy**:
|
|
||||||
Assicurati che il tuo reverse proxy (es. Traefik, Nginx) passi i seguenti header all'applicazione dopo l'autenticazione:
|
|
||||||
* `Remote-User`: Username dell'utente (spesso uguale all'email).
|
|
||||||
* `Remote-Email`: Email dell'utente.
|
|
||||||
* `Remote-Name`: Nome completo dell'utente (Opzionale).
|
|
||||||
* `Remote-Groups`: Gruppi di appartenenza (separati da virgola).
|
|
||||||
|
|
||||||
3. **Gestione Admin**:
|
|
||||||
L'applicazione assegna automaticamente il ruolo di **Admin** agli utenti che appartengono al gruppo specificato nella variabile `AUTHELIA_ADMIN_GROUP` (default: `parking_admins`).
|
|
||||||
* Se un utente viene rimosso da questo gruppo su Authelia, perderà i privilegi di admin al login successivo.
|
|
||||||
* Gli altri ruoli (Manager, Employee) e l'assegnazione agli uffici vengono gestiti manualmente all'interno di Org-Parking da un amministratore.
|
|
||||||
|
|
||||||
### Comportamento
|
|
||||||
|
|
||||||
* **Creazione Utenti**: Gli utenti vengono creati automaticamente nel database di Org-Parking al loro primo accesso riuscito tramite Authelia.
|
|
||||||
* **Login/Logout**: Le pagine di login e registrazione interne vengono disabilitate o reindirizzano al portale SSO.
|
|
||||||
* **Password**: Org-Parking non gestisce le password in questa modalità; l'autenticazione è interamente delegata al provider esterno.
|
|
||||||
* **Sicurezza**: L'applicazione si fida degli header `Remote-*` solo se `AUTHELIA_ENABLED` è attivo. Assicurarsi che il container non sia esposto direttamente a internet senza passare per il proxy di autenticazione.
|
|
||||||
|
|
||||||
## Note di Deployment
|
|
||||||
|
|
||||||
- **Database**: SQLite è usato di default per semplicità e basso overhead. I dati persistono in `./data/parking.db`.
|
|
||||||
- **Sicurezza**:
|
|
||||||
- Rate limiting è attivo sugli endpoint sensibili (Login/Register).
|
|
||||||
- Le password sono hashate con Bcrypt.
|
|
||||||
- L'autenticazione via cookie è sicura di default.
|
|
||||||
|
|
||||||
### Risoluzione Problemi Comuni
|
|
||||||
|
|
||||||
**Errore: "Il reindirizzamento è stato determinato per essere non sicuro"**
|
|
||||||
|
|
||||||
Questo errore appare se si tenta di accedere all'applicazione tramite HTTP (es. `http://lvh.me:8000`) mentre Authelia è su HTTPS. Authelia blocca i reindirizzamenti verso protocolli non sicuri.
|
|
||||||
**Soluzione**: Accedere all'applicazione tramite il Reverse Proxy (Caddy) usando HTTPS, ad esempio `https://parking.lvh.me`. Assicurarsi che Caddy sia configurato per gestire il dominio e l'autenticazione.
|
|
||||||
|
|
||||||
## Licenza
|
|
||||||
|
|
||||||
MIT
|
|
||||||
|
|||||||
@@ -26,13 +26,13 @@ services:
|
|||||||
networks:
|
networks:
|
||||||
- org-network
|
- org-network
|
||||||
labels:
|
labels:
|
||||||
- "caddy=parking.rocketscale.it"
|
- "caddy=parcheggio.rocketscale.it"
|
||||||
- "caddy.reverse_proxy={{upstreams 8000}}"
|
- "caddy.reverse_proxy={{upstreams 8000}}"
|
||||||
- "caddy.forward_auth=authelia:9091"
|
- "caddy.forward_auth=authelia:9091"
|
||||||
- "caddy.forward_auth.uri=/api/verify?rd=https://parking.rocketscale.it/"
|
- "caddy.forward_auth.uri=/api/verify?rd=https://parcheggio.rocketscale.it/"
|
||||||
- "caddy.forward_auth.copy_headers=Remote-User Remote-Groups Remote-Name Remote-Email"
|
- "caddy.forward_auth.copy_headers=Remote-User Remote-Groups Remote-Name Remote-Email"
|
||||||
# cambiare l'url delle label per il reverse proxy
|
# cambiare l'url delle label per il reverse proxy
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
org-network:
|
org-network:
|
||||||
external: true
|
external: true
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ function populateTimeSelects() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function loadOffices() {
|
async function loadOffices() {
|
||||||
const response = await api.get('/api/offices');
|
const response = await api.getCached('/api/offices', 60);
|
||||||
if (response && response.ok) {
|
if (response && response.ok) {
|
||||||
offices = await response.json();
|
offices = await response.json();
|
||||||
renderOffices();
|
renderOffices();
|
||||||
@@ -115,6 +115,7 @@ async function deleteOffice(officeId) {
|
|||||||
const response = await api.delete(`/api/offices/${officeId}`);
|
const response = await api.delete(`/api/offices/${officeId}`);
|
||||||
if (response && response.ok) {
|
if (response && response.ok) {
|
||||||
utils.showMessage('Ufficio eliminato', 'success');
|
utils.showMessage('Ufficio eliminato', 'success');
|
||||||
|
api.invalidateCache('/api/offices'); // Clear cache
|
||||||
await loadOffices();
|
await loadOffices();
|
||||||
} else {
|
} else {
|
||||||
const error = await response.json();
|
const error = await response.json();
|
||||||
@@ -177,6 +178,7 @@ async function handleOfficeSubmit(e) {
|
|||||||
if (response && response.ok) {
|
if (response && response.ok) {
|
||||||
closeModal();
|
closeModal();
|
||||||
utils.showMessage(officeId ? 'Ufficio aggiornato' : 'Ufficio creato', 'success');
|
utils.showMessage(officeId ? 'Ufficio aggiornato' : 'Ufficio creato', 'success');
|
||||||
|
api.invalidateCache('/api/offices'); // Clear cache
|
||||||
await loadOffices();
|
await loadOffices();
|
||||||
} else {
|
} else {
|
||||||
let errorMessage = 'Errore operazione';
|
let errorMessage = 'Errore operazione';
|
||||||
|
|||||||
@@ -23,14 +23,16 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
async function loadOffices() {
|
async function loadOffices() {
|
||||||
const response = await api.get('/api/offices');
|
// Cache offices for dropdown (60 min)
|
||||||
|
const response = await api.getCached('/api/offices', 60);
|
||||||
if (response && response.ok) {
|
if (response && response.ok) {
|
||||||
offices = await response.json();
|
offices = await response.json();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadUsers() {
|
async function loadUsers() {
|
||||||
const response = await api.get('/api/users');
|
// Cache users list (15 min)
|
||||||
|
const response = await api.getCached('/api/users', 15);
|
||||||
if (response && response.ok) {
|
if (response && response.ok) {
|
||||||
users = await response.json();
|
users = await response.json();
|
||||||
renderUsers();
|
renderUsers();
|
||||||
@@ -166,6 +168,8 @@ async function deleteUser(userId) {
|
|||||||
const response = await api.delete(`/api/users/${userId}`);
|
const response = await api.delete(`/api/users/${userId}`);
|
||||||
if (response && response.ok) {
|
if (response && response.ok) {
|
||||||
utils.showMessage('Utente eliminato', 'success');
|
utils.showMessage('Utente eliminato', 'success');
|
||||||
|
api.invalidateCache('/api/users');
|
||||||
|
api.invalidateCache('/api/offices'); // Invalidate office counts
|
||||||
await loadUsers();
|
await loadUsers();
|
||||||
} else {
|
} else {
|
||||||
const error = await response.json();
|
const error = await response.json();
|
||||||
@@ -210,6 +214,8 @@ function setupEventListeners() {
|
|||||||
if (response && response.ok) {
|
if (response && response.ok) {
|
||||||
document.getElementById('userModal').style.display = 'none';
|
document.getElementById('userModal').style.display = 'none';
|
||||||
utils.showMessage('Utente aggiornato', 'success');
|
utils.showMessage('Utente aggiornato', 'success');
|
||||||
|
api.invalidateCache('/api/users');
|
||||||
|
api.invalidateCache('/api/offices'); // Invalidate office counts
|
||||||
await loadUsers();
|
await loadUsers();
|
||||||
} else {
|
} else {
|
||||||
const error = await response.json();
|
const error = await response.json();
|
||||||
|
|||||||
@@ -23,6 +23,80 @@ const api = {
|
|||||||
*/
|
*/
|
||||||
clearToken() {
|
clearToken() {
|
||||||
localStorage.removeItem('access_token');
|
localStorage.removeItem('access_token');
|
||||||
|
this.clearCache();
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get data with caching - Returns Response obj or Mock Response
|
||||||
|
* @param {string} url - API endpoint
|
||||||
|
* @param {number} ttlMinutes - Time to live in minutes (default 60)
|
||||||
|
*/
|
||||||
|
async getCached(url, ttlMinutes = 60) {
|
||||||
|
const cacheKey = 'cache_' + url;
|
||||||
|
const cachedItem = localStorage.getItem(cacheKey);
|
||||||
|
|
||||||
|
if (cachedItem) {
|
||||||
|
try {
|
||||||
|
const { data, timestamp } = JSON.parse(cachedItem);
|
||||||
|
const age = (Date.now() - timestamp) / 1000 / 60;
|
||||||
|
|
||||||
|
if (age < ttlMinutes) {
|
||||||
|
console.log(`[Cache] Hit for ${url}`);
|
||||||
|
// Return a mock response-like object
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
status: 200,
|
||||||
|
json: async () => data
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
console.log(`[Cache] Expired for ${url}`);
|
||||||
|
localStorage.removeItem(cacheKey);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[Cache] Error parsing cache', e);
|
||||||
|
localStorage.removeItem(cacheKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[Cache] Miss for ${url}`);
|
||||||
|
const response = await this.get(url);
|
||||||
|
|
||||||
|
if (response && response.ok) {
|
||||||
|
try {
|
||||||
|
// Clone response to read body and still return it to caller
|
||||||
|
const clone = response.clone();
|
||||||
|
const data = await clone.json();
|
||||||
|
|
||||||
|
localStorage.setItem(cacheKey, JSON.stringify({
|
||||||
|
data: data,
|
||||||
|
timestamp: Date.now()
|
||||||
|
}));
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('[Cache] failed to save to localStorage', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invalidate specific cache key
|
||||||
|
*/
|
||||||
|
invalidateCache(url) {
|
||||||
|
localStorage.removeItem('cache_' + url);
|
||||||
|
console.log(`[Cache] Invalidated ${url}`);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear all API cache
|
||||||
|
*/
|
||||||
|
clearCache() {
|
||||||
|
Object.keys(localStorage).forEach(key => {
|
||||||
|
if (key.startsWith('cache_')) {
|
||||||
|
localStorage.removeItem(key);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
console.log('[Cache] Cleared all cache');
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -37,14 +111,36 @@ const api = {
|
|||||||
* Call this on page load to verify auth status
|
* Call this on page load to verify auth status
|
||||||
*/
|
*/
|
||||||
async checkAuth() {
|
async checkAuth() {
|
||||||
// Try to get current user - works with Authelia headers or JWT
|
const url = '/api/auth/me';
|
||||||
const response = await fetch('/api/auth/me', {
|
// 1. Try Cache (Short TTL: 5 min)
|
||||||
|
const cacheKey = 'cache_' + url;
|
||||||
|
const cachedItem = localStorage.getItem(cacheKey);
|
||||||
|
if (cachedItem) {
|
||||||
|
try {
|
||||||
|
const { data, timestamp } = JSON.parse(cachedItem);
|
||||||
|
if ((Date.now() - timestamp) / 1000 / 60 < 5) {
|
||||||
|
this._autheliaAuth = true;
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
} catch (e) { localStorage.removeItem(cacheKey); }
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Fetch from Network
|
||||||
|
const response = await fetch(url, {
|
||||||
headers: this.getToken() ? { 'Authorization': `Bearer ${this.getToken()}` } : {}
|
headers: this.getToken() ? { 'Authorization': `Bearer ${this.getToken()}` } : {}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
this._autheliaAuth = true;
|
this._autheliaAuth = true;
|
||||||
return await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
|
// Save to Cache
|
||||||
|
localStorage.setItem(cacheKey, JSON.stringify({
|
||||||
|
data: data,
|
||||||
|
timestamp: Date.now()
|
||||||
|
}));
|
||||||
|
|
||||||
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
this._autheliaAuth = false;
|
this._autheliaAuth = false;
|
||||||
|
|||||||
@@ -82,15 +82,14 @@ async function loadOffices() {
|
|||||||
if (currentUser.role === 'employee') return;
|
if (currentUser.role === 'employee') return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await api.get('/api/offices');
|
// Cache offices list
|
||||||
|
const response = await api.getCached('/api/offices', 60);
|
||||||
if (response && response.ok) {
|
if (response && response.ok) {
|
||||||
offices = await response.json();
|
offices = await response.json();
|
||||||
|
|
||||||
let filteredOffices = offices;
|
let filteredOffices = offices;
|
||||||
if (currentUser.role === 'manager') {
|
if (currentUser.role === 'manager') {
|
||||||
// Manager only sees their own office in the filter?
|
// Manager only sees their own office in the filter?
|
||||||
// Actually managers might want to filter if they (hypothetically) managed multiple,
|
|
||||||
// but currently User has 1 office.
|
|
||||||
if (currentUser.office_id) {
|
if (currentUser.office_id) {
|
||||||
filteredOffices = offices.filter(o => o.id === currentUser.office_id);
|
filteredOffices = offices.filter(o => o.id === currentUser.office_id);
|
||||||
} else {
|
} else {
|
||||||
@@ -254,8 +253,8 @@ async function loadClosingData() {
|
|||||||
const promises = officeIdsToLoad.map(async (oid) => {
|
const promises = officeIdsToLoad.map(async (oid) => {
|
||||||
try {
|
try {
|
||||||
const [weeklyRes, specificRes] = await Promise.all([
|
const [weeklyRes, specificRes] = await Promise.all([
|
||||||
api.get(`/api/offices/${oid}/weekly-closing-days`),
|
api.getCached(`/api/offices/${oid}/weekly-closing-days`, 60),
|
||||||
api.get(`/api/offices/${oid}/closing-days`)
|
api.getCached(`/api/offices/${oid}/closing-days`, 60)
|
||||||
]);
|
]);
|
||||||
|
|
||||||
officeClosingRules[oid] = { weekly: [], specific: [] };
|
officeClosingRules[oid] = { weekly: [], specific: [] };
|
||||||
|
|||||||
@@ -130,7 +130,7 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
async function loadProfile() {
|
async function loadProfile() {
|
||||||
const response = await api.get('/api/users/me/profile');
|
const response = await api.getCached('/api/users/me/profile', 60);
|
||||||
if (response && response.ok) {
|
if (response && response.ok) {
|
||||||
const profile = await response.json();
|
const profile = await response.json();
|
||||||
isLdapUser = profile.is_ldap_user;
|
isLdapUser = profile.is_ldap_user;
|
||||||
@@ -169,6 +169,8 @@
|
|||||||
const response = await api.put('/api/users/me/profile', data);
|
const response = await api.put('/api/users/me/profile', data);
|
||||||
if (response && response.ok) {
|
if (response && response.ok) {
|
||||||
utils.showMessage('Profilo aggiornato con successo', 'success');
|
utils.showMessage('Profilo aggiornato con successo', 'success');
|
||||||
|
api.invalidateCache('/api/users/me/profile');
|
||||||
|
api.invalidateCache('/api/auth/me'); // Update nav bar name too
|
||||||
// Update nav display
|
// Update nav display
|
||||||
const nameEl = document.getElementById('userName');
|
const nameEl = document.getElementById('userName');
|
||||||
if (nameEl) nameEl.textContent = data.name;
|
if (nameEl) nameEl.textContent = data.name;
|
||||||
|
|||||||
Reference in New Issue
Block a user