Files
2026-10-14 13:34:37 +02:00

293 lines
12 KiB
Python
Raw Permalink 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.
"""
Beheer van PIVOT vanaf de opdrachtregel.
python beheer.py init
Maakt de tabellen van de centrale database aan (Scholen, Gebruikers, rechten, Logs, Bugs).
python beheer.py nieuwe-schooldatabase DATABASE
Maakt een lege schooldatabase met de standaardstructuur (sql/school.sql).
python beheer.py gebruiker NAAM [--wachtwoord W] [--email E] [--admin]
Maakt een PIVOT-gebruiker aan, of zet een nieuw wachtwoord / maakt hem administrator.
python beheer.py rechten NAAM SCHOOLCODE [onderdeel ...|alles|geen]
Toont of zet de rechten van een gebruiker in een school.
python beheer.py importeer-config OUDE_CONFIG.ini [--google-sleutel sleutel.json] [--code kamxm] [--db Pivot_26-27]
Zet een school uit een config.ini van PIVOT 1.0.x in het scholenregister
(alle wachtwoorden en koppelingen worden versleuteld overgenomen).
python beheer.py lijst
Toont alle scholen.
python beheer.py kopieer-school BRONCODE NIEUWE_DATABASE
Maakt een nieuwe database als volledige kopie (tabellen + gegevens) van de database van BRONCODE.
Registreer ze daarna als school via PIVOT (Nieuwe school › Database aanmaken? › Nee, bestaat al).
python beheer.py nieuwe-sleutel
Toont een nieuwe versleutelingssleutel (voor PIVOT_SLEUTEL).
"""
import argparse
import configparser
import json
import os
import sys
from cryptography.fernet import Fernet
# Oude config.ini → velden in het scholenregister
MAPPING = {
('schoolgegevens', 'admin_email'): 'ADMIN_EMAIL',
('schooljaar', 'huidig_schooljaar'): 'SCHOOLYEAR',
('google', 'ou_nieuwe_lln'): 'STUDENT_OU',
('google', 'google_sso_clientid'): 'GOOGLE_SSO_CLIENTID',
('google', 'google_sso_clientsecret'): 'GOOGLE_SSO_CLIENTSECRET',
('google', 'google_smtp_server'): 'GOOGLE_SMTP_SERVER',
('google', 'google_smtp_port'): 'GOOGLE_SMTP_PORT',
('google', 'google_sender_adres'): 'GOOGLE_SMTP_SENDER_adres',
('google', 'google_email_passsword'): 'GOOGLE_SMTP_EMAIL_PASSWORD',
('google', 'google_api_scopes'): 'GOOGLE_API_SCOPES_RAW',
('google', 'tweestaps_uitzondering_groep'): 'TWEESTAPS_UITZONDERING_GROEP',
('google', 'tweestaps_ous'): 'TWEESTAPS_OUS',
('google', 'tweestaps_groepen'): 'TWEESTAPS_GROEPEN_RAW',
('informat', 'client_id'): 'INFORMAT_ID',
('informat', 'client_secret'): 'INFORMAT_SECRET',
('informat', 'intitute_number'): 'INFORMAT_INST_NUMBER',
('informat', 'autho_url'): 'INFORMAT_AUTH_URL',
('informat', 'api_base'): 'INFORMAT_API_BASE',
('informat', 'scope'): 'INFORMAT_SCOPE',
('smartschool', 'ss_client_id'): 'SS_CLIENT_ID',
('smartschool', 'ss_client_secret'): 'SS_CLIENT_SECRET',
('smartschool', 'ss_api_pw'): 'SS_API_PW',
('smartschool', 'ss_api_platform'): 'SS_API_PLATFORM',
('smartschool', 'afwezigheid_codes'): 'AFWEZIGHEID_CODES',
('smartschool', 'afwezigheid_sync_uur'): 'AFWEZIGHEID_SYNC_UUR',
('CO2_sensor', 'co2_netwerk'): 'CO2_NETWORK',
('CO2_sensor', 'co2_beginadres'): 'CO2_START_ADDR',
('CO2_sensor', 'co2_eindadres'): 'CO2_END_ADDR',
('CO2_sensor', 'co2_waardedrempel'): 'CO2_THRESHOLD',
('CO2_sensor', 'co2_scan_interval'): 'CO2_INTERVAL',
('personeel', 'personeel_ous'): 'PERSONEEL_OUS',
('personeel', 'directie'): 'PERSONEEL_DIRECTIE',
('personeel', 'secretariaat'): 'PERSONEEL_SECRETARIAAT',
('personeel', 'secretariaat_mail'): 'PERSONEEL_SECRETARIAAT_MAIL',
('personeel', 'mails_versturen'): 'PERSONEEL_MAILS',
('m4s', 'm4s_base_data_url'): 'M4S_BASE_URL',
('m4s', 'm4s_auth_url'): 'M4S_AUTH_URL',
('m4s', 'm4s_clientid'): 'M4S_CLIENT_ID',
('m4s', 'm4s_clientsecret'): 'M4S_CLIENT_SECRET',
('m4s', 'm4s_institutionguid'): 'M4S_INSTITUTIONGUID',
('m4s', 'm4s_repair_location_type'): 'M4S_REPAIR_LOCATION_TYPE',
('m4s', 'm4s_repair_status'): 'M4S_REPAIR_STATUS',
('m4s', 'm4s_truth_statement'): 'M4S_TRUTH_STATEMENT',
('m4s', 'm4s_repair_at_institution'): 'M4S_REPAIR_AT_INTITUTION',
('m4s', 'm4s_contact_first_name'): 'M4S_CONTACT_FIRST_NAME',
('m4s', 'm4s_contact_last_name'): 'M4S_CONTACT_LAST_NAME',
('m4s', 'm4s_contact_email'): 'M4S_CONTACT_EMAIL',
('m4s', 'm4s_contact_phone'): 'M4S_CONTACT_PHONE',
('m4s', 'm4s_repair_street'): 'M4S_REPAIR_STREET',
('m4s', 'm4s_repair_streetnumber'): 'M4S_REPAIR_STREETNUMBER',
('m4s', 'm4s_repair_zipcode'): 'M4S_REPAIR_ZIPCODE',
('m4s', 'm4s_repair_city'): 'M4S_REPAIIR_CITY',
('m4s', 'm4s_repair_countrycode'): 'M4S_REPAIR_COUNTRYCODE',
('m4s', 'm4s_language_code'): 'M4S_LANGUAGE_CODE',
('m4s', 'm4s_billing_first_name'): 'M4S_BILLING_FIRST_NAME',
('m4s', 'm4s_billing_last_name'): 'M4S_BILLING_LAST_NAME',
('m4s', 'm4s_billing_email'): 'M4S_BILLING_EMAIL',
('m4s', 'm4s_billing_phone'): 'M4S_BILLING_PHONE',
}
def lees_oude_config(pad):
cp = configparser.ConfigParser()
if not cp.read(pad, encoding='utf-8'):
sys.exit(f"Kan {pad} niet lezen.")
waarden = {}
for (sectie, key), veld in MAPPING.items():
if cp.has_option(sectie, key):
v = cp.get(sectie, key).strip()
if v and v != 'None':
waarden[veld] = v
# Smartschool-platform zonder https:// en pad
if 'SS_API_PLATFORM' in waarden:
waarden['SS_API_PLATFORM'] = waarden['SS_API_PLATFORM'].replace('https://', '').replace('http://', '').split('/')[0]
return cp, waarden
def cmd_importeer(args):
import tenancy
from config import Config
cp, waarden = lees_oude_config(args.config)
code = (args.code or cp.get('schoolgegevens', 'school_afkorting', fallback='')).strip().lower()
naam = cp.get('schoolgegevens', 'schoolnaam', fallback=code).strip()
domein = cp.get('schoolgegevens', 'schooldomein', fallback='').strip().lower()
db_naam = args.db or cp.get('database', 'database_name', fallback='').strip()
if not code or not domein or not db_naam:
sys.exit("Code, schooldomein of databasenaam ontbreekt (gebruik --code / --db).")
# Eigen databaseserver/gebruiker enkel overnemen als die verschilt van de nieuwe config.ini
for key, veld, standaard in [('host', 'DB_HOST', Config.DB_HOST), ('user', 'DB_USER', Config.DB_USER),
('password', 'DB_PASSWORD', Config.DB_PASSWORD)]:
v = cp.get('database', key, fallback='').strip()
if v and v != standaard:
waarden[veld] = v
# Google-serviceaccount
sleutel = None
kandidaten = [args.google_sleutel] if args.google_sleutel else [
cp.get('google', 'location_linux', fallback=''), cp.get('google', 'location', fallback='')]
for pad in kandidaten:
if pad and os.path.exists(pad):
with open(pad, encoding='utf-8') as f:
sleutel = json.load(f)
print(f"Google-sleutel gelezen uit {pad}")
break
if not sleutel:
print("⚠ Geen Google-sleutel gevonden; upload hem later via /scholen (of gebruik --google-sleutel).")
tenancy.ensure_schema()
bestaand = tenancy.school_op_code(code, ook_inactief=True)
if bestaand and not args.overschrijven:
sys.exit(f"School '{code}' bestaat al. Gebruik --overschrijven om bij te werken.")
tenancy.opslaan(code, naam, domein, '', db_naam, True, waarden, sleutel,
gebruiker='beheer.py', nieuw=not bestaand)
print(f"✓ School {naam} ({code}) opgeslagen: domein {domein}, database {db_naam}, "
f"{len(waarden)} instellingen{', Google-sleutel' if sleutel else ''}.")
def cmd_lijst(args):
import tenancy
tenancy.ensure_schema()
scholen = tenancy.alle_scholen(ook_inactief=True)
if not scholen:
print("Nog geen scholen.")
for s in scholen:
print(f"{s.code:<12} {s.naam:<40} {','.join(s.domeinen):<30} db={s.db_naam:<20} "
f"{'actief' if s.actief else 'INACTIEF'}{' google✓' if s.google_sleutel else ''}")
def cmd_kopieer(args):
import tenancy
from blueprints.scholen_bp import kopieer_school_db
bron = tenancy.school_op_code(args.bron, ook_inactief=True)
if not bron:
sys.exit(f"Onbekende school {args.bron}")
ok, msg = kopieer_school_db(bron, args.database, {})
print(msg)
sys.exit(0 if ok else 1)
def cmd_nieuwe_schooldatabase(args):
from blueprints.scholen_bp import maak_schooldatabase
ok, msg = maak_schooldatabase(args.database)
print(msg)
sys.exit(0 if ok else 1)
def cmd_gebruiker(args):
import getpass
import tenancy
import rechten
from database import central_query
from werkzeug.security import generate_password_hash
tenancy.ensure_schema()
pw = args.wachtwoord
bestaand = rechten.zoek_gebruiker(args.naam)
if pw is None and not bestaand:
pw = getpass.getpass("Wachtwoord: ")
groep = rechten.ADMIN if args.admin else None
if bestaand:
gid = bestaand['Gebruiker_ID']
if pw:
central_query("UPDATE Gebruikers SET Passwoordhash = %s WHERE Gebruiker_ID = %s",
(generate_password_hash(pw), gid), commit=True)
if args.email:
central_query("UPDATE Gebruikers SET Emailadres = %s WHERE Gebruiker_ID = %s",
(args.email.lower(), gid), commit=True)
if groep:
central_query("UPDATE Gebruikers SET Veiligheidsgroep = %s, Actief = 1 WHERE Gebruiker_ID = %s",
(groep, gid), commit=True)
print(f"✓ Gebruiker {bestaand['Gebruikersnaam']} bijgewerkt.")
else:
central_query("INSERT INTO Gebruikers (Gebruikersnaam, Veiligheidsgroep, Passwoordhash, Emailadres) "
"VALUES (%s, %s, %s, %s)",
(args.naam, groep or rechten.GEBRUIKER, generate_password_hash(pw) if pw else None,
args.email.lower() if args.email else None), commit=True)
print(f"✓ Gebruiker {args.naam} aangemaakt ({'Administrator' if groep else 'Gebruiker'}).")
def cmd_rechten(args):
import tenancy
import rechten
g = rechten.zoek_gebruiker(args.naam)
school = tenancy.school_op_code(args.school, ook_inactief=True)
if not g or not school:
sys.exit("Onbekende gebruiker of school.")
if args.onderdelen:
if args.onderdelen == ['alles']:
codes = rechten.CODES
elif args.onderdelen == ['geen']:
codes = []
else:
onbekend = [c for c in args.onderdelen if c not in rechten.CODES]
if onbekend:
sys.exit(f"Onbekende onderdelen: {', '.join(onbekend)}\nMogelijk: {', '.join(rechten.CODES)}")
codes = args.onderdelen
rechten.zet_rechten(g['Gebruiker_ID'], school.id, codes, door='beheer.py')
huidig = rechten.rechten_van(g['Gebruiker_ID'], school.id)
print(f"{g['Gebruikersnaam']} in {school.naam}:")
for code, naam, cat in rechten.ONDERDELEN:
print(f" [{'x' if code in huidig else ' '}] {code:<26} {naam}")
def main():
p = argparse.ArgumentParser(description="PIVOT beheer")
sub = p.add_subparsers(dest='cmd', required=True)
sub.add_parser('init')
i = sub.add_parser('importeer-config')
i.add_argument('config')
i.add_argument('--code')
i.add_argument('--db')
i.add_argument('--google-sleutel')
i.add_argument('--overschrijven', action='store_true')
sub.add_parser('lijst')
k = sub.add_parser('kopieer-school', aliases=['kopieer-schema'])
k.add_argument('bron')
k.add_argument('database')
sub.add_parser('nieuwe-sleutel')
n = sub.add_parser('nieuwe-schooldatabase')
n.add_argument('database')
gb = sub.add_parser('gebruiker')
gb.add_argument('naam')
gb.add_argument('--wachtwoord')
gb.add_argument('--email')
gb.add_argument('--admin', action='store_true')
r = sub.add_parser('rechten')
r.add_argument('naam')
r.add_argument('school')
r.add_argument('onderdelen', nargs='*')
args = p.parse_args()
if args.cmd == 'nieuwe-sleutel':
print(Fernet.generate_key().decode())
elif args.cmd == 'init':
import tenancy
tenancy.ensure_schema()
print("✓ Centrale tabellen aangemaakt (of bestonden al).")
elif args.cmd == 'importeer-config':
cmd_importeer(args)
elif args.cmd == 'lijst':
cmd_lijst(args)
elif args.cmd in ('kopieer-school', 'kopieer-schema'):
cmd_kopieer(args)
elif args.cmd == 'nieuwe-schooldatabase':
cmd_nieuwe_schooldatabase(args)
elif args.cmd == 'gebruiker':
cmd_gebruiker(args)
elif args.cmd == 'rechten':
cmd_rechten(args)
if __name__ == '__main__':
main()