site type routing

This commit is contained in:
Hamcha 2025-02-06 21:11:43 +01:00
parent 14242a8e37
commit e13beb9052
Signed by: hamcha
GPG key ID: 1669C533B8CF6D89
12 changed files with 133 additions and 29 deletions

1
.env Normal file
View file

@ -0,0 +1 @@
MABEL_ADMIN_HOST=admin.localhost

3
Makefile.toml Normal file
View file

@ -0,0 +1,3 @@
[tasks.test]
command = "cargo"
args = ["nextest", "run", "--features", "server"]

View file

@ -1,5 +1,2 @@
#[cfg(any(feature = "web", feature = "server"))] #[cfg(any(feature = "web", feature = "server"))]
pub mod renderer; pub mod renderer;
#[cfg(any(feature = "web", feature = "server"))]
pub mod admin_ui;

View file

@ -0,0 +1,30 @@
use dioxus::prelude::*;
#[derive(Clone, Routable, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
#[rustfmt::skip]
enum Route {
#[layout(AdminLayout)]
#[route("/")]
Home {},
}
pub fn App() -> Element {
rsx! {
Router::<Route> {}
}
}
fn Home() -> Element {
rsx! {
h2 { "Hello!" }
}
}
fn AdminLayout() -> Element {
rsx! {
h1 { "Admin UI" }
SuspenseBoundary { fallback: |_context: SuspenseContext| rsx! { "..." },
main { Outlet::<Route> {} }
}
}
}

View file

@ -1,7 +1,19 @@
use dioxus::prelude::*; use dioxus::prelude::*;
use serde::{Deserialize, Serialize};
use crate::domain::entities::site::{Page, SiteInfo}; use crate::domain::entities::site::{Page, SiteInfo};
#[derive(Serialize, Deserialize, Clone)]
pub enum SiteType {
UserSite(SiteInfo),
ControlPanel,
}
#[derive(Clone)]
pub struct UIContext {
pub site_type: SiteType,
}
#[derive(Clone)] #[derive(Clone)]
pub struct SiteContext { pub struct SiteContext {
pub info: SiteInfo, pub info: SiteInfo,

View file

@ -2,11 +2,12 @@
use dioxus::prelude::*; use dioxus::prelude::*;
use meta::SiteType;
use page::{Page, PostElement}; use page::{Page, PostElement};
use server::get_site_info;
use crate::domain::entities::site::PageContent; use crate::domain::entities::site::PageContent;
mod admin_ui;
mod meta; mod meta;
mod page; mod page;
mod server; mod server;
@ -31,21 +32,28 @@ enum Route {
} }
pub fn App() -> Element { pub fn App() -> Element {
// Retrieve site info // Check for special redirects
let site_info = use_server_future(get_site_info)?.suspend()?; let site_res = use_server_future(server::get_site_info)?.suspend()?;
let site_type = site_res.read().clone();
// Inject site info in context match site_type {
match &*site_info.read() { Ok(SiteType::ControlPanel) => {
Ok(info) => meta::set_site(info), rsx! {
Err(err) => { admin_ui::App {}
return rsx! {
h1 { "FATAL ERROR: {err}" }
} }
} }
} Ok(SiteType::UserSite(site_info)) => {
// Inject site info in context
meta::set_site(&site_info);
rsx! { rsx! {
Router::<Route> {} Router::<Route> {}
}
}
_ => {
rsx! {
h1 { "404" }
}
}
} }
} }
@ -72,7 +80,7 @@ fn PageLink(page: String, title: String, current: Route) -> Element {
} }
#[component] #[component]
pub fn SiteLayout() -> Element { fn SiteLayout() -> Element {
let site = meta::site(); let site = meta::site();
let route = use_route::<Route>(); let route = use_route::<Route>();
@ -100,7 +108,7 @@ pub fn SiteLayout() -> Element {
} }
#[component] #[component]
pub fn Home() -> Element { fn Home() -> Element {
let site = meta::site(); let site = meta::site();
let page_ref = let page_ref =
use_server_future(move || server::get_page(site.info.domain.clone(), "/".to_string()))? use_server_future(move || server::get_page(site.info.domain.clone(), "/".to_string()))?
@ -120,7 +128,7 @@ pub fn Home() -> Element {
} }
#[component] #[component]
pub fn Single(page: String) -> Element { fn Single(page: String) -> Element {
let site = meta::site(); let site = meta::site();
let page_ref = let page_ref =
use_server_future(move || server::get_page(site.info.domain.clone(), page.clone()))? use_server_future(move || server::get_page(site.info.domain.clone(), page.clone()))?
@ -140,7 +148,7 @@ pub fn Single(page: String) -> Element {
} }
#[component] #[component]
pub fn Post(page: String, id: String) -> Element { fn Post(page: String, id: String) -> Element {
let site = meta::site(); let site = meta::site();
let domain = site.info.domain.clone(); let domain = site.info.domain.clone();
let page_ref = let page_ref =
@ -194,7 +202,10 @@ mod tests {
use crate::{ use crate::{
domain::entities, domain::entities,
outbound::services::site::{MockSiteService, SiteServiceProvider}, outbound::{
config::AppConfig,
services::site::{MockSiteService, SiteServiceProvider},
},
}; };
use super::*; use super::*;
@ -218,6 +229,9 @@ mod tests {
}) })
}); });
server_context().insert(AppConfig {
admin_host: "admin.local".to_string(),
});
server_context().insert(SiteServiceProvider::with(mock_service)); server_context().insert(SiteServiceProvider::with(mock_service));
app.rebuild_in_place(); app.rebuild_in_place();
@ -303,4 +317,26 @@ mod tests {
let elem_str = dioxus::ssr::render(&app); let elem_str = dioxus::ssr::render(&app);
assert!(elem_str.contains("Test page name")); assert!(elem_str.contains("Test page name"));
} }
#[test]
fn redirects_to_admin_ui() {
let mut app = VirtualDom::new(|| {
rsx! {
App {}
}
});
server_context().insert(AppConfig {
admin_host: "admin.local".to_string(),
});
server_context()
.request_parts_mut()
.headers
.insert("host", "admin.local".parse().unwrap());
app.rebuild_in_place();
let elem_str = dioxus::ssr::render(&app);
println!("elem_str: {elem_str}");
assert!(elem_str.contains("Admin UI"));
}
} }

View file

@ -27,7 +27,7 @@ pub fn Page() -> Element {
} }
#[component] #[component]
pub fn Collection(collection_id: String) -> Element { fn Collection(collection_id: String) -> Element {
let site = meta::site(); let site = meta::site();
let posts = let posts =
use_server_future(move || get_posts(site.info.domain.clone(), collection_id.clone()))? use_server_future(move || get_posts(site.info.domain.clone(), collection_id.clone()))?
@ -59,7 +59,7 @@ pub fn PostElement(post: Post) -> Element {
} }
#[component] #[component]
pub fn BlockElement(block: Block) -> Element { fn BlockElement(block: Block) -> Element {
match block { match block {
Block::Text { text } => rsx! { Block::Text { text } => rsx! {
p { "{text}" } p { "{text}" }

View file

@ -2,13 +2,15 @@ use dioxus::prelude::*;
use crate::domain::entities::{ use crate::domain::entities::{
cursor::Paginated, cursor::Paginated,
site::{Page, Post, SiteInfo}, site::{Page, Post},
}; };
use super::meta::SiteType;
#[server] #[server]
pub async fn get_site_info() -> Result<SiteInfo, ServerFnError> { pub async fn get_site_info() -> Result<SiteType, ServerFnError> {
use crate::outbound::config::AppConfig;
use crate::outbound::services::site::SiteServiceProvider; use crate::outbound::services::site::SiteServiceProvider;
let FromContext(SiteServiceProvider { service }) = extract().await?;
let headers = server_context().request_parts().headers.clone(); let headers = server_context().request_parts().headers.clone();
let domain = match headers.get("host").and_then(|h| h.to_str().ok()) { let domain = match headers.get("host").and_then(|h| h.to_str().ok()) {
@ -20,8 +22,14 @@ pub async fn get_site_info() -> Result<SiteInfo, ServerFnError> {
} }
.to_string(); .to_string();
let site = service.get_site(domain.as_str()).await?; let FromContext(AppConfig { admin_host }) = extract().await?;
Ok(site) if domain == admin_host {
return Ok(SiteType::ControlPanel);
}
let FromContext(SiteServiceProvider { service }) = extract().await?;
let site = service.get_site(&domain).await?;
Ok(SiteType::UserSite(site))
} }
#[server] #[server]

View file

@ -16,10 +16,12 @@ fn main() {
let mut builder = dioxus::LaunchBuilder::new(); let mut builder = dioxus::LaunchBuilder::new();
server_only! { server_only! {
use outbound::services::site::{SiteServiceProvider, SiteServiceImpl}; use outbound::services::site::{SiteServiceProvider, SiteServiceImpl};
use outbound::config::AppConfig;
let store = tokio::runtime::Runtime::new().unwrap().block_on(setup_inmem_store()); let store = tokio::runtime::Runtime::new().unwrap().block_on(setup_inmem_store());
builder = builder.with_context_provider(move || { builder = builder.with_context_provider(move || {
Box::new(SiteServiceProvider::with(SiteServiceImpl::new(store.clone()))) Box::new(SiteServiceProvider::with(SiteServiceImpl::new(store.clone())))
}) }).with_context(AppConfig::from_env());
} }
builder.launch(inbound::renderer::App); builder.launch(inbound::renderer::App);

12
src/outbound/config.rs Normal file
View file

@ -0,0 +1,12 @@
#[derive(Clone)]
pub struct AppConfig {
pub admin_host: String,
}
impl AppConfig {
pub fn from_env() -> Self {
Self {
admin_host: std::env::var("MABEL_ADMIN_HOST").expect("MABEL_ADMIN_HOST must be set"),
}
}
}

View file

@ -1,4 +1,7 @@
#[cfg(feature = "server")] #[cfg(feature = "server")]
pub mod repository; pub mod repository;
#[cfg(feature = "server")]
pub mod config;
pub mod services; pub mod services;