Compare commits

...

5 commits

Author SHA1 Message Date
d521782f19 Merge pull request 'feat: public guest discovery endpoints + privacy fix (v0.3.0)' (#17) from feat/public-guest-endpoints into main
Reviewed-on: #17
2026-07-19 23:43:38 +00:00
6fa7774c9f chore: bump version 0.2.0 -> 0.3.0 (public guest endpoints)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019VUQCfdqiLSsFS2jcGnaFD
2026-07-20 01:39:39 +02:00
408b0e1d08 test: public endpoints + private-field strip
Asserts public_room_dict drops wallet + checkin_instructions, public list
shows active-only + stripped, and public get 404s on inactive. 32 pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019VUQCfdqiLSsFS2jcGnaFD
2026-07-20 01:39:39 +02:00
0631edf8a2 feat: public guest room discovery endpoints (slice 1 for webapp #141)
GET /public/rooms + /public/rooms/{id} — no auth, active rooms only,
operator-private fields stripped. The webapp guest UI needs these because
the existing GET /rooms is admin-scoped; availability + booking POST are
already public. Reuses the public_room_dict strip.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019VUQCfdqiLSsFS2jcGnaFD
2026-07-20 01:39:39 +02:00
4d6dba5487 fix: strip checkin_instructions from public room dicts (privacy leak)
The AUTH_NONE RPC room endpoints (chatelet_room_list/_get) stripped wallet
but NOT checkin_instructions — which was added in #5 after this code, so the
operator's private access details (address, gate code) were leaking to any
guest. Centralize a public_room_dict(room) helper (strips wallet +
checkin_instructions) and route both doors through it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019VUQCfdqiLSsFS2jcGnaFD
2026-07-20 01:39:39 +02:00
5 changed files with 101 additions and 4 deletions

View file

@ -1,6 +1,6 @@
{
"id": "chatelet",
"version": "0.2.0",
"version": "0.3.0",
"name": "Chatelet",
"repo": "https://git.atitlan.io/aiolabs/chatelet",
"short_description": "Nostr-native room rentals (Airbnb-style) for LNbits",

View file

@ -17,6 +17,7 @@ Design notes carried into the field definitions:
arbiter of "is this range open" Nostr events are requests, not locks.
"""
import json
from datetime import datetime, timezone
from enum import Enum
@ -194,6 +195,17 @@ class Block(BaseModel):
# ---------------------------------------------------------------------------
def public_room_dict(room: Room) -> dict:
"""A Room as public JSON for guests — strips operator-private fields: the
wallet id, and the check-in instructions (address/gate code, delivered only
in the encrypted post-payment DM). Shared by the HTTP and Nostr-RPC guest
doors so neither can leak them."""
d = json.loads(room.json())
d.pop("wallet", None)
d.pop("checkin_instructions", None)
return d
class AvailabilityQuery(BaseModel):
room_id: str
check_in: str # YYYY-MM-DD inclusive

View file

@ -0,0 +1,60 @@
"""Public guest discovery endpoints + the operator-private field strip
(privacy: check-in instructions must never reach a guest)."""
import asyncio
import pytest
from fastapi import HTTPException
from .. import crud, views_api
from ..models import RoomStatus, public_room_dict
from .conftest import make_room
def test_public_room_dict_strips_private_fields():
room = make_room(wallet="w1")
room.checkin_instructions = "gate code 4213, door on the left"
d = public_room_dict(room)
assert "wallet" not in d # operator-internal
assert "checkin_instructions" not in d # private, DM-only
assert d["title"] == "Tower Room" # public fields survive
assert d["price_amount"] == 100
def test_public_rooms_lists_active_only_and_stripped(monkeypatch):
active = make_room("a", status=RoomStatus.active)
active.checkin_instructions = "secret"
inactive = make_room("b", status=RoomStatus.inactive)
async def gr():
return [active, inactive]
monkeypatch.setattr(crud, "get_rooms", gr)
out = asyncio.run(views_api.api_public_rooms())
assert [r["id"] for r in out] == ["a"] # inactive hidden from guests
assert "checkin_instructions" not in out[0]
assert "wallet" not in out[0]
def test_public_room_404_when_inactive(monkeypatch):
async def gr(_):
return make_room(status=RoomStatus.inactive)
monkeypatch.setattr(crud, "get_room", gr)
with pytest.raises(HTTPException) as e:
asyncio.run(views_api.api_public_room("x"))
assert e.value.status_code == 404
def test_public_room_returns_stripped_when_active(monkeypatch):
room = make_room("a", status=RoomStatus.active)
room.checkin_instructions = "gate"
async def gr(_):
return room
monkeypatch.setattr(crud, "get_room", gr)
out = asyncio.run(views_api.api_public_room("a"))
assert out["id"] == "a"
assert "checkin_instructions" not in out
assert "wallet" not in out

View file

@ -196,6 +196,9 @@ def _to_dict(obj) -> dict:
def _public_room(room) -> dict:
d = _to_dict(room)
d.pop("wallet", None) # wallet id is operator-internal, not for guests
return d
# Shared with the HTTP door; strips wallet id AND checkin_instructions
# (the latter was leaking to guests before — added after this file's
# original public dict).
from .models import public_room_dict
return public_room_dict(room)

View file

@ -24,6 +24,7 @@ from .models import (
CreateRoomData,
Room,
RoomStatus,
public_room_dict,
)
from .nostr import service as nostr
@ -189,6 +190,27 @@ async def api_update_settings(
return await crud.update_settings(settings)
# --- public guest discovery (no auth) --------------------------------------
@chatelet_api_router.get("/api/v1/public/rooms")
async def api_public_rooms() -> list[dict]:
"""Active rooms for guest browsing — operator-private fields stripped."""
return [
public_room_dict(r)
for r in await crud.get_rooms()
if r.status == RoomStatus.active
]
@chatelet_api_router.get("/api/v1/public/rooms/{room_id}")
async def api_public_room(room_id: str) -> dict:
room = await crud.get_room(room_id)
if not room or room.status != RoomStatus.active:
raise HTTPException(404, "Room not available")
return public_room_dict(room)
# --- availability (public read) --------------------------------------------