273 lines
9.9 KiB
Python
273 lines
9.9 KiB
Python
"""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/<crewname> ───────────────────────────────────────
|
|
|
|
@legacy_bp.route('/member/<crewname>')
|
|
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/<crewname>/raw ───────────────────────────────────
|
|
|
|
@legacy_bp.route('/member/<crewname>/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/<crewname>/memo ───────────────────────────────────
|
|
|
|
@legacy_bp.route('/member/<crewname>/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/<crewname>/contributions ─────────────────────────
|
|
|
|
@legacy_bp.route('/member/<crewname>/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/<crewname>/<detail_type>/[<id>]/raw/ ─────────────
|
|
|
|
@legacy_bp.route('/member/<crewname>/<detail_type>/raw/')
|
|
@legacy_bp.route('/member/<crewname>/<detail_type>/<detail_id>/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)
|