Selaa lähdekoodia

Aggiornato requirements, aggiornato main e login con accesso tramite keycloak

Francesco 7 kuukautta sitten
vanhempi
sitoutus
fe7d6e376d
3 muutettua tiedostoa jossa 298 lisäystä ja 50 poistoa
  1. 267 48
      Backend/codie.py
  2. BIN
      Backend/requirements.txt
  3. 31 2
      Backend/templates/login.html

+ 267 - 48
Backend/codie.py

@@ -1,28 +1,69 @@
-from fastapi import FastAPI, Depends, HTTPException, Request, Form, status
-from fastapi.responses import RedirectResponse
+import geojson
+from fastapi import FastAPI, Depends, HTTPException, Request, Form, status, Body
+from fastapi.responses import JSONResponse, RedirectResponse, HTMLResponse
+from fastapi.staticfiles import StaticFiles
+from fastapi.middleware.cors import CORSMiddleware
 from fastapi.templating import Jinja2Templates
 from sqlalchemy import create_engine, Column, Integer, String, Date, Identity, Boolean, Text , TIMESTAMP
 from sqlalchemy.orm import sessionmaker, declarative_base, Session
 from geoalchemy2 import Geometry, Geography
-#from keycloak import KeycloakOpenID
+from geoalchemy2.types import WKBElement
+from geoalchemy2.shape import to_shape
+from starlette.middleware.sessions import SessionMiddleware
+import secrets
+import uvicorn
+import logging
+import requests  # Import the requests library here
+from typing import Annotated, Dict, List #import typing
 from pydantic import BaseModel
 from datetime import date, datetime
-# Configurazione Keycloak
-# keycloak_openid = KeycloakOpenID(server_url=os.getenv('KEYCLOAK_URL'),
-#                                  client_id=os.getenv('KEYCLOAK_CLIENT_ID'),
-#                                  realm_name=os.getenv('KEYCLOAK_REALM'),
-#                                  client_secret_key=os.getenv('KEYCLOAK_CLIENT_SECRET'))
+
+
+# --- Keycloak Configuration ---
+KEYCLOAK_SERVER_URL = "http://165.22.75.145:8080"  # Double-check this! Changed to http
+KEYCLOAK_REALM = "Generation"
+KEYCLOAK_CLIENT_ID = "web-app-pw"
+KEYCLOAK_CLIENT_SECRET = "fQGWt8HSPn65cCKTOzE5FigqZhf8QTYW"
+KEYCLOAK_SCOPE = "codicefiscale email openid ruolo"
+
+# --- App Initialization ---
+app = FastAPI()
+
+# --- Logging Configuration ---
+logging.basicConfig(level=logging.INFO)
+logger = logging.getLogger(__name__)
+
+# --- CORS ---
+origins = [
+
+    "http://localhost:3000",
+]
+
+app.add_middleware(
+    CORSMiddleware,
+    allow_origins=origins,
+    allow_credentials=True,
+    allow_methods=["*"],
+    allow_headers=["*"],
+)
+# --- Session ---
+app.add_middleware(SessionMiddleware, secret_key=secrets.token_hex(32))
+# app.add_middleware(SessionMiddleware, secret_key="your-secret-key")
+
 
 
 # Template Jinja2
+app.mount("/static", StaticFiles(directory="static"), name="static")
 templates = Jinja2Templates(directory="templates")
 
+
 # Configurazione del database PostgreSQL
-SQLALCHEMY_DATABASE_URL = "postgresql://postgres:postgres@165.22.75.145:15432/GenerationDAITA25"
-#SQLALCHEMY_DATABASE_URL = "postgresql://postgres:postgres@165.22.75.145:15432/backend"
+#SQLALCHEMY_DATABASE_URL = "postgresql://postgres:postgres@165.22.75.145:15432/GenerationDAITA25"
+SQLALCHEMY_DATABASE_URL = "postgresql://postgres:postgres@165.22.75.145:15432/backend"
 engine = create_engine(SQLALCHEMY_DATABASE_URL) 
 SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
 
+# Funzione per ottenere la sessione
 def get_db():
     db = SessionLocal()
     try:
@@ -30,81 +71,112 @@ def get_db():
     finally:
         db.close()
 
-# FastAPI
-app = FastAPI()
 
 Base = declarative_base()
 
-
+# Definizione delle tabelle del database
 class TabellaEdifici(Base):
-    __tablename__ = 'edifici'  # codice_catastale della tabella nel database
-    id_ = Column(Integer, primary_key=True, name='id_') # Identity() per autoincrement in PostgreSQL
+    __tablename__ = 'edifici'  
+    id_ = Column(Integer, primary_key=True, name='id_') # Chiave primaria, autoincrement
     id_codice_fiscale = Column(String(16), name='id_codice_fiscale')
-    id_edificio_osm = Column(String(30), nullable=False, name='id_edificio')
+    id_edificio_osm = Column(String(30), nullable=False, name='id_edificio', unique=True) 
     indirizzo = Column(String(100), nullable=False)
     codice_catastale = Column(String(50), nullable=True)
     coordinate = Column(Geometry(geometry_type='POINT', srid=4326)) # SRID 4326 è lo standard per latitudine/longitudine
-    poligono = Column(Geometry(geometry_type='POLYGON', srid=4326)) # Poligono
+    poligono = Column(Geometry(geometry_type='POLYGON', srid=4326)) 
     tipo_edificio = Column(String(100))
     stato = Column(Boolean, default=True)
     data_installazione = Column(Date, nullable=False)
     data_cancellazione = Column(Date, nullable=True)
 
-
-class TabellaTFO(Base):  # Puoi scegliere un nome più descrittivo per la tua tabella
-    __tablename__ = 'tfo' #  Scegli un nome per la tabella nel database
+    def to_dict(self):
+        result ={
+            'id_': self.id_,
+            'id_codice_fiscale': self.id_codice_fiscale,
+            'id_edificio': self.id_edificio_osm,
+            'indirizzo': self.indirizzo,
+            'codice_catastale': self.codice_catastale,
+            # 'coordinate': coordinate_json,
+            # 'poligono': poligono_json,
+            'tipo_edificio': self.tipo_edificio,
+            'stato': self.stato,
+            'data_installazione': self.data_installazione,
+            'data_cancellazione': self.data_cancellazione
+        }
+        if self.coordinate:
+            shape = to_shape(self.coordinate)
+            geojson_str = geojson.dumps(shape)
+            result['coordinate'] = geojson.loads(geojson_str)
+        if self.poligono:
+            shape = to_shape(self.poligono)
+            geojson_str = geojson.dumps(shape)
+            result['poligono'] = geojson.loads(geojson_str)
+        return result
+
+        
+class TabellaTFO(Base):  
+    __tablename__ = 'tfo' 
     id_ = Column(Integer, primary_key=True, name='id_') # Chiave primaria, autoincrement
     id_codice_fiscale = Column(String(16), name='id_codice_fiscale')
-    id_tfo = Column(String(30), nullable=False, name='id_tfo') # Not Null
-    codice_catastale = Column(String(50), nullable=True, name='codice_catastale') # Not Null
+    id_tfo = Column(String(30), nullable=False, name='id_tfo')
+    codice_catastale = Column(String(50), nullable=True, name='codice_catastale') 
     operatore = Column(String(255))
     piano = Column(String(50))
     scala = Column(String(50))
     interno = Column(String(50))
-    id_edificio = Column(String(30), nullable=False, name='id_edificio') # Not Null
+    id_edificio = Column(String(30), nullable=False, name='id_edificio') 
     data_installazione = Column(Date, nullable=False, name='data_installazione')
     data_cancellazione = Column(Date, nullable=True, name='data_cancellazione')
 
 
-class TabellaUtenti(Base): # Nome descrittivo per la tabella
-    __tablename__ = 'utenti' # Nome tabella nel database
+class TabellaUtenti(Base): 
+    __tablename__ = 'utenti' 
     id_ = Column(Integer, primary_key=True, name='id_') # Chiave primaria, autoincrement
     codice_fiscale = Column(String(16), name='codice_fiscale')
-    nome = Column(String(255), nullable=False) # Not Null
-    cognome = Column(String(255), nullable=False) # Not Null
-    email = Column(String(100), nullable=False) # Not Null
-    ente = Column(String(255))
+    nome = Column(String(255), nullable=False)
+    cognome = Column(String(255), nullable=False) 
+    email = Column(String(100), nullable=False) 
     ruolo = Column(String(255))
     stato_utente = Column(Boolean)
 
-class TabellaLogEventi(Base): # Nome descrittivo per la tabella di log
-    __tablename__ = 'log_' # Nome tabella nel database (scegli un nome appropriato)
+
+class TabellaLogEventi(Base): 
+    __tablename__ = 'log_'
     id_ = Column(Integer, primary_key=True, name='id_') # Chiave primaria, autoincrement
     id_edificio = Column(Integer, name='id_edificio')
     id_tfo = Column(String(30), name='id_tfo')
     id_utente = Column(String(16), name='id_utente')
-    data = Column(TIMESTAMP, default =datetime.now(), name='data') # Timestamp senza timezone, Not Null, default CURRENT_TIMESTAMP
+    data = Column(TIMESTAMP, default = datetime.now(), name='data') 
     categoria_modifica = Column(String(255), name='categoria_modifica')
     descrizione_evento = Column(String(4000), name='descrizione_evento')
 
+
 class Assistenza(Base):
     __tablename__ = 'assistenza'
-
     id_ticket = Column(Integer, primary_key=True, autoincrement=True)
     nome = Column(String(50))
     cognome = Column(String(50))
     email = Column(String(100), unique=True, nullable=False)
-    data_richiesta = Column(TIMESTAMP, default=func.current_timestamp())
+    data_richiesta = Column(TIMESTAMP, default=datetime.now())
     descrizione_problema = Column(String(4000))
 
-
-
 # Base.metadata.drop_all(engine)
 # Base.metadata.create_all(bind=engine)
+
+class Credenziali(BaseModel):
+    username: str
+    password: str
+
 ####
-accesso_modifica = "Francesco"
+accesso_modifica = "RSSMRA80A01H501Z"
 ####
 
+# funzioni per ottenere i dati dal database
+def get_all_buildings(db):
+    buildings = db.query(TabellaEdifici).all()
+    return [building.to_dict() for building in buildings]
+
+
 def get_buildings_by_user(db):
    return db.query(TabellaEdifici).filter(TabellaEdifici.id_codice_fiscale == accesso_modifica).all() 
 
@@ -140,13 +212,161 @@ def filtro_ricerca_totale(db, filtro_indirizzo, filtro_data, filtro_id_edificio_
     if filtro_id_codice_fiscale is not None:
         query1 = query1.filter(TabellaEdifici.id_codice_fiscale.ilike(f"%{filtro_id_codice_fiscale}%"))
     return query1.all()
-    
 
 
 
+def get_token_from_keycloak(username, password) -> Dict:
+    """Retrieves access and refresh tokens from Keycloak."""
+    url = f"{KEYCLOAK_SERVER_URL}/realms/{KEYCLOAK_REALM}/protocol/openid-connect/token"
+    payload = f'grant_type=password&client_id={KEYCLOAK_CLIENT_ID}&scope={KEYCLOAK_SCOPE}&username={username}&password={password}&client_secret={KEYCLOAK_CLIENT_SECRET}'
+    headers = {
+        'Content-Type': 'application/x-www-form-urlencoded'
+    }
+    try:
+        response = requests.post(url, headers=headers, data=payload, timeout=10)
+        response.raise_for_status()  # Raise HTTPError for bad responses (4xx or 5xx)
+        return response.json()
+    except requests.exceptions.RequestException as e:
+        logger.error(f"Error getting token from Keycloak: {e}")
+        raise HTTPException(status_code=500, detail=f"Failed to get token from Keycloak: {e}") from e
+
+def refresh_token_from_keycloak(refresh_token: str) -> Dict:
+    """Refreshes the access token using the refresh token."""
+    url = f"{KEYCLOAK_SERVER_URL}/realms/{KEYCLOAK_REALM}/protocol/openid-connect/token"
+    payload = f'grant_type=refresh_token&client_id={KEYCLOAK_CLIENT_ID}&client_secret={KEYCLOAK_CLIENT_SECRET}&refresh_token={refresh_token}'
+    headers = {
+        'Content-Type': 'application/x-www-form-urlencoded'
+    }
+    try:
+        response = requests.post(url, headers=headers, data=payload, timeout=10)
+        response.raise_for_status()
+        return response.json()
+    except requests.exceptions.RequestException as e:
+        logger.error(f"Error refreshing token: {e}")
+        raise HTTPException(status_code=500, detail=f"Failed to refresh token: {e}") from e
+
+
+def introspect_keycloak_token_request(access_token: str) -> Dict:
+    """Introspects a Keycloak token to verify if it's active."""
+    url = f"{KEYCLOAK_SERVER_URL}/realms/{KEYCLOAK_REALM}/protocol/openid-connect/token/introspect"
+    payload = f'token={access_token}&client_id={KEYCLOAK_CLIENT_ID}&client_secret={KEYCLOAK_CLIENT_SECRET}'
+    headers = {
+        'Content-Type': 'application/x-www-form-urlencoded'
+    }
+    try:
+        response = requests.post(url, headers=headers, data=payload, verify=False, timeout=10)
+        response.raise_for_status()
+        return response.json()
+    except requests.exceptions.RequestException as e:
+        logger.error(f"Error introspecting token: {e}")
+        raise HTTPException(status_code=500, detail=f"Failed to introspect token: {e}") from e
+
+def get_user_info_from_keycloak(access_token: str) -> Dict:
+    """Gets user information from Keycloak using the access token."""
+    url = f"{KEYCLOAK_SERVER_URL}/realms/{KEYCLOAK_REALM}/protocol/openid-connect/userinfo"
+    headers = {
+        'Authorization': f'Bearer {access_token}'
+    }
+    try:
+        response = requests.get(url, headers=headers, timeout=10)
+        response.raise_for_status()
+        return response.json()
+    except requests.exceptions.RequestException as e:
+        logger.error(f"Error getting user info: {e}")
+        raise HTTPException(status_code=500, detail=f"Failed to get user info: {e}") from e
+
+def logout_keycloak(refresh_token:str):
+    url = f"{KEYCLOAK_SERVER_URL}/realms/{KEYCLOAK_REALM}/protocol/openid-connect/logout"
+
+    payload = f'refresh_token={refresh_token}&client_id={KEYCLOAK_CLIENT_ID}&client_secret={KEYCLOAK_CLIENT_SECRET}'
+    headers = {
+    'Content-Type': 'application/x-www-form-urlencoded'
+    }
+    try:
+        response = requests.request("POST", url, headers=headers, data=payload)
+        response.raise_for_status()
+        return response.json()
+    except requests.exceptions.RequestException as e:
+        logger.error(f"Error logging out user: {e}")
+        raise HTTPException(status_code=500, detail=f"Failed to logout user: {e}") from e
+
+
+
+
+
+
+
+
+@app.get("/access")
+async def access(request: Request):
+    return templates.TemplateResponse("index.html", context={"request": request, "title": "Accesso"})
+
 @app.get("/")
 async def home(request: Request):
-    return templates.TemplateResponse("login.html", {"request": request})
+    return print("Ciao")
+    #return templates.TemplateResponse("index.html", {"request": request})
+
+@app.post("/login")
+async def login(credenziali: Credenziali = Body(...)):
+
+    """
+
+    Logs in a user by retrieving tokens from Keycloak.
+
+    Stores the tokens and user info in the session.
+
+    """
+    try:
+        tokens = get_token_from_keycloak(credenziali.username, credenziali.password)#request.body.json()["username"], request.json()["password"])
+        #RedirectResponse(url="localhost:8000/callback")
+        # Save tokens and information to the session
+
+        #request.session["access_token"] = tokens["access_token"]
+        #request.session["refresh_token"] = tokens["refresh_token"]
+        #print(tokens["access_token"])
+
+        # Get user info and save it to the session too
+        user_info = get_user_info_from_keycloak(tokens["access_token"])
+        #request.session["user_info"] = user_info
+        return tokens
+
+    except HTTPException as e:
+        return JSONResponse(content={"detail": e.detail}, status_code=e.status_code)
+
+    except Exception as e:
+        logger.error(f"An unexpected error occurred during login: {e}")
+        return JSONResponse(content={"detail": "An unexpected error occurred"}, status_code=500)
+   
+# @app.post("/login")
+# async def login(request: Request, credenziali: Credenziali = Body(...)):
+    
+#     try:
+#         tokens = get_token_from_keycloak(credenziali.username, credenziali.password)#request.body.json()["username"], request.json()["password"])
+#         RedirectResponse(url="localhost:8000/callback")
+#         # Save tokens and information to the session
+
+#         request.session["access_token"] = tokens["access_token"]
+
+#         request.session["refresh_token"] = tokens["refresh_token"]
+
+#         # Get user info and save it to the session too
+
+#         user_info = get_user_info_from_keycloak(tokens["access_token"])
+
+#         request.session["user_info"] = user_info
+
+#         return RedirectResponse(url="localhost:8000/callback")
+
+#     except HTTPException as e:
+
+#         return JSONResponse(content={"detail": e.detail}, status_code=e.status_code)
+
+#     except Exception as e:
+
+#         logger.error(f"An unexpected error occurred during login: {e}")
+
+#         return JSONResponse(content={"detail": "An unexpected error occurred"}, status_code=500)
+
 
 # @app.post("/login")
 # async def login(request: Request, code: str = Form(...)):
@@ -161,8 +381,6 @@ async def home(request: Request):
 #     return templates.TemplateResponse("form.html", {"request": request})
 
 
-
-
 # @app.post("/salva_dati")
 # async def salva_dati(request: Request,
 #                      id_codice_fiscale: str = Form(...),
@@ -193,7 +411,8 @@ async def home(request: Request):
 
 @app.get("/visualizza_pratiche")
 async def visualizza_pratiche(request: Request, db: Session = Depends(get_db)):
-   return get_buildings_by_user(db)
+   
+   return get_all_buildings(db)
 
 
    
@@ -235,10 +454,6 @@ async def ripristina(request: Request,
    db.commit()
    return RedirectResponse(url="/visualizza_pratiche", status_code=status.HTTP_303_SEE_OTHER)
 
-
-
-
-
 def aggiungi_prova():
     with SessionLocal() as db:
         edificio = TabellaEdifici(id_codice_fiscale="RSSMRA80A01H501Z",
@@ -253,11 +468,15 @@ def aggiungi_prova():
         db.add(edificio)
         db.commit()
 
-aggiungi_prova()
-
+# aggiungi_prova()
 
 
+def test_json():
+    with SessionLocal() as db:
+       return get_all_buildings(db)
 
+print(test_json())
 
+uvicorn.run(app, host="localhost", port=8000)
 # uvicorn main:app --reload
 

BIN
Backend/requirements.txt


+ 31 - 2
Backend/templates/login.html

@@ -133,13 +133,13 @@
                 <label for="password" class="form-label">Password:</label>
                 <input type="password" class="form-control" id="password" name="password" required>
             </div>
-            <div class="mb-3">
+            <!-- <div class="mb-3">
                 <label for="role" class="form-label">Ruolo:</label>
                 <select class="form-select" id="role" name="role">
                     <option value="admin">Amministratore</option>
                     <option value="operator">Operatore</option>
                 </select>
-            </div>
+            </div> -->
             <div class="d-grid">
                 <button type="submit" class="btn btn-primary">Accedi</button>
             </div>
@@ -230,5 +230,34 @@
     </footer>
 
     <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
+    <script>
+     const form = document.getElementById('login-form');
+
+    form.addEventListener('submit', function(event) {
+    event.preventDefault(); // Impedisce l'invio predefinito del modulo
+
+    const formData = new FormData(form);
+    const formDataObject = {};
+    formData.forEach((value, key) => {
+        formDataObject[key] = value;
+    });
+
+    fetch('http://localhost:8000/login', {
+        method: 'POST',
+        headers: {
+            'Content-Type': 'application/json'
+        },
+        body: JSON.stringify(formDataObject)
+    })
+    .then(response => response.json())
+    .then(data => {
+        console.log(data); // Gestisci la risposta del server
+    })
+    .catch(error => {
+        console.error('Errore:', error);
+    });
+});
+    </script>
+    
 </body>
 </html>