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 ("Toegang geweigerd" "
" "

Toegang geweigerd

Je hebt geen toegang tot deze site.

", 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)