add basic instance details page (untested)

This commit is contained in:
Felix Ableitner 2024-02-01 16:46:36 +01:00
parent 862297638f
commit f2180e5e0b
16 changed files with 91 additions and 52 deletions

View File

@ -1,6 +1,7 @@
create table instance ( create table instance (
id serial primary key, id serial primary key,
ap_id varchar(255) not null unique, ap_id varchar(255) not null unique,
description text,
inbox_url text not null, inbox_url text not null,
articles_url varchar(255) not null unique, articles_url varchar(255) not null unique,
public_key text not null, public_key text not null,
@ -23,7 +24,8 @@ create table person (
create table local_user ( create table local_user (
id serial primary key, id serial primary key,
password_encrypted text not null, password_encrypted text not null,
person_id int REFERENCES person ON UPDATE CASCADE ON DELETE CASCADE NOT NULL person_id int REFERENCES person ON UPDATE CASCADE ON DELETE CASCADE NOT NULL,
admin bool not null
); );
create table instance_follow ( create table instance_follow (

View File

@ -58,7 +58,8 @@ pub(in crate::backend::api) async fn register_user(
jar: CookieJar, jar: CookieJar,
Form(form): Form<RegisterUserData>, Form(form): Form<RegisterUserData>,
) -> MyResult<(CookieJar, Json<LocalUserView>)> { ) -> MyResult<(CookieJar, Json<LocalUserView>)> {
let user = DbPerson::create_local(form.username, form.password, &data)?; // TODO: make admin if its the first user account
let user = DbPerson::create_local(form.username, form.password, false, &data)?;
let token = generate_login_token(&user.local_user, &data)?; let token = generate_login_token(&user.local_user, &data)?;
let jar = jar.add(create_cookie(token, &data)); let jar = jar.add(create_cookie(token, &data));
Ok((jar, Json(user))) Ok((jar, Json(user)))

View File

@ -19,6 +19,7 @@ use std::sync::Mutex;
#[diesel(table_name = instance, check_for_backend(diesel::pg::Pg))] #[diesel(table_name = instance, check_for_backend(diesel::pg::Pg))]
pub struct DbInstanceForm { pub struct DbInstanceForm {
pub ap_id: ObjectId<DbInstance>, pub ap_id: ObjectId<DbInstance>,
pub description: Option<String>,
pub articles_url: CollectionId<DbArticleCollection>, pub articles_url: CollectionId<DbArticleCollection>,
pub inbox_url: String, pub inbox_url: String,
pub public_key: String, pub public_key: String,

View File

@ -43,6 +43,7 @@ diesel::table! {
id -> Int4, id -> Int4,
#[max_length = 255] #[max_length = 255]
ap_id -> Varchar, ap_id -> Varchar,
description -> Nullable<Text>,
inbox_url -> Text, inbox_url -> Text,
#[max_length = 255] #[max_length = 255]
articles_url -> Varchar, articles_url -> Varchar,
@ -74,6 +75,7 @@ diesel::table! {
id -> Int4, id -> Int4,
password_encrypted -> Text, password_encrypted -> Text,
person_id -> Int4, person_id -> Int4,
admin -> Bool,
} }
} }

View File

@ -19,6 +19,7 @@ use std::sync::Mutex;
pub struct DbLocalUserForm { pub struct DbLocalUserForm {
pub password_encrypted: String, pub password_encrypted: String,
pub person_id: i32, pub person_id: i32,
pub admin: bool,
} }
#[derive(Debug, Clone, Insertable, AsChangeset)] #[derive(Debug, Clone, Insertable, AsChangeset)]
@ -52,6 +53,7 @@ impl DbPerson {
pub fn create_local( pub fn create_local(
username: String, username: String,
password: String, password: String,
admin: bool,
data: &Data<MyDataHandle>, data: &Data<MyDataHandle>,
) -> MyResult<LocalUserView> { ) -> MyResult<LocalUserView> {
let mut conn = data.db_connection.lock().unwrap(); let mut conn = data.db_connection.lock().unwrap();
@ -76,6 +78,7 @@ impl DbPerson {
let local_user_form = DbLocalUserForm { let local_user_form = DbLocalUserForm {
password_encrypted: hash(password, DEFAULT_COST)?, password_encrypted: hash(password, DEFAULT_COST)?,
person_id: person.id, person_id: person.id,
admin,
}; };
let local_user = insert_into(local_user::table) let local_user = insert_into(local_user::table)

View File

@ -27,6 +27,7 @@ pub struct ApubInstance {
#[serde(rename = "type")] #[serde(rename = "type")]
kind: ServiceType, kind: ServiceType,
id: ObjectId<DbInstance>, id: ObjectId<DbInstance>,
content: Option<String>,
articles: CollectionId<DbArticleCollection>, articles: CollectionId<DbArticleCollection>,
inbox: Url, inbox: Url,
public_key: PublicKey, public_key: PublicKey,
@ -90,6 +91,7 @@ impl Object for DbInstance {
Ok(ApubInstance { Ok(ApubInstance {
kind: Default::default(), kind: Default::default(),
id: self.ap_id.clone(), id: self.ap_id.clone(),
content: self.description.clone(),
articles: self.articles_url.clone(), articles: self.articles_url.clone(),
inbox: Url::parse(&self.inbox_url)?, inbox: Url::parse(&self.inbox_url)?,
public_key: self.public_key(), public_key: self.public_key(),
@ -108,6 +110,7 @@ impl Object for DbInstance {
async fn from_json(json: Self::Kind, data: &Data<Self::DataType>) -> Result<Self, Self::Error> { async fn from_json(json: Self::Kind, data: &Data<Self::DataType>) -> Result<Self, Self::Error> {
let form = DbInstanceForm { let form = DbInstanceForm {
ap_id: json.id, ap_id: json.id,
description: json.content,
articles_url: json.articles, articles_url: json.articles,
inbox_url: json.inbox.to_string(), inbox_url: json.inbox.to_string(),
public_key: json.public_key.public_key_pem, public_key: json.public_key.public_key_pem,

View File

@ -65,6 +65,7 @@ pub async fn start(hostname: &str, database_url: &str) -> MyResult<()> {
let keypair = generate_actor_keypair()?; let keypair = generate_actor_keypair()?;
let form = DbInstanceForm { let form = DbInstanceForm {
ap_id, ap_id,
description: Some("New Ibis instance".to_string()),
articles_url, articles_url,
inbox_url, inbox_url,
public_key: keypair.public_key, public_key: keypair.public_key,

View File

@ -133,6 +133,7 @@ pub struct DbLocalUser {
pub id: i32, pub id: i32,
pub password_encrypted: String, pub password_encrypted: String,
pub person_id: i32, pub person_id: i32,
pub admin: bool,
} }
/// Federation related data from a local or remote user. /// Federation related data from a local or remote user.
@ -219,6 +220,7 @@ pub struct DbInstance {
pub ap_id: ObjectId<DbInstance>, pub ap_id: ObjectId<DbInstance>,
#[cfg(not(feature = "ssr"))] #[cfg(not(feature = "ssr"))]
pub ap_id: String, pub ap_id: String,
pub description: Option<String>,
#[cfg(feature = "ssr")] #[cfg(feature = "ssr")]
pub articles_url: CollectionId<DbArticleCollection>, pub articles_url: CollectionId<DbArticleCollection>,
#[cfg(not(feature = "ssr"))] #[cfg(not(feature = "ssr"))]

View File

@ -7,13 +7,13 @@ use crate::frontend::pages::article::history::ArticleHistory;
use crate::frontend::pages::article::list::ListArticles; use crate::frontend::pages::article::list::ListArticles;
use crate::frontend::pages::article::read::ReadArticle; use crate::frontend::pages::article::read::ReadArticle;
use crate::frontend::pages::diff::EditDiff; use crate::frontend::pages::diff::EditDiff;
use crate::frontend::pages::instance_details::InstanceDetails;
use crate::frontend::pages::login::Login; use crate::frontend::pages::login::Login;
use crate::frontend::pages::register::Register; use crate::frontend::pages::register::Register;
use crate::frontend::pages::search::Search; use crate::frontend::pages::search::Search;
use crate::frontend::pages::Page;
use leptos::{ use leptos::{
component, create_local_resource, create_rw_signal, expect_context, provide_context, component, create_local_resource, create_rw_signal, expect_context, provide_context,
use_context, view, IntoView, RwSignal, SignalGetUntracked, SignalUpdate, use_context, view, IntoView, RwSignal, SignalGet, SignalGetUntracked, SignalUpdate,
}; };
use leptos_meta::provide_meta_context; use leptos_meta::provide_meta_context;
use leptos_meta::*; use leptos_meta::*;
@ -47,6 +47,17 @@ impl GlobalState {
}, },
); );
} }
pub fn is_admin() -> fn() -> bool {
move || {
use_context::<RwSignal<GlobalState>>()
.expect("global state is provided")
.get()
.my_profile
.map(|p| p.local_user.admin)
.unwrap_or(false)
}
}
} }
#[component] #[component]
@ -71,15 +82,16 @@ pub fn App() -> impl IntoView {
<Nav /> <Nav />
<main> <main>
<Routes> <Routes>
<Route path={Page::Home.path()} view=ReadArticle/> <Route path="/" view=ReadArticle/>
<Route path="/article/:title" view=ReadArticle/> <Route path="/article/:title" view=ReadArticle/>
<Route path="/article/:title/edit" view=EditArticle/> <Route path="/article/:title/edit" view=EditArticle/>
<Route path="/article/:title/history" view=ArticleHistory/> <Route path="/article/:title/history" view=ArticleHistory/>
<Route path="/article/:title/diff/:hash" view=EditDiff/> <Route path="/article/:title/diff/:hash" view=EditDiff/>
<Route path="/article/create" view=CreateArticle/> <Route path="/article/create" view=CreateArticle/>
<Route path="/article/list" view=ListArticles/> <Route path="/article/list" view=ListArticles/>
<Route path={Page::Login.path()} view=Login/> <Route path="/instance/:hostname" view=InstanceDetails/>
<Route path={Page::Register.path()} view=Register/> <Route path="/login" view=Login/>
<Route path="/register" view=Register/>
<Route path="/search" view=Search/> <Route path="/search" view=Search/>
</Routes> </Routes>
</main> </main>

View File

@ -4,7 +4,7 @@ use leptos::*;
use leptos_router::*; use leptos_router::*;
#[component] #[component]
pub fn ArticleNav(article: Resource<String, ArticleView>) -> impl IntoView { pub fn ArticleNav(article: Resource<Option<String>, ArticleView>) -> impl IntoView {
let global_state = use_context::<RwSignal<GlobalState>>().unwrap(); let global_state = use_context::<RwSignal<GlobalState>>().unwrap();
view! { view! {
<Suspense> <Suspense>

View File

@ -8,8 +8,8 @@ use leptos_router::use_params_map;
#[component] #[component]
pub fn EditArticle() -> impl IntoView { pub fn EditArticle() -> impl IntoView {
let params = use_params_map(); let params = use_params_map();
let title = params.get_untracked().get("title").cloned(); let title = move || params.get().get("title").cloned();
let article = article_resource(title.unwrap()); let article = article_resource(title);
let (text, set_text) = create_signal(String::new()); let (text, set_text) = create_signal(String::new());
let (summary, set_summary) = create_signal(String::new()); let (summary, set_summary) = create_signal(String::new());

View File

@ -6,8 +6,8 @@ use leptos_router::*;
#[component] #[component]
pub fn ArticleHistory() -> impl IntoView { pub fn ArticleHistory() -> impl IntoView {
let params = use_params_map(); let params = use_params_map();
let title = params.get_untracked().get("title").cloned(); let title = move || params.get().get("title").cloned();
let article = article_resource(title.unwrap()); let article = article_resource(title);
view! { view! {
<ArticleNav article=article/> <ArticleNav article=article/>

View File

@ -7,11 +7,7 @@ use markdown_it::MarkdownIt;
#[component] #[component]
pub fn ReadArticle() -> impl IntoView { pub fn ReadArticle() -> impl IntoView {
let params = use_params_map(); let params = use_params_map();
let title = params let title = move || params.get().get("title").cloned();
.get_untracked()
.get("title")
.cloned()
.unwrap_or("Main_Page".to_string());
let article = article_resource(title); let article = article_resource(title);
view! { view! {

View File

@ -6,7 +6,7 @@ use leptos_router::*;
#[component] #[component]
pub fn EditDiff() -> impl IntoView { pub fn EditDiff() -> impl IntoView {
let params = use_params_map(); let params = use_params_map();
let title = params.get_untracked().get("title").cloned().unwrap(); let title = move || params.get().get("title").cloned();
let article = article_resource(title); let article = article_resource(title);
view! { view! {

View File

@ -0,0 +1,33 @@
use crate::common::DbInstance;
use crate::frontend::app::GlobalState;
use leptos::*;
use leptos_router::use_params_map;
use url::Url;
#[component]
pub fn InstanceDetails() -> impl IntoView {
let params = use_params_map();
let hostname = move || params.get().get("hostname").cloned().unwrap();
let instance_profile = create_resource(hostname, move |hostname| async move {
let url = Url::parse(&format!("http://{hostname}")).unwrap();
GlobalState::api_client()
.resolve_instance(url)
.await
.unwrap()
});
// TODO: display list of articles from instance?
view! {
<Suspense fallback=|| view! { "Loading..." }> {
move || instance_profile.get().map(|instance: DbInstance| {
view! {
<h1>{instance.ap_id.to_string()}</h1>
<Show when=GlobalState::is_admin()>
<button text="Follow"/>
</Show>
<div>{instance.description}</div>
}
})
}</Suspense>
}
}

View File

@ -4,32 +4,16 @@ use leptos::{create_resource, Resource};
pub(crate) mod article; pub(crate) mod article;
pub(crate) mod diff; pub(crate) mod diff;
pub mod login; pub(crate) mod instance_details;
pub mod register; pub(crate) mod login;
pub(crate) mod register;
pub(crate) mod search; pub(crate) mod search;
#[derive(Debug, Clone, Copy, Default)] fn article_resource(
pub enum Page { title: impl Fn() -> Option<String> + 'static,
#[default] ) -> Resource<Option<String>, ArticleView> {
Home, create_resource(title, move |title| async move {
Login, let title = title.unwrap_or("Main_Page".to_string());
Register,
}
impl Page {
pub fn path(&self) -> &'static str {
match self {
Self::Home => "/",
Self::Login => "/login",
Self::Register => "/register",
}
}
}
fn article_resource(title: String) -> Resource<String, ArticleView> {
create_resource(
move || title.clone(),
move |title| async move {
GlobalState::api_client() GlobalState::api_client()
.get_article(GetArticleData { .get_article(GetArticleData {
title: Some(title), title: Some(title),
@ -38,6 +22,5 @@ fn article_resource(title: String) -> Resource<String, ArticleView> {
}) })
.await .await
.unwrap() .unwrap()
}, })
)
} }