Files
Pivot/app.py
T
2026-10-14 13:34:37 +02:00

303 lines
15 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import os
from flask import Flask, render_template, redirect, url_for
from config import Config
import tenancy
import rechten
from tenancy import school_op_code, school_voor_host, GeenSchoolFout, KoppelingOntbreekt
#-----------------------------------------------------
# Blueprints importeren
#-----------------------------------------------------
from blueprints.devices import devices_bp
from blueprints.students import students_bp
from blueprints.repairs import repairs_bp
from blueprints.auth import auth_bp, oauth # OAuth toegevoegd aan import
from blueprints.dashboard_bp import main_bp
from blueprints.google_admin import google_bp
from blueprints.admin import admin_bp
from blueprints.tools import tools_bp
from blueprints.co2_sensor import co2_sensor_bp
from blueprints.smartschool_bp import smartschool_bp
from blueprints.leerlingenaantal_bp import leerlingenaantal_bp
from blueprints.handleidingen_bp import handleidingen_bp, lijst as handleidingen_lijst, mag_lezen as handleidingen_mag_lezen
from blueprints.nieuw_schooljaar_accounts_bp import account_verplaatsen_bp
from blueprints.nieuw_schooljaar_toestellen_bp import toestellen_verplaatsen_bp
from blueprints.qr_code_bp import qr_code_bp
from blueprints.smartschool_activiteit_bp import ss_bp
from services.smartschool_api import SmartschoolService
from blueprints.bugreport_bp import bugreport_bp
from blueprints.bugreport_overzicht_bp import bugreport_overzicht_bp
from blueprints.signpost_devices_bp import signpost_devices_bp
from blueprints.google_licenties_bp import google_licenties_bp
from blueprints.gmail_delegatie_bp import gmail_delegatie_bp
from blueprints.afwezigheden_bp import afwezigheden_bp
from services.afwezigheden_service import AfwezighedenService
from blueprints.tweestaps_bp import tweestaps_bp
from services.tweestaps_service import TweestapsService
from blueprints.personeel_bp import personeel_bp
from blueprints.scholen_bp import scholen_bp
from blueprints.skore_bp import skore_bp
from services import skore_service
from services import personeel_service
from flask import session, request, flash, g
#------------------------------------------------------
# Services importeren
#------------------------------------------------------
from services.co2_service import CO2Scanner
def create_app():
# Maak de app aan
app = Flask(__name__)
app.config.from_object(Config)
if Config.PROXY_AANTAL:
# Achter een reverse proxy: het echte IP-adres komt uit X-Forwarded-For (enkel van die proxy's vertrouwd)
from werkzeug.middleware.proxy_fix import ProxyFix
n = Config.PROXY_AANTAL
app.wsgi_app = ProxyFix(app.wsgi_app, x_for=n, x_proto=n, x_host=n)
# Handleidingen bevatten schermafbeeldingen (base64); Flask laat standaard maar 500 kB formulierdata toe
app.config['MAX_FORM_MEMORY_SIZE'] = 30 * 1024 * 1024
#------------------------------------------------------
# OAuth Initialisatie
#------------------------------------------------------
oauth.init_app(app)
# Google-login: clients worden per school (of gedeeld) aangemaakt in blueprints/auth.py
# Centrale tabellen (Scholen, Gebruikers, rechten, Logs, Bugs) aanmaken indien nodig
try:
tenancy.ensure_schema()
except Exception as e:
print(f"PIVOT: centrale database niet bereikbaar bij opstarten: {e}", flush=True)
# Databases van de scholen: tabelnamen met hoofdletters herstellen (Windows → Linux), zie services/tabelnamen.py
try:
from services.tabelnamen import herstel_alle_scholen
herstel_alle_scholen()
except Exception as e:
print(f"PIVOT: tabelnamen van de scholen nakijken mislukt: {e}", flush=True)
#------------------------------------------------------
# Blueprints registreren
#------------------------------------------------------
# Registreert alle url prefixen voor zichtbaarheid en om naamconflicten te vermijden
app.register_blueprint(auth_bp)
app.register_blueprint(main_bp)
app.register_blueprint(devices_bp, url_prefix='/devices')
app.register_blueprint(students_bp, url_prefix='/leerlingen')
app.register_blueprint(repairs_bp, url_prefix='/herstellingen')
app.register_blueprint(google_bp, url_prefix='/google')
app.register_blueprint(admin_bp, url_prefix='/admin')
app.register_blueprint(tools_bp, url_prefix='/tools')
app.register_blueprint(co2_sensor_bp, url_prefix='/co2')
app.register_blueprint(smartschool_bp, url_prefix='/ss')
#-------------------------
app.register_blueprint(leerlingenaantal_bp, url_prefix='/scholengroep')
from blueprints.gopro_bp import gopro_bp
app.register_blueprint(gopro_bp, url_prefix='/scholengroep/gopro')
from blueprints.facturatie_bp import facturatie_bp
app.register_blueprint(facturatie_bp, url_prefix='/leerlingfacturatie')
from blueprints.gmail_labels_bp import gmail_labels_bp
app.register_blueprint(gmail_labels_bp, url_prefix='/google/labels-verplaatsen')
from blueprints.handtekeningen_bp import handtekeningen_bp, handtekening_logo_bp
app.register_blueprint(handtekeningen_bp, url_prefix='/google/handtekeningen')
app.register_blueprint(handtekening_logo_bp)
from blueprints.contacten_bp import contacten_bp
from blueprints.gedeelde_drives_bp import gedeelde_drives_bp
app.register_blueprint(gedeelde_drives_bp, url_prefix='/google/gedeelde-drives')
from blueprints.basisonderwijs_bp import basisonderwijs_bp
app.register_blueprint(basisonderwijs_bp, url_prefix='/basisonderwijs')
app.register_blueprint(contacten_bp, url_prefix='/google/contacten')
from blueprints.internaat_bp import internaat_bp
app.register_blueprint(internaat_bp, url_prefix='/internaat')
from blueprints.sgr_groepen_bp import sgr_groepen_bp
app.register_blueprint(sgr_groepen_bp, url_prefix='/smartschool/sgr-groepen')
from blueprints.personeelsaantal_bp import personeelsaantal_bp
app.register_blueprint(personeelsaantal_bp, url_prefix='/personeelsaantallen')
#---------------------------
#-------------------------
app.register_blueprint(account_verplaatsen_bp, url_prefix='/nieuw_schooljaar')
#---------------------------
#----------------------------
app.register_blueprint(toestellen_verplaatsen_bp, url_prefix='/nieuw_schooljaar_toestellen')
#---------------------------
#---------------------------
app.register_blueprint(qr_code_bp, url_prefix='/qr_code')
#----------------------------
#-----------------------------
app.register_blueprint(ss_bp,url_prefix='/activiteit')
#------------------------------
#-----------------------------
app.register_blueprint(bugreport_bp,url_prefix='/bugreport')
app.register_blueprint(bugreport_overzicht_bp,url_prefix='/bugreport_overzicht')
#------------------------------
app.register_blueprint(signpost_devices_bp,url_prefix='/signpost_devices')
app.register_blueprint(google_licenties_bp, url_prefix='/google_licenties')
app.register_blueprint(gmail_delegatie_bp, url_prefix='/gmail_delegatie')
app.register_blueprint(afwezigheden_bp, url_prefix='/afwezigheden')
app.register_blueprint(tweestaps_bp, url_prefix='/2fa')
app.register_blueprint(personeel_bp, url_prefix='/personeel')
app.register_blueprint(scholen_bp, url_prefix='/scholen')
app.register_blueprint(skore_bp, url_prefix='/skore')
app.register_blueprint(handleidingen_bp, url_prefix='/handleidingen')
from blueprints.verzendlijsten_bp import verzendlijsten_bp
app.register_blueprint(verzendlijsten_bp, url_prefix='/google/verzendlijsten')
from blueprints.ipblokkade_bp import ipblokkade_bp
app.register_blueprint(ipblokkade_bp, url_prefix='/beheer/ip-blokkades')
from blueprints.database_bp import database_bp
app.register_blueprint(database_bp, url_prefix='/beheer/database') # Beheer › Database (Excel-import)
#------------------------------------------------------
# Welke school? (per request)
#------------------------------------------------------
# Endpoints die zonder gekozen school werken
ZONDER_SCHOOL = ('static', 'index', 'auth.', 'scholen.')
# Publieke pagina's (zonder login): school via hostnaam of ?school=code
PUBLIEK = ('tools.',)
# IP-blokkades: als eerste controle, voor elke aanvraag (ook de loginpagina en statische bestanden)
@app.before_request
def ip_blokkade():
from services import ipblokkade
if request.endpoint == 'handtekening_logo.logo':
return None # het logo van de Gmail-handtekening moet voor iedereen (ook Google) bereikbaar zijn
if ipblokkade.controleer(request.remote_addr):
return ("<!doctype html><meta charset='utf-8'><title>Toegang geweigerd</title>"
"<div style='font-family:system-ui,sans-serif;max-width:480px;margin:15vh auto;text-align:center'>"
"<h1>Toegang geweigerd</h1><p>Je hebt geen toegang tot deze site.</p></div>", 403)
return None
@app.before_request
def bepaal_school():
ep = request.endpoint or ''
host_school = school_voor_host(request.host)
g.school = None
if session.get('auth'):
code = session.get('school')
school = school_op_code(code) if code else None
if code and school is None:
session.clear()
flash("Je school is niet (meer) actief in PIVOT. Log opnieuw in.", "error")
return redirect(url_for('auth.login'))
if host_school and school and host_school.code != school.code and not session.get('superbeheerder'):
# Sessie van een andere school op het adres van deze school → opnieuw inloggen
session.clear()
return redirect(url_for('auth.login'))
g.school = school
if school is None and not ep.startswith(ZONDER_SCHOOL):
# Superbeheerder zonder gekozen school
return redirect(url_for('scholen.overzicht'))
else:
g.school = host_school
if g.school is None and ep.startswith(PUBLIEK):
g.school = school_op_code(request.args.get('school'))
if g.school is None and ep.startswith(PUBLIEK):
flash("Kies eerst een school.", "error")
return redirect(url_for('auth.login'))
return None
@app.errorhandler(GeenSchoolFout)
def geen_school(e):
session.clear()
flash(str(e), "error")
return redirect(url_for('auth.login'))
@app.errorhandler(KoppelingOntbreekt)
def koppeling_ontbreekt(e):
flash(str(e), "error")
return redirect(url_for('main.dashboard'))
# Leerkrachten die via Google inloggen (groep 5) hebben enkel toegang tot de personeelsmodule
@app.before_request
def beperk_leerkrachten():
if session.get('security_group') != personeel_service.LEERKRACHT_GROEP:
return None
ep = request.endpoint or ''
if ep == 'static' or ep.startswith('personeel.') or ep.startswith('auth.'):
return None
if ep not in ('main.dashboard', 'index'):
flash("Je hebt enkel toegang tot de module afwezigheden en activiteiten.", "error")
return redirect(url_for('personeel.mijn'))
# Dagelijkse synchronisatie van afwezigheden (B-codes) uit Smartschool.
# In debugmodus start Flask twee processen; enkel het 'echte' proces start de taak.
# Eén achtergrondtaak per soort, die telkens alle scholen overloopt.
if not app.debug or os.environ.get('WERKZEUG_RUN_MAIN') == 'true':
AfwezighedenService.start_scheduler()
TweestapsService.start_scheduler() # verlopen 2FA-uitzonderingen automatisch beëindigen
skore_service.start_scheduler() # geplande Skore-synchronisatie per school
from services import gopro_service
gopro_service.start_scheduler() # GO! Pro: 's nachts de groene naar de verzameltabel (als de schakelaar aan staat)
from services import handtekening_service
handtekening_service.start_scheduler() # Gmail-handtekeningen 's nachts opnieuw zetten (per school, schakelaar)
#------------------------------
@app.context_processor
def inject_school_info():
school = g.get('school')
info = {
'school': school,
'version': app.config.get('APP_VERSION'),
'is_superbeheerder': bool(session.get('superbeheerder')),
'school_name': school.naam if school else '',
'school_name_short': school.code if school else '',
'contact_email': school.get('CONTACT_EMAIL') if school else '',
'personeel_rollen': set(),
# Rechten: {% if mag('herstellingen') %} ... {% endif %}
'mag': rechten.heeft_recht,
'is_admin': rechten.is_admin(),
'andere_scholen': [],
'handleidingen_lijst': handleidingen_lijst,
'mag_handleidingen': handleidingen_mag_lezen, # functie: enkel opgehaald waar gebruikt (dashboard)
}
if school and session.get('auth'):
info['personeel_rollen'] = personeel_service.rollen(session)
info['andere_scholen'] = mijn_andere_scholen(school)
return info
def mijn_andere_scholen(school):
"""Andere scholen waarin de ingelogde gebruiker rechten heeft (om te wisselen)."""
try:
if rechten.is_admin():
return [s for s in tenancy.alle_scholen() if s.code != school.code]
gid = session.get('gebruiker_id')
if not gid:
return []
ids = rechten.scholen_met_rechten(gid)
return [s for s in tenancy.alle_scholen() if s.id in ids and s.code != school.code]
except Exception:
return []
# Logboek: elke bezochte pagina van een ingelogde gebruiker → tabel Logs (centrale database)
LOG_OVERSLAAN = ('static', 'co2_sensor.get_scan_status', 'co2_sensor.get_cleanup_status',
'afwezigheden.status')
LOGBOEK = Config.LOGBOEK
@app.after_request
def logboek(response):
if LOGBOEK and session.get('auth') and request.endpoint and request.endpoint not in LOG_OVERSLAAN \
and response.status_code < 500:
rechten.log(actie=request.method if request.method != 'GET' else None)
return response
# Definieert de root, wordt uitgevoerd zodra iemand op de startpagina komt
@app.route('/')
def index():
return redirect(url_for('auth.login'))
return app
# Start de applicatie
if __name__ == "__main__":
app = create_app()
# Debug aanzetten via config.ini › [algemeen] › debug = ja
app.run(debug=Config.DEBUG, host='0.0.0.0', port=5001)