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