"""Legacy API route handlers. Replaces all server.get() calls in startup.js. Blueprint URL prefix: /legacy Each endpoint follows the same pipeline: 1. Build permissions dict (role → {query, level, filter?}) 2. auth.authorize(ctx) — sets username, flags, query, filter 3. database.run_query(query_def, params) — executes SQL 4. filters.runfilter(ctx) — applies permission-level data filter 5. mappings.*_mapper(ctx) — transforms raw rows into API shape 6. renderers.json_output / csv_output(ctx) — returns HTTP response """ from flask import Blueprint, current_app, request, jsonify import auth import database as db import filters import mappings import renderers legacy_bp = Blueprint('legacy', __name__) # ── helper to build the context dict for authorize ─────────────── def _ctx(request, permissions): """Build a context dict ready for auth.authorize().""" return { 'config': current_app.cteward_config, 'request': request, 'permissions': permissions, } # ── /monitor ──────────────────────────────────────────────────── @legacy_bp.route('/monitor') def monitor(): """Health check endpoint.""" try: db.check_backend_okay() return jsonify({'status': 'OK'}) except Exception as exc: return jsonify({'status': 'BROKEN'}), 503 # ── /memberlist-oldformat (CSV) ──────────────────────────────── @legacy_bp.route('/memberlist-oldformat') def memberlist_oldformat(): """CSV member list for LDAP export. Permissions: board/bots : full MEMBERLIST query, level 0 members : active-only filter, level 1 passive : self-only filter, level 2 astronauts: self-only filter, level 3 """ ctx = _ctx(request, { '_board_': {'query': db.QUERY_MEMBERLIST, 'level': 0}, '_bot_': {'query': db.QUERY_MEMBERLIST, 'level': 0}, '_member_': {'query': db.QUERY_MEMBERLIST, 'level': 1, 'filter': filters.memberlist_active_only}, '_passive_': {'query': db.QUERY_MEMBERLIST, 'level': 2, 'filter': filters.memberlist_self_only}, '_astronaut_': {'query': db.QUERY_MEMBERLIST, 'level': 3, 'filter': filters.memberlist_self_only}, }) auth.authorize(ctx) ctx['data'] = db.run_query(ctx['query'], {'crewname': request.view_args.get('crewname', '')}) filters.runfilter(ctx) mappings.memberlist_to_ldapcsv_mapper(ctx) return renderers.csv_output(ctx) # ── /stats/* ─────────────────────────────────────────────────── @legacy_bp.route('/stats/members') def stats_members(): """Member count time series.""" ctx = _ctx(request, { '_bot_': {'query': db.QUERY_STATS_MEMBERS, 'level': 0}, '_member_': {'query': db.QUERY_STATS_MEMBERS, 'level': 0}, '_stats_': {'query': db.QUERY_STATS_MEMBERS, 'level': 0}, }) auth.authorize(ctx) ctx['data'] = db.run_query(ctx['query'], {}) filters.runfilter(ctx) return renderers.json_output(ctx) @legacy_bp.route('/stats/contracts') def stats_contracts(): """Contract statistics time series.""" ctx = _ctx(request, { '_bot_': {'query': db.QUERY_STATS_CONTRACTS, 'level': 0}, '_board_': {'query': db.QUERY_STATS_CONTRACTS, 'level': 0}, '_stats_': {'query': db.QUERY_STATS_CONTRACTS, 'level': 0}, }) auth.authorize(ctx) ctx['data'] = db.run_query(ctx['query'], {}) filters.runfilter(ctx) return renderers.json_output(ctx) @legacy_bp.route('/stats/genders') def stats_genders(): """Gender demographics time series.""" ctx = _ctx(request, { '_bot_': {'query': db.QUERY_STATS_GENDERS, 'level': 0}, '_board_': {'query': db.QUERY_STATS_GENDERS, 'level': 0}, '_stats_': {'query': db.QUERY_STATS_GENDERS, 'level': 0}, }) auth.authorize(ctx) ctx['data'] = db.run_query(ctx['query'], {}) filters.runfilter(ctx) return renderers.json_output(ctx) @legacy_bp.route('/stats/ages') def stats_ages(): """Age demographics time series. Accepts query params: step, min, max for age bucket configuration. """ ctx = _ctx(request, { '_bot_': {'query': db.QUERY_STATS_AGES, 'level': 0}, '_board_': {'query': db.QUERY_STATS_AGES, 'level': 0}, '_stats_': {'query': db.QUERY_STATS_AGES, 'level': 0}, }) auth.authorize(ctx) ctx['data'] = db.run_query(ctx['query'], dict(request.args)) filters.runfilter(ctx) return renderers.json_output(ctx) # ── /member/ ─────────────────────────────────────── @legacy_bp.route('/member/') def member(crewname): """Member details (or list when crewname is '' or '*').""" if crewname in ('', '*'): # Return a list with permission-based filtering ctx = _ctx(request, { '_board_': {'query': db.QUERY_MEMBERLIST, 'level': 0}, '_member_': {'query': db.QUERY_MEMBERLIST, 'level': 1, 'filter': filters.memberlist_active_only}, '_passive_': {'query': db.QUERY_MEMBERLIST, 'level': 2, 'filter': filters.memberlist_self_only}, '_astronaut_': {'query': db.QUERY_MEMBERLIST, 'level': 2, 'filter': filters.memberlist_self_only}, }) else: # Return single member record ctx = _ctx(request, { '_board_': {'query': db.QUERY_MEMBER_BY_CREWNAME, 'level': 0}, '_self_': {'query': db.QUERY_MEMBER_BY_CREWNAME, 'level': 0}, }) auth.authorize(ctx) ctx['data'] = db.run_query(ctx['query'], {'crewname': crewname}) filters.runfilter(ctx) # Use MEMBERLIST mapper for list queries, MEMBER mapper for single record if crewname in ('', '*'): mappings.memberlist_mapper(ctx) else: mappings.member_mapper(ctx) return renderers.json_output(ctx) # ── /member//raw ─────────────────────────────────── @legacy_bp.route('/member//raw') def member_raw(crewname): """Raw DB member record (board/bots only).""" if crewname in ('', '*'): ctx = _ctx(request, { '_board_': {'query': db.QUERY_MEMBERLIST_RAW, 'level': 0}, }) else: ctx = _ctx(request, { '_board_': {'query': db.QUERY_MEMBER_BY_CREWNAME, 'level': 0}, }) auth.authorize(ctx) ctx['data'] = db.run_query(ctx['query'], {'crewname': crewname}) filters.runfilter(ctx) mappings.none_mapper(ctx) return renderers.json_output(ctx) # ── /member//memo ─────────────────────────────────── @legacy_bp.route('/member//memo') def member_memo(crewname): """RTF memo for a member (board only).""" if not crewname: return jsonify({'error': 'Member details only displayed for individual members.'}), 400 ctx = _ctx(request, { '_board_': {'query': db.QUERY_MEMBER_MEMO_BY_CREWNAME, 'level': 0}, }) auth.authorize(ctx) ctx['data'] = db.run_query(ctx['query'], {'crewname': crewname}) filters.runfilter(ctx) mappings.memo_mapper(ctx) return renderers.json_output(ctx) # ── /member//contributions ───────────────────────── @legacy_bp.route('/member//contributions') def member_contributions(crewname): """Contribution summary for a member (board only).""" if not crewname: return jsonify({'error': 'Member details only displayed for individual members.'}), 400 ctx = _ctx(request, { '_board_': {'query': db.QUERY_DEBITLIST_BY_CREWNAME, 'level': 0}, }) auth.authorize(ctx) ctx['data'] = db.run_query(ctx['query'], {'crewname': crewname}) filters.runfilter(ctx) mappings.contributions_mapper(ctx) return renderers.json_output(ctx) # ── /member///[]/raw/ ───────────── @legacy_bp.route('/member///raw/') @legacy_bp.route('/member////raw/') def member_detail_raw(crewname, detail_type, detail_id=None): """Raw detail records (contract, debit, withdrawal, payment). Permissions: board and self can see their own data. """ if crewname == '*': return jsonify({'error': 'Member details only displayed for individual members.'}), 400 # Resolve the list query and the detail-id query from the type DETAIL_QUERIES = { 'contract': (db.QUERY_CONTRACTLIST_BY_CREWNAME, 'contract', db.QUERY_CONTRACT_BY_CREWNAME_AND_CONTRACT), 'debit': (db.QUERY_DEBITLIST_BY_CREWNAME, 'guid', db.QUERY_DEBIT_BY_CREWNAME_AND_GUID), 'withdrawal': (db.QUERY_WITHDRAWALLIST_BY_CREWNAME, 'guid', db.QUERY_WITHDRAWAL_BY_CREWNAME_AND_GUID), 'payment': (db.QUERY_PAYMENTLIST_BY_CREWNAME, None, None), # no ID sub-query for payment } info = DETAIL_QUERIES.get(detail_type) if info is None: return jsonify({'error': 'Invalid member detail requested.'}), 400 list_query, id_param, id_query = info perms = { '_board_': {'query': list_query, 'level': 0}, } ctx = _ctx(request, perms) auth.authorize(ctx) if id_param and detail_id and detail_id not in ('', '*'): # Fetch single record by ID perms['_board_']['query'] = id_query params = {'crewname': crewname} params[id_param] = detail_id else: # Fetch list params = {'crewname': crewname} ctx['data'] = db.run_query( perms['_board_']['query'], params, ) filters.runfilter(ctx) mappings.none_mapper(ctx) return renderers.json_output(ctx)