first commit

This commit is contained in:
2026-02-28 00:02:02 +01:00
commit 6295c58d33
36 changed files with 7017 additions and 0 deletions

View File

69
backend/services/audit.py Normal file
View File

@@ -0,0 +1,69 @@
"""
Audit logging service.
Gebruik: from services.audit import audit_log
audit_log('user.create', 'user', target_id=str(user.id), detail={'email': email})
"""
import json
from datetime import datetime
from flask import request
from flask_login import current_user
from app import db
def audit_log(action: str, category: str, *,
target_type: str = None, target_id: str = None,
detail: dict = None, school_id: int = None,
user_id: int = None):
"""
Schrijf een audit entry naar de database.
action: korte actienaam, bv. 'user.create', 'school.delete', 'login.success'
category: auth | user | school | class | assessment | doelen | system
target_type: wat er veranderd is, bv. 'user', 'school', 'class'
target_id: identifier van het object (als string)
detail: dict met extra context, wordt als JSON opgeslagen
school_id: override school_id (standaard current_user.school_id)
user_id: override user_id (standaard current_user.id)
"""
from models import AuditLog
try:
uid = user_id
sid = school_id
if uid is None:
try:
uid = current_user.id if current_user.is_authenticated else None
except Exception:
uid = None
if sid is None:
try:
sid = current_user.school_id if current_user.is_authenticated else None
except Exception:
sid = None
ip = None
try:
ip = request.remote_addr
except Exception:
pass
entry = AuditLog(
timestamp = datetime.utcnow(),
user_id = uid,
school_id = sid,
action = action,
category = category,
target_type = target_type,
target_id = str(target_id) if target_id is not None else None,
detail = json.dumps(detail, ensure_ascii=False) if detail else None,
ip_address = ip,
)
db.session.add(entry)
# Geen commit hier — de aanroeper commit zelf (of we flushen mee)
db.session.flush()
except Exception as e:
# Audit failures mogen de hoofdflow nooit blokkeren
import logging
logging.getLogger(__name__).warning(f"Audit log failed: {e}")

137
backend/services/doelen.py Normal file
View File

@@ -0,0 +1,137 @@
"""
Doelen service - centrale logica voor vak ID's, namen, validatie en upload.
Eén bron van waarheid. Geen andere plek in de app definieert vaknamen.
"""
import os
import json
import re
DOELEN_DIR = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'doelen')
VAK_NAMEN = {
'doelenset-bao-aardrijkskunde': 'Aardrijkskunde',
'doelenset-bao-burgerschap': 'Burgerschap',
'doelenset-bao-frans': 'Frans',
'doelenset-bao-geschiedenis': 'Geschiedenis',
'doelenset-bao-ict': 'ICT',
'doelenset-bao-leren-leren': 'Leren leren',
'doelenset-bao-lichamelijke-opvoeding': 'Lichamelijke opvoeding',
'doelenset-bao-muzische-vorming': 'Muzische vorming',
'doelenset-bao-nederlands': 'Nederlands',
'doelenset-bao-sociale-vaardigheden': 'Sociale vaardigheden',
'doelenset-bao-wetenschap-techniek': 'Wetenschap en techniek',
'doelenset-bao-wiskunde': 'Wiskunde',
}
def vak_naam(vak_id):
if vak_id in VAK_NAMEN:
return VAK_NAMEN[vak_id]
cleaned = re.sub(r'^doelenset-bao-', '', vak_id)
return cleaned.replace('-', ' ').title()
def is_valid_vak_id(vak_id):
return bool(re.match(r'^[a-z0-9][a-z0-9\-]{2,78}[a-z0-9]$', vak_id))
def get_doelen_path(vak_id):
return os.path.join(DOELEN_DIR, f'{vak_id}.json')
def list_installed_vakken():
if not os.path.exists(DOELEN_DIR):
return []
return sorted([
f[:-5] for f in os.listdir(DOELEN_DIR)
if f.endswith('.json') and f != 'index.json' and is_valid_vak_id(f[:-5])
])
def load_index():
path = os.path.join(DOELEN_DIR, 'index.json')
if not os.path.exists(path):
rebuild_index()
with open(path, encoding='utf-8') as f:
data = json.load(f)
for vak in data.get('vakken', []):
vak['naam'] = vak_naam(vak['id'])
data['vakken'].sort(key=lambda v: v['naam'])
return data
def load_vak(vak_id):
if not is_valid_vak_id(vak_id):
return None
path = get_doelen_path(vak_id)
if not os.path.exists(path):
return None
with open(path, encoding='utf-8') as f:
data = json.load(f)
data['vakNaam'] = vak_naam(vak_id)
return data
def validate_vak_json(data):
errors = []
for key in ['vak', 'versie', 'rijen']:
if key not in data:
errors.append(f'Verplicht veld ontbreekt: "{key}"')
if errors:
return errors
if not isinstance(data['rijen'], list) or len(data['rijen']) == 0:
errors.append('"rijen" moet een niet-lege lijst zijn')
return errors
doelzinnen = [r for r in data['rijen'] if r.get('type') == 'doelzin']
if not doelzinnen:
errors.append('Geen doelzinnen gevonden (type="doelzin") — verkeerd bestand?')
else:
zonder_nr = [r for r in doelzinnen if not r.get('goNr')]
if zonder_nr:
errors.append(f'{len(zonder_nr)} doelzin(nen) missen een GO! nummer (goNr)')
vak_id = data.get('vak', '')
if vak_id and not is_valid_vak_id(vak_id):
errors.append(f'Ongeldig vak ID: "{vak_id}"')
return errors
def save_vak(vak_id, data):
os.makedirs(DOELEN_DIR, exist_ok=True)
data['vakNaam'] = vak_naam(vak_id)
path = get_doelen_path(vak_id)
with open(path, 'w', encoding='utf-8') as f:
json.dump(data, f, ensure_ascii=False, indent=2)
rebuild_index()
def delete_vak(vak_id):
path = get_doelen_path(vak_id)
if not os.path.exists(path):
return False
os.remove(path)
rebuild_index()
return True
def rebuild_index():
os.makedirs(DOELEN_DIR, exist_ok=True)
vakken = []
for vak_id in list_installed_vakken():
try:
data = load_vak(vak_id)
if not data:
continue
doelzinnen = [r for r in data.get('rijen', []) if r.get('type') == 'doelzin']
vakken.append({
'id': vak_id,
'naam': vak_naam(vak_id),
'aantalDoelzinnen': len(doelzinnen),
'versie': data.get('versie', '?'),
})
except Exception:
pass
vakken.sort(key=lambda v: v['naam'])
index = {'versie': '2025-01', 'vakken': vakken}
with open(os.path.join(DOELEN_DIR, 'index.json'), 'w', encoding='utf-8') as f:
json.dump(index, f, ensure_ascii=False, indent=2)