site type routing
This commit is contained in:
parent
14242a8e37
commit
e13beb9052
12 changed files with 133 additions and 29 deletions
1
.env
Normal file
1
.env
Normal file
|
@ -0,0 +1 @@
|
||||||
|
MABEL_ADMIN_HOST=admin.localhost
|
3
Makefile.toml
Normal file
3
Makefile.toml
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
[tasks.test]
|
||||||
|
command = "cargo"
|
||||||
|
args = ["nextest", "run", "--features", "server"]
|
|
@ -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;
|
|
||||||
|
|
30
src/inbound/renderer/admin_ui/mod.rs
Normal file
30
src/inbound/renderer/admin_ui/mod.rs
Normal 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> {} }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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,
|
||||||
|
|
|
@ -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"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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}" }
|
||||||
|
|
|
@ -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]
|
||||||
|
|
|
@ -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
12
src/outbound/config.rs
Normal 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"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
|
Loading…
Add table
Reference in a new issue