Før koordinater-grensesnitt.
This commit is contained in:
parent
1da3e32a8a
commit
8df889cc00
7 changed files with 154 additions and 1 deletions
|
|
@ -9,7 +9,7 @@ LOV: Aldri trunker eller slett logikk for "effektivitet".
|
|||
---------------------------------------------------------------------------
|
||||
"""
|
||||
|
||||
from fastapi import FastAPI, HTTPException, Response, Request, Query
|
||||
from fastapi import FastAPI, HTTPException, Response, Request, Query, Header, Depends
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.responses import JSONResponse
|
||||
from contextlib import asynccontextmanager
|
||||
|
|
@ -96,6 +96,16 @@ FRONTEND_REVALIDATE_URL = os.getenv(
|
|||
).strip()
|
||||
FRONTEND_REVALIDATE_SECRET = os.getenv("FRONTEND_REVALIDATE_SECRET", PUBLIC_SESSION_SECRET).strip()
|
||||
|
||||
# Dedikert delt nøkkel for TeeCups server-til-server-tilgang til
|
||||
# hole-points-API-et (ingen fallback til andre secrets -- egen
|
||||
# sikkerhetsdomene, se docs/oppgaver.md om secret-gjenbruk).
|
||||
TEECUP_API_KEY = os.getenv("TEECUP_API_KEY", "").strip()
|
||||
|
||||
|
||||
async def require_teecup_api_key(x_api_key: str = Header(default="")) -> None:
|
||||
if not TEECUP_API_KEY or not secrets.compare_digest(x_api_key, TEECUP_API_KEY):
|
||||
raise HTTPException(status_code=401, detail="Ugyldig eller manglende API-nøkkel")
|
||||
|
||||
PUBLIC_FACILITIES_CACHE_TTLS = {
|
||||
"search": 900,
|
||||
"home": 900,
|
||||
|
|
@ -5006,6 +5016,84 @@ async def get_facility(slug: str, response: Response):
|
|||
return payload
|
||||
|
||||
|
||||
@app.get("/api/hole-points/poi-types", dependencies=[Depends(require_teecup_api_key)])
|
||||
async def get_hole_points_poi_types():
|
||||
"""
|
||||
Levende (ikke statisk) liste over alle poi_type-verdier som faktisk finnes
|
||||
i hole_points akkurat nå, med antall forekomster. Til bruk for eksterne
|
||||
konsumenter (f.eks. TeeCup) som vil kunne oppdage nye kategorier automatisk
|
||||
i stedet for at en oversettelse knekker stille når en ny type dukker opp.
|
||||
"""
|
||||
async with app.state.pool.acquire() as conn:
|
||||
rows = await conn.fetch(
|
||||
"SELECT poi_type, count(*) AS count FROM hole_points GROUP BY poi_type ORDER BY poi_type"
|
||||
)
|
||||
return [{"poi_type": row["poi_type"], "count": int(row["count"])} for row in rows]
|
||||
|
||||
|
||||
@app.get("/api/facilities/{slug}/courses/{course_id}/hole-points", dependencies=[Depends(require_teecup_api_key)])
|
||||
async def get_course_hole_points(slug: str, course_id: int):
|
||||
"""
|
||||
Flat liste med koordinatpunkter per hull (green/bunker/vann/tre/tee osv.)
|
||||
for én bane, til bruk for eksterne prosjekter som slår opp banen via
|
||||
facility-slug (f.eks. TeeCup sin GPS-/avstandsfunksjon). Taksonomien er
|
||||
dokumentert i migrations/2026-08-05_add_hole_points.sql og
|
||||
migrations/2026-08-06_add_hole_points_distance_meters.sql.
|
||||
"""
|
||||
normalized_slug = str(slug or "").strip().lower()
|
||||
async with app.state.pool.acquire() as conn:
|
||||
course_row = await conn.fetchrow(
|
||||
"""
|
||||
SELECT c.id
|
||||
FROM courses c
|
||||
JOIN facilities f ON f.id = c.facility_id
|
||||
WHERE f.slug = $1
|
||||
AND f.is_published IS DISTINCT FROM FALSE
|
||||
AND c.id = $2
|
||||
AND COALESCE(c.is_visible, TRUE) = TRUE
|
||||
""",
|
||||
normalized_slug,
|
||||
course_id,
|
||||
)
|
||||
if not course_row:
|
||||
raise HTTPException(status_code=404, detail="Banen ble ikke funnet på dette anlegget")
|
||||
|
||||
point_rows = await conn.fetch(
|
||||
"""
|
||||
SELECT
|
||||
h.hole_number,
|
||||
hp.poi_type,
|
||||
hp.location,
|
||||
hp.side_fairway,
|
||||
ST_Y(hp.geom::geometry) AS latitude,
|
||||
ST_X(hp.geom::geometry) AS longitude,
|
||||
hp.feature_group,
|
||||
hp.distance_meters,
|
||||
hp.note
|
||||
FROM hole_points hp
|
||||
JOIN holes h ON h.id = hp.hole_id
|
||||
WHERE h.course_id = $1
|
||||
ORDER BY h.hole_number ASC, hp.id ASC
|
||||
""",
|
||||
course_id,
|
||||
)
|
||||
|
||||
return [
|
||||
{
|
||||
"hole_number": int(row["hole_number"]),
|
||||
"poi_type": row["poi_type"],
|
||||
"location": row["location"],
|
||||
"side_fairway": row["side_fairway"],
|
||||
"latitude": row["latitude"],
|
||||
"longitude": row["longitude"],
|
||||
"feature_group": row["feature_group"],
|
||||
"distance_meters": row["distance_meters"],
|
||||
"note": row["note"],
|
||||
}
|
||||
for row in point_rows
|
||||
]
|
||||
|
||||
|
||||
@app.get("/api/place-pages/{slug}")
|
||||
async def get_place_page(slug: str, response: Response):
|
||||
normalized_slug = str(slug or "").strip().lower()
|
||||
|
|
|
|||
|
|
@ -21,6 +21,26 @@ teecup.golf, www.teecup.golf {
|
|||
format console
|
||||
}
|
||||
|
||||
# Sikkerhetshoder (sikkerhetsgjennomgang 2026-08-09, se
|
||||
# ARCHITECTURE_DECISIONS.md-tillegg). CSP-en er bevisst IKKE en streng
|
||||
# nonce-basert policy -- appen bruker inline `style`-attributter mange
|
||||
# steder (React `style={{...}}`, bl.a. den "clubhouse"-pallet-tonede
|
||||
# UI-en) og Next.js sin egen hydrerings-oppstart kan trenge inline
|
||||
# script, så 'unsafe-inline' er beholdt for script-src/style-src for å
|
||||
# unngå å knekke appen blindt. Verdien ligger likevel i det som ER
|
||||
# strammet inn: object-src/frame-ancestors/base-uri blokkerer hele
|
||||
# klasser av angrep, og connect-src/img-src er begrenset til KUN de
|
||||
# eksterne vertene appen faktisk bruker (Mapbox for slagmåling-kartet,
|
||||
# ADR-048) -- en fremtidig XSS-bug kan dermed ikke enkelt eksfiltrere
|
||||
# data til en vilkårlig tredjeparts-vert.
|
||||
header {
|
||||
X-Frame-Options "DENY"
|
||||
X-Content-Type-Options "nosniff"
|
||||
Referrer-Policy "strict-origin-when-cross-origin"
|
||||
Strict-Transport-Security "max-age=31536000; includeSubDomains"
|
||||
Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: https://api.mapbox.com; connect-src 'self' https://api.mapbox.com https://events.mapbox.com wss://teecup.golf; worker-src 'self' blob:; frame-ancestors 'none'; base-uri 'self'; object-src 'none'"
|
||||
}
|
||||
|
||||
# Offentlig, anonym lesing av opplastede bilder (MinIO, public-read
|
||||
# bucket-policy) -- bucket-navnet ligger i selve stien (path-style
|
||||
# adressering), derav /teecup-media/* som prefiks, ikke et eget
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ services:
|
|||
PUBLIC_FROM_EMAIL: ${PUBLIC_FROM_EMAIL}
|
||||
PUBLIC_MAGIC_LINK_MAX_AGE_MINUTES: ${PUBLIC_MAGIC_LINK_MAX_AGE_MINUTES}
|
||||
INDEXNOW_KEY: ${INDEXNOW_KEY}
|
||||
TEECUP_API_KEY: ${TEECUP_API_KEY}
|
||||
volumes:
|
||||
- ./backend:/app
|
||||
- ./frontend/src/content:/shared/frontend-content:ro
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ services:
|
|||
PUBLIC_FROM_EMAIL: ${PUBLIC_FROM_EMAIL}
|
||||
PUBLIC_MAGIC_LINK_MAX_AGE_MINUTES: ${PUBLIC_MAGIC_LINK_MAX_AGE_MINUTES}
|
||||
INDEXNOW_KEY: ${INDEXNOW_KEY}
|
||||
TEECUP_API_KEY: ${TEECUP_API_KEY}
|
||||
ports:
|
||||
- "8001:8000"
|
||||
volumes:
|
||||
|
|
|
|||
27
migrations/2026-08-05_add_hole_points.sql
Normal file
27
migrations/2026-08-05_add_hole_points.sql
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
-- Koordinat-punkter per hull (green/bunker/vann/tre/tee osv.), for GPS-/
|
||||
-- avstandsfunksjoner og for å kunne eksporteres til andre prosjekter
|
||||
-- (f.eks. TeeCup) som slår opp anlegg via facility-slug.
|
||||
--
|
||||
-- Taksonomi (poi_type / location / side_fairway) er bevisst holdt som fri
|
||||
-- tekst, ikke en Postgres ENUM, i tråd med resten av kodebasens mønster
|
||||
-- for kategori-felt (f.eks. banetype) -- listen med forventede verdier
|
||||
-- dokumenteres her, ikke håndheves i databasen:
|
||||
--
|
||||
-- poi_type: green, bunker_fairway, bunker_green, vann, trær, stein,
|
||||
-- landemerke, vei, dogleg, tee, avstandsmarkør
|
||||
-- location: front, midt, bak (nullable -- ikke alle poi_type har dette)
|
||||
-- side_fairway: venstre, senter, høyre (nullable -- ikke alle poi_type har dette)
|
||||
|
||||
CREATE TABLE IF NOT EXISTS hole_points (
|
||||
id SERIAL PRIMARY KEY,
|
||||
hole_id INTEGER NOT NULL REFERENCES holes(id) ON DELETE CASCADE,
|
||||
poi_type VARCHAR(50) NOT NULL,
|
||||
location VARCHAR(20),
|
||||
side_fairway VARCHAR(20),
|
||||
geom GEOGRAPHY(Point, 4326) NOT NULL,
|
||||
note TEXT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS hole_points_hole_id_idx ON hole_points (hole_id);
|
||||
CREATE INDEX IF NOT EXISTS hole_points_geom_gix ON hole_points USING GIST (geom);
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
-- Valgfri avstandsverdi for punkter som representerer en fysisk avstandsmarkør
|
||||
-- (f.eks. en 100/150/200-meter-plate). NULL for alle andre poi_type, og NULL
|
||||
-- for eksisterende avstandsmarkør-punkter -- disse representerer i dag "start
|
||||
-- av fairway (landingssone)", ikke en tallmerket avstandsplate, så det finnes
|
||||
-- ingen kildedata å fylle inn ennå.
|
||||
|
||||
ALTER TABLE hole_points ADD COLUMN IF NOT EXISTS distance_meters INTEGER;
|
||||
9
migrations/2026-08-06_add_hole_points_feature_group.sql
Normal file
9
migrations/2026-08-06_add_hole_points_feature_group.sql
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
-- Grupperer forkant/bakkant-punkt (og andre par) som hører til samme
|
||||
-- fysiske objekt, f.eks. hvilken "forkant" og "bakkant" som utgjør én og
|
||||
-- samme fairwaybunker når et hull har flere bunkere av samme type/side.
|
||||
-- Tallverdien er meningsfull kun innenfor ett hull (hole_id), ikke globalt
|
||||
-- -- kildedata er kildearkets egen "ID"-kolonne per hull, videreført as-is.
|
||||
-- NULL betyr at punktet er entydig alene på hullet (f.eks. Tee, Green,
|
||||
-- Dogleg) og ikke trenger gruppering.
|
||||
|
||||
ALTER TABLE hole_points ADD COLUMN IF NOT EXISTS feature_group INTEGER;
|
||||
Loading…
Reference in a new issue