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.middleware.cors import CORSMiddleware
|
||||||
from fastapi.responses import JSONResponse
|
from fastapi.responses import JSONResponse
|
||||||
from contextlib import asynccontextmanager
|
from contextlib import asynccontextmanager
|
||||||
|
|
@ -96,6 +96,16 @@ FRONTEND_REVALIDATE_URL = os.getenv(
|
||||||
).strip()
|
).strip()
|
||||||
FRONTEND_REVALIDATE_SECRET = os.getenv("FRONTEND_REVALIDATE_SECRET", PUBLIC_SESSION_SECRET).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 = {
|
PUBLIC_FACILITIES_CACHE_TTLS = {
|
||||||
"search": 900,
|
"search": 900,
|
||||||
"home": 900,
|
"home": 900,
|
||||||
|
|
@ -5006,6 +5016,84 @@ async def get_facility(slug: str, response: Response):
|
||||||
return payload
|
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}")
|
@app.get("/api/place-pages/{slug}")
|
||||||
async def get_place_page(slug: str, response: Response):
|
async def get_place_page(slug: str, response: Response):
|
||||||
normalized_slug = str(slug or "").strip().lower()
|
normalized_slug = str(slug or "").strip().lower()
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,26 @@ teecup.golf, www.teecup.golf {
|
||||||
format console
|
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
|
# Offentlig, anonym lesing av opplastede bilder (MinIO, public-read
|
||||||
# bucket-policy) -- bucket-navnet ligger i selve stien (path-style
|
# bucket-policy) -- bucket-navnet ligger i selve stien (path-style
|
||||||
# adressering), derav /teecup-media/* som prefiks, ikke et eget
|
# adressering), derav /teecup-media/* som prefiks, ikke et eget
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ services:
|
||||||
PUBLIC_FROM_EMAIL: ${PUBLIC_FROM_EMAIL}
|
PUBLIC_FROM_EMAIL: ${PUBLIC_FROM_EMAIL}
|
||||||
PUBLIC_MAGIC_LINK_MAX_AGE_MINUTES: ${PUBLIC_MAGIC_LINK_MAX_AGE_MINUTES}
|
PUBLIC_MAGIC_LINK_MAX_AGE_MINUTES: ${PUBLIC_MAGIC_LINK_MAX_AGE_MINUTES}
|
||||||
INDEXNOW_KEY: ${INDEXNOW_KEY}
|
INDEXNOW_KEY: ${INDEXNOW_KEY}
|
||||||
|
TEECUP_API_KEY: ${TEECUP_API_KEY}
|
||||||
volumes:
|
volumes:
|
||||||
- ./backend:/app
|
- ./backend:/app
|
||||||
- ./frontend/src/content:/shared/frontend-content:ro
|
- ./frontend/src/content:/shared/frontend-content:ro
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,7 @@ services:
|
||||||
PUBLIC_FROM_EMAIL: ${PUBLIC_FROM_EMAIL}
|
PUBLIC_FROM_EMAIL: ${PUBLIC_FROM_EMAIL}
|
||||||
PUBLIC_MAGIC_LINK_MAX_AGE_MINUTES: ${PUBLIC_MAGIC_LINK_MAX_AGE_MINUTES}
|
PUBLIC_MAGIC_LINK_MAX_AGE_MINUTES: ${PUBLIC_MAGIC_LINK_MAX_AGE_MINUTES}
|
||||||
INDEXNOW_KEY: ${INDEXNOW_KEY}
|
INDEXNOW_KEY: ${INDEXNOW_KEY}
|
||||||
|
TEECUP_API_KEY: ${TEECUP_API_KEY}
|
||||||
ports:
|
ports:
|
||||||
- "8001:8000"
|
- "8001:8000"
|
||||||
volumes:
|
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