|
6 | 6 | import re |
7 | 7 | import logging |
8 | 8 | import httpx |
9 | | -from bs4 import BeautifulSoup |
10 | 9 | from fastapi import APIRouter, HTTPException, Path, Request |
11 | 10 | from app.models.umap import ( |
12 | 11 | UMapFeatureCollection, |
13 | 12 | ShowcaseResponse, |
14 | 13 | UserMapsResponse, |
15 | | - UserTemplatesResponse, |
16 | 14 | ) |
17 | 15 | from app.core.cache import get_cached, set_cached, DEFAULT_TTL |
18 | 16 | from app.core.config import settings |
19 | 17 |
|
20 | | -# Setup logging |
21 | 18 | logger = logging.getLogger(__name__) |
22 | 19 |
|
23 | 20 | router = APIRouter(prefix="/umap") |
24 | 21 |
|
25 | | -# uMap HOT OSM URLs derived from settings |
26 | 22 | UMAP_BASE_URL = settings.umap_base_url |
27 | | -UMAP_LOCALE = settings.umap_locale |
28 | 23 | UMAP_API_BASE_URL = f"{UMAP_BASE_URL}/en/datalayer" |
29 | 24 | UMAP_SHOWCASE_URL = f"{UMAP_BASE_URL}/en/showcase/" |
30 | | - |
31 | | -# SSL verification: disabled by default for .test domains (self-signed certs) |
32 | | -# Set UMAP_VERIFY_SSL=true in production with valid certificates |
33 | 25 | UMAP_VERIFY_SSL = os.getenv("UMAP_VERIFY_SSL", "false").lower() == "true" |
34 | 26 |
|
35 | | -# Matches /es/map/slug_123 or /map/slug_123 (any locale prefix or none) |
36 | | -_MAP_HREF_RE = re.compile(r"^/(?:[a-z]{2}/)?map/(.+)$") |
37 | | - |
38 | | -logger.info(f"uMap Base URL: {UMAP_BASE_URL}") |
39 | | -logger.info(f"uMap Locale: {UMAP_LOCALE}") |
40 | | -logger.info(f"uMap SSL Verification: {UMAP_VERIFY_SSL}") |
41 | | - |
42 | | - |
43 | | -def _parse_map_links(html: str) -> list[dict]: |
44 | | - """Extract unique map entries from an HTML page using BeautifulSoup. |
45 | | -
|
46 | | - Returns a list of dicts with keys: id, slug, href, url. |
47 | | - Skips ?share / ?edit variants and deduplicates by map ID. |
48 | | - """ |
49 | | - soup = BeautifulSoup(html, "html.parser") |
50 | | - results = [] |
51 | | - seen: set[str] = set() |
52 | | - |
53 | | - for a in soup.find_all("a", href=True): |
54 | | - href: str = a["href"] |
55 | | - if "?share" in href or "?edit" in href: |
56 | | - continue |
57 | | - match = _MAP_HREF_RE.match(href) |
58 | | - if not match: |
59 | | - continue |
60 | | - slug = match.group(1) |
61 | | - parts = slug.rsplit("_", 1) |
62 | | - map_id = parts[-1] if len(parts) > 1 and parts[-1].isdigit() else slug |
63 | | - if map_id in seen: |
64 | | - continue |
65 | | - seen.add(map_id) |
66 | | - results.append( |
67 | | - {"id": map_id, "slug": slug, "href": href, "url": f"{UMAP_BASE_URL}{href}"} |
68 | | - ) |
69 | | - |
70 | | - return results |
71 | | - |
72 | | - |
73 | | -def _check_login_redirect(response: httpx.Response, html: str) -> bool: |
74 | | - """Return True if uMap redirected to the login page (auth failed).""" |
75 | | - return "/login" in str(response.url) or "Iniciar sesión" in html |
76 | | - |
77 | | - |
78 | | -@router.get("/user/templates", response_model=UserTemplatesResponse) |
79 | | -async def get_user_templates(request: Request) -> dict: |
80 | | - """Fetch the user's templates page from uMap and return a JSON list. |
81 | | -
|
82 | | - Uses Hanko authentication cookie to authenticate with the uMap instance. |
83 | | - Returns JSON with an array under `templates` containing objects with |
84 | | - `id`, `href`, `url` and `slug` keys. |
85 | | - """ |
86 | | - hanko_cookie = request.cookies.get("hanko") |
87 | | - logger.info(f"[Templates] Hanko cookie present: {bool(hanko_cookie)}") |
88 | | - |
89 | | - if not hanko_cookie: |
90 | | - logger.warning("No Hanko cookie found in request") |
91 | | - raise HTTPException( |
92 | | - status_code=401, |
93 | | - detail="Hanko authentication cookie not found. Please log in.", |
94 | | - ) |
95 | | - |
96 | | - url = f"{UMAP_BASE_URL}/{UMAP_LOCALE}/me/templates" |
97 | | - logger.info(f"[Templates] Target URL: {url}") |
98 | | - |
99 | | - try: |
100 | | - async with httpx.AsyncClient( |
101 | | - timeout=30.0, |
102 | | - verify=UMAP_VERIFY_SSL, |
103 | | - follow_redirects=True, |
104 | | - ) as client: |
105 | | - response = await client.get( |
106 | | - url, |
107 | | - headers={"User-Agent": "portal-umap-client/1.0"}, |
108 | | - cookies={"hanko": hanko_cookie}, |
109 | | - ) |
110 | | - response.raise_for_status() |
111 | | - html = response.text |
112 | 27 |
|
113 | | - logger.info(f"[Templates] Final URL: {response.url}") |
114 | | - logger.info(f"[Templates] Response length: {len(html)} chars") |
115 | | - logger.debug(f"[Templates] HTML preview: {html[:500]}") |
| 28 | +def _umap_client() -> httpx.AsyncClient: |
| 29 | + return httpx.AsyncClient(timeout=30.0, verify=UMAP_VERIFY_SSL, follow_redirects=True) |
116 | 30 |
|
117 | | - if _check_login_redirect(response, html): |
118 | | - logger.warning("[Templates] Auth failed - redirected to login page") |
119 | | - raise HTTPException( |
120 | | - status_code=401, |
121 | | - detail="uMap authentication failed. Your session may have expired.", |
122 | | - ) |
123 | 31 |
|
124 | | - templates = _parse_map_links(html) |
125 | | - logger.info(f"[Templates] Found {len(templates)} templates") |
126 | | - return {"templates": templates} |
| 32 | +def _require_hanko(request: Request) -> str: |
| 33 | + cookie = request.cookies.get("hanko") |
| 34 | + if not cookie: |
| 35 | + raise HTTPException(status_code=401, detail="Hanko authentication cookie not found.") |
| 36 | + return cookie |
127 | 37 |
|
128 | | - except httpx.HTTPStatusError as e: |
129 | | - logger.error(f"HTTP Error: {e.response.status_code} - {e.response.text}") |
130 | | - raise HTTPException( |
131 | | - status_code=e.response.status_code, |
132 | | - detail=f"Error fetching uMap templates: {e.response.text}", |
133 | | - ) |
134 | | - except httpx.RequestError as e: |
135 | | - logger.error(f"Request Error: {str(e)}") |
136 | | - raise HTTPException(status_code=503, detail=f"Connection error to uMap: {str(e)}") |
137 | | - except HTTPException: |
138 | | - raise |
139 | | - except Exception as e: |
140 | | - logger.error(f"Unexpected error: {str(e)}", exc_info=True) |
141 | | - raise HTTPException(status_code=500, detail=f"Unexpected error: {str(e)}") |
142 | 38 |
|
143 | 39 |
|
144 | 40 | @router.get("/user/maps", response_model=UserMapsResponse) |
145 | 41 | async def get_user_maps(request: Request) -> dict: |
146 | | - """Fetch the user's maps page from uMap and return a JSON list. |
147 | | -
|
148 | | - Uses Hanko authentication cookie to authenticate with the uMap instance. |
149 | | - Returns JSON with an array under `maps` containing objects with |
150 | | - `id`, `slug`, `href` and `url` keys. |
151 | | - """ |
152 | | - hanko_cookie = request.cookies.get("hanko") |
153 | | - logger.info(f"[Maps] Hanko cookie present: {bool(hanko_cookie)}") |
154 | | - |
155 | | - if not hanko_cookie: |
156 | | - logger.warning("No Hanko cookie found in request") |
157 | | - raise HTTPException( |
158 | | - status_code=401, |
159 | | - detail="Hanko authentication cookie not found. Please log in.", |
160 | | - ) |
161 | | - |
162 | | - url = f"{UMAP_BASE_URL}/{UMAP_LOCALE}/me" |
163 | | - logger.info(f"[Maps] Target URL: {url}") |
| 42 | + """Fetch the authenticated user's maps from uMap.""" |
| 43 | + hanko_cookie = _require_hanko(request) |
| 44 | + url = f"{UMAP_BASE_URL}/api/v1/maps/?source=mine" |
164 | 45 |
|
165 | 46 | try: |
166 | | - async with httpx.AsyncClient( |
167 | | - timeout=30.0, |
168 | | - verify=UMAP_VERIFY_SSL, |
169 | | - follow_redirects=True, |
170 | | - ) as client: |
171 | | - response = await client.get( |
172 | | - url, |
173 | | - headers={"User-Agent": "portal-umap-client/1.0"}, |
174 | | - cookies={"hanko": hanko_cookie}, |
175 | | - ) |
| 47 | + async with _umap_client() as client: |
| 48 | + response = await client.get(url, cookies={"hanko": hanko_cookie}) |
| 49 | + if response.status_code == 401: |
| 50 | + raise HTTPException(status_code=401, detail="uMap authentication failed.") |
176 | 51 | response.raise_for_status() |
177 | | - html = response.text |
178 | | - |
179 | | - logger.info(f"[Maps] Final URL: {response.url}") |
180 | | - logger.info(f"[Maps] Response length: {len(html)} chars") |
181 | | - |
182 | | - if _check_login_redirect(response, html): |
183 | | - logger.warning("[Maps] Auth failed - redirected to login page") |
184 | | - raise HTTPException( |
185 | | - status_code=401, |
186 | | - detail="uMap authentication failed. Your session may have expired.", |
187 | | - ) |
188 | | - |
189 | | - maps = _parse_map_links(html) |
190 | | - logger.info(f"[Maps] Found {len(maps)} maps") |
191 | | - return {"maps": maps} |
192 | | - |
| 52 | + data = response.json() |
| 53 | + logger.info(f"[Maps] Found {len(data.get('maps', []))} maps") |
| 54 | + return data |
193 | 55 | except httpx.HTTPStatusError as e: |
194 | | - logger.error(f"HTTP Error: {e.response.status_code} - {e.response.text}") |
195 | | - raise HTTPException( |
196 | | - status_code=e.response.status_code, |
197 | | - detail=f"Error fetching uMap maps: {e.response.text}", |
198 | | - ) |
| 56 | + raise HTTPException(status_code=e.response.status_code, detail=f"uMap error: {e.response.text}") |
199 | 57 | except httpx.RequestError as e: |
200 | | - logger.error(f"Request Error: {str(e)}") |
201 | 58 | raise HTTPException(status_code=503, detail=f"Connection error to uMap: {str(e)}") |
202 | | - except HTTPException: |
203 | | - raise |
204 | | - except Exception as e: |
205 | | - logger.error(f"Unexpected error: {str(e)}", exc_info=True) |
206 | | - raise HTTPException(status_code=500, detail=f"Unexpected error: {str(e)}") |
207 | 59 |
|
208 | 60 |
|
209 | 61 | @router.get("/showcase", response_model=ShowcaseResponse) |
|
0 commit comments