diff --git a/work/dashboard-frontend/src/lib/api.ts b/work/dashboard-frontend/src/lib/api.ts index dcdac6a..83c3bfe 100644 --- a/work/dashboard-frontend/src/lib/api.ts +++ b/work/dashboard-frontend/src/lib/api.ts @@ -614,6 +614,7 @@ export interface MeshcoreRoom { pubkey: string prefix: string path_established: boolean + password_set?: boolean } export interface MeshcoreRooms { active: boolean @@ -624,6 +625,36 @@ export async function getMeshcoreRooms(): Promise { return fetchJson('/api/meshcore/rooms') } +// Set (or clear, if password is empty) a MeshCore room server's login password. +// The value is never read back — only a boolean status is returned. +export async function setRoomPassword( + pubkey: string, + password: string, +): Promise<{ ok: boolean; password_set: boolean }> { + const response = await fetch(`/api/meshcore/room-password/${encodeURIComponent(pubkey)}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ value: password }), + }) + if (!response.ok) { + throw new Error(`API error: ${response.status} ${response.statusText}`) + } + return response.json() +} + +// Clear a MeshCore room server's stored login password. +export async function clearRoomPassword( + pubkey: string, +): Promise<{ ok: boolean; password_set: boolean }> { + const response = await fetch(`/api/meshcore/room-password/${encodeURIComponent(pubkey)}`, { + method: 'DELETE', + }) + if (!response.ok) { + throw new Error(`API error: ${response.status} ${response.statusText}`) + } + return response.json() +} + export interface MeshcoreContact { name: string | null pubkey: string diff --git a/work/dashboard-frontend/src/pages/MeshCoreRouting.tsx b/work/dashboard-frontend/src/pages/MeshCoreRouting.tsx index a2c1678..31018e6 100644 --- a/work/dashboard-frontend/src/pages/MeshCoreRouting.tsx +++ b/work/dashboard-frontend/src/pages/MeshCoreRouting.tsx @@ -14,7 +14,7 @@ import { type RegionCell, type RegionRoutes, } from './Notifications' -import { getMeshcoreRooms, type MeshcoreRoom } from '@/lib/api' +import { getMeshcoreRooms, setRoomPassword, clearRoomPassword, type MeshcoreRoom } from '@/lib/api' // Merge only the MeshCore-owned fields of `mine` into `fresh`, preserving every // other (Meshtastic / Other-channels / general) field on the family. The @@ -90,12 +90,6 @@ function mergeRegionRoutesForMc( return { mt_enabled, mc_enabled, cells: newCells } } -// TODO: room password UI needs a backend secrets endpoint. secrets_store.py -// has set/get/clear_room_password() keyed by room pubkey, but NO HTTP route -// exposes them (the generic /api/secrets endpoint is gated to a fixed -// SECRET_LABELS allowlist, so it cannot set a dynamic per-room password). -// Open rooms route fine without a password; password-protected rooms need -// that endpoint before a set/clear affordance can be added here. export default function MeshCoreRouting() { const { setDirty } = useDirty() const [config, setConfig] = useState(null) @@ -110,6 +104,10 @@ export default function MeshCoreRouting() { // targeted by writing the cell value as `room:`; channels stay bare. const [rooms, setRooms] = useState([]) const [roomsActive, setRoomsActive] = useState(false) + // Inline room-password editor: open for at most one room pubkey at a time. + const [pwdEditorPk, setPwdEditorPk] = useState(null) + const [pwdInput, setPwdInput] = useState('') + const [pwdError, setPwdError] = useState(null) // Local map: family key -> explicitly toggled on/off for region expand. const [regionExpandedMap, setRegionExpandedMap] = useState>({}) @@ -519,7 +517,8 @@ export default function MeshCoreRouting() { {targetsRoom ? ( - roomsActive || room ? ( + <> + {roomsActive || room ? (
setPwdInput(e.target.value)} + placeholder="room password" + className="w-28 px-1.5 py-1 bg-[#0a0e17] border border-[#1e2a3a] rounded text-xs text-slate-200 focus:outline-none focus:border-accent" + /> + + + + {pwdError && {pwdError}} +
+ )} + + ) + })()} + ) : ( bool: + return len(pk) >= 12 and all(c in _ROOM_PK_HEX for c in pk) + + +@router.put("/meshcore/room-password/{pubkey}") +async def set_room_password_route(pubkey: str, body: SecretUpdate): + """Set (or, if the value is empty, clear) a MeshCore room server's password. + The value is never read back — only a boolean status is returned.""" + pk = pubkey.strip() + if not _valid_room_pubkey(pk): + raise HTTPException(status_code=400, detail="invalid room pubkey") + value = (body.value or "").strip() + if value: + secrets_store.set_room_password(pk, value) + else: + secrets_store.delete_room_password(pk) + return {"ok": True, "password_set": secrets_store.room_password_is_set(pk)} + + +@router.delete("/meshcore/room-password/{pubkey}") +async def clear_room_password_route(pubkey: str): + """Clear a MeshCore room server's stored password.""" + pk = pubkey.strip() + if not _valid_room_pubkey(pk): + raise HTTPException(status_code=400, detail="invalid room pubkey") + secrets_store.delete_room_password(pk) + return {"ok": True, "password_set": False}