CC2 R5: defense-in-depth JWT + invite/reset token flows + audit

#1 JWT middleware:
- pgz_sport_api.py: starlette middleware require_jwt_on_admin runs before
  every /api/admin/* route. Even routes that lack Depends(require_user)
  cannot be reached without a valid Bearer token (verifies signature,
  exp, typ='access', revocation via user_sessions). OPTIONS passes for CORS.

#2 Invitation flow:
- pgz_sport.user_action_tokens table (token_hash, user_id, kind, expires_at,
  used_at, created_by, ip, meta). Single-use, raw token never persisted.
- POST /api/admin/users/{id}/invite — issues 'invite' token (TTL 7d),
  marks must_change_pwd, revokes existing sessions, returns invite_link.
- GET  /api/auth/setup-password?token=X — preflight (no consume).
- POST /api/auth/setup-password — consumes token, sets password, sets
  email_verified=true.

#3 Password reset flow:
- POST /api/auth/forgot-password — generic 'ako račun postoji' response;
  issues 'reset' token (TTL 2h) only for active users. Token returned in
  response only on localhost or if PGZ_REVEAL_RESET_TOKEN=1.
- GET  /api/auth/reset-password?token=X — preflight.
- POST /api/auth/reset-password — consumes token, sets new password,
  revokes all active sessions.

#4 Audit coverage (auth events):
- login.ok, login.fail (with reason), login.locked, login.2fa_required,
  login.2fa_fail, logout, auth.refresh, password.change, password.reset.ok,
  password.reset.fail, password.forgot.issue, password.forgot.miss,
  invite.consume.ok, invite.consume.fail, user.invite, user.create,
  user.update, user.delete, user.role.change, user.suspend, user.unsuspend,
  user.password.reset, 2fa.verify.ok, 2fa.verify.fail, 2fa.disable.

#5 Live tests: 41/41 across 6 demo users (incl. fresh invited+deleted user).
   Phase 2 verifies 14 endpoints reject no-auth and accept valid Bearer.
This commit is contained in:
Damir Radulić
2026-05-05 01:28:29 +02:00
parent 8dce58c5f9
commit 0046b8d695
24 changed files with 15419 additions and 72 deletions
+221
View File
@@ -0,0 +1,221 @@
{
"_meta": {
"version": 1,
"author": "dradulic@outlook.com",
"date": "2026-05-04",
"purpose": "Sport-aware enrichment routing for /v2/enrich/sportas. Each entry maps a sport name (lower-case, multiple aliases supported) to its national federation, optional PGŽ regional federation, and a list of search/scrape URLs. Used by routers/enrich_router.py and workers/enrichment_worker.py."
},
"_aliases": {
"kosarkaski": "košarka",
"košarkaški": "košarka",
"nogometni": "nogomet",
"rukometni": "rukomet",
"stoni tenis": "stolni tenis",
"stolnotenis": "stolni tenis",
"bocanje": "boćanje",
"boćanje (boules)": "boćanje",
"kuglacki": "kuglanje",
"vaterpolski": "vaterpolo",
"konjicki sport": "konjički sport",
"auto-sport": "auto sport",
"skijaski sport": "skijanje"
},
"boćanje": {
"national": {
"name": "HBS",
"long_name": "Hrvatski boćarski savez",
"url": "https://hrvatski-bocarski-savez.hr",
"search_url": "https://hrvatski-bocarski-savez.hr/?s={q}",
"profile_url_pattern": "https://hrvatski-bocarski-savez.hr/igraci/{slug}/"
},
"pgz": {
"name": "BS PGŽ",
"url": "https://hrvatski-bocarski-savez.hr/savez/zupanijski-savezi/"
}
},
"nogomet": {
"national": {
"name": "HNS",
"long_name": "Hrvatski nogometni savez",
"url": "https://hns-cff.hr",
"search_url": "https://semafor.hns.family/?s={q}",
"profile_search": "https://semafor.hns.family/igraci/?ime={q}",
"profile_url_pattern": "https://semafor.hns.family/igraci/{hns_pid}/{slug}/"
},
"pgz": {"name": "NS PGŽ", "url": "https://nogomet-pgz.hr"}
},
"košarka": {
"national": {
"name": "HKS",
"long_name": "Hrvatski košarkaški savez",
"url": "https://hks-cbf.hr",
"search_url": "https://hks-cbf.hr/?s={q}"
},
"pgz": {"name": "KS PGŽ", "url": "https://kosarka-pgz.hr"}
},
"rukomet": {
"national": {
"name": "HRS",
"long_name": "Hrvatski rukometni savez",
"url": "https://hrs.hr",
"search_url": "https://hrs.hr/?s={q}"
},
"pgz": {"name": "RS PGŽ", "url": "https://rs-pgz.hr"}
},
"odbojka": {
"national": {
"name": "HOS",
"long_name": "Hrvatski odbojkaški savez",
"url": "https://hos-cvf.hr",
"search_url": "https://hos-cvf.hr/?s={q}"
},
"pgz": {"name": "OS PGŽ", "url": "https://odbojkaski-savez-pgz.hr"}
},
"vaterpolo": {
"national": {
"name": "HVS",
"long_name": "Hrvatski vaterpolski savez",
"url": "https://hvs.hr",
"search_url": "https://hvs.hr/?s={q}"
}
},
"plivanje": {
"national": {
"name": "HPS",
"long_name": "Hrvatski plivački savez",
"url": "https://hps.hr",
"search_url": "https://hps.hr/?s={q}"
}
},
"atletika": {
"national": {
"name": "HAS",
"long_name": "Hrvatski atletski savez",
"url": "https://atletika.hr",
"search_url": "https://atletika.hr/?s={q}"
}
},
"tenis": {
"national": {
"name": "HTS",
"long_name": "Hrvatski teniski savez",
"url": "https://htsavez.hr",
"search_url": "https://htsavez.hr/?s={q}"
}
},
"judo": {
"national": {
"name": "HJS",
"long_name": "Hrvatski judo savez",
"url": "https://judo-savez.hr",
"search_url": "https://judo-savez.hr/?s={q}"
}
},
"karate": {
"national": {
"name": "HKaS",
"long_name": "Hrvatski karate savez",
"url": "https://karate.hr",
"search_url": "https://karate.hr/?s={q}"
}
},
"veslanje": {
"national": {
"name": "HVeS",
"long_name": "Hrvatski veslački savez",
"url": "https://veslacki-savez.hr",
"search_url": "https://veslacki-savez.hr/?s={q}"
}
},
"jedrenje": {
"national": {
"name": "HJedS",
"long_name": "Hrvatski jedriličarski savez",
"url": "https://hjs.hr",
"search_url": "https://hjs.hr/?s={q}"
}
},
"gimnastika": {
"national": {
"name": "HGS",
"long_name": "Hrvatski gimnastički savez",
"url": "https://gimnastika.hr",
"search_url": "https://gimnastika.hr/?s={q}"
}
},
"streličarstvo": {
"national": {
"name": "HStS",
"long_name": "Hrvatski streličarski savez",
"url": "https://hss.hr",
"search_url": "https://hss.hr/?s={q}"
}
},
"biciklizam": {
"national": {
"name": "HBciS",
"long_name": "Hrvatski biciklistički savez",
"url": "https://hbs.hr",
"search_url": "https://hbs.hr/?s={q}"
}
},
"stolni tenis": {
"national": {
"name": "HSTS",
"long_name": "Hrvatski stolnoteniski savez",
"url": "https://stolni-tenis.hr",
"search_url": "https://stolni-tenis.hr/?s={q}"
}
},
"triatlon": {
"national": {
"name": "HTrS",
"long_name": "Hrvatski triatlon savez",
"url": "https://triatlon.hr",
"search_url": "https://triatlon.hr/?s={q}"
}
},
"skijanje": {
"national": {
"name": "HZS",
"long_name": "Hrvatski skijaški savez",
"url": "https://skijaski-savez.hr",
"search_url": "https://skijaski-savez.hr/?s={q}"
}
},
"kuglanje": {
"national": {
"name": "HKgS",
"long_name": "Hrvatski kuglački savez",
"url": "https://kuglanje.hr",
"search_url": "https://kuglanje.hr/?s={q}"
}
},
"šah": {
"national": {
"name": "HŠS",
"long_name": "Hrvatski šahovski savez",
"url": "https://hsk.hr",
"search_url": "https://hsk.hr/?s={q}"
}
},
"konjički sport": {
"national": {
"name": "HKonjS",
"long_name": "Hrvatski konjički sportski savez",
"url": "https://konjs.hr"
}
},
"auto sport": {
"national": {
"name": "HAKS",
"long_name": "Hrvatski auto klub savez",
"url": "https://hsa.hr"
}
},
"_local_media_pgz": [
{"name": "Novi list", "search_url": "https://www.novilist.hr/?s={q}"},
{"name": "Glas Istre", "search_url": "https://www.glasistre.hr/pretraga?q={q}"},
{"name": "Rijeka.danas","search_url": "https://www.rijeka-danas.com/?s={q}"}
]
}