guest.rs
⎇
Raw
1//! The page behind a guest link: `/#l=<token>`.
2
3use api::{GuestAuth, GuestKey, GuestUnlock, GuestView};
4use leptos::prelude::*;
5use leptos::task::spawn_local;
6
7use crate::{ThemeToggle, http, map};
8
9#[derive(Clone, PartialEq)]
10enum State {
11 Loading,
12 Locked,
13 Open(String),
14 Gone,
15 Failed(String),
16}
17
18fn storage() -> Option<web_sys::Storage> {
19 window().session_storage().ok().flatten()
20}
21
22#[component]
23pub fn GuestPage(token: String) -> impl IntoView {
24 let storage_key = format!("link {token}");
25 let key = RwSignal::new(storage().and_then(|s| s.get_item(&storage_key).ok().flatten()));
26 let token = StoredValue::new(token);
27 let auth = move || GuestAuth {
28 token: token.get_value(),
29 key: key.get_untracked(),
30 };
31 let state = RwSignal::new(State::Loading);
32 let check = move || {
33 spawn_local(async move {
34 state.set(match http::post::<GuestView>("/api/guest", &auth()).await {
35 Ok(v) => State::Open(v.person.username),
36 Err(e) if e.status == 401 => State::Locked,
37 Err(e) if e.status == 404 => State::Gone,
38 Err(e) => State::Failed(e.to_string()),
39 })
40 })
41 };
42 check();
43
44 let password = RwSignal::new(String::new());
45 let error = RwSignal::new(None::<String>);
46 let unlock = move |ev: leptos::ev::SubmitEvent| {
47 ev.prevent_default();
48 let body = GuestUnlock {
49 token: token.get_value(),
50 password: password.get_untracked(),
51 };
52 let storage_key = storage_key.clone();
53 spawn_local(async move {
54 match http::post::<GuestKey>("/api/guest/unlock", &body).await {
55 Ok(k) => {
56 if let Some(s) = storage() {
57 let _ = s.set_item(&storage_key, &k.key);
58 }
59 key.set(Some(k.key));
60 error.set(None);
61 check();
62 }
63 Err(e) => error.set(Some(match e.status {
64 401 => "Wrong password.".into(),
65 429 => "Too many failed attempts. Try again in 15 minutes.".into(),
66 _ => e.to_string(),
67 })),
68 }
69 });
70 };
71
72 let title = move || match state.get() {
73 State::Open(name) => format!("Location of {name}"),
74 _ => "Shared location".into(),
75 };
76 view! {
77 <header>
78 <img class="brand" src="/icon.svg" alt="opentracker" width="28" height="28" />
79 <span class="guest-title">{title}</span>
80 <ThemeToggle />
81 </header>
82 {move || match state.get() {
83 State::Loading => ().into_any(),
84 State::Locked => {
85 view! {
86 <form class="login" on:submit=unlock.clone()>
87 <p>"This shared location is protected with a password."</p>
88 <label>
89 "Password"
90 <input type="password" autocomplete="current-password" required bind:value=password />
91 </label>
92 <button class="primary">"Show location"</button>
93 <p class="error">{move || error.get()}</p>
94 </form>
95 }
96 .into_any()
97 }
98 State::Open(_) => view! { <main><map::MapPage source=map::Source::Guest(auth()) /></main> }.into_any(),
99 State::Gone => {
100 view! { <p class="login">"This link is no longer valid. It may have expired or been deleted."</p> }
101 .into_any()
102 }
103 State::Failed(msg) => view! { <p class="login error">{msg}</p> }.into_any(),
104 }}
105 }
106}
107