diff --git a/src/backend/api/article.rs b/src/backend/api/article.rs index e6b1fd9..06193eb 100644 --- a/src/backend/api/article.rs +++ b/src/backend/api/article.rs @@ -68,7 +68,7 @@ pub(in crate::backend::api) async fn create_article( instance_id: local_instance.id, local: true, protected: false, - approved: !data.config.article_approval, + approved: !data.config.config.article_approval, }; let article = DbArticle::create(form, &data)?; @@ -214,7 +214,7 @@ pub(in crate::backend::api) async fn fork_article( instance_id: local_instance.id, local: true, protected: false, - approved: !data.config.article_approval, + approved: !data.config.config.article_approval, }; let article = DbArticle::create(form, &data)?; diff --git a/src/backend/api/mod.rs b/src/backend/api/mod.rs index 322bf11..9a78f1b 100644 --- a/src/backend/api/mod.rs +++ b/src/backend/api/mod.rs @@ -12,12 +12,12 @@ use crate::{ search_article, }, instance::{follow_instance, get_instance, resolve_instance}, - user::{get_user, login_user, logout_user, my_profile, register_user, validate}, + user::{get_user, login_user, logout_user, register_user, validate}, }, database::IbisData, error::MyResult, }, - common::{LocalUserView, AUTH_COOKIE}, + common::{LocalUserView, SiteView, AUTH_COOKIE}, }; use activitypub_federation::config::Data; use anyhow::anyhow; @@ -28,9 +28,12 @@ use axum::{ middleware::{self, Next}, response::Response, routing::{get, post}, + Extension, + Json, Router, }; use axum_extra::extract::CookieJar; +use axum_macros::debug_handler; use instance::list_remote_instances; use user::{count_notifications, list_notifications}; @@ -59,8 +62,8 @@ pub fn api_routes() -> Router<()> { .route("/user/notifications/count", get(count_notifications)) .route("/account/register", post(register_user)) .route("/account/login", post(login_user)) - .route("/account/my_profile", get(my_profile)) .route("/account/logout", get(logout_user)) + .route("/site", get(site_view)) .route_layer(middleware::from_fn(auth)) } @@ -73,8 +76,7 @@ async fn auth( let auth = request .headers() .get(AUTH_COOKIE) - .map(|h| h.to_str().ok()) - .flatten() + .and_then(|h| h.to_str().ok()) .or(jar.get(AUTH_COOKIE).map(|c| c.value())); if let Some(auth) = auth { @@ -92,3 +94,14 @@ fn check_is_admin(user: &LocalUserView) -> MyResult<()> { } Ok(()) } + +#[debug_handler] +pub(in crate::backend::api) async fn site_view( + data: Data, + user: Option>, +) -> MyResult> { + Ok(Json(SiteView { + my_profile: user.map(|u| u.0), + config: data.config.config.clone(), + })) +} diff --git a/src/backend/api/user.rs b/src/backend/api/user.rs index b7b8b7f..46daa6a 100644 --- a/src/backend/api/user.rs +++ b/src/backend/api/user.rs @@ -76,7 +76,7 @@ pub(in crate::backend::api) async fn register_user( jar: CookieJar, Form(form): Form, ) -> MyResult<(CookieJar, Json)> { - if !data.config.registration_open { + if !data.config.config.registration_open { return Err(anyhow!("Registration is closed").into()); } let user = DbPerson::create_local(form.username, form.password, false, &data)?; @@ -121,19 +121,6 @@ fn create_cookie(jwt: String, data: &Data) -> Cookie<'static> { .build() } -#[debug_handler] -pub(in crate::backend::api) async fn my_profile( - data: Data, - jar: CookieJar, -) -> MyResult> { - let jwt = jar.get(AUTH_COOKIE).map(|c| c.value()); - if let Some(jwt) = jwt { - Ok(Json(validate(jwt, &data).await?)) - } else { - Err(anyhow!("invalid/missing auth").into()) - } -} - #[debug_handler] pub(in crate::backend::api) async fn logout_user( data: Data, diff --git a/src/backend/config.rs b/src/backend/config.rs index 5015e3a..a2c2514 100644 --- a/src/backend/config.rs +++ b/src/backend/config.rs @@ -1,4 +1,4 @@ -use crate::backend::error::MyResult; +use crate::{backend::error::MyResult, common::SharedConfig}; use config::Config; use doku::Document; use serde::Deserialize; @@ -10,17 +10,10 @@ use smart_default::SmartDefault; pub struct IbisConfig { /// Details about the PostgreSQL database connection pub database: IbisConfigDatabase, - /// Whether users can create new accounts - #[default = true] - #[doku(example = "true")] - pub registration_open: bool, - /// Whether admins need to approve new articles - #[default = false] - #[doku(example = "false")] - pub article_approval: bool, /// Details of the initial admin account pub setup: IbisConfigSetup, pub federation: IbisConfigFederation, + pub config: SharedConfig, } impl IbisConfig { diff --git a/src/backend/database/instance.rs b/src/backend/database/instance.rs index 44f7cf9..be1d31f 100644 --- a/src/backend/database/instance.rs +++ b/src/backend/database/instance.rs @@ -86,7 +86,6 @@ impl DbInstance { Ok(InstanceView { instance, followers, - registration_open: data.config.registration_open, }) } diff --git a/src/backend/mod.rs b/src/backend/mod.rs index 0bdcd3c..2b2cd12 100644 --- a/src/backend/mod.rs +++ b/src/backend/mod.rs @@ -136,7 +136,7 @@ async fn leptos_routes_handler( let cookie = jar.get(AUTH_COOKIE).map(|c| c.value().to_string()); provide_context(Auth(cookie)); }, - move || view! { }, + move || view! { }, ); handler(req).await.into_response() diff --git a/src/backend/nodeinfo.rs b/src/backend/nodeinfo.rs index a20a419..6e35127 100644 --- a/src/backend/nodeinfo.rs +++ b/src/backend/nodeinfo.rs @@ -34,7 +34,7 @@ async fn node_info(data: Data) -> MyResult> { version: env!("CARGO_PKG_VERSION").to_string(), }, protocols: vec!["activitypub".to_string()], - open_registrations: data.config.registration_open, + open_registrations: data.config.config.registration_open, })) } diff --git a/src/common/mod.rs b/src/common/mod.rs index f5ef24f..15df071 100644 --- a/src/common/mod.rs +++ b/src/common/mod.rs @@ -6,6 +6,7 @@ use chrono::{DateTime, Utc}; use newtypes::{ArticleId, ConflictId, EditId, InstanceId, PersonId}; use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256}; +use smart_default::SmartDefault; use url::Url; use uuid::Uuid; #[cfg(feature = "ssr")] @@ -17,6 +18,7 @@ use { }, activitypub_federation::fetch::{collection_id::CollectionId, object_id::ObjectId}, diesel::{Identifiable, Queryable, Selectable}, + doku::Document, }; pub const MAIN_PAGE_NAME: &str = "Main_Page"; @@ -311,7 +313,6 @@ impl DbInstance { pub struct InstanceView { pub instance: DbInstance, pub followers: Vec, - pub registration_open: bool, } #[derive(Deserialize, Serialize, Clone, Debug)] @@ -320,6 +321,30 @@ pub struct GetUserForm { pub domain: Option, } +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, SmartDefault)] +#[serde(default)] +#[serde(deny_unknown_fields)] +#[cfg_attr(feature = "ssr", derive(Queryable, Document))] +#[cfg_attr(feature = "ssr", diesel(check_for_backend(diesel::pg::Pg)))] +pub struct SharedConfig { + /// Whether users can create new accounts + #[default = true] + #[cfg_attr(feature = "ssr", doku(example = "true"))] + pub registration_open: bool, + /// Whether admins need to approve new articles + #[default = false] + #[cfg_attr(feature = "ssr", doku(example = "false"))] + pub article_approval: bool, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Default)] +#[cfg_attr(feature = "ssr", derive(Queryable))] +#[cfg_attr(feature = "ssr", diesel(check_for_backend(diesel::pg::Pg)))] +pub struct SiteView { + pub my_profile: Option, + pub config: SharedConfig, +} + #[test] fn test_edit_versions() { let default = EditVersion::default(); diff --git a/src/frontend/api.rs b/src/frontend/api.rs index e29784f..3fc5d06 100644 --- a/src/frontend/api.rs +++ b/src/frontend/api.rs @@ -24,6 +24,7 @@ use crate::{ RegisterUserForm, ResolveObject, SearchArticleForm, + SiteView, }, frontend::error::MyResult, }; @@ -210,10 +211,8 @@ impl ApiClient { } } - pub async fn my_profile(&self) -> MyResult { - let req = self - .client - .get(self.request_endpoint("/api/v1/account/my_profile")); + pub async fn site(&self) -> MyResult { + let req = self.client.get(self.request_endpoint("/api/v1/site")); handle_json_res(req).await } diff --git a/src/frontend/app.rs b/src/frontend/app.rs index 605d09b..b966240 100644 --- a/src/frontend/app.rs +++ b/src/frontend/app.rs @@ -1,5 +1,5 @@ use crate::{ - common::LocalUserView, + common::SiteView, frontend::{ api::CLIENT, components::nav::Nav, @@ -23,60 +23,51 @@ use crate::{ }, }, }; -use leptos::{ - component, - create_local_resource, - create_rw_signal, - expect_context, - provide_context, - use_context, - view, - DynAttrs, - IntoView, - RwSignal, - SignalGet, - SignalUpdate, -}; +use leptos::*; use leptos_meta::{provide_meta_context, *}; use leptos_router::{Route, Router, Routes}; -// https://book.leptos.dev/15_global_state.html -#[derive(Clone)] -pub struct GlobalState { - pub(crate) my_profile: Option, +pub fn site() -> Resource<(), SiteView> { + use_context::>().unwrap() } -impl GlobalState { - pub fn update_my_profile() { - create_local_resource( - move || (), - |_| async move { - let my_profile = CLIENT.my_profile().await.ok(); - expect_context::>() - .update(|state| state.my_profile = my_profile.clone()); - }, - ); - } +pub fn is_logged_in() -> bool { + site().with_default(|site| site.my_profile.is_some()) +} +pub fn is_admin() -> bool { + site().with_default(|site| { + site.my_profile + .as_ref() + .map(|p| p.local_user.admin) + .unwrap_or(false) + }) +} - pub fn is_admin() -> fn() -> bool { - move || { - use_context::>() - .expect("global state is provided") - .get() - .my_profile - .map(|p| p.local_user.admin) - .unwrap_or(false) - } +pub trait DefaultResource { + fn with_default(&self, f: impl FnOnce(&T) -> O) -> O; +} + +impl DefaultResource for Resource<(), T> { + fn with_default(&self, f: impl FnOnce(&T) -> O) -> O { + self.with(|x| match x { + Some(x) => f(x), + None => f(&T::default()), + }) } } #[component] pub fn App() -> impl IntoView { + // TODO: should create_resource() but then things break + let site_resource = create_local_resource( + move || (), + |_| async move { + let site = CLIENT.site().await.unwrap(); + site + }, + ); + provide_context(site_resource); provide_meta_context(); - let global_state = GlobalState { my_profile: None }; - // Load user profile in case we are already logged in - GlobalState::update_my_profile(); - provide_context(create_rw_signal(global_state)); let darkmode = DarkMode::init(); provide_context(darkmode.clone()); diff --git a/src/frontend/components/article_nav.rs b/src/frontend/components/article_nav.rs index bcd85e1..eb2fe0e 100644 --- a/src/frontend/components/article_nav.rs +++ b/src/frontend/components/article_nav.rs @@ -2,7 +2,7 @@ use crate::{ common::{validation::can_edit_article, ArticleView, GetInstance}, frontend::{ api::CLIENT, - app::GlobalState, + app::{is_admin, is_logged_in}, article_link, article_title, components::instance_follow_button::InstanceFollowButton, @@ -32,7 +32,7 @@ pub fn ArticleNav( .get() .map(|article_| { let title = article_title(&article_.article); - let instance = create_local_resource( + let instance = create_resource( move || article_.article.instance_id, move |instance_id| async move { let form = GetInstance { @@ -41,7 +41,6 @@ pub fn ArticleNav( CLIENT.get_instance(&form).await.unwrap() }, ); - let global_state = use_context::>().unwrap(); let article_link = article_link(&article_.article); let article_link_ = article_link.clone(); let protected = article_.article.protected; @@ -54,24 +53,14 @@ pub fn ArticleNav( "History" "Edit" - + impl IntoView { - let global_state = use_context::>().unwrap(); let follow_action = create_action(move |instance_id: &InstanceId| { let instance_id = *instance_id; async move { let form = FollowInstance { id: instance_id }; CLIENT.follow_instance(form).await.unwrap(); - GlobalState::update_my_profile(); + site().refetch(); } }); - let is_following = global_state - .get_untracked() - .my_profile - .map(|p| p.following.contains(&instance)) + let is_following = site() + .with_default(|site| { + site.clone() + .my_profile + .map(|p| p.following.contains(&instance)) + }) .unwrap_or(false); let follow_text = if is_following { "Following instance" diff --git a/src/frontend/components/nav.rs b/src/frontend/components/nav.rs index 450e6bb..f0bab39 100644 --- a/src/frontend/components/nav.rs +++ b/src/frontend/components/nav.rs @@ -1,24 +1,17 @@ -use crate::frontend::{api::CLIENT, app::GlobalState, dark_mode::DarkMode}; -use leptos::{component, use_context, view, IntoView, RwSignal, SignalWith, *}; +use crate::frontend::{ + api::CLIENT, + app::{is_logged_in, site, DefaultResource}, + dark_mode::DarkMode, +}; +use leptos::{component, view, IntoView, *}; use leptos_router::*; #[component] pub fn Nav() -> impl IntoView { - let global_state = use_context::>().unwrap(); let logout_action = create_action(move |_| async move { CLIENT.logout().await.unwrap(); - GlobalState::update_my_profile(); + site().refetch(); }); - let registration_open = create_local_resource( - || (), - move |_| async move { - CLIENT - .get_local_instance() - .await - .map(|i| i.registration_open) - .unwrap_or_default() - }, - ); let notification_count = create_resource( || (), move |_| async move { CLIENT.notifications_count().await.unwrap_or_default() }, @@ -56,19 +49,21 @@ pub fn Nav() -> impl IntoView {
  • "Articles"
  • - -
  • - "Create Article" -
  • -
  • - - "Notifications " - - {notification_count} - - -
  • -
    + + +
  • + "Create Article" +
  • +
  • + + "Notifications " + + {move || notification_count.get()} + + +
  • +
    +
  • impl IntoView {
  • - - "Login" - - + + - "Register" + "Login" - + +
  • + "Register" +
  • +
    + } } - } - > + > - { - let my_profile = global_state - .with(|state| state.my_profile.clone().unwrap()); - let profile_link = format!("/user/{}", my_profile.person.username); - view! { -

    - "Logged in as " - {my_profile.person.username} - -

    - + { + let my_profile = site() + .with_default(|site| site.clone().my_profile.unwrap()); + let profile_link = format!("/user/{}", my_profile.person.username); + view! { +

    + "Logged in as " + {my_profile.person.username} + +

    + + } } - } -
    +
    +