feat(web): combine token and username in auth ctx

This commit is contained in:
əlemi 2024-05-01 18:22:25 +02:00
parent 4527ff884e
commit 62b426acc1
Signed by: alemi
GPG key ID: A4895B84D311642C
6 changed files with 73 additions and 69 deletions

View file

@ -7,17 +7,19 @@ use leptos_use::{use_cookie, use_cookie_with_options, utils::FromToStringCodec,
#[component] #[component]
pub fn App() -> impl IntoView { pub fn App() -> impl IntoView {
let (auth, set_auth) = use_cookie_with_options::<String, FromToStringCodec>( let (token, set_token) = use_cookie_with_options::<String, FromToStringCodec>(
"token", "token",
UseCookieOptions::default() UseCookieOptions::default()
.max_age(1000 * 60 * 60 * 6) .max_age(1000 * 60 * 60 * 6)
); );
let (username, set_username) = use_cookie::<String, FromToStringCodec>("username"); let (user, set_username) = use_cookie::<String, FromToStringCodec>("username");
let auth = Auth { token, user };
provide_context(auth); provide_context(auth);
let home_tl = Timeline::new(format!("{URL_BASE}/users/{}/inbox/page", username.get().unwrap_or_default())); let home_tl = Timeline::new(format!("{URL_BASE}/users/{}/inbox/page", auth.username()));
let server_tl = Timeline::new(format!("{URL_BASE}/inbox/page")); let server_tl = Timeline::new(format!("{URL_BASE}/inbox/page"));
let user_tl = Timeline::new(format!("{URL_BASE}/users/{}/outbox/page", username.get().unwrap_or_default())); let user_tl = Timeline::new(format!("{URL_BASE}/users/{}/outbox/page", auth.username()));
let context_tl = Timeline::new(format!("{URL_BASE}/outbox/page")); let context_tl = Timeline::new(format!("{URL_BASE}/outbox/page"));
let reply_controls = ReplyControls::default(); let reply_controls = ReplyControls::default();
@ -34,7 +36,7 @@ pub fn App() -> impl IntoView {
} }
}); });
if auth.get().is_some() { if auth.present() {
spawn_local(async move { spawn_local(async move {
if let Err(e) = home_tl.more(auth).await { if let Err(e) = home_tl.more(auth).await {
tracing::error!("error populating timeline: {e}"); tracing::error!("error populating timeline: {e}");
@ -56,10 +58,8 @@ pub fn App() -> impl IntoView {
<div class="two-col" > <div class="two-col" >
<div class="col-side sticky pb-s" class:hidden=move || menu.get() > <div class="col-side sticky pb-s" class:hidden=move || menu.get() >
<LoginBox <LoginBox
token_tx=set_auth token_tx=set_token
token=auth
username_tx=set_username username_tx=set_username
username=username
home_tl=home_tl home_tl=home_tl
server_tl=server_tl server_tl=server_tl
/> />
@ -67,9 +67,9 @@ pub fn App() -> impl IntoView {
<Navigator /> <Navigator />
<hr class="mt-1 mb-1" /> <hr class="mt-1 mb-1" />
{move || if advanced.get() { view! { {move || if advanced.get() { view! {
<AdvancedPostBox username=username advanced=set_advanced/> <AdvancedPostBox advanced=set_advanced/>
}} else { view! { }} else { view! {
<PostBox username=username advanced=set_advanced/> <PostBox advanced=set_advanced/>
}}} }}}
</div> </div>
<div class="col-main" class:w-100=move || menu.get() > <div class="col-main" class:w-100=move || menu.get() >
@ -91,7 +91,7 @@ pub fn App() -> impl IntoView {
<main> <main>
<Routes> <Routes>
<Route path="/web" view=move || <Route path="/web" view=move ||
if auth.get().is_some() { if auth.present() {
view! { <Redirect path="/web/home" /> } view! { <Redirect path="/web/home" /> }
} else { } else {
view! { <Redirect path="/web/server" /> } view! { <Redirect path="/web/server" /> }

View file

@ -2,40 +2,46 @@ use leptos::*;
use crate::prelude::*; use crate::prelude::*;
pub type Auth = Signal<Option<String>>;
pub trait AuthToken { pub trait AuthToken {
fn present(&self) -> bool; fn present(&self) -> bool;
fn token(&self) -> String; fn token(&self) -> String;
fn username(&self) -> String;
fn outbox(&self) -> String;
}
#[derive(Debug, Clone, Copy)]
pub struct Auth {
pub token: Signal<Option<String>>,
pub user: Signal<Option<String>>,
} }
#[component] #[component]
pub fn LoginBox( pub fn LoginBox(
token_tx: WriteSignal<Option<String>>, token_tx: WriteSignal<Option<String>>,
token: Signal<Option<String>>,
username: Signal<Option<String>>,
username_tx: WriteSignal<Option<String>>, username_tx: WriteSignal<Option<String>>,
home_tl: Timeline, home_tl: Timeline,
server_tl: Timeline, server_tl: Timeline,
) -> impl IntoView { ) -> impl IntoView {
let auth = use_context::<Auth>().expect("missing auth context");
let username_ref: NodeRef<html::Input> = create_node_ref(); let username_ref: NodeRef<html::Input> = create_node_ref();
let password_ref: NodeRef<html::Input> = create_node_ref(); let password_ref: NodeRef<html::Input> = create_node_ref();
view! { view! {
<div> <div>
<div class="w-100" class:hidden=move || !token.present() > <div class="w-100" class:hidden=move || !auth.present() >
"hi "<a href={move || Uri::web(FetchKind::User, &username.get().unwrap_or_default() )} >{move || username.get().unwrap_or_default() }</a> "hi "<a href={move || Uri::web(FetchKind::User, &auth.username() )} >{move || auth.username() }</a>
<input style="float:right" type="submit" value="logout" on:click=move |_| { <input style="float:right" type="submit" value="logout" on:click=move |_| {
token_tx.set(None); token_tx.set(None);
home_tl.reset(format!("{URL_BASE}/outbox/page")); home_tl.reset(format!("{URL_BASE}/outbox/page"));
server_tl.reset(format!("{URL_BASE}/inbox/page")); server_tl.reset(format!("{URL_BASE}/inbox/page"));
spawn_local(async move { spawn_local(async move {
if let Err(e) = server_tl.more(token).await { if let Err(e) = server_tl.more(auth).await {
logging::error!("failed refreshing server timeline: {e}"); logging::error!("failed refreshing server timeline: {e}");
} }
}); });
} /> } />
</div> </div>
<div class:hidden=move || token.present() > <div class:hidden=move || auth.present() >
<form on:submit=move|ev| { <form on:submit=move|ev| {
ev.prevent_default(); ev.prevent_default();
logging::log!("logging in..."); logging::log!("logging in...");
@ -48,26 +54,26 @@ pub fn LoginBox(
.send() .send()
.await .await
else { if let Some(rf) = password_ref.get() { rf.set_value("") }; return }; else { if let Some(rf) = password_ref.get() { rf.set_value("") }; return };
let Ok(auth) = res let Ok(auth_response) = res
.json::<AuthResponse>() .json::<AuthResponse>()
.await .await
else { if let Some(rf) = password_ref.get() { rf.set_value("") }; return }; else { if let Some(rf) = password_ref.get() { rf.set_value("") }; return };
logging::log!("logged in until {}", auth.expires); logging::log!("logged in until {}", auth_response.expires);
// update our username and token cookies // update our username and token cookies
let username = auth.user.split('/').last().unwrap_or_default().to_string(); let username = auth_response.user.split('/').last().unwrap_or_default().to_string();
username_tx.set(Some(username.clone())); username_tx.set(Some(username.clone()));
token_tx.set(Some(auth.token)); token_tx.set(Some(auth_response.token));
// reset home feed and point it to our user's inbox // reset home feed and point it to our user's inbox
home_tl.reset(format!("{URL_BASE}/users/{}/inbox/page", username)); home_tl.reset(format!("{URL_BASE}/users/{}/inbox/page", username));
spawn_local(async move { spawn_local(async move {
if let Err(e) = home_tl.more(token).await { if let Err(e) = home_tl.more(auth).await {
tracing::error!("failed refreshing home timeline: {e}"); tracing::error!("failed refreshing home timeline: {e}");
} }
}); });
// reset server feed: there may be more content now that we're authed // reset server feed: there may be more content now that we're authed
server_tl.reset(format!("{URL_BASE}/inbox/page")); server_tl.reset(format!("{URL_BASE}/inbox/page"));
spawn_local(async move { spawn_local(async move {
if let Err(e) = server_tl.more(token).await { if let Err(e) = server_tl.more(auth).await {
tracing::error!("failed refreshing server timeline: {e}"); tracing::error!("failed refreshing server timeline: {e}");
} }
}); });
@ -97,17 +103,20 @@ struct AuthResponse {
expires: chrono::DateTime<chrono::Utc>, expires: chrono::DateTime<chrono::Utc>,
} }
impl AuthToken for Signal<Option<String>> { impl AuthToken for Auth {
fn token(&self) -> String { fn token(&self) -> String {
match self.get() { self.token.get().unwrap_or_default()
None => String::new(),
Some(x) => x.clone(),
} }
fn username(&self) -> String {
self.user.get().unwrap_or_default()
} }
fn present(&self) -> bool { fn present(&self) -> bool {
match self.get() { self.token.get().map_or(false, |x| !x.is_empty())
None => false, }
Some(x) => !x.is_empty(),
} fn outbox(&self) -> String {
format!("{URL_BASE}/users/{}/outbox", self.user.get().unwrap_or_default())
} }
} }

View file

@ -95,7 +95,7 @@ pub fn Object(object: crate::Object) -> impl IntoView {
let likes = object.audience().get() let likes = object.audience().get()
.map_or(0, |x| x.total_items().unwrap_or(0)); .map_or(0, |x| x.total_items().unwrap_or(0));
let already_liked = object.audience().get() let already_liked = object.audience().get()
.map_or(false, |x| !x.ordered_items().is_empty()); .map_or(false, |x| !x.ordered_items().is_empty()); // TODO check if contains my uid
let attachments_padding = if object.attachment().is_empty() { let attachments_padding = if object.attachment().is_empty() {
None None
} else { } else {
@ -162,18 +162,17 @@ pub fn LikeButton(
view! { view! {
<span <span
class:emoji=clicked class:emoji=clicked
class:emoji-btn=move || auth.get().is_some() class:emoji-btn=move || auth.present()
class:cursor=move || clicked.get() && auth.get().is_some() class:cursor=move || clicked.get() && auth.present()
class="ml-2" class="ml-2"
on:click=move |_ev| { on:click=move |_ev| {
if auth.get().is_none() { return; } if !auth.present() { return; }
if !clicked.get() { return; } if !clicked.get() { return; }
let target_url = format!("{URL_BASE}/users/test/outbox");
let to = apb::Node::links(vec![author.to_string()]); let to = apb::Node::links(vec![author.to_string()]);
let cc = if private { apb::Node::Empty } else { let cc = if private { apb::Node::Empty } else {
apb::Node::links(vec![ apb::Node::links(vec![
apb::target::PUBLIC.to_string(), apb::target::PUBLIC.to_string(),
format!("{URL_BASE}/users/test/followers") format!("{URL_BASE}/users/{}/followers", auth.username())
]) ])
}; };
let payload = serde_json::Value::Object(serde_json::Map::default()) let payload = serde_json::Value::Object(serde_json::Map::default())
@ -182,7 +181,7 @@ pub fn LikeButton(
.set_to(to) .set_to(to)
.set_cc(cc); .set_cc(cc);
spawn_local(async move { spawn_local(async move {
match Http::post(&target_url, &payload, auth).await { match Http::post(&auth.outbox(), &payload, auth).await {
Ok(()) => { Ok(()) => {
set_clicked.set(false); set_clicked.set(false);
set_count.set(count.get() + 1); set_count.set(count.get() + 1);
@ -212,10 +211,10 @@ pub fn ReplyButton(n: u64, target: String) -> impl IntoView {
<span <span
class:emoji=move || !reply.reply_to.get().map_or(false, |x| x == _target) class:emoji=move || !reply.reply_to.get().map_or(false, |x| x == _target)
// TODO can we merge these two classes conditions? // TODO can we merge these two classes conditions?
class:emoji-btn=move || auth.get().is_some() class:emoji-btn=move || auth.present()
class:cursor=move || auth.get().is_some() class:cursor=move || auth.present()
class="ml-2" class="ml-2"
on:click=move |_ev| if auth.get().is_some() { reply.reply(&target) } on:click=move |_ev| if auth.present() { reply.reply(&target) }
> >
{comments} {comments}
" 📨" " 📨"
@ -231,23 +230,22 @@ pub fn RepostButton(n: u64, target: String) -> impl IntoView {
view! { view! {
<span <span
class:emoji=clicked class:emoji=clicked
class:emoji-btn=move || auth.get().is_some() class:emoji-btn=move || auth.present()
class:cursor=move || clicked.get() && auth.get().is_some() class:cursor=move || clicked.get() && auth.present()
class="ml-2" class="ml-2"
on:click=move |_ev| { on:click=move |_ev| {
if auth.get().is_none() { return; } if !auth.present() { return; }
if !clicked.get() { return; } if !clicked.get() { return; }
set_clicked.set(false); set_clicked.set(false);
let target_url = format!("{URL_BASE}/users/test/outbox");
let to = apb::Node::links(vec![apb::target::PUBLIC.to_string()]); let to = apb::Node::links(vec![apb::target::PUBLIC.to_string()]);
let cc = apb::Node::links(vec![format!("{URL_BASE}/users/test/followers")]); let cc = apb::Node::links(vec![format!("{URL_BASE}/users/{}/followers", auth.username())]);
let payload = serde_json::Value::Object(serde_json::Map::default()) let payload = serde_json::Value::Object(serde_json::Map::default())
.set_activity_type(Some(apb::ActivityType::Announce)) .set_activity_type(Some(apb::ActivityType::Announce))
.set_object(apb::Node::link(target.clone())) .set_object(apb::Node::link(target.clone()))
.set_to(to) .set_to(to)
.set_cc(cc); .set_cc(cc);
spawn_local(async move { spawn_local(async move {
match Http::post(&target_url, &payload, auth).await { match Http::post(&auth.outbox(), &payload, auth).await {
Ok(()) => set_count.set(count.get() + 1), Ok(()) => set_count.set(count.get() + 1),
Err(e) => tracing::error!("failed sending like: {e}"), Err(e) => tracing::error!("failed sending like: {e}"),
} }

View file

@ -27,14 +27,14 @@ impl Timeline {
self.over.set(false); self.over.set(false);
} }
pub async fn more(&self, auth: Signal<Option<String>>) -> reqwest::Result<()> { pub async fn more(&self, auth: Auth) -> reqwest::Result<()> {
self.loading.set(true); self.loading.set(true);
let res = self.more_inner(auth).await; let res = self.more_inner(auth).await;
self.loading.set(false); self.loading.set(false);
res res
} }
async fn more_inner(&self, auth: Signal<Option<String>>) -> reqwest::Result<()> { async fn more_inner(&self, auth: Auth) -> reqwest::Result<()> {
use apb::{Collection, CollectionPage}; use apb::{Collection, CollectionPage};
let feed_url = self.next.get(); let feed_url = self.next.get();
@ -202,10 +202,7 @@ pub fn TimelineFeed(tl: Timeline) -> impl IntoView {
} }
} }
async fn process_activities( async fn process_activities(activities: Vec<serde_json::Value>, auth: Auth) -> Vec<String> {
activities: Vec<serde_json::Value>,
auth: Signal<Option<String>>,
) -> Vec<String> {
use apb::ActivityMut; use apb::ActivityMut;
let mut sub_tasks : Vec<Pin<Box<dyn futures::Future<Output = ()>>>> = Vec::new(); let mut sub_tasks : Vec<Pin<Box<dyn futures::Future<Output = ()>>>> = Vec::new();
let mut gonna_fetch = BTreeSet::new(); let mut gonna_fetch = BTreeSet::new();
@ -269,14 +266,14 @@ async fn process_activities(
out out
} }
async fn fetch_and_update(kind: FetchKind, id: String, auth: Signal<Option<String>>) { async fn fetch_and_update(kind: FetchKind, id: String, auth: Auth) {
match Http::fetch(&Uri::api(kind, &id, false), auth).await { match Http::fetch(&Uri::api(kind, &id, false), auth).await {
Ok(data) => CACHE.put(id, Arc::new(data)), Ok(data) => CACHE.put(id, Arc::new(data)),
Err(e) => console_warn(&format!("could not fetch '{id}': {e}")), Err(e) => console_warn(&format!("could not fetch '{id}': {e}")),
} }
} }
async fn fetch_and_update_with_user(kind: FetchKind, id: String, auth: Signal<Option<String>>) { async fn fetch_and_update_with_user(kind: FetchKind, id: String, auth: Auth) {
fetch_and_update(kind.clone(), id.clone(), auth).await; fetch_and_update(kind.clone(), id.clone(), auth).await;
if let Some(obj) = CACHE.get(&id) { if let Some(obj) = CACHE.get(&id) {
if let Some(actor_id) = match kind { if let Some(actor_id) = match kind {

View file

@ -40,7 +40,7 @@ fn post_author(post_id: &str) -> Option<crate::Object> {
} }
#[component] #[component]
pub fn PostBox(username: Signal<Option<String>>, advanced: WriteSignal<bool>) -> impl IntoView { pub fn PostBox(advanced: WriteSignal<bool>) -> impl IntoView {
let auth = use_context::<Auth>().expect("missing auth context"); let auth = use_context::<Auth>().expect("missing auth context");
let reply = use_context::<ReplyControls>().expect("missing reply controls"); let reply = use_context::<ReplyControls>().expect("missing reply controls");
let (posting, set_posting) = create_signal(false); let (posting, set_posting) = create_signal(false);
@ -89,9 +89,9 @@ pub fn PostBox(username: Signal<Option<String>>, advanced: WriteSignal<bool>) ->
let summary = summary_ref.get().map(|x| x.value()); let summary = summary_ref.get().map(|x| x.value());
let content = content_ref.get().map(|x| x.value()).unwrap_or_default(); let content = content_ref.get().map(|x| x.value()).unwrap_or_default();
let (to, cc) = if get_checked(public_ref) { let (to, cc) = if get_checked(public_ref) {
(apb::Node::links(vec![apb::target::PUBLIC.to_string()]), apb::Node::links(vec![format!("{URL_BASE}/users/{}/followers", username.get().unwrap_or_default())])) (apb::Node::links(vec![apb::target::PUBLIC.to_string()]), apb::Node::links(vec![format!("{URL_BASE}/users/{}/followers", auth.username())]))
} else if get_checked(followers_ref) { } else if get_checked(followers_ref) {
(apb::Node::links(vec![format!("{URL_BASE}/users/{}/followers", username.get().unwrap_or_default())]), apb::Node::Empty) (apb::Node::links(vec![format!("{URL_BASE}/users/{}/followers", auth.username())]), apb::Node::Empty)
} else if get_checked(private_ref) { } else if get_checked(private_ref) {
(apb::Node::links(vec![]), apb::Node::Empty) (apb::Node::links(vec![]), apb::Node::Empty)
} else { } else {
@ -105,8 +105,7 @@ pub fn PostBox(username: Signal<Option<String>>, advanced: WriteSignal<bool>) ->
.set_in_reply_to(apb::Node::maybe_link(reply.reply_to.get())) .set_in_reply_to(apb::Node::maybe_link(reply.reply_to.get()))
.set_to(to) .set_to(to)
.set_cc(cc); .set_cc(cc);
let target_url = format!("{URL_BASE}/users/test/outbox"); match Http::post(&auth.outbox(), &payload, auth).await {
match Http::post(&target_url, &payload, auth).await {
Err(e) => set_error.set(Some(e.to_string())), Err(e) => set_error.set(Some(e.to_string())),
Ok(()) => { Ok(()) => {
set_error.set(None); set_error.set(None);
@ -134,7 +133,7 @@ pub fn PostBox(username: Signal<Option<String>>, advanced: WriteSignal<bool>) ->
} }
#[component] #[component]
pub fn AdvancedPostBox(username: Signal<Option<String>>, advanced: WriteSignal<bool>) -> impl IntoView { pub fn AdvancedPostBox(advanced: WriteSignal<bool>) -> impl IntoView {
let auth = use_context::<Auth>().expect("missing auth context"); let auth = use_context::<Auth>().expect("missing auth context");
let (posting, set_posting) = create_signal(false); let (posting, set_posting) = create_signal(false);
let (error, set_error) = create_signal(None); let (error, set_error) = create_signal(None);
@ -208,7 +207,7 @@ pub fn AdvancedPostBox(username: Signal<Option<String>>, advanced: WriteSignal<b
<td class="w-66"><input class="w-100" type="text" node_ref=bto_ref title="bto" placeholder="bto" /></td> <td class="w-66"><input class="w-100" type="text" node_ref=bto_ref title="bto" placeholder="bto" /></td>
</tr> </tr>
<tr> <tr>
<td class="w-33"><input class="w-100" type="text" node_ref=cc_ref title="cc" placeholder="cc" value=format!("{URL_BASE}/users/{}/followers", username.get().unwrap_or_default()) /></td> <td class="w-33"><input class="w-100" type="text" node_ref=cc_ref title="cc" placeholder="cc" value=format!("{URL_BASE}/users/{}/followers", auth.username()) /></td>
<td class="w-33"><input class="w-100" type="text" node_ref=bcc_ref title="bcc" placeholder="bcc" /></td> <td class="w-33"><input class="w-100" type="text" node_ref=bcc_ref title="bcc" placeholder="bcc" /></td>
</tr> </tr>
</table> </table>
@ -252,7 +251,7 @@ pub fn AdvancedPostBox(username: Signal<Option<String>>, advanced: WriteSignal<b
apb::Node::maybe_link(object_id) apb::Node::maybe_link(object_id)
} }
); );
let target_url = format!("{URL_BASE}/users/{}/outbox", username.get().unwrap_or_default()); let target_url = format!("{URL_BASE}/users/{}/outbox", auth.username());
match Http::post(&target_url, &payload, auth).await { match Http::post(&target_url, &payload, auth).await {
Err(e) => set_error.set(Some(e.to_string())), Err(e) => set_error.set(Some(e.to_string())),
Ok(()) => set_error.set(None), Ok(()) => set_error.set(None),

View file

@ -14,6 +14,7 @@ pub const URL_SENSITIVE: &str = "https://cdn.alemi.dev/social/nsfw.png";
pub const NAME: &str = "μ"; pub const NAME: &str = "μ";
use std::sync::Arc; use std::sync::Arc;
use auth::Auth;
@ -81,14 +82,14 @@ impl Http {
method: reqwest::Method, method: reqwest::Method,
url: &str, url: &str,
data: Option<&T>, data: Option<&T>,
token: leptos::Signal<Option<String>> auth: Auth,
) -> reqwest::Result<reqwest::Response> { ) -> reqwest::Result<reqwest::Response> {
use leptos::SignalGet; use leptos::SignalGet;
let mut req = reqwest::Client::new() let mut req = reqwest::Client::new()
.request(method, url); .request(method, url);
if let Some(auth) = token.get() { if let Some(auth) = auth.token.get().filter(|x| !x.is_empty()) {
req = req.header("Authorization", format!("Bearer {}", auth)); req = req.header("Authorization", format!("Bearer {}", auth));
} }
@ -101,14 +102,14 @@ impl Http {
.error_for_status() .error_for_status()
} }
pub async fn fetch<T: serde::de::DeserializeOwned>(url: &str, token: leptos::Signal<Option<String>>) -> reqwest::Result<T> { pub async fn fetch<T: serde::de::DeserializeOwned>(url: &str, token: Auth) -> reqwest::Result<T> {
Self::request::<()>(reqwest::Method::GET, url, None, token) Self::request::<()>(reqwest::Method::GET, url, None, token)
.await? .await?
.json::<T>() .json::<T>()
.await .await
} }
pub async fn post<T: serde::ser::Serialize>(url: &str, data: &T, token: leptos::Signal<Option<String>>) -> reqwest::Result<()> { pub async fn post<T: serde::ser::Serialize>(url: &str, data: &T, token: Auth) -> reqwest::Result<()> {
Self::request(reqwest::Method::POST, url, Some(data), token) Self::request(reqwest::Method::POST, url, Some(data), token)
.await?; .await?;
Ok(()) Ok(())