mirror of
https://github.com/leptos-rs/leptos
synced 2024-11-10 06:44:17 +00:00
Merge pull request #2158 from leptos-rs/leptos_v0.6
This commit is contained in:
commit
c84c6ee8cd
172 changed files with 7862 additions and 6467 deletions
1
.github/workflows/ci.yml
vendored
1
.github/workflows/ci.yml
vendored
|
@ -22,7 +22,6 @@ jobs:
|
|||
[
|
||||
integrations/actix,
|
||||
integrations/axum,
|
||||
integrations/viz,
|
||||
integrations/utils,
|
||||
leptos,
|
||||
leptos_config,
|
||||
|
|
29
Cargo.toml
29
Cargo.toml
|
@ -16,7 +16,6 @@ members = [
|
|||
# integrations
|
||||
"integrations/actix",
|
||||
"integrations/axum",
|
||||
"integrations/viz",
|
||||
"integrations/utils",
|
||||
|
||||
# libraries
|
||||
|
@ -26,22 +25,22 @@ members = [
|
|||
exclude = ["benchmarks", "examples"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.5.7"
|
||||
version = "0.6.0-beta"
|
||||
|
||||
[workspace.dependencies]
|
||||
leptos = { path = "./leptos", version = "0.5.7" }
|
||||
leptos_dom = { path = "./leptos_dom", version = "0.5.7" }
|
||||
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.5.7" }
|
||||
leptos_macro = { path = "./leptos_macro", version = "0.5.7" }
|
||||
leptos_reactive = { path = "./leptos_reactive", version = "0.5.7" }
|
||||
leptos_server = { path = "./leptos_server", version = "0.5.7" }
|
||||
server_fn = { path = "./server_fn", version = "0.5.7" }
|
||||
server_fn_macro = { path = "./server_fn_macro", version = "0.5.7" }
|
||||
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", version = "0.5.7" }
|
||||
leptos_config = { path = "./leptos_config", version = "0.5.7" }
|
||||
leptos_router = { path = "./router", version = "0.5.7" }
|
||||
leptos_meta = { path = "./meta", version = "0.5.7" }
|
||||
leptos_integration_utils = { path = "./integrations/utils", version = "0.5.7" }
|
||||
leptos = { path = "./leptos", version = "0.6.0-beta" }
|
||||
leptos_dom = { path = "./leptos_dom", version = "0.6.0-beta" }
|
||||
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.6.0-beta" }
|
||||
leptos_macro = { path = "./leptos_macro", version = "0.6.0-beta" }
|
||||
leptos_reactive = { path = "./leptos_reactive", version = "0.6.0-beta" }
|
||||
leptos_server = { path = "./leptos_server", version = "0.6.0-beta" }
|
||||
server_fn = { path = "./server_fn", version = "0.6.0-beta" }
|
||||
server_fn_macro = { path = "./server_fn_macro", version = "0.6.0-beta" }
|
||||
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", version = "0.6" }
|
||||
leptos_config = { path = "./leptos_config", version = "0.6.0-beta" }
|
||||
leptos_router = { path = "./router", version = "0.6.0-beta" }
|
||||
leptos_meta = { path = "./meta", version = "0.6.0-beta" }
|
||||
leptos_integration_utils = { path = "./integrations/utils", version = "0.6.0-beta" }
|
||||
|
||||
[profile.release]
|
||||
codegen-units = 1
|
||||
|
|
|
@ -21,6 +21,7 @@ CARGO_MAKE_CRATE_WORKSPACE_MEMBERS = [
|
|||
"login_with_token_csr_only",
|
||||
"parent_child",
|
||||
"router",
|
||||
"server_fns_axum",
|
||||
"session_auth_axum",
|
||||
"slots",
|
||||
"ssr_modes",
|
||||
|
@ -32,7 +33,6 @@ CARGO_MAKE_CRATE_WORKSPACE_MEMBERS = [
|
|||
"timer",
|
||||
"todo_app_sqlite",
|
||||
"todo_app_sqlite_axum",
|
||||
"todo_app_sqlite_viz",
|
||||
"todomvc",
|
||||
]
|
||||
|
||||
|
|
90
examples/action-form-error-handling/Cargo.toml
Normal file
90
examples/action-form-error-handling/Cargo.toml
Normal file
|
@ -0,0 +1,90 @@
|
|||
[package]
|
||||
name = "action-form-error-handling"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[dependencies]
|
||||
actix-files = { version = "0.6", optional = true }
|
||||
actix-web = { version = "4", optional = true, features = ["macros"] }
|
||||
console_error_panic_hook = "0.1"
|
||||
cfg-if = "1"
|
||||
http = { version = "0.2", optional = true }
|
||||
leptos = { path = "../../leptos" }
|
||||
leptos_meta = { path = "../../meta" }
|
||||
leptos_actix = { path = "../../integrations/actix", optional = true }
|
||||
leptos_router = { path = "../../router" }
|
||||
wasm-bindgen = "0.2"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
|
||||
[features]
|
||||
csr = ["leptos/csr", "leptos_meta/csr", "leptos_router/csr"]
|
||||
hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate"]
|
||||
ssr = [
|
||||
"dep:actix-files",
|
||||
"dep:actix-web",
|
||||
"dep:leptos_actix",
|
||||
"leptos/ssr",
|
||||
"leptos_meta/ssr",
|
||||
"leptos_router/ssr",
|
||||
]
|
||||
|
||||
# Defines a size-optimized profile for the WASM bundle in release mode
|
||||
[profile.wasm-release]
|
||||
inherits = "release"
|
||||
opt-level = 'z'
|
||||
lto = true
|
||||
codegen-units = 1
|
||||
panic = "abort"
|
||||
|
||||
[package.metadata.leptos]
|
||||
# The name used by wasm-bindgen/cargo-leptos for the JS/WASM bundle. Defaults to the crate name
|
||||
output-name = "leptos_start"
|
||||
# The site root folder is where cargo-leptos generate all output. WARNING: all content of this folder will be erased on a rebuild. Use it in your server setup.
|
||||
site-root = "target/site"
|
||||
# The site-root relative folder where all compiled output (JS, WASM and CSS) is written
|
||||
# Defaults to pkg
|
||||
site-pkg-dir = "pkg"
|
||||
# [Optional] The source CSS file. If it ends with .sass or .scss then it will be compiled by dart-sass into CSS. The CSS is optimized by Lightning CSS before being written to <site-root>/<site-pkg>/app.css
|
||||
style-file = "style/main.scss"
|
||||
# The IP and port (ex: 127.0.0.1:3000) where the server serves the content. Use it in your server setup.
|
||||
site-addr = "127.0.0.1:3000"
|
||||
# The port to use for automatic reload monitoring
|
||||
reload-port = 3001
|
||||
# [Optional] Command to use when running end2end tests. It will run in the end2end dir.
|
||||
# [Windows] for non-WSL use "npx.cmd playwright test"
|
||||
# This binary name can be checked in Powershell with Get-Command npx
|
||||
end2end-cmd = "npx playwright test"
|
||||
end2end-dir = "end2end"
|
||||
# The browserlist query used for optimizing the CSS.
|
||||
browserquery = "defaults"
|
||||
# Set by cargo-leptos watch when building with that tool. Controls whether autoreload JS will be included in the head
|
||||
watch = false
|
||||
# The environment Leptos will run in, usually either "DEV" or "PROD"
|
||||
env = "DEV"
|
||||
# The features to use when compiling the bin target
|
||||
#
|
||||
# Optional. Can be over-ridden with the command line parameter --bin-features
|
||||
bin-features = ["ssr"]
|
||||
|
||||
# If the --no-default-features flag should be used when compiling the bin target
|
||||
#
|
||||
# Optional. Defaults to false.
|
||||
bin-default-features = false
|
||||
|
||||
# The features to use when compiling the lib target
|
||||
#
|
||||
# Optional. Can be over-ridden with the command line parameter --lib-features
|
||||
lib-features = ["hydrate"]
|
||||
|
||||
# If the --no-default-features flag should be used when compiling the lib target
|
||||
#
|
||||
# Optional. Defaults to false.
|
||||
lib-default-features = false
|
||||
|
||||
# The profile to use for the lib target when compiling for release
|
||||
#
|
||||
# Optional. Defaults to "release".
|
||||
lib-profile-release = "wasm-release"
|
8
examples/action-form-error-handling/Makefile.toml
Normal file
8
examples/action-form-error-handling/Makefile.toml
Normal file
|
@ -0,0 +1,8 @@
|
|||
extend = [
|
||||
{ path = "../cargo-make/main.toml" },
|
||||
{ path = "../cargo-make/cargo-leptos.toml" },
|
||||
]
|
||||
|
||||
[env]
|
||||
|
||||
CLIENT_PROCESS_NAME = "action_form_error_handling"
|
68
examples/action-form-error-handling/README.md
Normal file
68
examples/action-form-error-handling/README.md
Normal file
|
@ -0,0 +1,68 @@
|
|||
<picture>
|
||||
<source srcset="https://raw.githubusercontent.com/leptos-rs/leptos/main/docs/logos/Leptos_logo_Solid_White.svg" media="(prefers-color-scheme: dark)">
|
||||
<img src="https://raw.githubusercontent.com/leptos-rs/leptos/main/docs/logos/Leptos_logo_RGB.svg" alt="Leptos Logo">
|
||||
</picture>
|
||||
|
||||
# Leptos Starter Template
|
||||
|
||||
This is a template for use with the [Leptos](https://github.com/leptos-rs/leptos) web framework and the [cargo-leptos](https://github.com/akesson/cargo-leptos) tool.
|
||||
|
||||
## Creating your template repo
|
||||
|
||||
If you don't have `cargo-leptos` installed you can install it with
|
||||
|
||||
`cargo install cargo-leptos`
|
||||
|
||||
Then run
|
||||
|
||||
`cargo leptos new --git leptos-rs/start`
|
||||
|
||||
to generate a new project template (you will be prompted to enter a project name).
|
||||
|
||||
`cd {projectname}`
|
||||
|
||||
to go to your newly created project.
|
||||
|
||||
Of course, you should explore around the project structure, but the best place to start with your application code is in `src/app.rs`.
|
||||
|
||||
## Running your project
|
||||
|
||||
`cargo leptos watch`
|
||||
By default, you can access your local project at `http://localhost:3000`
|
||||
|
||||
## Installing Additional Tools
|
||||
|
||||
By default, `cargo-leptos` uses `nightly` Rust, `cargo-generate`, and `sass`. If you run into any trouble, you may need to install one or more of these tools.
|
||||
|
||||
1. `rustup toolchain install nightly --allow-downgrade` - make sure you have Rust nightly
|
||||
2. `rustup target add wasm32-unknown-unknown` - add the ability to compile Rust to WebAssembly
|
||||
3. `cargo install cargo-generate` - install `cargo-generate` binary (should be installed automatically in future)
|
||||
4. `npm install -g sass` - install `dart-sass` (should be optional in future)
|
||||
|
||||
## Executing a Server on a Remote Machine Without the Toolchain
|
||||
After running a `cargo leptos build --release` the minimum files needed are:
|
||||
|
||||
1. The server binary located in `target/server/release`
|
||||
2. The `site` directory and all files within located in `target/site`
|
||||
|
||||
Copy these files to your remote server. The directory structure should be:
|
||||
```text
|
||||
leptos_start
|
||||
site/
|
||||
```
|
||||
Set the following environment variables (updating for your project as needed):
|
||||
```sh
|
||||
export LEPTOS_OUTPUT_NAME="leptos_start"
|
||||
export LEPTOS_SITE_ROOT="site"
|
||||
export LEPTOS_SITE_PKG_DIR="pkg"
|
||||
export LEPTOS_SITE_ADDR="127.0.0.1:3000"
|
||||
export LEPTOS_RELOAD_PORT="3001"
|
||||
```
|
||||
Finally, run the server binary.
|
||||
|
||||
## Notes about CSR and Trunk:
|
||||
Although it is not recommended, you can also run your project without server integration using the feature `csr` and `trunk serve`:
|
||||
|
||||
`trunk serve --open --features csr`
|
||||
|
||||
This may be useful for integrating external tools which require a static site, e.g. `tauri`.
|
97
examples/action-form-error-handling/src/app.rs
Normal file
97
examples/action-form-error-handling/src/app.rs
Normal file
|
@ -0,0 +1,97 @@
|
|||
use leptos::*;
|
||||
use leptos_meta::*;
|
||||
use leptos_router::*;
|
||||
|
||||
#[component]
|
||||
pub fn App() -> impl IntoView {
|
||||
// Provides context that manages stylesheets, titles, meta tags, etc.
|
||||
provide_meta_context();
|
||||
|
||||
view! {
|
||||
// injects a stylesheet into the document <head>
|
||||
// id=leptos means cargo-leptos will hot-reload this stylesheet
|
||||
<Stylesheet id="leptos" href="/pkg/leptos_start.css"/>
|
||||
|
||||
// sets the document title
|
||||
<Title text="Welcome to Leptos"/>
|
||||
|
||||
// content for this welcome page
|
||||
<Router>
|
||||
<main id="app">
|
||||
<Routes>
|
||||
<Route path="" view=HomePage/>
|
||||
<Route path="/*any" view=NotFound/>
|
||||
</Routes>
|
||||
</main>
|
||||
</Router>
|
||||
}
|
||||
}
|
||||
|
||||
#[server]
|
||||
async fn do_something(
|
||||
should_error: Option<String>,
|
||||
) -> Result<String, ServerFnError> {
|
||||
if should_error.is_none() {
|
||||
Ok(String::from("Successful submit"))
|
||||
} else {
|
||||
Err(ServerFnError::ServerError(String::from(
|
||||
"You got an error!",
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
/// Renders the home page of your application.
|
||||
#[component]
|
||||
fn HomePage() -> impl IntoView {
|
||||
let do_something_action = Action::<DoSomething, _>::server();
|
||||
let value = Signal::derive(move || {
|
||||
do_something_action
|
||||
.value()
|
||||
.get()
|
||||
.unwrap_or_else(|| Ok(String::new()))
|
||||
});
|
||||
|
||||
Effect::new_isomorphic(move |_| {
|
||||
logging::log!("Got value = {:?}", value.get());
|
||||
});
|
||||
|
||||
view! {
|
||||
<h1>"Test the action form!"</h1>
|
||||
<ErrorBoundary fallback=move |error| format!("{:#?}", error
|
||||
.get()
|
||||
.into_iter()
|
||||
.next()
|
||||
.unwrap()
|
||||
.1.into_inner()
|
||||
.to_string())
|
||||
>
|
||||
{value}
|
||||
<ActionForm action=do_something_action class="form">
|
||||
<label>Should error: <input type="checkbox" name="should_error"/></label>
|
||||
<button type="submit">Submit</button>
|
||||
</ActionForm>
|
||||
</ErrorBoundary>
|
||||
}
|
||||
}
|
||||
|
||||
/// 404 - Not Found
|
||||
#[component]
|
||||
fn NotFound() -> impl IntoView {
|
||||
// set an HTTP status code 404
|
||||
// this is feature gated because it can only be done during
|
||||
// initial server-side rendering
|
||||
// if you navigate to the 404 page subsequently, the status
|
||||
// code will not be set because there is not a new HTTP request
|
||||
// to the server
|
||||
#[cfg(feature = "ssr")]
|
||||
{
|
||||
// this can be done inline because it's synchronous
|
||||
// if it were async, we'd use a server function
|
||||
let resp = expect_context::<leptos_actix::ResponseOptions>();
|
||||
resp.set_status(actix_web::http::StatusCode::NOT_FOUND);
|
||||
}
|
||||
|
||||
view! {
|
||||
<h1>"Not Found"</h1>
|
||||
}
|
||||
}
|
18
examples/action-form-error-handling/src/lib.rs
Normal file
18
examples/action-form-error-handling/src/lib.rs
Normal file
|
@ -0,0 +1,18 @@
|
|||
pub mod app;
|
||||
use cfg_if::cfg_if;
|
||||
|
||||
cfg_if! {
|
||||
if #[cfg(feature = "hydrate")] {
|
||||
|
||||
use wasm_bindgen::prelude::wasm_bindgen;
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn hydrate() {
|
||||
use app::*;
|
||||
|
||||
console_error_panic_hook::set_once();
|
||||
|
||||
leptos::mount_to_body(App);
|
||||
}
|
||||
}
|
||||
}
|
53
examples/action-form-error-handling/src/main.rs
Normal file
53
examples/action-form-error-handling/src/main.rs
Normal file
|
@ -0,0 +1,53 @@
|
|||
#[cfg(feature = "ssr")]
|
||||
#[actix_web::main]
|
||||
async fn main() -> std::io::Result<()> {
|
||||
use action_form_error_handling::app::*;
|
||||
use actix_files::Files;
|
||||
use actix_web::*;
|
||||
use leptos::*;
|
||||
use leptos_actix::{generate_route_list, LeptosRoutes};
|
||||
|
||||
let conf = get_configuration(None).await.unwrap();
|
||||
let addr = conf.leptos_options.site_addr;
|
||||
// Generate the list of routes in your Leptos App
|
||||
let routes = generate_route_list(App);
|
||||
println!("listening on http://{}", &addr);
|
||||
|
||||
HttpServer::new(move || {
|
||||
let leptos_options = &conf.leptos_options;
|
||||
let site_root = &leptos_options.site_root;
|
||||
|
||||
App::new()
|
||||
.route("/api/{tail:.*}", leptos_actix::handle_server_fns())
|
||||
// serve JS/WASM/CSS from `pkg`
|
||||
.service(Files::new("/pkg", format!("{site_root}/pkg")))
|
||||
.leptos_routes(leptos_options.to_owned(), routes.to_owned(), App)
|
||||
.app_data(web::Data::new(leptos_options.to_owned()))
|
||||
//.wrap(middleware::Compress::default())
|
||||
})
|
||||
.bind(&addr)?
|
||||
.run()
|
||||
.await
|
||||
}
|
||||
|
||||
#[cfg(not(any(feature = "ssr", feature = "csr")))]
|
||||
pub fn main() {
|
||||
// no client-side main function
|
||||
// unless we want this to work with e.g., Trunk for pure client-side testing
|
||||
// see lib.rs for hydration function instead
|
||||
// see optional feature `csr` instead
|
||||
}
|
||||
|
||||
#[cfg(all(not(feature = "ssr"), feature = "csr"))]
|
||||
pub fn main() {
|
||||
// a client-side main function is required for using `trunk serve`
|
||||
// prefer using `cargo leptos serve` instead
|
||||
// to run: `trunk serve --open --features csr`
|
||||
use action_form_error_handling::app::*;
|
||||
use leptos::*;
|
||||
use wasm_bindgen::prelude::wasm_bindgen;
|
||||
|
||||
console_error_panic_hook::set_once();
|
||||
|
||||
leptos::mount_to_body(App);
|
||||
}
|
15
examples/action-form-error-handling/style/main.scss
Normal file
15
examples/action-form-error-handling/style/main.scss
Normal file
|
@ -0,0 +1,15 @@
|
|||
body {
|
||||
font-family: sans-serif;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#app {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
|
@ -17,7 +17,6 @@ broadcaster = "1"
|
|||
console_log = "1"
|
||||
console_error_panic_hook = "0.1"
|
||||
futures = "0.3"
|
||||
cfg-if = "1"
|
||||
lazy_static = "1"
|
||||
leptos = { path = "../../leptos" }
|
||||
leptos_actix = { path = "../../integrations/actix", optional = true }
|
||||
|
|
|
@ -1,34 +1,35 @@
|
|||
use cfg_if::cfg_if;
|
||||
use leptos::*;
|
||||
use leptos_meta::*;
|
||||
use leptos_router::*;
|
||||
#[cfg(feature = "ssr")]
|
||||
use tracing::instrument;
|
||||
|
||||
cfg_if! {
|
||||
if #[cfg(feature = "ssr")] {
|
||||
use std::sync::atomic::{AtomicI32, Ordering};
|
||||
use broadcaster::BroadcastChannel;
|
||||
use once_cell::sync::OnceCell;
|
||||
#[cfg(feature = "ssr")]
|
||||
pub mod ssr_imports {
|
||||
pub use broadcaster::BroadcastChannel;
|
||||
pub use once_cell::sync::OnceCell;
|
||||
pub use std::sync::atomic::{AtomicI32, Ordering};
|
||||
|
||||
static COUNT: AtomicI32 = AtomicI32::new(0);
|
||||
pub static COUNT: AtomicI32 = AtomicI32::new(0);
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
pub static ref COUNT_CHANNEL: BroadcastChannel<i32> = BroadcastChannel::new();
|
||||
}
|
||||
lazy_static::lazy_static! {
|
||||
pub static ref COUNT_CHANNEL: BroadcastChannel<i32> = BroadcastChannel::new();
|
||||
}
|
||||
|
||||
static LOG_INIT: OnceCell<()> = OnceCell::new();
|
||||
fn init_logging() {
|
||||
LOG_INIT.get_or_init(|| {
|
||||
simple_logger::SimpleLogger::new().env().init().unwrap();
|
||||
});
|
||||
}
|
||||
static LOG_INIT: OnceCell<()> = OnceCell::new();
|
||||
|
||||
pub fn init_logging() {
|
||||
LOG_INIT.get_or_init(|| {
|
||||
simple_logger::SimpleLogger::new().env().init().unwrap();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[server]
|
||||
#[cfg_attr(feature = "ssr", instrument)]
|
||||
pub async fn get_server_count() -> Result<i32, ServerFnError> {
|
||||
use ssr_imports::*;
|
||||
|
||||
Ok(COUNT.load(Ordering::Relaxed))
|
||||
}
|
||||
|
||||
|
@ -38,6 +39,8 @@ pub async fn adjust_server_count(
|
|||
delta: i32,
|
||||
msg: String,
|
||||
) -> Result<i32, ServerFnError> {
|
||||
use ssr_imports::*;
|
||||
|
||||
let new = COUNT.load(Ordering::Relaxed) + delta;
|
||||
COUNT.store(new, Ordering::Relaxed);
|
||||
_ = COUNT_CHANNEL.send(&new).await;
|
||||
|
@ -48,6 +51,8 @@ pub async fn adjust_server_count(
|
|||
#[server]
|
||||
#[cfg_attr(feature = "ssr", instrument)]
|
||||
pub async fn clear_server_count() -> Result<i32, ServerFnError> {
|
||||
use ssr_imports::*;
|
||||
|
||||
COUNT.store(0, Ordering::Relaxed);
|
||||
_ = COUNT_CHANNEL.send(&0).await;
|
||||
Ok(0)
|
||||
|
@ -55,7 +60,7 @@ pub async fn clear_server_count() -> Result<i32, ServerFnError> {
|
|||
#[component]
|
||||
pub fn Counters() -> impl IntoView {
|
||||
#[cfg(feature = "ssr")]
|
||||
init_logging();
|
||||
ssr_imports::init_logging();
|
||||
|
||||
provide_meta_context();
|
||||
view! {
|
||||
|
@ -113,9 +118,9 @@ pub fn Counters() -> impl IntoView {
|
|||
// This is the typical pattern for a CRUD app
|
||||
#[component]
|
||||
pub fn Counter() -> impl IntoView {
|
||||
let dec = create_action(|_| adjust_server_count(-1, "decing".into()));
|
||||
let inc = create_action(|_| adjust_server_count(1, "incing".into()));
|
||||
let clear = create_action(|_| clear_server_count());
|
||||
let dec = create_action(|_: &()| adjust_server_count(-1, "decing".into()));
|
||||
let inc = create_action(|_: &()| adjust_server_count(1, "incing".into()));
|
||||
let clear = create_action(|_: &()| clear_server_count());
|
||||
let counter = create_resource(
|
||||
move || {
|
||||
(
|
||||
|
@ -217,9 +222,10 @@ pub fn FormCounter() -> impl IntoView {
|
|||
#[component]
|
||||
pub fn MultiuserCounter() -> impl IntoView {
|
||||
let dec =
|
||||
create_action(|_| adjust_server_count(-1, "dec dec goose".into()));
|
||||
let inc = create_action(|_| adjust_server_count(1, "inc inc moose".into()));
|
||||
let clear = create_action(|_| clear_server_count());
|
||||
create_action(|_: &()| adjust_server_count(-1, "dec dec goose".into()));
|
||||
let inc =
|
||||
create_action(|_: &()| adjust_server_count(1, "inc inc moose".into()));
|
||||
let clear = create_action(|_: &()| clear_server_count());
|
||||
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
let multiplayer_value = {
|
||||
|
|
|
@ -1,21 +1,13 @@
|
|||
use cfg_if::cfg_if;
|
||||
pub mod counters;
|
||||
|
||||
// Needs to be in lib.rs AFAIK because wasm-bindgen needs us to be compiling a lib. I may be wrong.
|
||||
cfg_if! {
|
||||
if #[cfg(feature = "hydrate")] {
|
||||
use leptos::*;
|
||||
use wasm_bindgen::prelude::wasm_bindgen;
|
||||
use crate::counters::*;
|
||||
#[cfg(feature = "hydrate")]
|
||||
#[wasm_bindgen::prelude::wasm_bindgen]
|
||||
pub fn hydrate() {
|
||||
use crate::counters::*;
|
||||
use leptos::*;
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn hydrate() {
|
||||
_ = console_log::init_with_level(log::Level::Debug);
|
||||
console_error_panic_hook::set_once();
|
||||
_ = console_log::init_with_level(log::Level::Debug);
|
||||
console_error_panic_hook::set_once();
|
||||
|
||||
mount_to_body(|| {
|
||||
view! { <Counters/> }
|
||||
});
|
||||
}
|
||||
}
|
||||
mount_to_body(Counters);
|
||||
}
|
||||
|
|
|
@ -1,72 +1,54 @@
|
|||
use cfg_if::cfg_if;
|
||||
mod counters;
|
||||
|
||||
// boilerplate to run in different modes
|
||||
cfg_if! {
|
||||
// server-only stuff
|
||||
if #[cfg(feature = "ssr")] {
|
||||
use leptos::*;
|
||||
use actix_files::{Files};
|
||||
use actix_web::*;
|
||||
use crate::counters::*;
|
||||
use leptos_actix::{generate_route_list, LeptosRoutes};
|
||||
use crate::counters::*;
|
||||
use actix_files::Files;
|
||||
use actix_web::*;
|
||||
use leptos::*;
|
||||
use leptos_actix::{generate_route_list, LeptosRoutes};
|
||||
|
||||
#[get("/api/events")]
|
||||
async fn counter_events() -> impl Responder {
|
||||
use futures::StreamExt;
|
||||
#[get("/api/events")]
|
||||
async fn counter_events() -> impl Responder {
|
||||
use crate::counters::ssr_imports::*;
|
||||
use futures::StreamExt;
|
||||
|
||||
let stream =
|
||||
futures::stream::once(async { crate::counters::get_server_count().await.unwrap_or(0) })
|
||||
.chain(COUNT_CHANNEL.clone())
|
||||
.map(|value| {
|
||||
Ok(web::Bytes::from(format!(
|
||||
"event: message\ndata: {value}\n\n"
|
||||
))) as Result<web::Bytes>
|
||||
});
|
||||
HttpResponse::Ok()
|
||||
.insert_header(("Content-Type", "text/event-stream"))
|
||||
.streaming(stream)
|
||||
}
|
||||
|
||||
#[actix_web::main]
|
||||
async fn main() -> std::io::Result<()> {
|
||||
|
||||
// Explicit server function registration is no longer required
|
||||
// on the main branch. On 0.3.0 and earlier, uncomment the lines
|
||||
// below to register the server functions.
|
||||
// _ = GetServerCount::register();
|
||||
// _ = AdjustServerCount::register();
|
||||
// _ = ClearServerCount::register();
|
||||
|
||||
// Setting this to None means we'll be using cargo-leptos and its env vars.
|
||||
// when not using cargo-leptos None must be replaced with Some("Cargo.toml")
|
||||
let conf = get_configuration(None).await.unwrap();
|
||||
|
||||
let addr = conf.leptos_options.site_addr;
|
||||
let routes = generate_route_list(|| view! { <Counters/> });
|
||||
|
||||
HttpServer::new(move || {
|
||||
let leptos_options = &conf.leptos_options;
|
||||
let site_root = &leptos_options.site_root;
|
||||
|
||||
App::new()
|
||||
.service(counter_events)
|
||||
.route("/api/{tail:.*}", leptos_actix::handle_server_fns())
|
||||
.leptos_routes(leptos_options.to_owned(), routes.to_owned(), || view! { <Counters/> })
|
||||
.service(Files::new("/", site_root))
|
||||
//.wrap(middleware::Compress::default())
|
||||
})
|
||||
.bind(&addr)?
|
||||
.run()
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
// client-only main for Trunk
|
||||
else {
|
||||
pub fn main() {
|
||||
// isomorphic counters cannot work in a Client-Side-Rendered only
|
||||
// app as a server is required to maintain state
|
||||
}
|
||||
}
|
||||
let stream = futures::stream::once(async {
|
||||
crate::counters::get_server_count().await.unwrap_or(0)
|
||||
})
|
||||
.chain(COUNT_CHANNEL.clone())
|
||||
.map(|value| {
|
||||
Ok(web::Bytes::from(format!(
|
||||
"event: message\ndata: {value}\n\n"
|
||||
))) as Result<web::Bytes>
|
||||
});
|
||||
HttpResponse::Ok()
|
||||
.insert_header(("Content-Type", "text/event-stream"))
|
||||
.streaming(stream)
|
||||
}
|
||||
|
||||
#[actix_web::main]
|
||||
async fn main() -> std::io::Result<()> {
|
||||
// Setting this to None means we'll be using cargo-leptos and its env vars.
|
||||
// when not using cargo-leptos None must be replaced with Some("Cargo.toml")
|
||||
let conf = get_configuration(None).await.unwrap();
|
||||
|
||||
let addr = conf.leptos_options.site_addr;
|
||||
let routes = generate_route_list(Counters);
|
||||
|
||||
HttpServer::new(move || {
|
||||
let leptos_options = &conf.leptos_options;
|
||||
let site_root = &leptos_options.site_root;
|
||||
|
||||
App::new()
|
||||
.service(counter_events)
|
||||
.leptos_routes(
|
||||
leptos_options.to_owned(),
|
||||
routes.to_owned(),
|
||||
Counters,
|
||||
)
|
||||
.service(Files::new("/", site_root))
|
||||
//.wrap(middleware::Compress::default())
|
||||
})
|
||||
.bind(&addr)?
|
||||
.run()
|
||||
.await
|
||||
}
|
||||
|
|
|
@ -7,22 +7,21 @@ edition = "2021"
|
|||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[dependencies]
|
||||
console_log = "1.0.0"
|
||||
console_error_panic_hook = "0.1.7"
|
||||
cfg-if = "1.0.0"
|
||||
console_log = "1.0"
|
||||
console_error_panic_hook = "0.1"
|
||||
leptos = { path = "../../leptos", features = ["nightly"] }
|
||||
leptos_axum = { path = "../../integrations/axum", optional = true }
|
||||
leptos_meta = { path = "../../meta" }
|
||||
leptos_router = { path = "../../router" }
|
||||
log = "0.4.17"
|
||||
log = "0.4"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
simple_logger = "4.0.0"
|
||||
axum = { version = "0.6.1", optional = true }
|
||||
tower = { version = "0.4.13", optional = true }
|
||||
tower-http = { version = "0.4", features = ["fs"], optional = true }
|
||||
tokio = { version = "1.22.0", features = ["full"], optional = true }
|
||||
http = { version = "0.2.8" }
|
||||
thiserror = "1.0.38"
|
||||
simple_logger = "4.0"
|
||||
axum = { version = "0.7", optional = true }
|
||||
tower = { version = "0.4", optional = true }
|
||||
tower-http = { version = "0.5", features = ["fs"], optional = true }
|
||||
tokio = { version = "1", features = ["full"], optional = true }
|
||||
http = { version = "1.0" }
|
||||
thiserror = "1.0"
|
||||
wasm-bindgen = "0.2"
|
||||
|
||||
[features]
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
use crate::errors::AppError;
|
||||
use cfg_if::cfg_if;
|
||||
use leptos::{logging::log, Errors, *};
|
||||
#[cfg(feature = "ssr")]
|
||||
use leptos_axum::ResponseOptions;
|
||||
|
@ -30,12 +29,13 @@ pub fn ErrorTemplate(
|
|||
|
||||
// Only the response code for the first error is actually sent from the server
|
||||
// this may be customized by the specific application
|
||||
cfg_if! { if #[cfg(feature="ssr")] {
|
||||
#[cfg(feature = "ssr")]
|
||||
{
|
||||
let response = use_context::<ResponseOptions>();
|
||||
if let Some(response) = response {
|
||||
response.set_status(errors[0].status_code());
|
||||
}
|
||||
}}
|
||||
}
|
||||
|
||||
view! {
|
||||
<h1>{if errors.len() > 1 {"Errors"} else {"Error"}}</h1>
|
||||
|
|
|
@ -1,43 +1,48 @@
|
|||
use cfg_if::cfg_if;
|
||||
use crate::landing::App;
|
||||
use axum::{
|
||||
body::Body,
|
||||
extract::State,
|
||||
http::{Request, Response, StatusCode, Uri},
|
||||
response::{IntoResponse, Response as AxumResponse},
|
||||
};
|
||||
use leptos::{view, LeptosOptions};
|
||||
use tower::ServiceExt;
|
||||
use tower_http::services::ServeDir;
|
||||
|
||||
cfg_if! { if #[cfg(feature = "ssr")] {
|
||||
use axum::{
|
||||
body::{boxed, Body, BoxBody},
|
||||
extract::State,
|
||||
response::IntoResponse,
|
||||
http::{Request, Response, StatusCode, Uri},
|
||||
};
|
||||
use axum::response::Response as AxumResponse;
|
||||
use tower::ServiceExt;
|
||||
use tower_http::services::ServeDir;
|
||||
use leptos::{LeptosOptions, view};
|
||||
use crate::landing::App;
|
||||
pub async fn file_and_error_handler(
|
||||
uri: Uri,
|
||||
State(options): State<LeptosOptions>,
|
||||
req: Request<Body>,
|
||||
) -> AxumResponse {
|
||||
let root = options.site_root.clone();
|
||||
let res = get_static_file(uri.clone(), &root).await.unwrap();
|
||||
|
||||
pub async fn file_and_error_handler(uri: Uri, State(options): State<LeptosOptions>, req: Request<Body>) -> AxumResponse {
|
||||
let root = options.site_root.clone();
|
||||
let res = get_static_file(uri.clone(), &root).await.unwrap();
|
||||
|
||||
if res.status() == StatusCode::OK {
|
||||
res.into_response()
|
||||
} else{
|
||||
let handler = leptos_axum::render_app_to_stream(
|
||||
options.to_owned(),
|
||||
move || view!{ <App/> }
|
||||
);
|
||||
handler(req).await.into_response()
|
||||
}
|
||||
if res.status() == StatusCode::OK {
|
||||
res.into_response()
|
||||
} else {
|
||||
let handler = leptos_axum::render_app_to_stream(
|
||||
options.to_owned(),
|
||||
move || view! { <App/> },
|
||||
);
|
||||
handler(req).await.into_response()
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_static_file(uri: Uri, root: &str) -> Result<Response<BoxBody>, (StatusCode, String)> {
|
||||
let req = Request::builder().uri(uri.clone()).body(Body::empty()).unwrap();
|
||||
// `ServeDir` implements `tower::Service` so we can call it with `tower::ServiceExt::oneshot`
|
||||
// This path is relative to the cargo root
|
||||
match ServeDir::new(root).oneshot(req).await {
|
||||
Ok(res) => Ok(res.map(boxed)),
|
||||
Err(err) => Err((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("Something went wrong: {err}"),
|
||||
)),
|
||||
}
|
||||
async fn get_static_file(
|
||||
uri: Uri,
|
||||
root: &str,
|
||||
) -> Result<Response<Body>, (StatusCode, String)> {
|
||||
let req = Request::builder()
|
||||
.uri(uri.clone())
|
||||
.body(Body::empty())
|
||||
.unwrap();
|
||||
// `ServeDir` implements `tower::Service` so we can call it with `tower::ServiceExt::oneshot`
|
||||
// This path is relative to the cargo root
|
||||
match ServeDir::new(root).oneshot(req).await {
|
||||
Ok(res) => Ok(res.into_response()),
|
||||
Err(err) => Err((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("Something went wrong: {err}"),
|
||||
)),
|
||||
}
|
||||
}}
|
||||
}
|
||||
|
|
|
@ -1,24 +1,21 @@
|
|||
use cfg_if::cfg_if;
|
||||
pub mod error_template;
|
||||
pub mod errors;
|
||||
#[cfg(feature = "ssr")]
|
||||
pub mod fallback;
|
||||
pub mod landing;
|
||||
|
||||
// Needs to be in lib.rs AFAIK because wasm-bindgen needs us to be compiling a lib. I may be wrong.
|
||||
cfg_if! {
|
||||
if #[cfg(feature = "hydrate")] {
|
||||
use leptos::*;
|
||||
use wasm_bindgen::prelude::wasm_bindgen;
|
||||
use crate::landing::*;
|
||||
use wasm_bindgen::prelude::wasm_bindgen;
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn hydrate() {
|
||||
_ = console_log::init_with_level(log::Level::Debug);
|
||||
console_error_panic_hook::set_once();
|
||||
#[cfg(feature = "hydrate")]
|
||||
#[wasm_bindgen]
|
||||
pub fn hydrate() {
|
||||
use crate::landing::*;
|
||||
use leptos::*;
|
||||
|
||||
leptos::mount_to_body(|| {
|
||||
view! { <App/> }
|
||||
});
|
||||
}
|
||||
}
|
||||
_ = console_log::init_with_level(log::Level::Debug);
|
||||
console_error_panic_hook::set_once();
|
||||
|
||||
leptos::mount_to_body(|| {
|
||||
view! { <App/> }
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,41 +1,39 @@
|
|||
use cfg_if::cfg_if;
|
||||
|
||||
cfg_if! { if #[cfg(feature = "ssr")] {
|
||||
use crate::fallback::file_and_error_handler;
|
||||
use crate::landing::*;
|
||||
use axum::body::Body as AxumBody;
|
||||
use axum::{
|
||||
extract::{State, Path},
|
||||
#[cfg(feature = "ssr")]
|
||||
mod ssr_imports {
|
||||
pub use axum::{
|
||||
body::Body as AxumBody,
|
||||
extract::{Path, State},
|
||||
http::Request,
|
||||
response::{IntoResponse, Response},
|
||||
routing::{get, post},
|
||||
routing::get,
|
||||
Router,
|
||||
};
|
||||
use errors_axum::*;
|
||||
use leptos::{logging::log, *};
|
||||
use leptos_axum::{generate_route_list, LeptosRoutes};
|
||||
}}
|
||||
pub use errors_axum::{fallback::*, landing::App};
|
||||
pub use leptos::{logging::log, *};
|
||||
pub use leptos_axum::{generate_route_list, LeptosRoutes};
|
||||
|
||||
//Define a handler to test extractor with state
|
||||
#[cfg(feature = "ssr")]
|
||||
async fn custom_handler(
|
||||
Path(id): Path<String>,
|
||||
State(options): State<LeptosOptions>,
|
||||
req: Request<AxumBody>,
|
||||
) -> Response {
|
||||
let handler = leptos_axum::render_app_to_stream_with_context(
|
||||
options.clone(),
|
||||
move || {
|
||||
provide_context(id.clone());
|
||||
},
|
||||
App,
|
||||
);
|
||||
handler(req).await.into_response()
|
||||
// This custom handler lets us provide Axum State via context
|
||||
pub async fn custom_handler(
|
||||
Path(id): Path<String>,
|
||||
State(options): State<LeptosOptions>,
|
||||
req: Request<AxumBody>,
|
||||
) -> Response {
|
||||
let handler = leptos_axum::render_app_to_stream_with_context(
|
||||
options.clone(),
|
||||
move || {
|
||||
provide_context(id.clone());
|
||||
},
|
||||
App,
|
||||
);
|
||||
handler(req).await.into_response()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
use ssr_imports::*;
|
||||
|
||||
simple_logger::init_with_level(log::Level::Debug)
|
||||
.expect("couldn't initialize logging");
|
||||
|
||||
|
@ -52,7 +50,6 @@ async fn main() {
|
|||
|
||||
// build our application with a route
|
||||
let app = Router::new()
|
||||
.route("/api/*fn_name", post(leptos_axum::handle_server_fns))
|
||||
.route("/special/:id", get(custom_handler))
|
||||
.leptos_routes(&leptos_options, routes, App)
|
||||
.fallback(file_and_error_handler)
|
||||
|
@ -61,8 +58,8 @@ async fn main() {
|
|||
// run our app with hyper
|
||||
// `axum::Server` is a re-export of `hyper::Server`
|
||||
log!("listening on http://{}", &addr);
|
||||
axum::Server::bind(&addr)
|
||||
.serve(app.into_make_service())
|
||||
let listener = tokio::net::TcpListener::bind(&addr).await.unwrap();
|
||||
axum::serve(listener, app.into_make_service())
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
@ -71,5 +68,5 @@ async fn main() {
|
|||
#[cfg(not(feature = "ssr"))]
|
||||
pub fn main() {
|
||||
// This example cannot be built as a trunk standalone CSR-only app.
|
||||
// The server is needed to demonstrate the error statuses.
|
||||
// The server is needed to demonstrate the error statuses.
|
||||
}
|
||||
|
|
|
@ -15,7 +15,6 @@ actix-files = { version = "0.6", optional = true }
|
|||
actix-web = { version = "4", optional = true, features = ["macros"] }
|
||||
console_log = "1"
|
||||
console_error_panic_hook = "0.1"
|
||||
cfg-if = "1"
|
||||
leptos = { path = "../../leptos", features = ["nightly"] }
|
||||
leptos_meta = { path = "../../meta", features = ["nightly"] }
|
||||
leptos_actix = { path = "../../integrations/actix", optional = true }
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
use cfg_if::cfg_if;
|
||||
use leptos::*;
|
||||
use leptos_meta::*;
|
||||
use leptos_router::*;
|
||||
|
@ -33,16 +32,10 @@ pub fn App() -> impl IntoView {
|
|||
}
|
||||
}
|
||||
|
||||
// Needs to be in lib.rs AFAIK because wasm-bindgen needs us to be compiling a lib. I may be wrong.
|
||||
cfg_if! {
|
||||
if #[cfg(feature = "hydrate")] {
|
||||
use wasm_bindgen::prelude::wasm_bindgen;
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn hydrate() {
|
||||
_ = console_log::init_with_level(log::Level::Debug);
|
||||
console_error_panic_hook::set_once();
|
||||
leptos::mount_to_body(App);
|
||||
}
|
||||
}
|
||||
#[cfg(feature = "hydrate")]
|
||||
#[wasm_bindgen::prelude::wasm_bindgen]
|
||||
pub fn hydrate() {
|
||||
_ = console_log::init_with_level(log::Level::Debug);
|
||||
console_error_panic_hook::set_once();
|
||||
leptos::mount_to_body(App);
|
||||
}
|
||||
|
|
|
@ -1,56 +1,56 @@
|
|||
use cfg_if::cfg_if;
|
||||
use leptos::*;
|
||||
// server-only stuff
|
||||
#[cfg(feature = "ssr")]
|
||||
mod ssr_imports {
|
||||
pub use actix_files::Files;
|
||||
pub use actix_web::*;
|
||||
pub use hackernews::App;
|
||||
pub use leptos_actix::{generate_route_list, LeptosRoutes};
|
||||
|
||||
// boilerplate to run in different modes
|
||||
cfg_if! {
|
||||
// server-only stuff
|
||||
if #[cfg(feature = "ssr")] {
|
||||
use actix_files::{Files};
|
||||
use actix_web::*;
|
||||
use hackernews::{App};
|
||||
use leptos_actix::{LeptosRoutes, generate_route_list};
|
||||
|
||||
#[get("/style.css")]
|
||||
async fn css() -> impl Responder {
|
||||
actix_files::NamedFile::open_async("./style.css").await
|
||||
}
|
||||
#[get("/favicon.ico")]
|
||||
async fn favicon() -> impl Responder {
|
||||
actix_files::NamedFile::open_async("./target/site//favicon.ico").await
|
||||
}
|
||||
|
||||
#[actix_web::main]
|
||||
async fn main() -> std::io::Result<()> {
|
||||
// Setting this to None means we'll be using cargo-leptos and its env vars.
|
||||
let conf = get_configuration(None).await.unwrap();
|
||||
|
||||
let addr = conf.leptos_options.site_addr;
|
||||
// Generate the list of routes in your Leptos App
|
||||
let routes = generate_route_list(App);
|
||||
|
||||
HttpServer::new(move || {
|
||||
let leptos_options = &conf.leptos_options;
|
||||
let site_root = &leptos_options.site_root;
|
||||
|
||||
App::new()
|
||||
.service(css)
|
||||
.service(favicon)
|
||||
.route("/api/{tail:.*}", leptos_actix::handle_server_fns())
|
||||
.leptos_routes(leptos_options.to_owned(), routes.to_owned(), App)
|
||||
.service(Files::new("/", site_root))
|
||||
//.wrap(middleware::Compress::default())
|
||||
})
|
||||
.bind(&addr)?
|
||||
.run()
|
||||
.await
|
||||
}
|
||||
} else {
|
||||
fn main() {
|
||||
use hackernews::{App};
|
||||
|
||||
_ = console_log::init_with_level(log::Level::Debug);
|
||||
console_error_panic_hook::set_once();
|
||||
mount_to_body(App)
|
||||
}
|
||||
#[get("/style.css")]
|
||||
pub async fn css() -> impl Responder {
|
||||
actix_files::NamedFile::open_async("./style.css").await
|
||||
}
|
||||
#[get("/favicon.ico")]
|
||||
pub async fn favicon() -> impl Responder {
|
||||
actix_files::NamedFile::open_async("./target/site//favicon.ico").await
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
#[actix_web::main]
|
||||
async fn main() -> std::io::Result<()> {
|
||||
use leptos::get_configuration;
|
||||
use ssr_imports::*;
|
||||
|
||||
// Setting this to None means we'll be using cargo-leptos and its env vars.
|
||||
let conf = get_configuration(None).await.unwrap();
|
||||
|
||||
let addr = conf.leptos_options.site_addr;
|
||||
// Generate the list of routes in your Leptos App
|
||||
let routes = generate_route_list(App);
|
||||
|
||||
HttpServer::new(move || {
|
||||
let leptos_options = &conf.leptos_options;
|
||||
let site_root = &leptos_options.site_root;
|
||||
|
||||
App::new()
|
||||
.service(css)
|
||||
.service(favicon)
|
||||
.leptos_routes(leptos_options.to_owned(), routes.to_owned(), App)
|
||||
.service(Files::new("/", site_root))
|
||||
//.wrap(middleware::Compress::default())
|
||||
})
|
||||
.bind(&addr)?
|
||||
.run()
|
||||
.await
|
||||
}
|
||||
|
||||
// CSR-only setup
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
fn main() {
|
||||
use hackernews::App;
|
||||
|
||||
_ = console_log::init_with_level(log::Level::Debug);
|
||||
console_error_panic_hook::set_once();
|
||||
mount_to_body(App)
|
||||
}
|
||||
|
|
|
@ -11,24 +11,23 @@ codegen-units = 1
|
|||
lto = true
|
||||
|
||||
[dependencies]
|
||||
console_log = "1.0.0"
|
||||
console_error_panic_hook = "0.1.7"
|
||||
cfg-if = "1.0.0"
|
||||
console_log = "1.0"
|
||||
console_error_panic_hook = "0.1"
|
||||
leptos = { path = "../../leptos", features = ["nightly"] }
|
||||
leptos_axum = { path = "../../integrations/axum", optional = true }
|
||||
leptos_meta = { path = "../../meta", features = ["nightly"] }
|
||||
leptos_router = { path = "../../router", features = ["nightly"] }
|
||||
log = "0.4.17"
|
||||
simple_logger = "4.0.0"
|
||||
serde = { version = "1.0.148", features = ["derive"] }
|
||||
log = "0.4"
|
||||
simple_logger = "4.0"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
tracing = "0.1"
|
||||
gloo-net = { version = "0.2.5", features = ["http"] }
|
||||
reqwest = { version = "0.11.13", features = ["json"] }
|
||||
axum = { version = "0.6.1", optional = true }
|
||||
tower = { version = "0.4.13", optional = true }
|
||||
tower-http = { version = "0.4", features = ["fs"], optional = true }
|
||||
tokio = { version = "1.22.0", features = ["full"], optional = true }
|
||||
http = { version = "0.2.11", optional = true }
|
||||
gloo-net = { version = "0.4", features = ["http"] }
|
||||
reqwest = { version = "0.11", features = ["json"] }
|
||||
axum = { version = "0.7", optional = true }
|
||||
tower = { version = "0.4", optional = true }
|
||||
tower-http = { version = "0.5", features = ["fs"], optional = true }
|
||||
tokio = { version = "1", features = ["full"], optional = true }
|
||||
http = { version = "1.0", optional = true }
|
||||
web-sys = { version = "0.3", features = ["AbortController", "AbortSignal"] }
|
||||
wasm-bindgen = "0.2"
|
||||
|
||||
|
|
|
@ -1,44 +1,48 @@
|
|||
use cfg_if::cfg_if;
|
||||
use crate::error_template::error_template;
|
||||
use axum::{
|
||||
body::Body,
|
||||
extract::State,
|
||||
http::{Request, Response, StatusCode, Uri},
|
||||
response::{IntoResponse, Response as AxumResponse},
|
||||
};
|
||||
use leptos::LeptosOptions;
|
||||
use tower::ServiceExt;
|
||||
use tower_http::services::ServeDir;
|
||||
|
||||
cfg_if! {
|
||||
if #[cfg(feature = "ssr")] {
|
||||
use axum::{
|
||||
body::{boxed, Body, BoxBody},
|
||||
extract::State,
|
||||
response::IntoResponse,
|
||||
http::{Request, Response, StatusCode, Uri},
|
||||
};
|
||||
use axum::response::Response as AxumResponse;
|
||||
use tower::ServiceExt;
|
||||
use tower_http::services::ServeDir;
|
||||
use leptos::{LeptosOptions};
|
||||
use crate::error_template::error_template;
|
||||
pub async fn file_and_error_handler(
|
||||
uri: Uri,
|
||||
State(options): State<LeptosOptions>,
|
||||
req: Request<Body>,
|
||||
) -> AxumResponse {
|
||||
let root = options.site_root.clone();
|
||||
let res = get_static_file(uri.clone(), &root).await.unwrap();
|
||||
|
||||
pub async fn file_and_error_handler(uri: Uri, State(options): State<LeptosOptions>, req: Request<Body>) -> AxumResponse {
|
||||
let root = options.site_root.clone();
|
||||
let res = get_static_file(uri.clone(), &root).await.unwrap();
|
||||
|
||||
if res.status() == StatusCode::OK {
|
||||
res.into_response()
|
||||
} else{
|
||||
let handler = leptos_axum::render_app_to_stream(options.to_owned(), || error_template( None));
|
||||
handler(req).await.into_response()
|
||||
}
|
||||
if res.status() == StatusCode::OK {
|
||||
res.into_response()
|
||||
} else {
|
||||
let handler =
|
||||
leptos_axum::render_app_to_stream(options.to_owned(), || {
|
||||
error_template(None)
|
||||
});
|
||||
handler(req).await.into_response()
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_static_file(uri: Uri, root: &str) -> Result<Response<BoxBody>, (StatusCode, String)> {
|
||||
let req = Request::builder().uri(uri.clone()).body(Body::empty()).unwrap();
|
||||
// `ServeDir` implements `tower::Service` so we can call it with `tower::ServiceExt::oneshot`
|
||||
// This path is relative to the cargo root
|
||||
match ServeDir::new(root).oneshot(req).await {
|
||||
Ok(res) => Ok(res.map(boxed)),
|
||||
Err(err) => Err((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("Something went wrong: {}", err),
|
||||
)),
|
||||
}
|
||||
async fn get_static_file(
|
||||
uri: Uri,
|
||||
root: &str,
|
||||
) -> Result<Response<Body>, (StatusCode, String)> {
|
||||
let req = Request::builder()
|
||||
.uri(uri.clone())
|
||||
.body(Body::empty())
|
||||
.unwrap();
|
||||
// `ServeDir` implements `tower::Service` so we can call it with `tower::ServiceExt::oneshot`
|
||||
// This path is relative to the cargo root
|
||||
match ServeDir::new(root).oneshot(req).await {
|
||||
Ok(res) => Ok(res.into_response()),
|
||||
Err(err) => Err((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("Something went wrong: {}", err),
|
||||
)),
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,63 +1,68 @@
|
|||
use cfg_if::cfg_if;
|
||||
use axum::{
|
||||
body::Body,
|
||||
http::{Request, Response, StatusCode, Uri},
|
||||
response::IntoResponse,
|
||||
};
|
||||
use tower::ServiceExt;
|
||||
use tower_http::services::ServeDir;
|
||||
|
||||
cfg_if! {
|
||||
if #[cfg(feature = "ssr")] {
|
||||
use axum::{
|
||||
body::{boxed, Body, BoxBody},
|
||||
http::{Request, Response, StatusCode, Uri},
|
||||
};
|
||||
use tower::ServiceExt;
|
||||
use tower_http::services::ServeDir;
|
||||
pub async fn file_handler(
|
||||
uri: Uri,
|
||||
) -> Result<Response<Body>, (StatusCode, String)> {
|
||||
let res = get_static_file(uri.clone(), "/pkg").await?;
|
||||
|
||||
pub async fn file_handler(uri: Uri) -> Result<Response<BoxBody>, (StatusCode, String)> {
|
||||
let res = get_static_file(uri.clone(), "/pkg").await?;
|
||||
|
||||
if res.status() == StatusCode::NOT_FOUND {
|
||||
// try with `.html`
|
||||
// TODO: handle if the Uri has query parameters
|
||||
match format!("{}.html", uri).parse() {
|
||||
Ok(uri_html) => get_static_file(uri_html, "/pkg").await,
|
||||
Err(_) => Err((StatusCode::INTERNAL_SERVER_ERROR, "Invalid URI".to_string())),
|
||||
}
|
||||
} else {
|
||||
Ok(res)
|
||||
if res.status() == StatusCode::NOT_FOUND {
|
||||
// try with `.html`
|
||||
// TODO: handle if the Uri has query parameters
|
||||
match format!("{}.html", uri).parse() {
|
||||
Ok(uri_html) => get_static_file(uri_html, "/pkg").await,
|
||||
Err(_) => Err((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
"Invalid URI".to_string(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_static_file_handler(uri: Uri) -> Result<Response<BoxBody>, (StatusCode, String)> {
|
||||
let res = get_static_file(uri.clone(), "/static").await?;
|
||||
|
||||
if res.status() == StatusCode::NOT_FOUND {
|
||||
Err((StatusCode::INTERNAL_SERVER_ERROR, "Invalid URI".to_string()))
|
||||
} else {
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_static_file(uri: Uri, base: &str) -> Result<Response<BoxBody>, (StatusCode, String)> {
|
||||
let req = Request::builder().uri(&uri).body(Body::empty()).unwrap();
|
||||
|
||||
// `ServeDir` implements `tower::Service` so we can call it with `tower::ServiceExt::oneshot`
|
||||
// When run normally, the root should be the crate root
|
||||
if base == "/static" {
|
||||
match ServeDir::new("./static").oneshot(req).await {
|
||||
Ok(res) => Ok(res.map(boxed)),
|
||||
Err(err) => Err((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("Something went wrong: {}", err),
|
||||
))
|
||||
}
|
||||
} else if base == "/pkg" {
|
||||
match ServeDir::new("./pkg").oneshot(req).await {
|
||||
Ok(res) => Ok(res.map(boxed)),
|
||||
Err(err) => Err((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("Something went wrong: {}", err),
|
||||
)),
|
||||
}
|
||||
} else{
|
||||
Err((StatusCode::NOT_FOUND, "Not Found".to_string()))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_static_file_handler(
|
||||
uri: Uri,
|
||||
) -> Result<Response<Body>, (StatusCode, String)> {
|
||||
let res = get_static_file(uri.clone(), "/static").await?;
|
||||
|
||||
if res.status() == StatusCode::NOT_FOUND {
|
||||
Err((StatusCode::INTERNAL_SERVER_ERROR, "Invalid URI".to_string()))
|
||||
} else {
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_static_file(
|
||||
uri: Uri,
|
||||
base: &str,
|
||||
) -> Result<Response<Body>, (StatusCode, String)> {
|
||||
let req = Request::builder().uri(&uri).body(Body::empty()).unwrap();
|
||||
|
||||
// `ServeDir` implements `tower::Service` so we can call it with `tower::ServiceExt::oneshot`
|
||||
// When run normally, the root should be the crate root
|
||||
if base == "/static" {
|
||||
match ServeDir::new("./static").oneshot(req).await {
|
||||
Ok(res) => Ok(res.into_response()),
|
||||
Err(err) => Err((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("Something went wrong: {}", err),
|
||||
)),
|
||||
}
|
||||
} else if base == "/pkg" {
|
||||
match ServeDir::new("./pkg").oneshot(req).await {
|
||||
Ok(res) => Ok(res.into_response()),
|
||||
Err(err) => Err((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("Something went wrong: {}", err),
|
||||
)),
|
||||
}
|
||||
} else {
|
||||
Err((StatusCode::NOT_FOUND, "Not Found".to_string()))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
use cfg_if::cfg_if;
|
||||
use leptos::{component, view, IntoView};
|
||||
use leptos_meta::*;
|
||||
use leptos_router::*;
|
||||
mod api;
|
||||
pub mod error_template;
|
||||
#[cfg(feature = "ssr")]
|
||||
pub mod fallback;
|
||||
#[cfg(feature = "ssr")]
|
||||
pub mod handlers;
|
||||
mod routes;
|
||||
use routes::{nav::*, stories::*, story::*, users::*};
|
||||
|
@ -12,38 +13,28 @@ use routes::{nav::*, stories::*, story::*, users::*};
|
|||
#[component]
|
||||
pub fn App() -> impl IntoView {
|
||||
provide_meta_context();
|
||||
|
||||
view! {
|
||||
|
||||
<>
|
||||
<Link rel="shortcut icon" type_="image/ico" href="/favicon.ico"/>
|
||||
<Stylesheet id="leptos" href="/pkg/hackernews_axum.css"/>
|
||||
<Meta name="description" content="Leptos implementation of a HackerNews demo."/>
|
||||
<Router>
|
||||
<Nav />
|
||||
<main>
|
||||
<Routes>
|
||||
<Route path="users/:id" view=User/>
|
||||
<Route path="stories/:id" view=Story/>
|
||||
<Route path=":stories?" view=Stories/>
|
||||
</Routes>
|
||||
</main>
|
||||
</Router>
|
||||
</>
|
||||
<Link rel="shortcut icon" type_="image/ico" href="/favicon.ico"/>
|
||||
<Stylesheet id="leptos" href="/pkg/hackernews_axum.css"/>
|
||||
<Meta name="description" content="Leptos implementation of a HackerNews demo."/>
|
||||
<Router>
|
||||
<Nav />
|
||||
<main>
|
||||
<Routes>
|
||||
<Route path="users/:id" view=User/>
|
||||
<Route path="stories/:id" view=Story/>
|
||||
<Route path=":stories?" view=Stories/>
|
||||
</Routes>
|
||||
</main>
|
||||
</Router>
|
||||
}
|
||||
}
|
||||
|
||||
// Needs to be in lib.rs AFAIK because wasm-bindgen needs us to be compiling a lib. I may be wrong.
|
||||
cfg_if! {
|
||||
if #[cfg(feature = "hydrate")] {
|
||||
use wasm_bindgen::prelude::wasm_bindgen;
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn hydrate() {
|
||||
_ = console_log::init_with_level(log::Level::Debug);
|
||||
console_error_panic_hook::set_once();
|
||||
leptos::mount_to_body(move || {
|
||||
view! { <App/> }
|
||||
});
|
||||
}
|
||||
}
|
||||
#[cfg(feature = "hydrate")]
|
||||
#[wasm_bindgen::prelude::wasm_bindgen]
|
||||
pub fn hydrate() {
|
||||
_ = console_log::init_with_level(log::Level::Debug);
|
||||
console_error_panic_hook::set_once();
|
||||
leptos::mount_to_body(App);
|
||||
}
|
||||
|
|
|
@ -1,54 +1,41 @@
|
|||
use cfg_if::cfg_if;
|
||||
use leptos::{logging::log, *};
|
||||
|
||||
// boilerplate to run in different modes
|
||||
cfg_if! {
|
||||
if #[cfg(feature = "ssr")] {
|
||||
use axum::{
|
||||
Router,
|
||||
routing::get,
|
||||
};
|
||||
#[cfg(feature = "ssr")]
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
use axum::{routing::get, Router};
|
||||
use hackernews_axum::{fallback::file_and_error_handler, *};
|
||||
use leptos::get_configuration;
|
||||
use leptos_axum::{generate_route_list, LeptosRoutes};
|
||||
use hackernews_axum::fallback::file_and_error_handler;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
use hackernews_axum::*;
|
||||
let conf = get_configuration(Some("Cargo.toml")).await.unwrap();
|
||||
let leptos_options = conf.leptos_options;
|
||||
let addr = leptos_options.site_addr;
|
||||
let routes = generate_route_list(App);
|
||||
|
||||
let conf = get_configuration(Some("Cargo.toml")).await.unwrap();
|
||||
let leptos_options = conf.leptos_options;
|
||||
let addr = leptos_options.site_addr;
|
||||
let routes = generate_route_list(App);
|
||||
simple_logger::init_with_level(log::Level::Debug)
|
||||
.expect("couldn't initialize logging");
|
||||
|
||||
simple_logger::init_with_level(log::Level::Debug).expect("couldn't initialize logging");
|
||||
|
||||
// build our application with a route
|
||||
let app = Router::new()
|
||||
// build our application with a route
|
||||
let app = Router::new()
|
||||
.route("/favicon.ico", get(file_and_error_handler))
|
||||
.leptos_routes(&leptos_options, routes, || view! { <App/> } )
|
||||
.leptos_routes(&leptos_options, routes, App)
|
||||
.fallback(file_and_error_handler)
|
||||
.with_state(leptos_options);
|
||||
|
||||
// run our app with hyper
|
||||
// `axum::Server` is a re-export of `hyper::Server`
|
||||
log!("listening on {}", addr);
|
||||
axum::Server::bind(&addr)
|
||||
.serve(app.into_make_service())
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
// run our app with hyper
|
||||
// `axum::Server` is a re-export of `hyper::Server`
|
||||
println!("listening on {}", addr);
|
||||
let listener = tokio::net::TcpListener::bind(&addr).await.unwrap();
|
||||
axum::serve(listener, app.into_make_service())
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
// client-only stuff for Trunk
|
||||
else {
|
||||
use hackernews_axum::*;
|
||||
// client-only stuff for Trunk
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
pub fn main() {
|
||||
use hackernews_axum::*;
|
||||
|
||||
pub fn main() {
|
||||
_ = console_log::init_with_level(log::Level::Debug);
|
||||
console_error_panic_hook::set_once();
|
||||
mount_to_body(|| {
|
||||
view! { <App/> }
|
||||
});
|
||||
}
|
||||
}
|
||||
_ = console_log::init_with_level(log::Level::Debug);
|
||||
console_error_panic_hook::set_once();
|
||||
mount_to_body(App);
|
||||
}
|
||||
|
|
|
@ -11,9 +11,8 @@ codegen-units = 1
|
|||
lto = true
|
||||
|
||||
[dependencies]
|
||||
console_log = "1.0.0"
|
||||
console_error_panic_hook = "0.1.7"
|
||||
cfg-if = "1.0.0"
|
||||
console_log = "1.0"
|
||||
console_error_panic_hook = "0.1"
|
||||
leptos = { path = "../../leptos", features = [
|
||||
"nightly",
|
||||
"experimental-islands",
|
||||
|
@ -23,20 +22,20 @@ leptos_axum = { path = "../../integrations/axum", optional = true, features = [
|
|||
] }
|
||||
leptos_meta = { path = "../../meta", features = ["nightly"] }
|
||||
leptos_router = { path = "../../router", features = ["nightly"] }
|
||||
log = "0.4.17"
|
||||
simple_logger = "4.0.0"
|
||||
serde = { version = "1.0.148", features = ["derive"] }
|
||||
log = "0.4"
|
||||
simple_logger = "4.0"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
tracing = "0.1"
|
||||
gloo-net = { version = "0.2.5", features = ["http"] }
|
||||
reqwest = { version = "0.11.13", features = ["json"] }
|
||||
axum = { version = "0.6.1", optional = true, features = ["http2"] }
|
||||
tower = { version = "0.4.13", optional = true }
|
||||
tower-http = { version = "0.4", features = [
|
||||
gloo-net = { version = "0.4", features = ["http"] }
|
||||
reqwest = { version = "0.11", features = ["json"] }
|
||||
axum = { version = "0.7", optional = true, features = ["http2"] }
|
||||
tower = { version = "0.4", optional = true }
|
||||
tower-http = { version = "0.5", features = [
|
||||
"fs",
|
||||
"compression-br",
|
||||
], optional = true }
|
||||
tokio = { version = "1.22.0", features = ["full"], optional = true }
|
||||
http = { version = "0.2.11", optional = true }
|
||||
tokio = { version = "1", features = ["full"], optional = true }
|
||||
http = { version = "1.0", optional = true }
|
||||
web-sys = { version = "0.3", features = ["AbortController", "AbortSignal"] }
|
||||
wasm-bindgen = "0.2"
|
||||
lazy_static = "1.4.0"
|
||||
|
|
|
@ -1,44 +1,48 @@
|
|||
use cfg_if::cfg_if;
|
||||
use crate::error_template::error_template;
|
||||
use axum::{
|
||||
body::Body,
|
||||
extract::State,
|
||||
http::{Request, Response, StatusCode, Uri},
|
||||
response::{IntoResponse, Response as AxumResponse},
|
||||
};
|
||||
use leptos::LeptosOptions;
|
||||
use tower::ServiceExt;
|
||||
use tower_http::services::ServeDir;
|
||||
|
||||
cfg_if! {
|
||||
if #[cfg(feature = "ssr")] {
|
||||
use axum::{
|
||||
body::{boxed, Body, BoxBody},
|
||||
extract::State,
|
||||
response::IntoResponse,
|
||||
http::{Request, Response, StatusCode, Uri},
|
||||
};
|
||||
use axum::response::Response as AxumResponse;
|
||||
use tower::ServiceExt;
|
||||
use tower_http::services::ServeDir;
|
||||
use leptos::{LeptosOptions};
|
||||
use crate::error_template::error_template;
|
||||
pub async fn file_and_error_handler(
|
||||
uri: Uri,
|
||||
State(options): State<LeptosOptions>,
|
||||
req: Request<Body>,
|
||||
) -> AxumResponse {
|
||||
let root = options.site_root.clone();
|
||||
let res = get_static_file(uri.clone(), &root).await.unwrap();
|
||||
|
||||
pub async fn file_and_error_handler(uri: Uri, State(options): State<LeptosOptions>, req: Request<Body>) -> AxumResponse {
|
||||
let root = options.site_root.clone();
|
||||
let res = get_static_file(uri.clone(), &root).await.unwrap();
|
||||
|
||||
if res.status() == StatusCode::OK {
|
||||
res.into_response()
|
||||
} else{
|
||||
let handler = leptos_axum::render_app_to_stream(options.to_owned(), || error_template( None));
|
||||
handler(req).await.into_response()
|
||||
}
|
||||
if res.status() == StatusCode::OK {
|
||||
res.into_response()
|
||||
} else {
|
||||
let handler =
|
||||
leptos_axum::render_app_to_stream(options.to_owned(), || {
|
||||
error_template(None)
|
||||
});
|
||||
handler(req).await.into_response()
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_static_file(uri: Uri, root: &str) -> Result<Response<BoxBody>, (StatusCode, String)> {
|
||||
let req = Request::builder().uri(uri.clone()).body(Body::empty()).unwrap();
|
||||
// `ServeDir` implements `tower::Service` so we can call it with `tower::ServiceExt::oneshot`
|
||||
// This path is relative to the cargo root
|
||||
match ServeDir::new(root).oneshot(req).await {
|
||||
Ok(res) => Ok(res.map(boxed)),
|
||||
Err(err) => Err((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("Something went wrong: {}", err),
|
||||
)),
|
||||
}
|
||||
async fn get_static_file(
|
||||
uri: Uri,
|
||||
root: &str,
|
||||
) -> Result<Response<Body>, (StatusCode, String)> {
|
||||
let req = Request::builder()
|
||||
.uri(uri.clone())
|
||||
.body(Body::empty())
|
||||
.unwrap();
|
||||
// `ServeDir` implements `tower::Service` so we can call it with `tower::ServiceExt::oneshot`
|
||||
// This path is relative to the cargo root
|
||||
match ServeDir::new(root).oneshot(req).await {
|
||||
Ok(res) => Ok(res.into_response()),
|
||||
Err(err) => Err((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("Something went wrong: {}", err),
|
||||
)),
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,63 +0,0 @@
|
|||
use cfg_if::cfg_if;
|
||||
|
||||
cfg_if! {
|
||||
if #[cfg(feature = "ssr")] {
|
||||
use axum::{
|
||||
body::{boxed, Body, BoxBody},
|
||||
http::{Request, Response, StatusCode, Uri},
|
||||
};
|
||||
use tower::ServiceExt;
|
||||
use tower_http::services::ServeDir;
|
||||
|
||||
pub async fn file_handler(uri: Uri) -> Result<Response<BoxBody>, (StatusCode, String)> {
|
||||
let res = get_static_file(uri.clone(), "/pkg").await?;
|
||||
|
||||
if res.status() == StatusCode::NOT_FOUND {
|
||||
// try with `.html`
|
||||
// TODO: handle if the Uri has query parameters
|
||||
match format!("{}.html", uri).parse() {
|
||||
Ok(uri_html) => get_static_file(uri_html, "/pkg").await,
|
||||
Err(_) => Err((StatusCode::INTERNAL_SERVER_ERROR, "Invalid URI".to_string())),
|
||||
}
|
||||
} else {
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_static_file_handler(uri: Uri) -> Result<Response<BoxBody>, (StatusCode, String)> {
|
||||
let res = get_static_file(uri.clone(), "/static").await?;
|
||||
|
||||
if res.status() == StatusCode::NOT_FOUND {
|
||||
Err((StatusCode::INTERNAL_SERVER_ERROR, "Invalid URI".to_string()))
|
||||
} else {
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_static_file(uri: Uri, base: &str) -> Result<Response<BoxBody>, (StatusCode, String)> {
|
||||
let req = Request::builder().uri(&uri).body(Body::empty()).unwrap();
|
||||
|
||||
// `ServeDir` implements `tower::Service` so we can call it with `tower::ServiceExt::oneshot`
|
||||
// When run normally, the root should be the crate root
|
||||
if base == "/static" {
|
||||
match ServeDir::new("./static").oneshot(req).await {
|
||||
Ok(res) => Ok(res.map(boxed)),
|
||||
Err(err) => Err((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("Something went wrong: {}", err),
|
||||
))
|
||||
}
|
||||
} else if base == "/pkg" {
|
||||
match ServeDir::new("./pkg").oneshot(req).await {
|
||||
Ok(res) => Ok(res.map(boxed)),
|
||||
Err(err) => Err((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("Something went wrong: {}", err),
|
||||
)),
|
||||
}
|
||||
} else{
|
||||
Err((StatusCode::NOT_FOUND, "Not Found".to_string()))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,11 +1,11 @@
|
|||
#![feature(lazy_cell)]
|
||||
|
||||
use cfg_if::cfg_if;
|
||||
use leptos::*;
|
||||
use leptos_meta::*;
|
||||
use leptos_router::*;
|
||||
mod api;
|
||||
pub mod error_template;
|
||||
#[cfg(feature = "ssr")]
|
||||
pub mod fallback;
|
||||
mod routes;
|
||||
use routes::{nav::*, stories::*, story::*, users::*};
|
||||
|
@ -31,16 +31,10 @@ pub fn App() -> impl IntoView {
|
|||
}
|
||||
}
|
||||
|
||||
// Needs to be in lib.rs AFAIK because wasm-bindgen needs us to be compiling a lib. I may be wrong.
|
||||
cfg_if! {
|
||||
if #[cfg(feature = "hydrate")] {
|
||||
use wasm_bindgen::prelude::wasm_bindgen;
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn hydrate() {
|
||||
#[cfg(debug_assertions)]
|
||||
console_error_panic_hook::set_once();
|
||||
leptos::leptos_dom::HydrationCtx::stop_hydrating();
|
||||
}
|
||||
}
|
||||
#[cfg(feature = "hydrate")]
|
||||
#[wasm_bindgen::prelude::wasm_bindgen]
|
||||
pub fn hydrate() {
|
||||
#[cfg(debug_assertions)]
|
||||
console_error_panic_hook::set_once();
|
||||
leptos::leptos_dom::HydrationCtx::stop_hydrating();
|
||||
}
|
||||
|
|
|
@ -1,16 +1,11 @@
|
|||
#[cfg(feature = "ssr")]
|
||||
mod ssr_imports {
|
||||
pub use axum::{routing::get, Router};
|
||||
pub use hackernews_islands::fallback::file_and_error_handler;
|
||||
pub use leptos::*;
|
||||
pub use leptos_axum::{generate_route_list, LeptosRoutes};
|
||||
}
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
pub use axum::{routing::get, Router};
|
||||
pub use hackernews_islands::fallback::file_and_error_handler;
|
||||
use hackernews_islands::*;
|
||||
use ssr_imports::*;
|
||||
pub use leptos::get_configuration;
|
||||
pub use leptos_axum::{generate_route_list, LeptosRoutes};
|
||||
|
||||
let conf = get_configuration(Some("Cargo.toml")).await.unwrap();
|
||||
let leptos_options = conf.leptos_options;
|
||||
|
@ -26,9 +21,9 @@ async fn main() {
|
|||
|
||||
// run our app with hyper
|
||||
// `axum::Server` is a re-export of `hyper::Server`
|
||||
logging::log!("listening on {}", addr);
|
||||
axum::Server::bind(&addr)
|
||||
.serve(app.into_make_service())
|
||||
println!("listening on {}", addr);
|
||||
let listener = tokio::net::TcpListener::bind(&addr).await.unwrap();
|
||||
axum::serve(listener, app.into_make_service())
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
@ -40,7 +35,5 @@ pub fn main() {
|
|||
use leptos::*;
|
||||
_ = console_log::init_with_level(log::Level::Debug);
|
||||
console_error_panic_hook::set_once();
|
||||
mount_to_body(|| {
|
||||
view! { <App/> }
|
||||
});
|
||||
mount_to_body(App);
|
||||
}
|
||||
|
|
|
@ -14,10 +14,10 @@ lto = true
|
|||
console_log = "1.0.0"
|
||||
console_error_panic_hook = "0.1.7"
|
||||
cfg-if = "1.0.0"
|
||||
leptos = { path = "../../leptos", features = ["nightly"] }
|
||||
leptos_axum = { path = "../../integrations/axum", default-features = false, optional = true }
|
||||
leptos_meta = { path = "../../meta", features = ["nightly"] }
|
||||
leptos_router = { path = "../../router", features = ["nightly"] }
|
||||
leptos = { version = "0.5", features = ["nightly"] }
|
||||
leptos_axum = { version = "0.5", default-features = false, optional = true }
|
||||
leptos_meta = { version = "0.5", features = ["nightly"] }
|
||||
leptos_router = { version = "0.5", features = ["nightly"] }
|
||||
log = "0.4.17"
|
||||
simple_logger = "4.0.0"
|
||||
serde = { version = "1.0.148", features = ["derive"] }
|
||||
|
|
|
@ -2,6 +2,8 @@
|
|||
|
||||
This example uses the basic Hacker News example as its basis, but shows how to run the server side as WASM running in a JS environment. In this example, Deno is used as the runtime.
|
||||
|
||||
**NOTE**: This example is slightly out of date pending an update to [`axum-js-fetch`](https://github.com/seanaye/axum-js-fetch/), which was waiting on a version of `gloo-net` that uses `http` 1.0. It still works with Leptos 0.5 and Axum 0.6, but not with the versions of Leptos (0.6 and later) that support Axum 1.0.
|
||||
|
||||
## Server Side Rendering with Deno
|
||||
|
||||
To run the Deno version, run
|
||||
|
|
|
@ -1,39 +1,43 @@
|
|||
use cfg_if::cfg_if;
|
||||
use crate::error_template::error_template;
|
||||
use axum::{
|
||||
body::Body,
|
||||
extract::State,
|
||||
http::{Request, Response, StatusCode, Uri},
|
||||
response::{IntoResponse, Response as AxumResponse},
|
||||
};
|
||||
//use tower::ServiceExt;
|
||||
use leptos::LeptosOptions;
|
||||
|
||||
cfg_if! {
|
||||
if #[cfg(feature = "ssr")] {
|
||||
use axum::{
|
||||
body::{Body, BoxBody},
|
||||
extract::State,
|
||||
response::IntoResponse,
|
||||
http::{Request, Response, StatusCode, Uri},
|
||||
};
|
||||
use axum::response::Response as AxumResponse;
|
||||
//use tower::ServiceExt;
|
||||
use leptos::{LeptosOptions};
|
||||
use crate::error_template::error_template;
|
||||
pub async fn file_and_error_handler(
|
||||
uri: Uri,
|
||||
State(options): State<LeptosOptions>,
|
||||
req: Request<Body>,
|
||||
) -> AxumResponse {
|
||||
let root = options.site_root.clone();
|
||||
let res = get_static_file(uri.clone(), &root).await.unwrap();
|
||||
|
||||
pub async fn file_and_error_handler(uri: Uri, State(options): State<LeptosOptions>, req: Request<Body>) -> AxumResponse {
|
||||
let root = options.site_root.clone();
|
||||
let res = get_static_file(uri.clone(), &root).await.unwrap();
|
||||
|
||||
if res.status() == StatusCode::OK {
|
||||
res.into_response()
|
||||
} else{
|
||||
let handler = leptos_axum::render_app_to_stream(options.to_owned(), || error_template(None));
|
||||
handler(req).await.into_response()
|
||||
}
|
||||
if res.status() == StatusCode::OK {
|
||||
res.into_response()
|
||||
} else {
|
||||
let handler =
|
||||
leptos_axum::render_app_to_stream(options.to_owned(), || {
|
||||
error_template(None)
|
||||
});
|
||||
handler(req).await.into_response()
|
||||
}
|
||||
|
||||
async fn get_static_file(uri: Uri, root: &str) -> Result<Response<BoxBody>, (StatusCode, String)> {
|
||||
let req = Request::builder().uri(uri.clone()).body(Body::empty()).unwrap();
|
||||
// `ServeDir` implements `tower::Service` so we can call it with `tower::ServiceExt::oneshot`
|
||||
// This path is relative to the cargo root
|
||||
_ = req;
|
||||
_ = root;
|
||||
todo!()
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
async fn get_static_file(
|
||||
uri: Uri,
|
||||
root: &str,
|
||||
) -> Result<Response<Body>, (StatusCode, String)> {
|
||||
let req = Request::builder()
|
||||
.uri(uri.clone())
|
||||
.body(Body::empty())
|
||||
.unwrap();
|
||||
// `ServeDir` implements `tower::Service` so we can call it with `tower::ServiceExt::oneshot`
|
||||
// This path is relative to the cargo root
|
||||
_ = req;
|
||||
_ = root;
|
||||
todo!()
|
||||
}
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
use cfg_if::cfg_if;
|
||||
use leptos::{component, view, IntoView};
|
||||
use leptos_meta::*;
|
||||
use leptos_router::*;
|
||||
mod api;
|
||||
pub mod error_template;
|
||||
#[cfg(feature = "ssr")]
|
||||
pub mod fallback;
|
||||
mod routes;
|
||||
use routes::{nav::*, stories::*, story::*, users::*};
|
||||
|
@ -29,25 +29,22 @@ pub fn App() -> impl IntoView {
|
|||
}
|
||||
}
|
||||
|
||||
cfg_if! {
|
||||
if #[cfg(feature = "hydrate")] {
|
||||
#[wasm_bindgen]
|
||||
pub fn hydrate() {
|
||||
_ = console_log::init_with_level(log::Level::Debug);
|
||||
console_error_panic_hook::set_once();
|
||||
leptos::mount_to_body(move || {
|
||||
view! { <App/> }
|
||||
});
|
||||
}
|
||||
} else if #[cfg(feature = "ssr")] {
|
||||
#[cfg(feature = "hydrate")]
|
||||
#[wasm_bindgen]
|
||||
pub fn hydrate() {
|
||||
_ = console_log::init_with_level(log::Level::Debug);
|
||||
console_error_panic_hook::set_once();
|
||||
leptos::mount_to_body(App);
|
||||
}
|
||||
|
||||
use axum::{
|
||||
Router,
|
||||
routing::post
|
||||
};
|
||||
use leptos_axum::{generate_route_list, LeptosRoutes};
|
||||
#[cfg(feature = "ssr")]
|
||||
mod ssr_imports {
|
||||
use crate::App;
|
||||
use axum::{routing::post, Router};
|
||||
use leptos::*;
|
||||
use leptos_axum::{generate_route_list, LeptosRoutes};
|
||||
use log::{info, Level};
|
||||
use wasm_bindgen::prelude::wasm_bindgen;
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub struct Handler(axum_js_fetch::App);
|
||||
|
@ -55,17 +52,20 @@ cfg_if! {
|
|||
#[wasm_bindgen]
|
||||
impl Handler {
|
||||
pub async fn new() -> Self {
|
||||
console_log::init_with_level(Level::Debug);
|
||||
_ = console_log::init_with_level(Level::Debug);
|
||||
console_error_panic_hook::set_once();
|
||||
|
||||
let leptos_options = LeptosOptions::builder().output_name("client").site_pkg_dir("pkg").build();
|
||||
let leptos_options = LeptosOptions::builder()
|
||||
.output_name("client")
|
||||
.site_pkg_dir("pkg")
|
||||
.build();
|
||||
let routes = generate_route_list(App);
|
||||
|
||||
// build our application with a route
|
||||
let app: axum::Router<(), axum::body::Body> = Router::new()
|
||||
.leptos_routes(&leptos_options, routes, || view! { <App/> } )
|
||||
.route("/api/*fn_name", post(leptos_axum::handle_server_fns))
|
||||
.with_state(leptos_options);
|
||||
.leptos_routes(&leptos_options, routes, || view! { <App/> })
|
||||
.route("/api/*fn_name", post(leptos_axum::handle_server_fns))
|
||||
.with_state(leptos_options);
|
||||
|
||||
info!("creating handler instance");
|
||||
|
||||
|
@ -77,4 +77,3 @@ cfg_if! {
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,7 +14,7 @@ leptos_router = { path = "../../../router", features = ["csr"] }
|
|||
log = "0.4"
|
||||
console_error_panic_hook = "0.1"
|
||||
console_log = "1"
|
||||
gloo-net = "0.2"
|
||||
gloo-storage = "0.2"
|
||||
gloo-net = "0.5"
|
||||
gloo-storage = "0.3"
|
||||
serde = "1.0"
|
||||
thiserror = "1.0"
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
use api_boundary::*;
|
||||
use gloo_net::http::{Request, Response};
|
||||
use gloo_net::http::{Request, RequestBuilder, Response};
|
||||
use serde::de::DeserializeOwned;
|
||||
use thiserror::Error;
|
||||
|
||||
|
@ -41,7 +41,7 @@ impl AuthorizedApi {
|
|||
fn auth_header_value(&self) -> String {
|
||||
format!("Bearer {}", self.token.token)
|
||||
}
|
||||
async fn send<T>(&self, req: Request) -> Result<T>
|
||||
async fn send<T>(&self, req: RequestBuilder) -> Result<T>
|
||||
where
|
||||
T: DeserializeOwned,
|
||||
{
|
||||
|
|
|
@ -5,14 +5,18 @@ edition = "2021"
|
|||
publish = false
|
||||
|
||||
[dependencies]
|
||||
api-boundary = "=0.0.0"
|
||||
|
||||
anyhow = "1.0"
|
||||
api-boundary = "*"
|
||||
axum = { version = "0.6", features = ["headers"] }
|
||||
axum = "0.7"
|
||||
axum-extra = { version = "0.9.2", features = ["typed-header"] }
|
||||
env_logger = "0.10"
|
||||
log = "0.4"
|
||||
mailparse = "0.14"
|
||||
pwhash = "1.0"
|
||||
thiserror = "1.0"
|
||||
tokio = { version = "1.25", features = ["macros", "rt-multi-thread"] }
|
||||
tower-http = { version = "0.4", features = ["cors"] }
|
||||
uuid = { version = "1.3", features = ["v4"] }
|
||||
tokio = { version = "1.35", features = ["macros", "rt-multi-thread"] }
|
||||
tower-http = { version = "0.5", features = ["cors"] }
|
||||
uuid = { version = "1.6", features = ["v4"] }
|
||||
parking_lot = "0.12.1"
|
||||
headers = "0.4.0"
|
||||
|
|
|
@ -1,38 +1,36 @@
|
|||
use mailparse::addrparse;
|
||||
use pwhash::bcrypt;
|
||||
use std::{collections::HashMap, str::FromStr, sync::RwLock};
|
||||
use std::{collections::HashMap, str::FromStr};
|
||||
use thiserror::Error;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct AppState {
|
||||
users: RwLock<HashMap<EmailAddress, Password>>,
|
||||
tokens: RwLock<HashMap<Uuid, EmailAddress>>,
|
||||
users: HashMap<EmailAddress, Password>,
|
||||
tokens: HashMap<Uuid, EmailAddress>,
|
||||
}
|
||||
|
||||
impl AppState {
|
||||
pub fn create_user(
|
||||
&self,
|
||||
&mut self,
|
||||
credentials: Credentials,
|
||||
) -> Result<(), CreateUserError> {
|
||||
let Credentials { email, password } = credentials;
|
||||
let user_exists = self.users.read().unwrap().get(&email).is_some();
|
||||
let user_exists = self.users.get(&email).is_some();
|
||||
if user_exists {
|
||||
return Err(CreateUserError::UserExists);
|
||||
}
|
||||
self.users.write().unwrap().insert(email, password);
|
||||
self.users.insert(email, password);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn login(
|
||||
&self,
|
||||
&mut self,
|
||||
email: EmailAddress,
|
||||
password: &str,
|
||||
) -> Result<Uuid, LoginError> {
|
||||
let valid_credentials = self
|
||||
.users
|
||||
.read()
|
||||
.unwrap()
|
||||
.get(&email)
|
||||
.map(|hashed_password| hashed_password.verify(password))
|
||||
.unwrap_or(false);
|
||||
|
@ -40,16 +38,16 @@ impl AppState {
|
|||
Err(LoginError::InvalidEmailOrPassword)
|
||||
} else {
|
||||
let token = Uuid::new_v4();
|
||||
self.tokens.write().unwrap().insert(token, email);
|
||||
self.tokens.insert(token, email);
|
||||
Ok(token)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn logout(&self, token: &str) -> Result<(), LogoutError> {
|
||||
pub fn logout(&mut self, token: &str) -> Result<(), LogoutError> {
|
||||
let token = token
|
||||
.parse::<Uuid>()
|
||||
.map_err(|_| LogoutError::NotLoggedIn)?;
|
||||
self.tokens.write().unwrap().remove(&token);
|
||||
self.tokens.remove(&token);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
@ -62,8 +60,6 @@ impl AppState {
|
|||
.map_err(|_| AuthError::NotAuthorized)
|
||||
.and_then(|token| {
|
||||
self.tokens
|
||||
.read()
|
||||
.unwrap()
|
||||
.get(&token)
|
||||
.cloned()
|
||||
.map(|email| CurrentUser { email, token })
|
||||
|
|
|
@ -1,13 +1,16 @@
|
|||
use api_boundary as json;
|
||||
use axum::{
|
||||
extract::{State, TypedHeader},
|
||||
headers::{authorization::Bearer, Authorization},
|
||||
extract::State,
|
||||
http::Method,
|
||||
response::Json,
|
||||
routing::{get, post},
|
||||
Router,
|
||||
};
|
||||
use std::{env, sync::Arc};
|
||||
use axum_extra::TypedHeader;
|
||||
use headers::{authorization::Bearer, Authorization};
|
||||
use parking_lot::RwLock;
|
||||
use std::{env, net::SocketAddr, sync::Arc};
|
||||
use tokio::net::TcpListener;
|
||||
use tower_http::cors::{Any, CorsLayer};
|
||||
|
||||
mod adapters;
|
||||
|
@ -32,7 +35,7 @@ async fn main() -> anyhow::Result<()> {
|
|||
}
|
||||
env_logger::init();
|
||||
|
||||
let shared_state = Arc::new(AppState::default());
|
||||
let shared_state = Arc::new(RwLock::new(AppState::default()));
|
||||
|
||||
let cors_layer = CorsLayer::new()
|
||||
.allow_methods([Method::GET, Method::POST])
|
||||
|
@ -46,11 +49,10 @@ async fn main() -> anyhow::Result<()> {
|
|||
.route_layer(cors_layer)
|
||||
.with_state(shared_state);
|
||||
|
||||
let addr = "0.0.0.0:3000".parse().unwrap();
|
||||
log::info!("Listen on {addr}");
|
||||
axum::Server::bind(&addr)
|
||||
.serve(app.into_make_service())
|
||||
.await?;
|
||||
let addr = "0.0.0.0:3000".parse::<SocketAddr>()?;
|
||||
log::info!("Start listening on http://{addr}");
|
||||
let listener = TcpListener::bind(addr).await?;
|
||||
axum::serve(listener, app.into_make_service()).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
@ -73,40 +75,43 @@ enum Error {
|
|||
}
|
||||
|
||||
async fn create_user(
|
||||
State(state): State<Arc<AppState>>,
|
||||
State(state): State<Arc<RwLock<AppState>>>,
|
||||
Json(credentials): Json<json::Credentials>,
|
||||
) -> Result<()> {
|
||||
let credentials = Credentials::try_from(credentials)?;
|
||||
state.create_user(credentials)?;
|
||||
state.write().create_user(credentials)?;
|
||||
Ok(Json(()))
|
||||
}
|
||||
|
||||
async fn login(
|
||||
State(state): State<Arc<AppState>>,
|
||||
State(state): State<Arc<RwLock<AppState>>>,
|
||||
Json(credentials): Json<json::Credentials>,
|
||||
) -> Result<json::ApiToken> {
|
||||
let json::Credentials { email, password } = credentials;
|
||||
log::debug!("{email} tries to login");
|
||||
let email = email.parse().map_err(|_|
|
||||
// Here we don't want to leak detailed info.
|
||||
LoginError::InvalidEmailOrPassword)?;
|
||||
let token = state.login(email, &password).map(|s| s.to_string())?;
|
||||
// Here we don't want to leak detailed info.
|
||||
LoginError::InvalidEmailOrPassword)?;
|
||||
let token = state
|
||||
.write()
|
||||
.login(email, &password)
|
||||
.map(|s| s.to_string())?;
|
||||
Ok(Json(json::ApiToken { token }))
|
||||
}
|
||||
|
||||
async fn logout(
|
||||
State(state): State<Arc<AppState>>,
|
||||
State(state): State<Arc<RwLock<AppState>>>,
|
||||
TypedHeader(auth): TypedHeader<Authorization<Bearer>>,
|
||||
) -> Result<()> {
|
||||
state.logout(auth.token())?;
|
||||
state.write().logout(auth.token())?;
|
||||
Ok(Json(()))
|
||||
}
|
||||
|
||||
async fn get_user_info(
|
||||
State(state): State<Arc<AppState>>,
|
||||
State(state): State<Arc<RwLock<AppState>>>,
|
||||
TypedHeader(auth): TypedHeader<Authorization<Bearer>>,
|
||||
) -> Result<json::UserInfo> {
|
||||
let user = state.authorize_user(auth.token())?;
|
||||
let user = state.read().authorize_user(auth.token())?;
|
||||
let CurrentUser { email, .. } = user;
|
||||
Ok(Json(json::UserInfo {
|
||||
email: email.into_string(),
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
[package]
|
||||
name = "todo_app_sqlite_viz"
|
||||
name = "server_fns_axum"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
|
@ -7,48 +7,52 @@ edition = "2021"
|
|||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[dependencies]
|
||||
console_log = "1.0.0"
|
||||
console_error_panic_hook = "0.1.7"
|
||||
futures = "0.3.25"
|
||||
cfg-if = "1.0.0"
|
||||
leptos = { path = "../../leptos" }
|
||||
leptos_viz = { path = "../../integrations/viz", optional = true }
|
||||
console_log = "1.0"
|
||||
console_error_panic_hook = "0.1"
|
||||
futures = "0.3"
|
||||
http = "1.0"
|
||||
leptos = { path = "../../leptos", features = ["nightly"] }
|
||||
leptos_axum = { path = "../../integrations/axum", optional = true }
|
||||
leptos_meta = { path = "../../meta", features = ["nightly"] }
|
||||
leptos_router = { path = "../../router", features = ["nightly"] }
|
||||
leptos_reactive = { path = "../../leptos_reactive", features = ["nightly"] }
|
||||
log = "0.4.17"
|
||||
simple_logger = "4.0.0"
|
||||
server_fn = { path = "../../server_fn", features = ["serde-lite", "rkyv", "multipart" ]}
|
||||
log = "0.4"
|
||||
simple_logger = "4.0"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
viz = { version = "0.4.8", features = ["serve"], optional = true }
|
||||
tokio = { version = "1.25.0", features = ["full"], optional = true }
|
||||
http = { version = "0.2.11" }
|
||||
sqlx = { version = "0.6.2", features = [
|
||||
"runtime-tokio-rustls",
|
||||
"sqlite",
|
||||
], optional = true }
|
||||
thiserror = "1.0.38"
|
||||
axum = { version = "0.7", optional = true }
|
||||
tower = { version = "0.4", optional = true }
|
||||
tower-http = { version = "0.5", features = ["fs", "tracing", "trace"], optional = true }
|
||||
tokio = { version = "1", features = ["full"], optional = true }
|
||||
thiserror = "1.0"
|
||||
wasm-bindgen = "0.2"
|
||||
serde_toml = "0.0.1"
|
||||
toml = "0.8.8"
|
||||
web-sys = { version = "0.3.67", features = ["FileList", "File"] }
|
||||
strum = { version = "0.25.0", features = ["strum_macros", "derive"] }
|
||||
notify = { version = "6.1.1", optional = true }
|
||||
pin-project-lite = "0.2.13"
|
||||
|
||||
[features]
|
||||
csr = ["leptos/csr", "leptos_meta/csr", "leptos_router/csr"]
|
||||
hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate"]
|
||||
ssr = [
|
||||
"dep:viz",
|
||||
"dep:tokio",
|
||||
"dep:sqlx",
|
||||
"leptos/ssr",
|
||||
"leptos_meta/ssr",
|
||||
"leptos_router/ssr",
|
||||
"dep:leptos_viz",
|
||||
"dep:axum",
|
||||
"dep:tower",
|
||||
"dep:tower-http",
|
||||
"dep:tokio",
|
||||
"leptos/ssr",
|
||||
"leptos_meta/ssr",
|
||||
"leptos_router/ssr",
|
||||
"dep:leptos_axum",
|
||||
"dep:notify"
|
||||
]
|
||||
|
||||
[package.metadata.cargo-all-features]
|
||||
denylist = ["viz", "tokio", "sqlx", "leptos_viz"]
|
||||
denylist = ["axum", "tower", "tower-http", "tokio", "leptos_axum"]
|
||||
skip_feature_sets = [["csr", "ssr"], ["csr", "hydrate"], ["ssr", "hydrate"]]
|
||||
|
||||
[package.metadata.leptos]
|
||||
# The name used by wasm-bindgen/cargo-leptos for the JS/WASM bundle. Defaults to the crate name
|
||||
output-name = "todo_app_sqlite_viz"
|
||||
output-name = "server_fns_axum"
|
||||
# The site root folder is where cargo-leptos generate all output. WARNING: all content of this folder will be erased on a rebuild. Use it in your server setup.
|
||||
site-root = "target/site"
|
||||
# The site-root relative folder where all compiled output (JS, WASM and CSS) is written
|
||||
|
@ -63,7 +67,8 @@ site-addr = "127.0.0.1:3000"
|
|||
# The port to use for automatic reload monitoring
|
||||
reload-port = 3001
|
||||
# [Optional] Command to use when running end2end tests. It will run in the end2end dir.
|
||||
end2end-cmd = "npx playwright test"
|
||||
end2end-cmd = "cargo make test-ui"
|
||||
end2end-dir = "e2e"
|
||||
# The browserlist query used for optimizing the CSS.
|
||||
browserquery = "defaults"
|
||||
# Set by cargo-leptos watch when building with tha tool. Controls whether autoreload JS will be included in the head
|
|
@ -5,4 +5,4 @@ extend = [
|
|||
|
||||
[env]
|
||||
|
||||
CLIENT_PROCESS_NAME = "todo_app_sqlite_viz"
|
||||
CLIENT_PROCESS_NAME = "server_fns_axum"
|
19
examples/server_fns_axum/README.md
Normal file
19
examples/server_fns_axum/README.md
Normal file
|
@ -0,0 +1,19 @@
|
|||
# Leptos Todo App Sqlite with Axum
|
||||
|
||||
This example creates a basic todo app with an Axum backend that uses Leptos' server functions to call sqlx from the client and seamlessly run it on the server.
|
||||
|
||||
## Getting Started
|
||||
|
||||
See the [Examples README](../README.md) for setup and run instructions.
|
||||
|
||||
## E2E Testing
|
||||
|
||||
See the [E2E README](./e2e/README.md) for more information about the testing strategy.
|
||||
|
||||
## Rendering
|
||||
|
||||
See the [SSR Notes](../SSR_NOTES.md) for more information about Server Side Rendering.
|
||||
|
||||
## Quick Start
|
||||
|
||||
Run `cargo leptos watch` to run this example.
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
505
examples/server_fns_axum/src/app.rs
Normal file
505
examples/server_fns_axum/src/app.rs
Normal file
|
@ -0,0 +1,505 @@
|
|||
use futures::StreamExt;
|
||||
use leptos::{html::Input, *};
|
||||
use leptos_meta::{provide_meta_context, Link, Meta, Stylesheet};
|
||||
use leptos_router::{ActionForm, Route, Router, Routes};
|
||||
use server_fn::codec::{
|
||||
GetUrl, MultipartData, MultipartFormData, Rkyv, SerdeLite, StreamingText,
|
||||
TextStream,
|
||||
};
|
||||
#[cfg(feature = "ssr")]
|
||||
use std::sync::{
|
||||
atomic::{AtomicU8, Ordering},
|
||||
Mutex,
|
||||
};
|
||||
use strum::{Display, EnumString};
|
||||
use wasm_bindgen::JsCast;
|
||||
use web_sys::{FormData, HtmlFormElement, SubmitEvent};
|
||||
|
||||
#[component]
|
||||
pub fn TodoApp() -> impl IntoView {
|
||||
provide_meta_context();
|
||||
|
||||
view! {
|
||||
<Meta name="color-scheme" content="dark light"/>
|
||||
<Link rel="shortcut icon" type_="image/ico" href="/favicon.ico"/>
|
||||
<Stylesheet id="leptos" href="/pkg/server_fns_axum.css"/>
|
||||
<Router>
|
||||
<header>
|
||||
<h1>"Server Function Demo"</h1>
|
||||
</header>
|
||||
<main>
|
||||
<Routes>
|
||||
<Route path="" view=HomePage/>
|
||||
</Routes>
|
||||
</main>
|
||||
</Router>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn HomePage() -> impl IntoView {
|
||||
view! {
|
||||
<h2>"Some Simple Server Functions"</h2>
|
||||
<SpawnLocal/>
|
||||
<WithAnAction/>
|
||||
<WithActionForm/>
|
||||
<h2>"Custom Error Types"</h2>
|
||||
<CustomErrorTypes/>
|
||||
<h2>"Alternative Encodings"</h2>
|
||||
<ServerFnArgumentExample/>
|
||||
<RkyvExample/>
|
||||
<FileUpload/>
|
||||
<FileWatcher/>
|
||||
}
|
||||
}
|
||||
|
||||
/// A server function is really just an API call to your server. But it provides a plain async
|
||||
/// function as a wrapper around that. This means you can call it like any other async code, just
|
||||
/// by spawning a task with `spawn_local`.
|
||||
///
|
||||
/// In reality, you usually want to use a resource to load data from the server or an action to
|
||||
/// mutate data on the server. But a simple `spawn_local` can make it more obvious what's going on.
|
||||
#[component]
|
||||
pub fn SpawnLocal() -> impl IntoView {
|
||||
/// A basic server function can be called like any other async function.
|
||||
///
|
||||
/// You can define a server function at any scope. This one, for example, is only available
|
||||
/// inside the SpawnLocal component. **However**, note that all server functions are publicly
|
||||
/// available API endpoints: This scoping means you can only call this server function
|
||||
/// from inside this component, but it is still available at its URL to any caller, from within
|
||||
/// your app or elsewhere.
|
||||
#[server]
|
||||
pub async fn shouting_text(input: String) -> Result<String, ServerFnError> {
|
||||
// insert a simulated wait
|
||||
tokio::time::sleep(std::time::Duration::from_millis(250)).await;
|
||||
Ok(input.to_ascii_uppercase())
|
||||
}
|
||||
|
||||
let input_ref = NodeRef::<Input>::new();
|
||||
let (shout_result, set_shout_result) =
|
||||
create_signal("Click me".to_string());
|
||||
|
||||
view! {
|
||||
<h3>Using <code>spawn_local</code></h3>
|
||||
<p>
|
||||
"You can call a server function by using "<code>"spawn_local"</code> " in an event listener. "
|
||||
"Clicking this button should alert with the uppercase version of the input."
|
||||
</p>
|
||||
<input node_ref=input_ref placeholder="Type something here."/>
|
||||
<button
|
||||
on:click=move |_| {
|
||||
let value = input_ref.get().unwrap().value();
|
||||
spawn_local(async move {
|
||||
let uppercase_text = shouting_text(value).await.unwrap_or_else(|e| e.to_string());
|
||||
set_shout_result(uppercase_text);
|
||||
});
|
||||
}
|
||||
>
|
||||
{shout_result}
|
||||
</button>
|
||||
}
|
||||
}
|
||||
|
||||
/// Pretend this is a database and we're storing some rows in memory!
|
||||
/// This exists only on the server.
|
||||
#[cfg(feature = "ssr")]
|
||||
static ROWS: Mutex<Vec<String>> = Mutex::new(Vec::new());
|
||||
|
||||
/// Imagine this server function mutates some state on the server, like a database row.
|
||||
/// Every third time, it will return an error.
|
||||
///
|
||||
/// This kind of mutation is often best handled by an Action.
|
||||
/// Remember, if you're loading data, use a resource; if you're running an occasional action,
|
||||
/// use an action.
|
||||
#[server]
|
||||
pub async fn add_row(text: String) -> Result<usize, ServerFnError> {
|
||||
static N: AtomicU8 = AtomicU8::new(0);
|
||||
|
||||
// insert a simulated wait
|
||||
tokio::time::sleep(std::time::Duration::from_millis(250)).await;
|
||||
|
||||
let nth_run = N.fetch_add(1, Ordering::Relaxed);
|
||||
// this will print on the server, like any server function
|
||||
println!("Adding {text:?} to the database!");
|
||||
if nth_run % 3 == 2 {
|
||||
Err(ServerFnError::new("Oh no! Couldn't add to database!"))
|
||||
} else {
|
||||
let mut rows = ROWS.lock().unwrap();
|
||||
rows.push(text);
|
||||
Ok(rows.len())
|
||||
}
|
||||
}
|
||||
|
||||
/// Simply returns the number of rows.
|
||||
#[server]
|
||||
pub async fn get_rows() -> Result<usize, ServerFnError> {
|
||||
// insert a simulated wait
|
||||
tokio::time::sleep(std::time::Duration::from_millis(250)).await;
|
||||
|
||||
Ok(ROWS.lock().unwrap().len())
|
||||
}
|
||||
|
||||
/// An action abstracts over the process of spawning a future and setting a signal when it
|
||||
/// resolves. Its .input() signal holds the most recent argument while it's still pending,
|
||||
/// and its .value() signal holds the most recent result. Its .version() signal can be fed
|
||||
/// into a resource, telling it to refetch whenever the action has successfully resolved.
|
||||
///
|
||||
/// This makes actions useful for mutations, i.e., some server function that invalidates
|
||||
/// loaded previously loaded from another server function.
|
||||
#[component]
|
||||
pub fn WithAnAction() -> impl IntoView {
|
||||
let input_ref = NodeRef::<Input>::new();
|
||||
|
||||
// a server action can be created by using the server function's type name as a generic
|
||||
// the type name defaults to the PascalCased function name
|
||||
let action = create_server_action::<AddRow>();
|
||||
|
||||
// this resource will hold the total number of rows
|
||||
// passing it action.version() means it will refetch whenever the action resolves successfully
|
||||
let row_count = create_resource(action.version(), |_| get_rows());
|
||||
|
||||
view! {
|
||||
<h3>Using <code>create_action</code></h3>
|
||||
<p>
|
||||
"Some server functions are conceptually \"mutations,\", which change something on the server. "
|
||||
"These often work well as actions."
|
||||
</p>
|
||||
<input node_ref=input_ref placeholder="Type something here."/>
|
||||
<button
|
||||
on:click=move |_| {
|
||||
let text = input_ref.get().unwrap().value();
|
||||
action.dispatch(text.into());
|
||||
// note: technically, this `action` takes `AddRow` (the server fn type) as its
|
||||
// argument
|
||||
//
|
||||
// however, for any one-argument server functions, `From<_>` is implemented between
|
||||
// the server function type and the type of this single argument
|
||||
}
|
||||
>
|
||||
Submit
|
||||
</button>
|
||||
<p>You submitted: {move || format!("{:?}", action.input().get())}</p>
|
||||
<p>The result was: {move || format!("{:?}", action.value().get())}</p>
|
||||
<Transition>
|
||||
<p>Total rows: {row_count}</p>
|
||||
</Transition>
|
||||
}
|
||||
}
|
||||
|
||||
/// An <ActionForm/> lets you do the same thing as dispatching an action, but automates the
|
||||
/// creation of the dispatched argument struct using a <form>. This means it also gracefully
|
||||
/// degrades well when JS/WASM are not available.
|
||||
///
|
||||
/// Try turning off WASM in your browser. The form still works, and successfully displays the error
|
||||
/// message if the server function returns an error. Otherwise, it loads the new resource data.
|
||||
#[component]
|
||||
pub fn WithActionForm() -> impl IntoView {
|
||||
let action = create_server_action::<AddRow>();
|
||||
let row_count = create_resource(action.version(), |_| get_rows());
|
||||
|
||||
view! {
|
||||
<h3>Using <code>"<ActionForm/>"</code></h3>
|
||||
<p>
|
||||
<code>"<ActionForm/>"</code> "lets you use an HTML " <code>"<form>"</code>
|
||||
"to call a server function in a way that gracefully degrades."
|
||||
</p>
|
||||
<ActionForm action>
|
||||
<input
|
||||
// the `name` of the input corresponds to the argument name
|
||||
name="text"
|
||||
placeholder="Type something here."
|
||||
/>
|
||||
<button> Submit </button>
|
||||
</ActionForm>
|
||||
<p>You submitted: {move || format!("{:?}", action.input().get())}</p>
|
||||
<p>The result was: {move || format!("{:?}", action.value().get())}</p>
|
||||
<Transition>archive underaligned: need alignment 4 but have alignment 1
|
||||
<p>Total rows: {row_count}</p>
|
||||
</Transition>
|
||||
}
|
||||
}
|
||||
|
||||
/// The plain `#[server]` macro gives sensible defaults for the settings needed to create a server
|
||||
/// function, but those settings can also be customized. For example, you can set a specific unique
|
||||
/// path rather than the hashed path, or you can choose a different combination of input and output
|
||||
/// encodings.
|
||||
///
|
||||
/// Arguments to the server macro can be specified as named key-value pairs, like `name = value`.
|
||||
#[server(
|
||||
// this server function will be exposed at /api2/custom_path
|
||||
prefix = "/api2",
|
||||
endpoint = "custom_path",
|
||||
// it will take its arguments as a URL-encoded GET request (useful for caching)
|
||||
input = GetUrl,
|
||||
// it will return its output using SerdeLite
|
||||
// (this needs to be enabled with the `serde-lite` feature on the `server_fn` crate
|
||||
output = SerdeLite,
|
||||
)]
|
||||
// You can use the `#[middleware]` macro to add appropriate middleware
|
||||
// In this case, any `tower::Layer` that takes services of `Request<Body>` will work
|
||||
#[middleware(crate::middleware::LoggingLayer)]
|
||||
pub async fn length_of_input(input: String) -> Result<usize, ServerFnError> {
|
||||
println!("2. Running server function.");
|
||||
// insert a simulated wait
|
||||
tokio::time::sleep(std::time::Duration::from_millis(250)).await;
|
||||
Ok(input.len())
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn ServerFnArgumentExample() -> impl IntoView {
|
||||
let input_ref = NodeRef::<Input>::new();
|
||||
let (result, set_result) = create_signal(0);
|
||||
|
||||
view! {
|
||||
<h3>Custom arguments to the <code>#[server]</code> " macro"</h3>
|
||||
<p>
|
||||
This example shows how to specify additional behavior including
|
||||
<ul>
|
||||
<li>Specific server function <strong>paths</strong></li>
|
||||
<li>Mixing and matching input and output <strong>encodings</strong></li>
|
||||
<li>Adding custom <strong>middleware</strong> on a per-server-fn basis</li>
|
||||
</ul>
|
||||
</p>
|
||||
<input node_ref=input_ref placeholder="Type something here."/>
|
||||
<button
|
||||
on:click=move |_| {
|
||||
let value = input_ref.get().unwrap().value();
|
||||
spawn_local(async move {
|
||||
let length = length_of_input(value).await.unwrap_or(0);
|
||||
set_result(length);
|
||||
});
|
||||
}
|
||||
>
|
||||
Click to see length
|
||||
</button>
|
||||
<p>Length is {result}</p>
|
||||
}
|
||||
}
|
||||
|
||||
/// `server_fn` supports a wide variety of input and output encodings, each of which can be
|
||||
/// referred to as a PascalCased struct name
|
||||
/// - Toml
|
||||
/// - Cbor
|
||||
/// - Rkyv
|
||||
/// - etc.
|
||||
#[server(
|
||||
input = Rkyv,
|
||||
output = Rkyv
|
||||
)]
|
||||
pub async fn rkyv_example(input: String) -> Result<String, ServerFnError> {
|
||||
// insert a simulated wait
|
||||
tokio::time::sleep(std::time::Duration::from_millis(250)).await;
|
||||
Ok(input.to_ascii_uppercase())
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn RkyvExample() -> impl IntoView {
|
||||
let input_ref = NodeRef::<Input>::new();
|
||||
let (input, set_input) = create_signal(String::new());
|
||||
let rkyv_result = create_resource(input, rkyv_example);
|
||||
|
||||
view! {
|
||||
<h3>Using <code>rkyv</code> encoding</h3>
|
||||
<p>
|
||||
</p>
|
||||
<input node_ref=input_ref placeholder="Type something here."/>
|
||||
<button
|
||||
on:click=move |_| {
|
||||
let value = input_ref.get().unwrap().value();
|
||||
set_input(value);
|
||||
}
|
||||
>
|
||||
Click to see length
|
||||
</button>
|
||||
<p>{input}</p>
|
||||
<Transition>
|
||||
{rkyv_result}
|
||||
</Transition>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn FileUpload() -> impl IntoView {
|
||||
/// A simple file upload function, which does just returns the length of the file.
|
||||
///
|
||||
/// On the server, this uses the `multer` crate, which provides a streaming API.
|
||||
#[server(
|
||||
input = MultipartFormData,
|
||||
)]
|
||||
pub async fn file_length(
|
||||
data: MultipartData,
|
||||
) -> Result<usize, ServerFnError> {
|
||||
// `.into_inner()` returns the inner `multer` stream
|
||||
// it is `None` if we call this on the client, but always `Some(_)` on the server, so is safe to
|
||||
// unwrap
|
||||
let mut data = data.into_inner().unwrap();
|
||||
|
||||
// this will just measure the total number of bytes uploaded
|
||||
let mut count = 0;
|
||||
while let Ok(Some(mut field)) = data.next_field().await {
|
||||
println!("\n[NEXT FIELD]\n");
|
||||
let name = field.name().unwrap_or_default().to_string();
|
||||
println!(" [NAME] {name}");
|
||||
while let Ok(Some(chunk)) = field.chunk().await {
|
||||
let len = chunk.len();
|
||||
count += len;
|
||||
println!(" [CHUNK] {len}");
|
||||
// in a real server function, you'd do something like saving the file here
|
||||
}
|
||||
}
|
||||
|
||||
Ok(count)
|
||||
}
|
||||
|
||||
let upload_action = create_action(|data: &FormData| {
|
||||
let data = data.clone();
|
||||
// `MultipartData` implements `From<FormData>`
|
||||
file_length(data.into())
|
||||
});
|
||||
|
||||
view! {
|
||||
<h3>File Upload</h3>
|
||||
<p>Uploading files is fairly easy using multipart form data.</p>
|
||||
<form on:submit=move |ev: SubmitEvent| {
|
||||
ev.prevent_default();
|
||||
let target = ev.target().unwrap().unchecked_into::<HtmlFormElement>();
|
||||
let form_data = FormData::new_with_form(&target).unwrap();
|
||||
upload_action.dispatch(form_data);
|
||||
}>
|
||||
<input type="file" name="file_to_upload"/>
|
||||
<input type="submit"/>
|
||||
</form>
|
||||
<p>
|
||||
{move || if upload_action.input().get().is_none() && upload_action.value().get().is_none() {
|
||||
"Upload a file.".to_string()
|
||||
} else if upload_action.pending().get() {
|
||||
"Uploading...".to_string()
|
||||
} else if let Some(Ok(value)) = upload_action.value().get() {
|
||||
value.to_string()
|
||||
} else {
|
||||
format!("{:?}", upload_action.value().get())
|
||||
}}
|
||||
</p>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn FileWatcher() -> impl IntoView {
|
||||
#[server(input = GetUrl, output = StreamingText)]
|
||||
pub async fn watched_files() -> Result<TextStream, ServerFnError> {
|
||||
use notify::{
|
||||
Config, Error, Event, RecommendedWatcher, RecursiveMode, Watcher,
|
||||
};
|
||||
use std::path::Path;
|
||||
|
||||
let (tx, rx) = futures::channel::mpsc::unbounded();
|
||||
|
||||
let mut watcher = RecommendedWatcher::new(
|
||||
move |res: Result<Event, Error>| {
|
||||
if let Ok(ev) = res {
|
||||
if let Some(path) = ev.paths.last() {
|
||||
let filename = path
|
||||
.file_name()
|
||||
.unwrap()
|
||||
.to_str()
|
||||
.unwrap()
|
||||
.to_string();
|
||||
_ = tx.unbounded_send(filename); //res);
|
||||
}
|
||||
}
|
||||
},
|
||||
Config::default(),
|
||||
)?;
|
||||
watcher
|
||||
.watch(Path::new("./watched_files"), RecursiveMode::Recursive)?;
|
||||
std::mem::forget(watcher);
|
||||
|
||||
Ok(TextStream::from(rx))
|
||||
}
|
||||
|
||||
let (files, set_files) = create_signal(Vec::new());
|
||||
|
||||
create_effect(move |_| {
|
||||
spawn_local(async move {
|
||||
while let Some(res) =
|
||||
watched_files().await.unwrap().into_inner().next().await
|
||||
{
|
||||
if let Ok(filename) = res {
|
||||
set_files.update(|n| n.push(filename));
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
view! {
|
||||
<h3>Watching files and returning a streaming response</h3>
|
||||
<p>Files changed since you loaded the page:</p>
|
||||
<ul>
|
||||
{move || files.get().into_iter().map(|file| view! { <li><code>{file}</code></li> }).collect::<Vec<_>>()}
|
||||
</ul>
|
||||
<p><em>Add or remove some text files in the <code>watched_files</code> directory and see the list of changes here.</em></p>
|
||||
}
|
||||
}
|
||||
|
||||
/// The `ServerFnError` type is generic over a custom error type, which defaults to `NoCustomError`
|
||||
/// for backwards compatibility and to support the most common use case.
|
||||
///
|
||||
/// A custom error type should implement `FromStr` and `Display`, which allows it to be converted
|
||||
/// into and from a string easily to be sent over the network. It does *not* need to implement
|
||||
/// `Serialize` and `Deserialize`, although these can be used to generate the `FromStr`/`Display`
|
||||
/// implementations if you'd like. However, it's much lighter weight to use something like `strum`
|
||||
/// simply to generate those trait implementations.
|
||||
#[server]
|
||||
pub async fn ascii_uppercase(
|
||||
text: String,
|
||||
) -> Result<String, ServerFnError<InvalidArgument>> {
|
||||
if text.len() < 5 {
|
||||
Err(InvalidArgument::TooShort.into())
|
||||
} else if text.len() > 15 {
|
||||
Err(InvalidArgument::TooLong.into())
|
||||
} else if text.is_ascii() {
|
||||
Ok(text.to_ascii_uppercase())
|
||||
} else {
|
||||
Err(InvalidArgument::NotAscii.into())
|
||||
}
|
||||
}
|
||||
|
||||
// The EnumString and Display derive macros are provided by strum
|
||||
#[derive(Debug, Clone, EnumString, Display)]
|
||||
pub enum InvalidArgument {
|
||||
TooShort,
|
||||
TooLong,
|
||||
NotAscii,
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn CustomErrorTypes() -> impl IntoView {
|
||||
let input_ref = NodeRef::<Input>::new();
|
||||
let (result, set_result) = create_signal(None);
|
||||
|
||||
view! {
|
||||
<h3>Using custom error types</h3>
|
||||
<p>
|
||||
"Server functions can use a custom error type that is preserved across the network boundary."
|
||||
</p>
|
||||
<p>
|
||||
"Try typing a message that is between 5 and 15 characters of ASCII text below. Then try breaking \
|
||||
the rules!"
|
||||
</p>
|
||||
<input node_ref=input_ref placeholder="Type something here."/>
|
||||
<button
|
||||
on:click=move |_| {
|
||||
let value = input_ref.get().unwrap().value();
|
||||
spawn_local(async move {
|
||||
let data = ascii_uppercase(value).await;
|
||||
set_result(Some(data));
|
||||
});
|
||||
}
|
||||
>
|
||||
"Submit"
|
||||
</button>
|
||||
<p>
|
||||
{move || format!("{:?}", result.get())}
|
||||
</p>
|
||||
}
|
||||
}
|
|
@ -1,8 +1,7 @@
|
|||
use crate::errors::TodoAppError;
|
||||
use cfg_if::cfg_if;
|
||||
use leptos::{Errors, *};
|
||||
#[cfg(feature = "ssr")]
|
||||
use leptos_viz::ResponseOptions;
|
||||
use leptos_axum::ResponseOptions;
|
||||
|
||||
// A basic function to display errors served by the error boundaries. Feel free to do more complicated things
|
||||
// here than just displaying them
|
||||
|
@ -24,18 +23,17 @@ pub fn ErrorTemplate(
|
|||
let errors: Vec<TodoAppError> = errors
|
||||
.get()
|
||||
.into_iter()
|
||||
.filter_map(|(_k, v)| v.downcast_ref::<TodoAppError>().cloned())
|
||||
.filter_map(|(_, v)| v.downcast_ref::<TodoAppError>().cloned())
|
||||
.collect();
|
||||
|
||||
// Only the response code for the first error is actually sent from the server
|
||||
// this may be customized by the specific application
|
||||
cfg_if! {
|
||||
if #[cfg(feature="ssr")]{
|
||||
#[cfg(feature = "ssr")]
|
||||
{
|
||||
let response = use_context::<ResponseOptions>();
|
||||
if let Some(response) = response{
|
||||
response.set_status(errors[0].status_code());
|
||||
if let Some(response) = response {
|
||||
response.set_status(errors[0].status_code());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
view! {
|
||||
|
@ -46,11 +44,10 @@ pub fn ErrorTemplate(
|
|||
// a unique key for each item as a reference
|
||||
key=|(index, _error)| *index
|
||||
// renders each item to a view
|
||||
children= move |error| {
|
||||
children=move |error| {
|
||||
let error_string = error.1.to_string();
|
||||
let error_code= error.1.status_code();
|
||||
view! {
|
||||
|
||||
<h2>{error_code.to_string()}</h2>
|
||||
<p>"Error: " {error_string}</p>
|
||||
}
|
50
examples/server_fns_axum/src/fallback.rs
Normal file
50
examples/server_fns_axum/src/fallback.rs
Normal file
|
@ -0,0 +1,50 @@
|
|||
use crate::{error_template::ErrorTemplate, errors::TodoAppError};
|
||||
use axum::{
|
||||
body::Body,
|
||||
extract::State,
|
||||
http::{Request, Response, StatusCode, Uri},
|
||||
response::{IntoResponse, Response as AxumResponse},
|
||||
};
|
||||
use leptos::{view, Errors, LeptosOptions};
|
||||
use tower::ServiceExt;
|
||||
use tower_http::services::ServeDir;
|
||||
|
||||
pub async fn file_and_error_handler(
|
||||
uri: Uri,
|
||||
State(options): State<LeptosOptions>,
|
||||
req: Request<Body>,
|
||||
) -> AxumResponse {
|
||||
let root = options.site_root.clone();
|
||||
let res = get_static_file(uri.clone(), &root).await.unwrap();
|
||||
|
||||
if res.status() == StatusCode::OK {
|
||||
res.into_response()
|
||||
} else {
|
||||
let mut errors = Errors::default();
|
||||
errors.insert_with_default_key(TodoAppError::NotFound);
|
||||
let handler = leptos_axum::render_app_to_stream(
|
||||
options.to_owned(),
|
||||
move || view! {<ErrorTemplate outside_errors=errors.clone()/>},
|
||||
);
|
||||
handler(req).await.into_response()
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_static_file(
|
||||
uri: Uri,
|
||||
root: &str,
|
||||
) -> Result<Response<Body>, (StatusCode, String)> {
|
||||
let req = Request::builder()
|
||||
.uri(uri.clone())
|
||||
.body(Body::empty())
|
||||
.unwrap();
|
||||
// `ServeDir` implements `tower::Service` so we can call it with `tower::ServiceExt::oneshot`
|
||||
// This path is relative to the cargo root
|
||||
match ServeDir::new(root).oneshot(req).await {
|
||||
Ok(res) => Ok(res.into_response()),
|
||||
Err(err) => Err((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("Something went wrong: {err}"),
|
||||
)),
|
||||
}
|
||||
}
|
17
examples/server_fns_axum/src/lib.rs
Normal file
17
examples/server_fns_axum/src/lib.rs
Normal file
|
@ -0,0 +1,17 @@
|
|||
pub mod app;
|
||||
pub mod error_template;
|
||||
pub mod errors;
|
||||
#[cfg(feature = "ssr")]
|
||||
pub mod fallback;
|
||||
#[cfg(feature = "ssr")]
|
||||
pub mod middleware;
|
||||
|
||||
#[wasm_bindgen::prelude::wasm_bindgen]
|
||||
pub fn hydrate() {
|
||||
use crate::app::TodoApp;
|
||||
|
||||
_ = console_log::init_with_level(log::Level::Error);
|
||||
console_error_panic_hook::set_once();
|
||||
|
||||
leptos::mount_to_body(TodoApp);
|
||||
}
|
31
examples/server_fns_axum/src/main.rs
Normal file
31
examples/server_fns_axum/src/main.rs
Normal file
|
@ -0,0 +1,31 @@
|
|||
use crate::{app::*, fallback::file_and_error_handler};
|
||||
use axum::Router;
|
||||
use leptos::{get_configuration, logging};
|
||||
use leptos_axum::{generate_route_list, LeptosRoutes};
|
||||
use server_fns_axum::*;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
simple_logger::init_with_level(log::Level::Error)
|
||||
.expect("couldn't initialize logging");
|
||||
|
||||
// Setting this to None means we'll be using cargo-leptos and its env vars
|
||||
let conf = get_configuration(None).await.unwrap();
|
||||
let leptos_options = conf.leptos_options;
|
||||
let addr = leptos_options.site_addr;
|
||||
let routes = generate_route_list(TodoApp);
|
||||
|
||||
// build our application with a route
|
||||
let app = Router::new()
|
||||
.leptos_routes(&leptos_options, routes, TodoApp)
|
||||
.fallback(file_and_error_handler)
|
||||
.with_state(leptos_options);
|
||||
|
||||
// run our app with hyper
|
||||
// `axum::Server` is a re-export of `hyper::Server`
|
||||
let listener = tokio::net::TcpListener::bind(&addr).await.unwrap();
|
||||
logging::log!("listening on http://{}", &addr);
|
||||
axum::serve(listener, app.into_make_service())
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
72
examples/server_fns_axum/src/middleware.rs
Normal file
72
examples/server_fns_axum/src/middleware.rs
Normal file
|
@ -0,0 +1,72 @@
|
|||
use axum::body::Body;
|
||||
use http::Request;
|
||||
use pin_project_lite::pin_project;
|
||||
use std::{
|
||||
future::Future,
|
||||
pin::Pin,
|
||||
task::{Context, Poll},
|
||||
};
|
||||
use tower::{Layer, Service};
|
||||
|
||||
pub struct LoggingLayer;
|
||||
|
||||
impl<S> Layer<S> for LoggingLayer {
|
||||
type Service = LoggingService<S>;
|
||||
|
||||
fn layer(&self, inner: S) -> Self::Service {
|
||||
LoggingService { inner }
|
||||
}
|
||||
}
|
||||
|
||||
pub struct LoggingService<T> {
|
||||
inner: T,
|
||||
}
|
||||
|
||||
impl<T> Service<Request<Body>> for LoggingService<T>
|
||||
where
|
||||
T: Service<Request<Body>>,
|
||||
{
|
||||
type Response = T::Response;
|
||||
type Error = T::Error;
|
||||
type Future = LoggingServiceFuture<T::Future>;
|
||||
|
||||
fn poll_ready(
|
||||
&mut self,
|
||||
cx: &mut Context<'_>,
|
||||
) -> Poll<Result<(), Self::Error>> {
|
||||
self.inner.poll_ready(cx)
|
||||
}
|
||||
|
||||
fn call(&mut self, req: Request<Body>) -> Self::Future {
|
||||
println!("1. Running my middleware!");
|
||||
|
||||
LoggingServiceFuture {
|
||||
inner: self.inner.call(req),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pin_project! {
|
||||
pub struct LoggingServiceFuture<T> {
|
||||
#[pin]
|
||||
inner: T,
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Future for LoggingServiceFuture<T>
|
||||
where
|
||||
T: Future,
|
||||
{
|
||||
type Output = T::Output;
|
||||
|
||||
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
|
||||
let this = self.project();
|
||||
match this.inner.poll(cx) {
|
||||
Poll::Pending => Poll::Pending,
|
||||
Poll::Ready(output) => {
|
||||
println!("3. Running my middleware!");
|
||||
Poll::Ready(output)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,3 +1,3 @@
|
|||
.pending {
|
||||
color: purple;
|
||||
}
|
||||
}
|
|
@ -7,40 +7,40 @@ edition = "2021"
|
|||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.66"
|
||||
console_log = "1.0.0"
|
||||
rand = { version = "0.8.5", features = ["min_const_gen"], optional = true }
|
||||
console_error_panic_hook = "0.1.7"
|
||||
futures = "0.3.25"
|
||||
cfg-if = "1.0.0"
|
||||
anyhow = "1.0"
|
||||
console_log = "1.0"
|
||||
rand = { version = "0.8", features = ["min_const_gen"], optional = true }
|
||||
console_error_panic_hook = "0.1"
|
||||
futures = "0.3"
|
||||
leptos = { path = "../../leptos", features = ["nightly"] }
|
||||
leptos_meta = { path = "../../meta", features = ["nightly"] }
|
||||
leptos_axum = { path = "../../integrations/axum", optional = true }
|
||||
leptos_router = { path = "../../router", features = ["nightly"] }
|
||||
log = "0.4.17"
|
||||
simple_logger = "4.0.0"
|
||||
serde = { version = "1.0.148", features = ["derive"] }
|
||||
axum = { version = "0.6.1", optional = true, features=["macros"] }
|
||||
tower = { version = "0.4.13", optional = true }
|
||||
tower-http = { version = "0.4", features = ["fs"], optional = true }
|
||||
tokio = { version = "1.22.0", features = ["full"], optional = true }
|
||||
http = { version = "0.2.11" }
|
||||
log = "0.4"
|
||||
simple_logger = "4.0"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
axum = { version = "0.7", optional = true, features=["macros"] }
|
||||
tower = { version = "0.4", optional = true }
|
||||
tower-http = { version = "0.5", features = ["fs"], optional = true }
|
||||
tokio = { version = "1", features = ["full"], optional = true }
|
||||
http = { version = "1.0" }
|
||||
sqlx = { version = "0.7.2", features = [
|
||||
"runtime-tokio-rustls",
|
||||
"sqlite",
|
||||
], optional = true }
|
||||
thiserror = "1.0.38"
|
||||
thiserror = "1.0"
|
||||
wasm-bindgen = "0.2"
|
||||
axum_session_auth = { version = "0.9.0", features = [
|
||||
axum_session_auth = { version = "0.10", features = [
|
||||
"sqlite-rustls",
|
||||
], optional = true }
|
||||
axum_session = { version = "0.9.0", features = [
|
||||
axum_session = { version = "0.10", features = [
|
||||
"sqlite-rustls",
|
||||
], optional = true }
|
||||
bcrypt = { version = "0.14", optional = true }
|
||||
async-trait = { version = "0.1.64", optional = true }
|
||||
bcrypt = { version = "0.15", optional = true }
|
||||
async-trait = { version = "0.1", optional = true }
|
||||
|
||||
[features]
|
||||
default = ["ssr"]
|
||||
hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate"]
|
||||
ssr = [
|
||||
"dep:axum",
|
||||
|
|
Binary file not shown.
|
@ -5,11 +5,11 @@
|
|||
"systems": "systems"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1681202837,
|
||||
"narHash": "sha256-H+Rh19JDwRtpVPAWp64F+rlEtxUWBAQW28eAi3SRSzg=",
|
||||
"lastModified": 1701680307,
|
||||
"narHash": "sha256-kAuep2h5ajznlPMD9rnQyffWG8EM/C73lejGofXvdM8=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "cfacdce06f30d2b68473a46042957675eebb3401",
|
||||
"rev": "4022d587cbbfd70fe950c1e2083a02621806a725",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
@ -38,11 +38,11 @@
|
|||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1672580127,
|
||||
"narHash": "sha256-3lW3xZslREhJogoOkjeZtlBtvFMyxHku7I/9IVehhT8=",
|
||||
"lastModified": 1685573264,
|
||||
"narHash": "sha256-Zffu01pONhs/pqH07cjlF10NnMDLok8ix5Uk4rhOnZQ=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "0874168639713f547c05947c76124f78441ea46c",
|
||||
"rev": "380be19fbd2d9079f677978361792cb25e8a3635",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
@ -67,11 +67,11 @@
|
|||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1681525152,
|
||||
"narHash": "sha256-KzI+ILcmU03iFWtB+ysPqtNmp8TP8v1BBReTuPP8MJY=",
|
||||
"lastModified": 1703902408,
|
||||
"narHash": "sha256-qXdWvu+tlgNjeoz8yQMRKSom6QyRROfgpmeOhwbujqw=",
|
||||
"owner": "oxalica",
|
||||
"repo": "rust-overlay",
|
||||
"rev": "b6f8d87208336d7cb85003b2e439fc707c38f92a",
|
||||
"rev": "319f57cd2c34348c55970a4bf2b35afe82088681",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
|
|
@ -1,17 +1,7 @@
|
|||
use cfg_if::cfg_if;
|
||||
use leptos::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashSet;
|
||||
|
||||
cfg_if! {
|
||||
if #[cfg(feature = "ssr")] {
|
||||
use sqlx::SqlitePool;
|
||||
use axum_session_auth::{SessionSqlitePool, Authentication, HasPermission};
|
||||
use bcrypt::{hash, verify, DEFAULT_COST};
|
||||
use crate::todo::{pool, auth};
|
||||
pub type AuthSession = axum_session_auth::AuthSession<User, i64, SessionSqlitePool, SqlitePool>;
|
||||
}}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct User {
|
||||
pub id: i64,
|
||||
|
@ -33,17 +23,33 @@ impl Default for User {
|
|||
}
|
||||
}
|
||||
|
||||
cfg_if! {
|
||||
if #[cfg(feature = "ssr")] {
|
||||
use async_trait::async_trait;
|
||||
#[cfg(feature = "ssr")]
|
||||
pub mod ssr {
|
||||
pub use super::User;
|
||||
pub use axum_session_auth::{
|
||||
Authentication, HasPermission, SessionSqlitePool,
|
||||
};
|
||||
pub use sqlx::SqlitePool;
|
||||
pub use std::collections::HashSet;
|
||||
pub type AuthSession = axum_session_auth::AuthSession<
|
||||
User,
|
||||
i64,
|
||||
SessionSqlitePool,
|
||||
SqlitePool,
|
||||
>;
|
||||
pub use crate::todo::ssr::{auth, pool};
|
||||
pub use async_trait::async_trait;
|
||||
pub use bcrypt::{hash, verify, DEFAULT_COST};
|
||||
|
||||
impl User {
|
||||
pub async fn get(id: i64, pool: &SqlitePool) -> Option<Self> {
|
||||
let sqluser = sqlx::query_as::<_, SqlUser>("SELECT * FROM users WHERE id = ?")
|
||||
.bind(id)
|
||||
.fetch_one(pool)
|
||||
.await
|
||||
.ok()?;
|
||||
let sqluser = sqlx::query_as::<_, SqlUser>(
|
||||
"SELECT * FROM users WHERE id = ?",
|
||||
)
|
||||
.bind(id)
|
||||
.fetch_one(pool)
|
||||
.await
|
||||
.ok()?;
|
||||
|
||||
//lets just get all the tokens the user can use, we will only use the full permissions if modifing them.
|
||||
let sql_user_perms = sqlx::query_as::<_, SqlPermissionTokens>(
|
||||
|
@ -57,12 +63,17 @@ if #[cfg(feature = "ssr")] {
|
|||
Some(sqluser.into_user(Some(sql_user_perms)))
|
||||
}
|
||||
|
||||
pub async fn get_from_username(name: String, pool: &SqlitePool) -> Option<Self> {
|
||||
let sqluser = sqlx::query_as::<_, SqlUser>("SELECT * FROM users WHERE username = ?")
|
||||
.bind(name)
|
||||
.fetch_one(pool)
|
||||
.await
|
||||
.ok()?;
|
||||
pub async fn get_from_username(
|
||||
name: String,
|
||||
pool: &SqlitePool,
|
||||
) -> Option<Self> {
|
||||
let sqluser = sqlx::query_as::<_, SqlUser>(
|
||||
"SELECT * FROM users WHERE username = ?",
|
||||
)
|
||||
.bind(name)
|
||||
.fetch_one(pool)
|
||||
.await
|
||||
.ok()?;
|
||||
|
||||
//lets just get all the tokens the user can use, we will only use the full permissions if modifing them.
|
||||
let sql_user_perms = sqlx::query_as::<_, SqlPermissionTokens>(
|
||||
|
@ -84,7 +95,10 @@ if #[cfg(feature = "ssr")] {
|
|||
|
||||
#[async_trait]
|
||||
impl Authentication<User, i64, SqlitePool> for User {
|
||||
async fn load_user(userid: i64, pool: Option<&SqlitePool>) -> Result<User, anyhow::Error> {
|
||||
async fn load_user(
|
||||
userid: i64,
|
||||
pool: Option<&SqlitePool>,
|
||||
) -> Result<User, anyhow::Error> {
|
||||
let pool = pool.unwrap();
|
||||
|
||||
User::get(userid, pool)
|
||||
|
@ -120,7 +134,10 @@ if #[cfg(feature = "ssr")] {
|
|||
}
|
||||
|
||||
impl SqlUser {
|
||||
pub fn into_user(self, sql_user_perms: Option<Vec<SqlPermissionTokens>>) -> User {
|
||||
pub fn into_user(
|
||||
self,
|
||||
sql_user_perms: Option<Vec<SqlPermissionTokens>>,
|
||||
) -> User {
|
||||
User {
|
||||
id: self.id,
|
||||
username: self.username,
|
||||
|
@ -137,15 +154,16 @@ if #[cfg(feature = "ssr")] {
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[server(Foo, "/api")]
|
||||
#[server]
|
||||
pub async fn foo() -> Result<String, ServerFnError> {
|
||||
Ok(String::from("Bar!"))
|
||||
}
|
||||
|
||||
#[server(GetUser, "/api")]
|
||||
#[server]
|
||||
pub async fn get_user() -> Result<Option<User>, ServerFnError> {
|
||||
use crate::todo::ssr::auth;
|
||||
|
||||
let auth = auth()?;
|
||||
|
||||
Ok(auth.current_user)
|
||||
|
@ -157,14 +175,14 @@ pub async fn login(
|
|||
password: String,
|
||||
remember: Option<String>,
|
||||
) -> Result<(), ServerFnError> {
|
||||
use self::ssr::*;
|
||||
|
||||
let pool = pool()?;
|
||||
let auth = auth()?;
|
||||
|
||||
let user: User = User::get_from_username(username, &pool)
|
||||
.await
|
||||
.ok_or_else(|| {
|
||||
ServerFnError::ServerError("User does not exist.".into())
|
||||
})?;
|
||||
.ok_or_else(|| ServerFnError::new("User does not exist."))?;
|
||||
|
||||
match verify(password, &user.password)? {
|
||||
true => {
|
||||
|
@ -186,6 +204,8 @@ pub async fn signup(
|
|||
password_confirmation: String,
|
||||
remember: Option<String>,
|
||||
) -> Result<(), ServerFnError> {
|
||||
use self::ssr::*;
|
||||
|
||||
let pool = pool()?;
|
||||
let auth = auth()?;
|
||||
|
||||
|
@ -207,9 +227,7 @@ pub async fn signup(
|
|||
User::get_from_username(username, &pool)
|
||||
.await
|
||||
.ok_or_else(|| {
|
||||
ServerFnError::ServerError(
|
||||
"Signup failed: User does not exist.".into(),
|
||||
)
|
||||
ServerFnError::new("Signup failed: User does not exist.")
|
||||
})?;
|
||||
|
||||
auth.login_user(user.id);
|
||||
|
@ -222,6 +240,8 @@ pub async fn signup(
|
|||
|
||||
#[server(Logout, "/api")]
|
||||
pub async fn logout() -> Result<(), ServerFnError> {
|
||||
use self::ssr::*;
|
||||
|
||||
let auth = auth()?;
|
||||
|
||||
auth.logout_user();
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
use crate::errors::TodoAppError;
|
||||
use cfg_if::cfg_if;
|
||||
use leptos::{Errors, *};
|
||||
#[cfg(feature = "ssr")]
|
||||
use leptos_axum::ResponseOptions;
|
||||
|
@ -29,13 +28,12 @@ pub fn ErrorTemplate(
|
|||
|
||||
// Only the response code for the first error is actually sent from the server
|
||||
// this may be customized by the specific application
|
||||
cfg_if! {
|
||||
if #[cfg(feature="ssr")]{
|
||||
#[cfg(feature = "ssr")]
|
||||
{
|
||||
let response = use_context::<ResponseOptions>();
|
||||
if let Some(response) = response{
|
||||
response.set_status(errors[0].status_code());
|
||||
if let Some(response) = response {
|
||||
response.set_status(errors[0].status_code());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
view! {
|
||||
|
|
|
@ -1,47 +1,50 @@
|
|||
use cfg_if::cfg_if;
|
||||
use crate::{error_template::ErrorTemplate, errors::TodoAppError};
|
||||
use axum::{
|
||||
body::Body,
|
||||
extract::State,
|
||||
http::{Request, Response, StatusCode, Uri},
|
||||
response::{IntoResponse, Response as AxumResponse},
|
||||
};
|
||||
use leptos::{view, Errors, LeptosOptions};
|
||||
use tower::ServiceExt;
|
||||
use tower_http::services::ServeDir;
|
||||
|
||||
cfg_if! {
|
||||
if #[cfg(feature = "ssr")] {
|
||||
use axum::{
|
||||
body::{boxed, Body, BoxBody},
|
||||
extract::State,
|
||||
response::IntoResponse,
|
||||
http::{Request, Response, StatusCode, Uri},
|
||||
};
|
||||
use axum::response::Response as AxumResponse;
|
||||
use tower::ServiceExt;
|
||||
use tower_http::services::ServeDir;
|
||||
use leptos::{LeptosOptions, Errors, view};
|
||||
use crate::error_template::ErrorTemplate;
|
||||
use crate::errors::TodoAppError;
|
||||
pub async fn file_and_error_handler(
|
||||
uri: Uri,
|
||||
State(options): State<LeptosOptions>,
|
||||
req: Request<Body>,
|
||||
) -> AxumResponse {
|
||||
let root = options.site_root.clone();
|
||||
let res = get_static_file(uri.clone(), &root).await.unwrap();
|
||||
|
||||
pub async fn file_and_error_handler(uri: Uri, State(options): State<LeptosOptions>, req: Request<Body>) -> AxumResponse {
|
||||
let root = options.site_root.clone();
|
||||
let res = get_static_file(uri.clone(), &root).await.unwrap();
|
||||
|
||||
if res.status() == StatusCode::OK {
|
||||
res.into_response()
|
||||
} else{
|
||||
let mut errors = Errors::default();
|
||||
errors.insert_with_default_key(TodoAppError::NotFound);
|
||||
let handler = leptos_axum::render_app_to_stream(options.to_owned(), move || view!{<ErrorTemplate outside_errors=errors.clone()/>});
|
||||
handler(req).await.into_response()
|
||||
}
|
||||
if res.status() == StatusCode::OK {
|
||||
res.into_response()
|
||||
} else {
|
||||
let mut errors = Errors::default();
|
||||
errors.insert_with_default_key(TodoAppError::NotFound);
|
||||
let handler = leptos_axum::render_app_to_stream(
|
||||
options.to_owned(),
|
||||
move || view! {<ErrorTemplate outside_errors=errors.clone()/>},
|
||||
);
|
||||
handler(req).await.into_response()
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_static_file(uri: Uri, root: &str) -> Result<Response<BoxBody>, (StatusCode, String)> {
|
||||
let req = Request::builder().uri(uri.clone()).body(Body::empty()).unwrap();
|
||||
// `ServeDir` implements `tower::Service` so we can call it with `tower::ServiceExt::oneshot`
|
||||
// This path is relative to the cargo root
|
||||
match ServeDir::new(root).oneshot(req).await {
|
||||
Ok(res) => Ok(res.map(boxed)),
|
||||
Err(err) => Err((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("Something went wrong: {err}"),
|
||||
)),
|
||||
}
|
||||
async fn get_static_file(
|
||||
uri: Uri,
|
||||
root: &str,
|
||||
) -> Result<Response<Body>, (StatusCode, String)> {
|
||||
let req = Request::builder()
|
||||
.uri(uri.clone())
|
||||
.body(Body::empty())
|
||||
.unwrap();
|
||||
// `ServeDir` implements `tower::Service` so we can call it with `tower::ServiceExt::oneshot`
|
||||
// This path is relative to the cargo root
|
||||
match ServeDir::new(root).oneshot(req).await {
|
||||
Ok(res) => Ok(res.into_response()),
|
||||
Err(err) => Err((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("Something went wrong: {err}"),
|
||||
)),
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,27 +1,18 @@
|
|||
use cfg_if::cfg_if;
|
||||
|
||||
pub mod auth;
|
||||
pub mod error_template;
|
||||
pub mod errors;
|
||||
#[cfg(feature = "ssr")]
|
||||
pub mod fallback;
|
||||
#[cfg(feature = "ssr")]
|
||||
pub mod state;
|
||||
pub mod todo;
|
||||
|
||||
// Needs to be in lib.rs AFAIK because wasm-bindgen needs us to be compiling a lib. I may be wrong.
|
||||
cfg_if! {
|
||||
if #[cfg(feature = "hydrate")] {
|
||||
use wasm_bindgen::prelude::wasm_bindgen;
|
||||
use crate::todo::*;
|
||||
use leptos::view;
|
||||
#[cfg(feature = "hydrate")]
|
||||
#[wasm_bindgen::prelude::wasm_bindgen]
|
||||
pub fn hydrate() {
|
||||
use crate::todo::*;
|
||||
_ = console_log::init_with_level(log::Level::Debug);
|
||||
console_error_panic_hook::set_once();
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn hydrate() {
|
||||
_ = console_log::init_with_level(log::Level::Debug);
|
||||
console_error_panic_hook::set_once();
|
||||
|
||||
leptos::mount_to_body(|| {
|
||||
view! { <TodoApp/> }
|
||||
});
|
||||
}
|
||||
}
|
||||
leptos::mount_to_body(TodoApp);
|
||||
}
|
||||
|
|
|
@ -1,117 +1,131 @@
|
|||
use cfg_if::cfg_if;
|
||||
use axum::{
|
||||
body::Body as AxumBody,
|
||||
extract::{Path, State},
|
||||
http::Request,
|
||||
response::{IntoResponse, Response},
|
||||
routing::get,
|
||||
Router,
|
||||
};
|
||||
use axum_session::{SessionConfig, SessionLayer, SessionStore};
|
||||
use axum_session_auth::{AuthConfig, AuthSessionLayer, SessionSqlitePool};
|
||||
use leptos::{get_configuration, logging::log, provide_context};
|
||||
use leptos_axum::{
|
||||
generate_route_list, handle_server_fns_with_context, LeptosRoutes,
|
||||
};
|
||||
use session_auth_axum::{
|
||||
auth::{ssr::AuthSession, User},
|
||||
fallback::file_and_error_handler,
|
||||
state::AppState,
|
||||
todo::*,
|
||||
};
|
||||
use sqlx::{sqlite::SqlitePoolOptions, SqlitePool};
|
||||
|
||||
// boilerplate to run in different modes
|
||||
cfg_if! {
|
||||
if #[cfg(feature = "ssr")] {
|
||||
use axum::{
|
||||
response::{Response, IntoResponse},
|
||||
routing::get,
|
||||
extract::{Path, State, RawQuery},
|
||||
http::{Request, header::HeaderMap},
|
||||
body::Body as AxumBody,
|
||||
Router,
|
||||
};
|
||||
use session_auth_axum::todo::*;
|
||||
use session_auth_axum::auth::*;
|
||||
use session_auth_axum::state::AppState;
|
||||
use session_auth_axum::fallback::file_and_error_handler;
|
||||
use leptos_axum::{generate_route_list, LeptosRoutes, handle_server_fns_with_context};
|
||||
use leptos::{logging::log, provide_context, get_configuration};
|
||||
use sqlx::{SqlitePool, sqlite::SqlitePoolOptions};
|
||||
use axum_session::{SessionConfig, SessionLayer, SessionStore};
|
||||
use axum_session_auth::{AuthSessionLayer, AuthConfig, SessionSqlitePool};
|
||||
async fn server_fn_handler(
|
||||
State(app_state): State<AppState>,
|
||||
auth_session: AuthSession,
|
||||
path: Path<String>,
|
||||
request: Request<AxumBody>,
|
||||
) -> impl IntoResponse {
|
||||
log!("{:?}", path);
|
||||
|
||||
async fn server_fn_handler(State(app_state): State<AppState>, auth_session: AuthSession, path: Path<String>, headers: HeaderMap, raw_query: RawQuery,
|
||||
request: Request<AxumBody>) -> impl IntoResponse {
|
||||
|
||||
log!("{:?}", path);
|
||||
|
||||
handle_server_fns_with_context(path, headers, raw_query, move || {
|
||||
handle_server_fns_with_context(
|
||||
move || {
|
||||
provide_context(auth_session.clone());
|
||||
provide_context(app_state.pool.clone());
|
||||
}, request).await
|
||||
},
|
||||
request,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn leptos_routes_handler(
|
||||
auth_session: AuthSession,
|
||||
State(app_state): State<AppState>,
|
||||
req: Request<AxumBody>,
|
||||
) -> Response {
|
||||
let handler = leptos_axum::render_route_with_context(
|
||||
app_state.leptos_options.clone(),
|
||||
app_state.routes.clone(),
|
||||
move || {
|
||||
provide_context(auth_session.clone());
|
||||
provide_context(app_state.pool.clone());
|
||||
},
|
||||
TodoApp,
|
||||
);
|
||||
handler(req).await.into_response()
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
simple_logger::init_with_level(log::Level::Info)
|
||||
.expect("couldn't initialize logging");
|
||||
|
||||
let pool = SqlitePoolOptions::new()
|
||||
.connect("sqlite:Todos.db")
|
||||
.await
|
||||
.expect("Could not make pool.");
|
||||
|
||||
// Auth section
|
||||
let session_config =
|
||||
SessionConfig::default().with_table_name("axum_sessions");
|
||||
let auth_config = AuthConfig::<i64>::default();
|
||||
let session_store = SessionStore::<SessionSqlitePool>::new(
|
||||
Some(pool.clone().into()),
|
||||
session_config,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
if let Err(e) = sqlx::migrate!().run(&pool).await {
|
||||
eprintln!("{e:?}");
|
||||
}
|
||||
|
||||
async fn leptos_routes_handler(auth_session: AuthSession, State(app_state): State<AppState>, req: Request<AxumBody>) -> Response{
|
||||
let handler = leptos_axum::render_route_with_context(app_state.leptos_options.clone(),
|
||||
app_state.routes.clone(),
|
||||
move || {
|
||||
provide_context(auth_session.clone());
|
||||
provide_context(app_state.pool.clone());
|
||||
},
|
||||
TodoApp
|
||||
);
|
||||
handler(req).await.into_response()
|
||||
}
|
||||
// Explicit server function registration is no longer required
|
||||
// on the main branch. On 0.3.0 and earlier, uncomment the lines
|
||||
// below to register the server functions.
|
||||
// _ = GetTodos::register();
|
||||
// _ = AddTodo::register();
|
||||
// _ = DeleteTodo::register();
|
||||
// _ = Login::register();
|
||||
// _ = Logout::register();
|
||||
// _ = Signup::register();
|
||||
// _ = GetUser::register();
|
||||
// _ = Foo::register();
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
simple_logger::init_with_level(log::Level::Info).expect("couldn't initialize logging");
|
||||
// Setting this to None means we'll be using cargo-leptos and its env vars
|
||||
let conf = get_configuration(None).await.unwrap();
|
||||
let leptos_options = conf.leptos_options;
|
||||
let addr = leptos_options.site_addr;
|
||||
let routes = generate_route_list(TodoApp);
|
||||
|
||||
let pool = SqlitePoolOptions::new()
|
||||
.connect("sqlite:Todos.db")
|
||||
.await
|
||||
.expect("Could not make pool.");
|
||||
let app_state = AppState {
|
||||
leptos_options,
|
||||
pool: pool.clone(),
|
||||
routes: routes.clone(),
|
||||
};
|
||||
|
||||
// Auth section
|
||||
let session_config = SessionConfig::default().with_table_name("axum_sessions");
|
||||
let auth_config = AuthConfig::<i64>::default();
|
||||
let session_store = SessionStore::<SessionSqlitePool>::new(Some(pool.clone().into()), session_config).await.unwrap();
|
||||
|
||||
sqlx::migrate!()
|
||||
.run(&pool)
|
||||
.await
|
||||
.expect("could not run SQLx migrations");
|
||||
|
||||
// Explicit server function registration is no longer required
|
||||
// on the main branch. On 0.3.0 and earlier, uncomment the lines
|
||||
// below to register the server functions.
|
||||
// _ = GetTodos::register();
|
||||
// _ = AddTodo::register();
|
||||
// _ = DeleteTodo::register();
|
||||
// _ = Login::register();
|
||||
// _ = Logout::register();
|
||||
// _ = Signup::register();
|
||||
// _ = GetUser::register();
|
||||
// _ = Foo::register();
|
||||
|
||||
// Setting this to None means we'll be using cargo-leptos and its env vars
|
||||
let conf = get_configuration(None).await.unwrap();
|
||||
let leptos_options = conf.leptos_options;
|
||||
let addr = leptos_options.site_addr;
|
||||
let routes = generate_route_list(TodoApp);
|
||||
|
||||
let app_state = AppState{
|
||||
leptos_options,
|
||||
pool: pool.clone(),
|
||||
routes: routes.clone(),
|
||||
};
|
||||
|
||||
// build our application with a route
|
||||
let app = Router::new()
|
||||
.route("/api/*fn_name", get(server_fn_handler).post(server_fn_handler))
|
||||
.leptos_routes_with_handler(routes, get(leptos_routes_handler) )
|
||||
// build our application with a route
|
||||
let app = Router::new()
|
||||
.route(
|
||||
"/api/*fn_name",
|
||||
get(server_fn_handler).post(server_fn_handler),
|
||||
)
|
||||
.leptos_routes_with_handler(routes, get(leptos_routes_handler))
|
||||
.fallback(file_and_error_handler)
|
||||
.layer(AuthSessionLayer::<User, i64, SessionSqlitePool, SqlitePool>::new(Some(pool.clone()))
|
||||
.with_config(auth_config))
|
||||
.layer(
|
||||
AuthSessionLayer::<User, i64, SessionSqlitePool, SqlitePool>::new(
|
||||
Some(pool.clone()),
|
||||
)
|
||||
.with_config(auth_config),
|
||||
)
|
||||
.layer(SessionLayer::new(session_store))
|
||||
.with_state(app_state);
|
||||
|
||||
// run our app with hyper
|
||||
// `axum::Server` is a re-export of `hyper::Server`
|
||||
log!("listening on http://{}", &addr);
|
||||
axum::Server::bind(&addr)
|
||||
.serve(app.into_make_service())
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
// client-only stuff for Trunk
|
||||
else {
|
||||
pub fn main() {
|
||||
// This example cannot be built as a trunk standalone CSR-only app.
|
||||
// Only the server may directly connect to the database.
|
||||
}
|
||||
}
|
||||
// run our app with hyper
|
||||
// `axum::Server` is a re-export of `hyper::Server`
|
||||
log!("listening on http://{}", &addr);
|
||||
let listener = tokio::net::TcpListener::bind(&addr).await.unwrap();
|
||||
axum::serve(listener, app.into_make_service())
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
|
|
@ -1,18 +1,13 @@
|
|||
use cfg_if::cfg_if;
|
||||
|
||||
cfg_if! {
|
||||
if #[cfg(feature = "ssr")] {
|
||||
use leptos::LeptosOptions;
|
||||
use sqlx::SqlitePool;
|
||||
use axum::extract::FromRef;
|
||||
use leptos::LeptosOptions;
|
||||
use leptos_router::RouteListing;
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
/// This takes advantage of Axum's SubStates feature by deriving FromRef. This is the only way to have more than one
|
||||
/// item in Axum's State. Leptos requires you to have leptosOptions in your State struct for the leptos route handlers
|
||||
#[derive(FromRef, Debug, Clone)]
|
||||
pub struct AppState{
|
||||
pub struct AppState {
|
||||
pub leptos_options: LeptosOptions,
|
||||
pub pool: SqlitePool,
|
||||
pub routes: Vec<RouteListing>,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
use crate::{auth::*, error_template::ErrorTemplate};
|
||||
use cfg_if::cfg_if;
|
||||
use leptos::*;
|
||||
use leptos_meta::*;
|
||||
use leptos_router::*;
|
||||
|
@ -14,40 +13,41 @@ pub struct Todo {
|
|||
completed: bool,
|
||||
}
|
||||
|
||||
cfg_if! {
|
||||
if #[cfg(feature = "ssr")] {
|
||||
#[cfg(feature = "ssr")]
|
||||
pub mod ssr {
|
||||
use super::Todo;
|
||||
use crate::auth::{ssr::AuthSession, User};
|
||||
use leptos::*;
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
use sqlx::SqlitePool;
|
||||
use futures::future::join_all;
|
||||
pub fn pool() -> Result<SqlitePool, ServerFnError> {
|
||||
use_context::<SqlitePool>()
|
||||
.ok_or_else(|| ServerFnError::ServerError("Pool missing.".into()))
|
||||
}
|
||||
|
||||
pub fn pool() -> Result<SqlitePool, ServerFnError> {
|
||||
use_context::<SqlitePool>()
|
||||
.ok_or_else(|| ServerFnError::ServerError("Pool missing.".into()))
|
||||
}
|
||||
pub fn auth() -> Result<AuthSession, ServerFnError> {
|
||||
use_context::<AuthSession>().ok_or_else(|| {
|
||||
ServerFnError::ServerError("Auth session missing.".into())
|
||||
})
|
||||
}
|
||||
|
||||
pub fn auth() -> Result<AuthSession, ServerFnError> {
|
||||
use_context::<AuthSession>()
|
||||
.ok_or_else(|| ServerFnError::ServerError("Auth session missing.".into()))
|
||||
}
|
||||
#[derive(sqlx::FromRow, Clone)]
|
||||
pub struct SqlTodo {
|
||||
id: u32,
|
||||
user_id: i64,
|
||||
title: String,
|
||||
created_at: String,
|
||||
completed: bool,
|
||||
}
|
||||
|
||||
#[derive(sqlx::FromRow, Clone)]
|
||||
pub struct SqlTodo {
|
||||
id: u32,
|
||||
user_id: i64,
|
||||
title: String,
|
||||
created_at: String,
|
||||
completed: bool,
|
||||
}
|
||||
|
||||
impl SqlTodo {
|
||||
pub async fn into_todo(self, pool: &SqlitePool) -> Todo {
|
||||
Todo {
|
||||
id: self.id,
|
||||
user: User::get(self.user_id, pool).await,
|
||||
title: self.title,
|
||||
created_at: self.created_at,
|
||||
completed: self.completed,
|
||||
}
|
||||
impl SqlTodo {
|
||||
pub async fn into_todo(self, pool: &SqlitePool) -> Todo {
|
||||
Todo {
|
||||
id: self.id,
|
||||
user: User::get(self.user_id, pool).await,
|
||||
title: self.title,
|
||||
created_at: self.created_at,
|
||||
completed: self.completed,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -55,6 +55,9 @@ cfg_if! {
|
|||
|
||||
#[server(GetTodos, "/api")]
|
||||
pub async fn get_todos() -> Result<Vec<Todo>, ServerFnError> {
|
||||
use self::ssr::{pool, SqlTodo};
|
||||
use futures::future::join_all;
|
||||
|
||||
let pool = pool()?;
|
||||
|
||||
Ok(join_all(
|
||||
|
@ -69,6 +72,8 @@ pub async fn get_todos() -> Result<Vec<Todo>, ServerFnError> {
|
|||
|
||||
#[server(AddTodo, "/api")]
|
||||
pub async fn add_todo(title: String) -> Result<(), ServerFnError> {
|
||||
use self::ssr::*;
|
||||
|
||||
let user = get_user().await?;
|
||||
let pool = pool()?;
|
||||
|
||||
|
@ -93,6 +98,8 @@ pub async fn add_todo(title: String) -> Result<(), ServerFnError> {
|
|||
// The struct name and path prefix arguments are optional.
|
||||
#[server]
|
||||
pub async fn delete_todo(id: u16) -> Result<(), ServerFnError> {
|
||||
use self::ssr::*;
|
||||
|
||||
let pool = pool()?;
|
||||
|
||||
Ok(sqlx::query("DELETE FROM todos WHERE id = $1")
|
||||
|
|
|
@ -7,40 +7,39 @@ edition = "2021"
|
|||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[dependencies]
|
||||
oauth2 = {version="4.4.2",optional=true}
|
||||
oauth2 = { version = "4.4.2", optional = true }
|
||||
anyhow = "1.0.66"
|
||||
console_log = "1.0.0"
|
||||
rand = { version = "0.8.5", features = ["min_const_gen"], optional = true }
|
||||
console_error_panic_hook = "0.1.7"
|
||||
futures = "0.3.25"
|
||||
cfg-if = "1.0.0"
|
||||
leptos = { path = "../../leptos"}
|
||||
leptos = { path = "../../leptos" }
|
||||
leptos_meta = { path = "../../meta" }
|
||||
leptos_axum = { path = "../../integrations/axum", optional = true }
|
||||
leptos_router = { path = "../../router"}
|
||||
leptos_router = { path = "../../router" }
|
||||
log = "0.4.17"
|
||||
simple_logger = "4.0.0"
|
||||
serde = { version = "1.0.148", features = ["derive"] }
|
||||
serde_json = {version="1.0.108", optional = true }
|
||||
axum = { version = "0.6.1", optional = true, features=["macros"] }
|
||||
tower = { version = "0.4.13", optional = true }
|
||||
tower-http = { version = "0.4", features = ["fs"], optional = true }
|
||||
serde_json = { version = "1.0.108", optional = true }
|
||||
axum = { version = "0.7", optional = true, features = ["macros"] }
|
||||
tower = { version = "0.4", optional = true }
|
||||
tower-http = { version = "0.5", features = ["fs"], optional = true }
|
||||
tokio = { version = "1.22.0", features = ["full"], optional = true }
|
||||
http = { version = "0.2.8" }
|
||||
http = { version = "1" }
|
||||
sqlx = { version = "0.7", features = [
|
||||
"runtime-tokio-rustls",
|
||||
"sqlite",
|
||||
], optional = true }
|
||||
thiserror = "1.0.38"
|
||||
wasm-bindgen = "0.2"
|
||||
axum_session_auth = { version = "0.9", features = [
|
||||
axum_session_auth = { version = "0.12", features = [
|
||||
"sqlite-rustls",
|
||||
], optional = true }
|
||||
axum_session = { version = "0.9", features = [
|
||||
axum_session = { version = "0.12", features = [
|
||||
"sqlite-rustls",
|
||||
], optional = true }
|
||||
async-trait = { version = "0.1.64", optional = true }
|
||||
reqwest= {version="0.11",optional=true, features=["json"]}
|
||||
reqwest = { version = "0.11", optional = true, features = ["json"] }
|
||||
|
||||
[features]
|
||||
hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate"]
|
||||
|
@ -63,7 +62,9 @@ ssr = [
|
|||
"dep:leptos_axum",
|
||||
]
|
||||
|
||||
|
||||
[package.metadata.cargo-all-features]
|
||||
denylist = ["axum", "tower", "tower-http", "tokio", "sqlx", "leptos_axum"]
|
||||
skip_feature_sets = [["csr", "ssr"], ["csr", "hydrate"], ["ssr", "hydrate"]]
|
||||
|
||||
[package.metadata.leptos]
|
||||
# The name used by wasm-bindgen/cargo-leptos for the JS/WASM bundle. Defaults to the crate name
|
||||
|
|
|
@ -1,11 +1,8 @@
|
|||
extend = { path = "../cargo-make/main.toml" }
|
||||
extend = [
|
||||
{ path = "../cargo-make/main.toml" },
|
||||
{ path = "../cargo-make/cargo-leptos.toml" },
|
||||
]
|
||||
|
||||
[tasks.build]
|
||||
command = "cargo"
|
||||
args = ["+nightly", "build-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
[env]
|
||||
|
||||
[tasks.check]
|
||||
command = "cargo"
|
||||
args = ["+nightly", "check-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
CLIENT_PROCESS_NAME = "sso_auth_axum"
|
||||
|
|
|
@ -1,14 +1,6 @@
|
|||
use cfg_if::cfg_if;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashSet;
|
||||
|
||||
cfg_if! {
|
||||
if #[cfg(feature = "ssr")] {
|
||||
use sqlx::SqlitePool;
|
||||
use axum_session_auth::{SessionSqlitePool, Authentication, HasPermission};
|
||||
pub type AuthSession = axum_session_auth::AuthSession<User, i64, SessionSqlitePool, SqlitePool>;
|
||||
}}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct User {
|
||||
pub id: i64,
|
||||
|
@ -28,36 +20,56 @@ impl Default for User {
|
|||
}
|
||||
}
|
||||
|
||||
cfg_if! {
|
||||
if #[cfg(feature = "ssr")] {
|
||||
#[cfg(feature = "ssr")]
|
||||
pub mod ssr_imports {
|
||||
use super::User;
|
||||
pub use axum_session_auth::{
|
||||
Authentication, HasPermission, SessionSqlitePool,
|
||||
};
|
||||
pub use sqlx::SqlitePool;
|
||||
use std::collections::HashSet;
|
||||
pub type AuthSession = axum_session_auth::AuthSession<
|
||||
User,
|
||||
i64,
|
||||
SessionSqlitePool,
|
||||
SqlitePool,
|
||||
>;
|
||||
|
||||
use async_trait::async_trait;
|
||||
|
||||
impl User {
|
||||
pub async fn get(id: i64, pool: &SqlitePool) -> Option<Self> {
|
||||
let sqluser = sqlx::query_as::<_, SqlUser>("SELECT * FROM users WHERE id = ?")
|
||||
.bind(id)
|
||||
.fetch_one(pool)
|
||||
.await
|
||||
.ok()?;
|
||||
let sqluser = sqlx::query_as::<_, SqlUser>(
|
||||
"SELECT * FROM users WHERE id = ?",
|
||||
)
|
||||
.bind(id)
|
||||
.fetch_one(pool)
|
||||
.await
|
||||
.ok()?;
|
||||
|
||||
//lets just get all the tokens the user can use, we will only use the full permissions if modifing them.
|
||||
let sql_user_perms = sqlx::query_as::<_, SqlPermissionTokens>(
|
||||
"SELECT token FROM user_permissions WHERE user_id = ?;",
|
||||
)
|
||||
.bind(id)
|
||||
.fetch_all(pool)
|
||||
.await
|
||||
.ok()?;
|
||||
.bind(id)
|
||||
.fetch_all(pool)
|
||||
.await
|
||||
.ok()?;
|
||||
|
||||
Some(sqluser.into_user(Some(sql_user_perms)))
|
||||
}
|
||||
|
||||
pub async fn get_from_email(email: &str, pool: &SqlitePool) -> Option<Self> {
|
||||
let sqluser = sqlx::query_as::<_, SqlUser>("SELECT * FROM users WHERE email = ?")
|
||||
.bind(email)
|
||||
.fetch_one(pool)
|
||||
.await
|
||||
.ok()?;
|
||||
pub async fn get_from_email(
|
||||
email: &str,
|
||||
pool: &SqlitePool,
|
||||
) -> Option<Self> {
|
||||
let sqluser = sqlx::query_as::<_, SqlUser>(
|
||||
"SELECT * FROM users WHERE email = ?",
|
||||
)
|
||||
.bind(email)
|
||||
.fetch_one(pool)
|
||||
.await
|
||||
.ok()?;
|
||||
|
||||
//lets just get all the tokens the user can use, we will only use the full permissions if modifing them.
|
||||
let sql_user_perms = sqlx::query_as::<_, SqlPermissionTokens>(
|
||||
|
@ -84,7 +96,10 @@ if #[cfg(feature = "ssr")] {
|
|||
|
||||
#[async_trait]
|
||||
impl Authentication<User, i64, SqlitePool> for User {
|
||||
async fn load_user(userid: i64, pool: Option<&SqlitePool>) -> Result<User, anyhow::Error> {
|
||||
async fn load_user(
|
||||
userid: i64,
|
||||
pool: Option<&SqlitePool>,
|
||||
) -> Result<User, anyhow::Error> {
|
||||
let pool = pool.unwrap();
|
||||
|
||||
User::get(userid, pool)
|
||||
|
@ -123,9 +138,11 @@ if #[cfg(feature = "ssr")] {
|
|||
pub secret: String,
|
||||
}
|
||||
|
||||
|
||||
impl SqlUser {
|
||||
pub fn into_user(self, sql_user_perms: Option<Vec<SqlPermissionTokens>>) -> User {
|
||||
pub fn into_user(
|
||||
self,
|
||||
sql_user_perms: Option<Vec<SqlPermissionTokens>>,
|
||||
) -> User {
|
||||
User {
|
||||
id: self.id,
|
||||
email: self.email,
|
||||
|
@ -141,4 +158,3 @@ if #[cfg(feature = "ssr")] {
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,7 +14,6 @@ pub fn error_template(errors: RwSignal<Errors>) -> View {
|
|||
children= move | (_, error)| {
|
||||
let error_string = error.to_string();
|
||||
view! {
|
||||
|
||||
<p>"Error: " {error_string}</p>
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,47 +1,49 @@
|
|||
use cfg_if::cfg_if;
|
||||
use crate::error_template::error_template;
|
||||
use axum::{
|
||||
body::Body,
|
||||
extract::State,
|
||||
http::{Request, Response, StatusCode, Uri},
|
||||
response::{IntoResponse, Response as AxumResponse},
|
||||
};
|
||||
use leptos::*;
|
||||
use tower::ServiceExt;
|
||||
use tower_http::services::ServeDir;
|
||||
|
||||
cfg_if! {
|
||||
if #[cfg(feature = "ssr")] {
|
||||
use axum::{
|
||||
body::{boxed, Body, BoxBody},
|
||||
extract::State,
|
||||
response::IntoResponse,
|
||||
http::{Request, Response, StatusCode, Uri},
|
||||
};
|
||||
use axum::response::Response as AxumResponse;
|
||||
use tower::ServiceExt;
|
||||
use tower_http::services::ServeDir;
|
||||
use leptos::*;
|
||||
use crate::error_template::error_template;
|
||||
pub async fn file_and_error_handler(
|
||||
uri: Uri,
|
||||
State(options): State<LeptosOptions>,
|
||||
req: Request<Body>,
|
||||
) -> AxumResponse {
|
||||
let root = options.site_root.clone();
|
||||
let res = get_static_file(uri.clone(), &root).await.unwrap();
|
||||
|
||||
pub async fn file_and_error_handler(uri: Uri, State(options): State<LeptosOptions>, req: Request<Body>) -> AxumResponse {
|
||||
let root = options.site_root.clone();
|
||||
let res = get_static_file(uri.clone(), &root).await.unwrap();
|
||||
|
||||
if res.status() == StatusCode::OK {
|
||||
res.into_response()
|
||||
} else {
|
||||
leptos::logging::log!("{:?}:{}",res.status(),uri);
|
||||
let handler = leptos_axum::render_app_to_stream(
|
||||
options.to_owned(),
|
||||
|| error_template(create_rw_signal(leptos::Errors::default())
|
||||
)
|
||||
);
|
||||
handler(req).await.into_response()
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_static_file(uri: Uri, root: &str) -> Result<Response<BoxBody>, (StatusCode, String)> {
|
||||
let req = Request::builder().uri(uri.clone()).body(Body::empty()).unwrap();
|
||||
// `ServeDir` implements `tower::Service` so we can call it with `tower::ServiceExt::oneshot`
|
||||
// This path is relative to the cargo root
|
||||
match ServeDir::new(root).oneshot(req).await {
|
||||
Ok(res) => Ok(res.map(boxed)),
|
||||
Err(err) => Err((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("Something went wrong: {}", err),
|
||||
)),
|
||||
}
|
||||
}
|
||||
if res.status() == StatusCode::OK {
|
||||
res.into_response()
|
||||
} else {
|
||||
leptos::logging::log!("{:?}:{}", res.status(), uri);
|
||||
let handler =
|
||||
leptos_axum::render_app_to_stream(options.to_owned(), || {
|
||||
error_template(create_rw_signal(leptos::Errors::default()))
|
||||
});
|
||||
handler(req).await.into_response()
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_static_file(
|
||||
uri: Uri,
|
||||
root: &str,
|
||||
) -> Result<Response<Body>, (StatusCode, String)> {
|
||||
let req = Request::builder()
|
||||
.uri(uri.clone())
|
||||
.body(Body::empty())
|
||||
.unwrap();
|
||||
// `ServeDir` implements `tower::Service` so we can call it with `tower::ServiceExt::oneshot`
|
||||
// This path is relative to the cargo root
|
||||
match ServeDir::new(root).oneshot(req).await {
|
||||
Ok(res) => Ok(res.into_response()),
|
||||
Err(err) => Err((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("Something went wrong: {}", err),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,36 +1,30 @@
|
|||
use cfg_if::cfg_if;
|
||||
|
||||
pub mod auth;
|
||||
pub mod error_template;
|
||||
#[cfg(feature = "ssr")]
|
||||
pub mod fallback;
|
||||
pub mod sign_in_sign_up;
|
||||
#[cfg(feature = "ssr")]
|
||||
pub mod state;
|
||||
use leptos::{leptos_dom::helpers::TimeoutHandle, *};
|
||||
use leptos_meta::*;
|
||||
use leptos_router::*;
|
||||
use sign_in_sign_up::*;
|
||||
|
||||
cfg_if! {
|
||||
if #[cfg(feature = "ssr")] {
|
||||
use crate::{
|
||||
state::AppState,
|
||||
auth::{AuthSession,User,SqlRefreshToken}
|
||||
};
|
||||
use oauth2::{
|
||||
reqwest::async_http_client,
|
||||
TokenResponse
|
||||
};
|
||||
use sqlx::SqlitePool;
|
||||
#[cfg(feature = "ssr")]
|
||||
mod ssr_imports {
|
||||
pub use crate::auth::ssr_imports::{AuthSession, SqlRefreshToken};
|
||||
pub use leptos::{use_context, ServerFnError};
|
||||
pub use oauth2::{reqwest::async_http_client, TokenResponse};
|
||||
pub use sqlx::SqlitePool;
|
||||
|
||||
pub fn pool() -> Result<SqlitePool, ServerFnError> {
|
||||
use_context::<SqlitePool>()
|
||||
.ok_or_else(|| ServerFnError::ServerError("Pool missing.".into()))
|
||||
}
|
||||
pub fn pool() -> Result<SqlitePool, ServerFnError> {
|
||||
use_context::<SqlitePool>()
|
||||
.ok_or_else(|| ServerFnError::new("Pool missing."))
|
||||
}
|
||||
|
||||
pub fn auth() -> Result<AuthSession, ServerFnError> {
|
||||
use_context::<AuthSession>()
|
||||
.ok_or_else(|| ServerFnError::ServerError("Auth session missing.".into()))
|
||||
}
|
||||
pub fn auth() -> Result<AuthSession, ServerFnError> {
|
||||
use_context::<AuthSession>()
|
||||
.ok_or_else(|| ServerFnError::new("Auth session missing."))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -40,11 +34,14 @@ pub struct Email(RwSignal<Option<String>>);
|
|||
pub struct ExpiresIn(RwSignal<u64>);
|
||||
#[server]
|
||||
pub async fn refresh_token(email: String) -> Result<u64, ServerFnError> {
|
||||
use crate::{auth::User, state::AppState};
|
||||
use ssr_imports::*;
|
||||
|
||||
let pool = pool()?;
|
||||
let oauth_client = expect_context::<AppState>().client;
|
||||
let user = User::get_from_email(&email, &pool)
|
||||
.await
|
||||
.ok_or(ServerFnError::ServerError("User not found".to_string()))?;
|
||||
.ok_or(ServerFnError::new("User not found"))?;
|
||||
|
||||
let refresh_secret = sqlx::query_as::<_, SqlRefreshToken>(
|
||||
"SELECT secret FROM google_refresh_tokens WHERE user_id = ?",
|
||||
|
@ -77,6 +74,7 @@ pub async fn refresh_token(email: String) -> Result<u64, ServerFnError> {
|
|||
.await?;
|
||||
Ok(expires_in)
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn App() -> impl IntoView {
|
||||
provide_meta_context();
|
||||
|
@ -143,20 +141,11 @@ pub fn App() -> impl IntoView {
|
|||
}
|
||||
}
|
||||
|
||||
// Needs to be in lib.rs AFAIK because wasm-bindgen needs us to be compiling a lib. I may be wrong.
|
||||
cfg_if! {
|
||||
if #[cfg(feature = "hydrate")] {
|
||||
use wasm_bindgen::prelude::wasm_bindgen;
|
||||
use leptos::view;
|
||||
#[cfg(feature = "hydrate")]
|
||||
#[wasm_bindgen::prelude::wasm_bindgen]
|
||||
pub fn hydrate() {
|
||||
_ = console_log::init_with_level(log::Level::Debug);
|
||||
console_error_panic_hook::set_once();
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn hydrate() {
|
||||
_ = console_log::init_with_level(log::Level::Debug);
|
||||
console_error_panic_hook::set_once();
|
||||
|
||||
leptos::mount_to_body(|| {
|
||||
view! { <App/> }
|
||||
});
|
||||
}
|
||||
}
|
||||
leptos::mount_to_body(App);
|
||||
}
|
||||
|
|
|
@ -1,136 +1,154 @@
|
|||
use cfg_if::cfg_if;
|
||||
use crate::ssr_imports::*;
|
||||
use axum::{
|
||||
body::Body as AxumBody,
|
||||
extract::{Path, State},
|
||||
http::Request,
|
||||
response::IntoResponse,
|
||||
routing::get,
|
||||
Router,
|
||||
};
|
||||
use axum_session::{Key, SessionConfig, SessionLayer, SessionStore};
|
||||
use axum_session_auth::{AuthConfig, AuthSessionLayer, SessionSqlitePool};
|
||||
use leptos::{get_configuration, logging::log, provide_context, view};
|
||||
use leptos_axum::{
|
||||
generate_route_list, handle_server_fns_with_context, LeptosRoutes,
|
||||
};
|
||||
use sqlx::{sqlite::SqlitePoolOptions, SqlitePool};
|
||||
use sso_auth_axum::{
|
||||
auth::*, fallback::file_and_error_handler, state::AppState,
|
||||
};
|
||||
|
||||
// boilerplate to run in different modes
|
||||
cfg_if! {
|
||||
if #[cfg(feature = "ssr")] {
|
||||
use axum::{
|
||||
response::{IntoResponse},
|
||||
routing::get,
|
||||
extract::{Path, State, RawQuery},
|
||||
http::{Request, header::HeaderMap},
|
||||
body::Body as AxumBody,
|
||||
Router,
|
||||
};
|
||||
use sso_auth_axum::auth::*;
|
||||
use sso_auth_axum::state::AppState;
|
||||
use sso_auth_axum::fallback::file_and_error_handler;
|
||||
use leptos_axum::{generate_route_list, handle_server_fns_with_context, LeptosRoutes};
|
||||
use leptos::{logging::log, view, provide_context, get_configuration};
|
||||
use sqlx::{SqlitePool, sqlite::SqlitePoolOptions};
|
||||
use axum_session::{SessionConfig, SessionLayer, SessionStore,Key, SecurityMode};
|
||||
use axum_session_auth::{AuthSessionLayer, AuthConfig, SessionSqlitePool};
|
||||
async fn server_fn_handler(
|
||||
State(app_state): State<AppState>,
|
||||
auth_session: AuthSession,
|
||||
path: Path<String>,
|
||||
request: Request<AxumBody>,
|
||||
) -> impl IntoResponse {
|
||||
log!("{:?}", path);
|
||||
|
||||
async fn server_fn_handler(State(app_state): State<AppState>, auth_session: AuthSession, path: Path<String>, headers: HeaderMap, raw_query: RawQuery,
|
||||
request: Request<AxumBody>) -> impl IntoResponse {
|
||||
|
||||
log!("{:?}", path);
|
||||
|
||||
handle_server_fns_with_context(path, headers, raw_query, move || {
|
||||
handle_server_fns_with_context(
|
||||
move || {
|
||||
provide_context(app_state.clone());
|
||||
provide_context(auth_session.clone());
|
||||
provide_context(app_state.pool.clone());
|
||||
}, request).await
|
||||
}
|
||||
},
|
||||
request,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn leptos_routes_handler(
|
||||
auth_session: AuthSession,
|
||||
State(app_state): State<AppState>,
|
||||
axum::extract::State(option): axum::extract::State<leptos::LeptosOptions>,
|
||||
request: Request<AxumBody>,
|
||||
) -> axum::response::Response {
|
||||
let handler = leptos_axum::render_app_async_with_context(
|
||||
option.clone(),
|
||||
move || {
|
||||
provide_context(app_state.clone());
|
||||
provide_context(auth_session.clone());
|
||||
provide_context(app_state.pool.clone());
|
||||
},
|
||||
move || view! { <sso_auth_axum::App/> },
|
||||
);
|
||||
pub async fn leptos_routes_handler(
|
||||
auth_session: AuthSession,
|
||||
State(app_state): State<AppState>,
|
||||
axum::extract::State(option): axum::extract::State<leptos::LeptosOptions>,
|
||||
request: Request<AxumBody>,
|
||||
) -> axum::response::Response {
|
||||
let handler = leptos_axum::render_app_async_with_context(
|
||||
option.clone(),
|
||||
move || {
|
||||
provide_context(app_state.clone());
|
||||
provide_context(auth_session.clone());
|
||||
provide_context(app_state.pool.clone());
|
||||
},
|
||||
move || view! { <sso_auth_axum::App/> },
|
||||
);
|
||||
|
||||
handler(request).await.into_response()
|
||||
}
|
||||
handler(request).await.into_response()
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
simple_logger::init_with_level(log::Level::Info)
|
||||
.expect("couldn't initialize logging");
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
simple_logger::init_with_level(log::Level::Info).expect("couldn't initialize logging");
|
||||
let pool = SqlitePoolOptions::new()
|
||||
.connect("sqlite:sso.db")
|
||||
.await
|
||||
.expect("Could not make pool.");
|
||||
|
||||
let pool = SqlitePoolOptions::new()
|
||||
.connect("sqlite:sso.db")
|
||||
.await
|
||||
.expect("Could not make pool.");
|
||||
// Auth section
|
||||
let session_config = SessionConfig::default()
|
||||
.with_table_name("sessions_table")
|
||||
.with_key(Key::generate())
|
||||
.with_database_key(Key::generate());
|
||||
// .with_security_mode(SecurityMode::PerSession); // FIXME did this disappear?
|
||||
|
||||
// Auth section
|
||||
let session_config = SessionConfig::default()
|
||||
.with_table_name("sessions_table")
|
||||
.with_key(Key::generate())
|
||||
.with_database_key(Key::generate())
|
||||
.with_security_mode(SecurityMode::PerSession);
|
||||
let auth_config = AuthConfig::<i64>::default();
|
||||
let session_store = SessionStore::<SessionSqlitePool>::new(
|
||||
Some(pool.clone().into()),
|
||||
session_config,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let auth_config = AuthConfig::<i64>::default();
|
||||
let session_store = SessionStore::<SessionSqlitePool>::new(Some(pool.clone().into()), session_config).await.unwrap();
|
||||
sqlx::migrate!()
|
||||
.run(&pool)
|
||||
.await
|
||||
.expect("could not run SQLx migrations");
|
||||
|
||||
sqlx::migrate!()
|
||||
.run(&pool)
|
||||
.await
|
||||
.expect("could not run SQLx migrations");
|
||||
// Setting this to None means we'll be using cargo-leptos and its env vars
|
||||
let conf = get_configuration(None).await.unwrap();
|
||||
let leptos_options = conf.leptos_options;
|
||||
let addr = leptos_options.site_addr;
|
||||
let routes = generate_route_list(sso_auth_axum::App);
|
||||
|
||||
|
||||
|
||||
// Setting this to None means we'll be using cargo-leptos and its env vars
|
||||
let conf = get_configuration(None).await.unwrap();
|
||||
let leptos_options = conf.leptos_options;
|
||||
let addr = leptos_options.site_addr;
|
||||
let routes = generate_route_list(sso_auth_axum::App);
|
||||
|
||||
// We create our client using provided environment variables.
|
||||
// We create our client using provided environment variables.
|
||||
let client = oauth2::basic::BasicClient::new(
|
||||
oauth2::ClientId::new(std::env::var("G_AUTH_CLIENT_ID").expect("G_AUTH_CLIENT Env var to be set.")),
|
||||
Some(oauth2::ClientSecret::new(std::env::var("G_AUTH_SECRET").expect("G_AUTH_SECRET Env var to be set"))),
|
||||
oauth2::ClientId::new(
|
||||
std::env::var("G_AUTH_CLIENT_ID")
|
||||
.expect("G_AUTH_CLIENT Env var to be set."),
|
||||
),
|
||||
Some(oauth2::ClientSecret::new(
|
||||
std::env::var("G_AUTH_SECRET")
|
||||
.expect("G_AUTH_SECRET Env var to be set"),
|
||||
)),
|
||||
oauth2::AuthUrl::new(
|
||||
"https://accounts.google.com/o/oauth2/v2/auth".to_string(),
|
||||
)
|
||||
.unwrap(),
|
||||
Some(
|
||||
oauth2::TokenUrl::new("https://oauth2.googleapis.com/token".to_string())
|
||||
.unwrap(),
|
||||
oauth2::TokenUrl::new(
|
||||
"https://oauth2.googleapis.com/token".to_string(),
|
||||
)
|
||||
.unwrap(),
|
||||
),
|
||||
)
|
||||
.set_redirect_uri(oauth2::RedirectUrl::new(std::env::var("REDIRECT_URL").expect("REDIRECT_URL Env var to be set")).unwrap());
|
||||
.set_redirect_uri(
|
||||
oauth2::RedirectUrl::new(
|
||||
std::env::var("REDIRECT_URL")
|
||||
.expect("REDIRECT_URL Env var to be set"),
|
||||
)
|
||||
.unwrap(),
|
||||
);
|
||||
|
||||
let app_state = AppState {
|
||||
leptos_options,
|
||||
pool: pool.clone(),
|
||||
client,
|
||||
};
|
||||
|
||||
let app_state = AppState{
|
||||
leptos_options,
|
||||
pool: pool.clone(),
|
||||
client,
|
||||
};
|
||||
// build our application with a route
|
||||
let app = Router::new()
|
||||
.route(
|
||||
"/api/*fn_name",
|
||||
get(server_fn_handler).post(server_fn_handler),
|
||||
)
|
||||
.leptos_routes_with_handler(routes, get(leptos_routes_handler))
|
||||
.fallback(file_and_error_handler)
|
||||
.layer(
|
||||
AuthSessionLayer::<User, i64, SessionSqlitePool, SqlitePool>::new(
|
||||
Some(pool.clone()),
|
||||
)
|
||||
.with_config(auth_config),
|
||||
)
|
||||
.layer(SessionLayer::new(session_store))
|
||||
.with_state(app_state);
|
||||
|
||||
// build our application with a route
|
||||
let app = Router::new()
|
||||
.route("/api/*fn_name", get(server_fn_handler).post(server_fn_handler))
|
||||
.leptos_routes_with_handler(routes, get(leptos_routes_handler) )
|
||||
.fallback(file_and_error_handler)
|
||||
.layer(AuthSessionLayer::<User, i64, SessionSqlitePool, SqlitePool>::new(Some(pool.clone()))
|
||||
.with_config(auth_config))
|
||||
.layer(SessionLayer::new(session_store))
|
||||
.with_state(app_state);
|
||||
|
||||
// run our app with hyper
|
||||
// `axum::Server` is a re-export of `hyper::Server`
|
||||
log!("listening on http://{}", &addr);
|
||||
axum::Server::bind(&addr)
|
||||
.serve(app.into_make_service())
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
// client-only stuff for Trunk
|
||||
else {
|
||||
pub fn main() {
|
||||
// This example cannot be built as a trunk standalone CSR-only app.
|
||||
// Only the server may directly connect to the database.
|
||||
}
|
||||
}
|
||||
// run our app with hyper
|
||||
// `axum::Server` is a re-export of `hyper::Server`
|
||||
let listener = tokio::net::TcpListener::bind(&addr).await.unwrap();
|
||||
log!("listening on http://{}", &addr);
|
||||
axum::serve(listener, app.into_make_service())
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
|
|
@ -1,23 +1,23 @@
|
|||
use super::*;
|
||||
|
||||
cfg_if! {
|
||||
if #[cfg(feature="ssr")]{
|
||||
use oauth2::{
|
||||
AuthorizationCode,
|
||||
TokenResponse,
|
||||
reqwest::async_http_client,
|
||||
CsrfToken,
|
||||
Scope,
|
||||
};
|
||||
use serde_json::Value;
|
||||
use crate::{
|
||||
auth::{User,SqlCsrfToken},
|
||||
state::AppState
|
||||
};
|
||||
}
|
||||
#[cfg(feature = "ssr")]
|
||||
pub mod ssr_imports {
|
||||
pub use crate::{
|
||||
auth::{ssr_imports::SqlCsrfToken, User},
|
||||
state::AppState,
|
||||
};
|
||||
pub use oauth2::{
|
||||
reqwest::async_http_client, AuthorizationCode, CsrfToken, Scope,
|
||||
TokenResponse,
|
||||
};
|
||||
pub use serde_json::Value;
|
||||
}
|
||||
|
||||
#[server]
|
||||
pub async fn google_sso() -> Result<String, ServerFnError> {
|
||||
use crate::ssr_imports::*;
|
||||
use ssr_imports::*;
|
||||
|
||||
let oauth_client = expect_context::<AppState>().client;
|
||||
let pool = pool()?;
|
||||
|
||||
|
@ -80,6 +80,9 @@ pub async fn handle_g_auth_redirect(
|
|||
provided_csrf: String,
|
||||
code: String,
|
||||
) -> Result<(String, u64), ServerFnError> {
|
||||
use crate::ssr_imports::*;
|
||||
use ssr_imports::*;
|
||||
|
||||
let oauth_client = expect_context::<AppState>().client;
|
||||
let pool = pool()?;
|
||||
let auth_session = auth()?;
|
||||
|
@ -90,9 +93,7 @@ pub async fn handle_g_auth_redirect(
|
|||
.bind(provided_csrf)
|
||||
.fetch_one(&pool)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
ServerFnError::ServerError(format!("CSRF_TOKEN error : {err:?}"))
|
||||
})?;
|
||||
.map_err(|err| ServerFnError::new(format!("CSRF_TOKEN error : {err:?}")))?;
|
||||
|
||||
let token_response = oauth_client
|
||||
.exchange_code(AuthorizationCode::new(code.clone()))
|
||||
|
@ -118,7 +119,7 @@ pub async fn handle_g_auth_redirect(
|
|||
.expect("email to parse to string")
|
||||
.to_string()
|
||||
} else {
|
||||
return Err(ServerFnError::ServerError(format!(
|
||||
return Err(ServerFnError::new(format!(
|
||||
"Response from google has status of {}",
|
||||
response.status()
|
||||
)));
|
||||
|
@ -193,6 +194,8 @@ pub fn HandleGAuth() -> impl IntoView {
|
|||
|
||||
#[server]
|
||||
pub async fn logout() -> Result<(), ServerFnError> {
|
||||
use crate::ssr_imports::*;
|
||||
|
||||
let auth = auth()?;
|
||||
auth.logout_user();
|
||||
leptos_axum::redirect("/");
|
||||
|
|
|
@ -1,18 +1,12 @@
|
|||
use cfg_if::cfg_if;
|
||||
|
||||
cfg_if! {
|
||||
if #[cfg(feature = "ssr")] {
|
||||
use axum::extract::FromRef;
|
||||
use leptos::LeptosOptions;
|
||||
use sqlx::SqlitePool;
|
||||
use axum::extract::FromRef;
|
||||
|
||||
/// This takes advantage of Axum's SubStates feature by deriving FromRef. This is the only way to have more than one
|
||||
/// item in Axum's State. Leptos requires you to have leptosOptions in your State struct for the leptos route handlers
|
||||
#[derive(FromRef, Debug, Clone)]
|
||||
pub struct AppState{
|
||||
pub struct AppState {
|
||||
pub leptos_options: LeptosOptions,
|
||||
pub pool: SqlitePool,
|
||||
pub client:oauth2::basic::BasicClient,
|
||||
}
|
||||
}
|
||||
pub client: oauth2::basic::BasicClient,
|
||||
}
|
||||
|
|
|
@ -11,7 +11,6 @@ actix-files = { version = "0.6", optional = true }
|
|||
actix-web = { version = "4", optional = true, features = ["macros"] }
|
||||
console_error_panic_hook = "0.1"
|
||||
console_log = "1"
|
||||
cfg-if = "1"
|
||||
lazy_static = "1"
|
||||
leptos = { path = "../../leptos", features = ["nightly"] }
|
||||
leptos_meta = { path = "../../meta", features = ["nightly"] }
|
||||
|
|
|
@ -1,25 +1,15 @@
|
|||
#![feature(result_flattening)]
|
||||
|
||||
pub mod app;
|
||||
use cfg_if::cfg_if;
|
||||
|
||||
cfg_if! {
|
||||
if #[cfg(feature = "hydrate")] {
|
||||
#[cfg(feature = "hydrate")]
|
||||
#[wasm_bindgen::prelude::wasm_bindgen]
|
||||
pub fn hydrate() {
|
||||
use app::*;
|
||||
|
||||
use wasm_bindgen::prelude::wasm_bindgen;
|
||||
// initializes logging using the `log` crate
|
||||
_ = console_log::init_with_level(log::Level::Debug);
|
||||
console_error_panic_hook::set_once();
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn hydrate() {
|
||||
use app::*;
|
||||
use leptos::*;
|
||||
|
||||
// initializes logging using the `log` crate
|
||||
_ = console_log::init_with_level(log::Level::Debug);
|
||||
console_error_panic_hook::set_once();
|
||||
|
||||
leptos::mount_to_body(move || {
|
||||
view! { <App/> }
|
||||
});
|
||||
}
|
||||
}
|
||||
leptos::mount_to_body(App);
|
||||
}
|
||||
|
|
|
@ -23,12 +23,7 @@ async fn main() -> std::io::Result<()> {
|
|||
let site_root = &leptos_options.site_root;
|
||||
|
||||
App::new()
|
||||
.route("/api/{tail:.*}", leptos_actix::handle_server_fns())
|
||||
.leptos_routes(
|
||||
leptos_options.to_owned(),
|
||||
routes.to_owned(),
|
||||
|| view! { <App/> },
|
||||
)
|
||||
.leptos_routes(leptos_options.to_owned(), routes.to_owned(), App)
|
||||
.service(Files::new("/", site_root))
|
||||
//.wrap(middleware::Compress::default())
|
||||
})
|
||||
|
|
|
@ -9,7 +9,6 @@ crate-type = ["cdylib", "rlib"]
|
|||
[dependencies]
|
||||
console_error_panic_hook = "0.1"
|
||||
console_log = "1"
|
||||
cfg-if = "1"
|
||||
lazy_static = "1"
|
||||
leptos = { path = "../../leptos", features = ["nightly"] }
|
||||
leptos_meta = { path = "../../meta", features = ["nightly"] }
|
||||
|
@ -18,9 +17,9 @@ leptos_router = { path = "../../router", features = ["nightly"] }
|
|||
log = "0.4"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
thiserror = "1"
|
||||
axum = { version = "0.6.1", optional = true }
|
||||
tower = { version = "0.4.13", optional = true }
|
||||
tower-http = { version = "0.4", features = ["fs"], optional = true }
|
||||
axum = { version = "0.7", optional = true }
|
||||
tower = { version = "0.4", optional = true }
|
||||
tower-http = { version = "0.5", features = ["fs"], optional = true }
|
||||
tokio = { version = "1", features = ["time"], optional = true }
|
||||
wasm-bindgen = "0.2"
|
||||
|
||||
|
|
|
@ -1,43 +1,48 @@
|
|||
use cfg_if::cfg_if;
|
||||
use crate::app::App;
|
||||
use axum::{
|
||||
body::Body,
|
||||
extract::State,
|
||||
http::{Request, Response, StatusCode, Uri},
|
||||
response::{IntoResponse, Response as AxumResponse},
|
||||
};
|
||||
use leptos::{view, LeptosOptions};
|
||||
use tower::ServiceExt;
|
||||
use tower_http::services::ServeDir;
|
||||
|
||||
cfg_if! { if #[cfg(feature = "ssr")] {
|
||||
use axum::{
|
||||
body::{boxed, Body, BoxBody},
|
||||
extract::State,
|
||||
response::IntoResponse,
|
||||
http::{Request, Response, StatusCode, Uri},
|
||||
};
|
||||
use axum::response::Response as AxumResponse;
|
||||
use tower::ServiceExt;
|
||||
use tower_http::services::ServeDir;
|
||||
use leptos::{LeptosOptions, view};
|
||||
use crate::app::App;
|
||||
pub async fn file_and_error_handler(
|
||||
uri: Uri,
|
||||
State(options): State<LeptosOptions>,
|
||||
req: Request<Body>,
|
||||
) -> AxumResponse {
|
||||
let root = options.site_root.clone();
|
||||
let res = get_static_file(uri.clone(), &root).await.unwrap();
|
||||
|
||||
pub async fn file_and_error_handler(uri: Uri, State(options): State<LeptosOptions>, req: Request<Body>) -> AxumResponse {
|
||||
let root = options.site_root.clone();
|
||||
let res = get_static_file(uri.clone(), &root).await.unwrap();
|
||||
|
||||
if res.status() == StatusCode::OK {
|
||||
res.into_response()
|
||||
} else{
|
||||
let handler = leptos_axum::render_app_to_stream(
|
||||
options.to_owned(),
|
||||
move || view!{ <App/> }
|
||||
);
|
||||
handler(req).await.into_response()
|
||||
}
|
||||
if res.status() == StatusCode::OK {
|
||||
res.into_response()
|
||||
} else {
|
||||
let handler = leptos_axum::render_app_to_stream(
|
||||
options.to_owned(),
|
||||
move || view! { <App/> },
|
||||
);
|
||||
handler(req).await.into_response()
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_static_file(uri: Uri, root: &str) -> Result<Response<BoxBody>, (StatusCode, String)> {
|
||||
let req = Request::builder().uri(uri.clone()).body(Body::empty()).unwrap();
|
||||
// `ServeDir` implements `tower::Service` so we can call it with `tower::ServiceExt::oneshot`
|
||||
// This path is relative to the cargo root
|
||||
match ServeDir::new(root).oneshot(req).await {
|
||||
Ok(res) => Ok(res.map(boxed)),
|
||||
Err(err) => Err((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("Something went wrong: {err}"),
|
||||
)),
|
||||
}
|
||||
async fn get_static_file(
|
||||
uri: Uri,
|
||||
root: &str,
|
||||
) -> Result<Response<Body>, (StatusCode, String)> {
|
||||
let req = Request::builder()
|
||||
.uri(uri.clone())
|
||||
.body(Body::empty())
|
||||
.unwrap();
|
||||
// `ServeDir` implements `tower::Service` so we can call it with `tower::ServiceExt::oneshot`
|
||||
// This path is relative to the cargo root
|
||||
match ServeDir::new(root).oneshot(req).await {
|
||||
Ok(res) => Ok(res.into_response()),
|
||||
Err(err) => Err((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("Something went wrong: {err}"),
|
||||
)),
|
||||
}
|
||||
}}
|
||||
}
|
||||
|
|
|
@ -1,26 +1,18 @@
|
|||
#![feature(result_flattening)]
|
||||
|
||||
pub mod app;
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
pub mod fallback;
|
||||
use cfg_if::cfg_if;
|
||||
|
||||
cfg_if! {
|
||||
if #[cfg(feature = "hydrate")] {
|
||||
#[cfg(feature = "hydrate")]
|
||||
#[wasm_bindgen::prelude::wasm_bindgen]
|
||||
pub fn hydrate() {
|
||||
use app::*;
|
||||
|
||||
use wasm_bindgen::prelude::wasm_bindgen;
|
||||
// initializes logging using the `log` crate
|
||||
_ = console_log::init_with_level(log::Level::Debug);
|
||||
console_error_panic_hook::set_once();
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn hydrate() {
|
||||
use app::*;
|
||||
use leptos::*;
|
||||
|
||||
// initializes logging using the `log` crate
|
||||
_ = console_log::init_with_level(log::Level::Debug);
|
||||
console_error_panic_hook::set_once();
|
||||
|
||||
leptos::mount_to_body(move || {
|
||||
view! { <App/> }
|
||||
});
|
||||
}
|
||||
}
|
||||
leptos::mount_to_body(App);
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
#[cfg(feature = "ssr")]
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
use axum::{routing::post, Router};
|
||||
use axum::Router;
|
||||
use leptos::{logging::log, *};
|
||||
use leptos_axum::{generate_route_list, LeptosRoutes};
|
||||
use ssr_modes_axum::{app::*, fallback::file_and_error_handler};
|
||||
|
@ -19,7 +19,6 @@ async fn main() {
|
|||
// _ = ListPostMetadata::register();
|
||||
|
||||
let app = Router::new()
|
||||
.route("/api/*fn_name", post(leptos_axum::handle_server_fns))
|
||||
.leptos_routes(&leptos_options, routes, || view! { <App/> })
|
||||
.fallback(file_and_error_handler)
|
||||
.with_state(leptos_options);
|
||||
|
@ -27,8 +26,8 @@ async fn main() {
|
|||
// run our app with hyper
|
||||
// `axum::Server` is a re-export of `hyper::Server`
|
||||
log!("listening on http://{}", &addr);
|
||||
axum::Server::bind(&addr)
|
||||
.serve(app.into_make_service())
|
||||
let listener = tokio::net::TcpListener::bind(&addr).await.unwrap();
|
||||
axum::serve(listener, app.into_make_service())
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
|
|
@ -11,7 +11,6 @@ actix-files = { version = "0.6", optional = true }
|
|||
actix-web = { version = "4", optional = true, features = ["macros"] }
|
||||
console_error_panic_hook = "0.1"
|
||||
console_log = "1"
|
||||
cfg-if = "1"
|
||||
leptos = { path = "../../leptos", features = ["serde"] }
|
||||
leptos_actix = { path = "../../integrations/actix", optional = true }
|
||||
leptos_router = { path = "../../router" }
|
||||
|
|
|
@ -1,23 +1,12 @@
|
|||
pub mod app;
|
||||
use cfg_if::cfg_if;
|
||||
|
||||
cfg_if! {
|
||||
if #[cfg(feature = "hydrate")] {
|
||||
#[wasm_bindgen::prelude::wasm_bindgen]
|
||||
pub fn hydrate() {
|
||||
use app::*;
|
||||
|
||||
use wasm_bindgen::prelude::wasm_bindgen;
|
||||
// initializes logging using the `log` crate
|
||||
_ = console_log::init_with_level(log::Level::Debug);
|
||||
console_error_panic_hook::set_once();
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn hydrate() {
|
||||
use app::*;
|
||||
use leptos::*;
|
||||
|
||||
// initializes logging using the `log` crate
|
||||
_ = console_log::init_with_level(log::Level::Debug);
|
||||
console_error_panic_hook::set_once();
|
||||
|
||||
leptos::mount_to_body(|| {
|
||||
view! { <App/> }
|
||||
});
|
||||
}
|
||||
}
|
||||
leptos::mount_to_body(App);
|
||||
}
|
||||
|
|
|
@ -10,19 +10,14 @@ async fn main() -> std::io::Result<()> {
|
|||
let conf = get_configuration(None).await.unwrap();
|
||||
let addr = conf.leptos_options.site_addr;
|
||||
// Generate the list of routes in your Leptos App
|
||||
let routes = generate_route_list(|| view! { <App/> });
|
||||
let routes = generate_route_list(App);
|
||||
|
||||
HttpServer::new(move || {
|
||||
let leptos_options = &conf.leptos_options;
|
||||
let site_root = &leptos_options.site_root;
|
||||
|
||||
App::new()
|
||||
.route("/api/{tail:.*}", leptos_actix::handle_server_fns())
|
||||
.leptos_routes(
|
||||
leptos_options.to_owned(),
|
||||
routes.to_owned(),
|
||||
|| view! { <App/> },
|
||||
)
|
||||
.leptos_routes(leptos_options.to_owned(), routes.to_owned(), App)
|
||||
.service(Files::new("/", site_root))
|
||||
//.wrap(middleware::Compress::default())
|
||||
})
|
||||
|
|
|
@ -16,7 +16,6 @@ leptos_meta = { path = "../../meta", features = ["nightly"] }
|
|||
leptos_router = { path = "../../router", features = ["nightly"] }
|
||||
gloo-net = { version = "0.2", features = ["http"] }
|
||||
log = "0.4"
|
||||
cfg-if = "1.0"
|
||||
|
||||
# dependecies for client (enable when csr or hydrate set)
|
||||
wasm-bindgen = { version = "0.2", optional = true }
|
||||
|
|
|
@ -1,39 +1,27 @@
|
|||
mod app;
|
||||
use cfg_if::cfg_if;
|
||||
|
||||
cfg_if! {
|
||||
if #[cfg(feature = "hydrate")] {
|
||||
use wasm_bindgen::prelude::wasm_bindgen;
|
||||
use crate::app::*;
|
||||
use leptos::*;
|
||||
#[cfg(feature = "hydrate")]
|
||||
#[wasm_bindgen::prelude::wasm_bindgen]
|
||||
pub fn hydrate() {
|
||||
use crate::app::App;
|
||||
use leptos::{logging, mount_to_body};
|
||||
_ = console_log::init_with_level(log::Level::Debug);
|
||||
console_error_panic_hook::set_once();
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn hydrate() {
|
||||
_ = console_log::init_with_level(log::Level::Debug);
|
||||
console_error_panic_hook::set_once();
|
||||
logging::log!("hydrate mode - hydrating");
|
||||
|
||||
logging::log!("hydrate mode - hydrating");
|
||||
|
||||
leptos::mount_to_body(|| {
|
||||
view! { <App/> }
|
||||
});
|
||||
}
|
||||
}
|
||||
else if #[cfg(feature = "csr")] {
|
||||
use wasm_bindgen::prelude::wasm_bindgen;
|
||||
|
||||
#[wasm_bindgen(start)]
|
||||
pub fn main() {
|
||||
use app::*;
|
||||
use leptos::*;
|
||||
_ = console_log::init_with_level(log::Level::Debug);
|
||||
console_error_panic_hook::set_once();
|
||||
|
||||
logging::log!("csr mode - mounting to body");
|
||||
|
||||
mount_to_body(|| {
|
||||
view! { <App /> }
|
||||
});
|
||||
}
|
||||
}
|
||||
mount_to_body(App);
|
||||
}
|
||||
|
||||
#[cfg(feature = "csr")]
|
||||
#[wasm_bindgen::prelude::wasm_bindgen(start)]
|
||||
pub fn main() {
|
||||
use crate::app::App;
|
||||
use leptos::{logging, mount_to_body};
|
||||
_ = console_log::init_with_level(log::Level::Debug);
|
||||
console_error_panic_hook::set_once();
|
||||
|
||||
logging::log!("csr mode - mounting to body");
|
||||
|
||||
mount_to_body(App);
|
||||
}
|
||||
|
|
|
@ -1,40 +1,35 @@
|
|||
mod app;
|
||||
use cfg_if::cfg_if;
|
||||
|
||||
cfg_if! {
|
||||
if #[cfg(feature = "ssr")] {
|
||||
use actix_files::Files;
|
||||
use actix_web::*;
|
||||
use leptos::*;
|
||||
use crate::app::*;
|
||||
use leptos_actix::{generate_route_list, LeptosRoutes};
|
||||
use crate::app::*;
|
||||
use actix_files::Files;
|
||||
use actix_web::*;
|
||||
use leptos::*;
|
||||
use leptos_actix::{generate_route_list, LeptosRoutes};
|
||||
|
||||
#[actix_web::main]
|
||||
async fn main() -> std::io::Result<()> {
|
||||
#[actix_web::main]
|
||||
async fn main() -> std::io::Result<()> {
|
||||
// Setting this to None means we'll be using cargo-leptos and its env vars.
|
||||
let conf = get_configuration(None).await.unwrap();
|
||||
|
||||
// Setting this to None means we'll be using cargo-leptos and its env vars.
|
||||
let conf = get_configuration(None).await.unwrap();
|
||||
let addr = conf.leptos_options.site_addr;
|
||||
|
||||
let addr = conf.leptos_options.site_addr;
|
||||
// Generate the list of routes in your Leptos App
|
||||
let routes = generate_route_list(|| view! { <App/> });
|
||||
|
||||
// Generate the list of routes in your Leptos App
|
||||
let routes = generate_route_list(|| view! { <App/> });
|
||||
|
||||
HttpServer::new(move || {
|
||||
let leptos_options = &conf.leptos_options;
|
||||
let site_root = &leptos_options.site_root;
|
||||
let routes = &routes;
|
||||
App::new()
|
||||
.leptos_routes(leptos_options.to_owned(), routes.to_owned(), || view! { <App/> })
|
||||
.service(Files::new("/", site_root))
|
||||
.wrap(middleware::Compress::default())
|
||||
})
|
||||
.bind(&addr)?
|
||||
.run()
|
||||
.await
|
||||
}
|
||||
}
|
||||
else {
|
||||
pub fn main() {}
|
||||
}
|
||||
HttpServer::new(move || {
|
||||
let leptos_options = &conf.leptos_options;
|
||||
let site_root = &leptos_options.site_root;
|
||||
let routes = &routes;
|
||||
App::new()
|
||||
.leptos_routes(
|
||||
leptos_options.to_owned(),
|
||||
routes.to_owned(),
|
||||
|| view! { <App/> },
|
||||
)
|
||||
.service(Files::new("/", site_root))
|
||||
.wrap(middleware::Compress::default())
|
||||
})
|
||||
.bind(&addr)?
|
||||
.run()
|
||||
.await
|
||||
}
|
||||
|
|
|
@ -7,23 +7,22 @@ edition = "2021"
|
|||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[dependencies]
|
||||
axum = { version = "0.6.18", optional = true }
|
||||
console_error_panic_hook = "0.1.7"
|
||||
axum = { version = "0.7", optional = true }
|
||||
console_error_panic_hook = "0.1"
|
||||
console_log = "1"
|
||||
cfg-if = "1"
|
||||
leptos = { path = "../../leptos", features = ["nightly"] }
|
||||
leptos_meta = { path = "../../meta", features = ["nightly"] }
|
||||
leptos_axum = { path = "../../integrations/axum", optional = true }
|
||||
leptos_router = { path = "../../router", features = ["nightly"] }
|
||||
log = "0.4.17"
|
||||
simple_logger = "4"
|
||||
tokio = { version = "1.28.1", optional = true }
|
||||
tower = { version = "0.4.13", optional = true }
|
||||
tower-http = { version = "0.4", features = ["fs"], optional = true }
|
||||
wasm-bindgen = "0.2.84"
|
||||
thiserror = "1.0.40"
|
||||
tracing = { version = "0.1.37", optional = true }
|
||||
http = "0.2.11"
|
||||
tokio = { version = "1", optional = true }
|
||||
tower = { version = "0.4", optional = true }
|
||||
tower-http = { version = "0.5", features = ["fs"], optional = true }
|
||||
wasm-bindgen = "0.2"
|
||||
thiserror = "1.0"
|
||||
tracing = { version = "0.1", optional = true }
|
||||
http = "1.0"
|
||||
|
||||
[features]
|
||||
hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate"]
|
||||
|
|
|
@ -1,43 +1,48 @@
|
|||
use cfg_if::cfg_if;
|
||||
use crate::app::App;
|
||||
use axum::{
|
||||
body::Body,
|
||||
extract::State,
|
||||
http::{Request, Response, StatusCode, Uri},
|
||||
response::{IntoResponse, Response as AxumResponse},
|
||||
};
|
||||
use leptos::{view, LeptosOptions};
|
||||
use tower::ServiceExt;
|
||||
use tower_http::services::ServeDir;
|
||||
|
||||
cfg_if! { if #[cfg(feature = "ssr")] {
|
||||
use axum::{
|
||||
body::{boxed, Body, BoxBody},
|
||||
extract::State,
|
||||
response::IntoResponse,
|
||||
http::{Request, Response, StatusCode, Uri},
|
||||
};
|
||||
use axum::response::Response as AxumResponse;
|
||||
use tower::ServiceExt;
|
||||
use tower_http::services::ServeDir;
|
||||
use leptos::{LeptosOptions, view};
|
||||
use crate::app::App;
|
||||
pub async fn file_and_error_handler(
|
||||
uri: Uri,
|
||||
State(options): State<LeptosOptions>,
|
||||
req: Request<Body>,
|
||||
) -> AxumResponse {
|
||||
let root = options.site_root.clone();
|
||||
let res = get_static_file(uri.clone(), &root).await.unwrap();
|
||||
|
||||
pub async fn file_and_error_handler(uri: Uri, State(options): State<LeptosOptions>, req: Request<Body>) -> AxumResponse {
|
||||
let root = options.site_root.clone();
|
||||
let res = get_static_file(uri.clone(), &root).await.unwrap();
|
||||
|
||||
if res.status() == StatusCode::OK {
|
||||
res.into_response()
|
||||
} else{
|
||||
let handler = leptos_axum::render_app_to_stream(
|
||||
options.to_owned(),
|
||||
move || view!{ <App/> }
|
||||
);
|
||||
handler(req).await.into_response()
|
||||
}
|
||||
if res.status() == StatusCode::OK {
|
||||
res.into_response()
|
||||
} else {
|
||||
let handler = leptos_axum::render_app_to_stream(
|
||||
options.to_owned(),
|
||||
move || view! { <App/> },
|
||||
);
|
||||
handler(req).await.into_response()
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_static_file(uri: Uri, root: &str) -> Result<Response<BoxBody>, (StatusCode, String)> {
|
||||
let req = Request::builder().uri(uri.clone()).body(Body::empty()).unwrap();
|
||||
// `ServeDir` implements `tower::Service` so we can call it with `tower::ServiceExt::oneshot`
|
||||
// This path is relative to the cargo root
|
||||
match ServeDir::new(root).oneshot(req).await {
|
||||
Ok(res) => Ok(res.map(boxed)),
|
||||
Err(err) => Err((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("Something went wrong: {err}"),
|
||||
)),
|
||||
}
|
||||
async fn get_static_file(
|
||||
uri: Uri,
|
||||
root: &str,
|
||||
) -> Result<Response<Body>, (StatusCode, String)> {
|
||||
let req = Request::builder()
|
||||
.uri(uri.clone())
|
||||
.body(Body::empty())
|
||||
.unwrap();
|
||||
// `ServeDir` implements `tower::Service` so we can call it with `tower::ServiceExt::oneshot`
|
||||
// This path is relative to the cargo root
|
||||
match ServeDir::new(root).oneshot(req).await {
|
||||
Ok(res) => Ok(res.into_response()),
|
||||
Err(err) => Err((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("Something went wrong: {err}"),
|
||||
)),
|
||||
}
|
||||
}}
|
||||
}
|
||||
|
|
|
@ -1,20 +1,16 @@
|
|||
use cfg_if::cfg_if;
|
||||
pub mod app;
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
pub mod fallback;
|
||||
|
||||
cfg_if! { if #[cfg(feature = "hydrate")] {
|
||||
use leptos::*;
|
||||
use wasm_bindgen::prelude::wasm_bindgen;
|
||||
use crate::app::*;
|
||||
#[cfg(feature = "hydrate")]
|
||||
#[wasm_bindgen::prelude::wasm_bindgen]
|
||||
pub fn hydrate() {
|
||||
use crate::app::App;
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn hydrate() {
|
||||
// initializes logging using the `log` crate
|
||||
_ = console_log::init_with_level(log::Level::Debug);
|
||||
console_error_panic_hook::set_once();
|
||||
// initializes logging using the `log` crate
|
||||
_ = console_log::init_with_level(log::Level::Debug);
|
||||
console_error_panic_hook::set_once();
|
||||
|
||||
leptos::mount_to_body(move || {
|
||||
view! { <App/> }
|
||||
});
|
||||
}
|
||||
}}
|
||||
leptos::mount_to_body(App);
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
#[cfg(feature = "ssr")]
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
use axum::{routing::post, Router};
|
||||
use axum::Router;
|
||||
use leptos::*;
|
||||
use leptos_axum::{generate_route_list, LeptosRoutes};
|
||||
use leptos_tailwind::{app::*, fallback::file_and_error_handler};
|
||||
|
@ -23,16 +23,15 @@ async fn main() {
|
|||
|
||||
// build our application with a route
|
||||
let app = Router::new()
|
||||
.route("/api/*fn_name", post(leptos_axum::handle_server_fns))
|
||||
.leptos_routes(&leptos_options, routes, || view! { <App/> })
|
||||
.leptos_routes(&leptos_options, routes, App)
|
||||
.fallback(file_and_error_handler)
|
||||
.with_state(leptos_options);
|
||||
|
||||
// run our app with hyper
|
||||
// `axum::Server` is a re-export of `hyper::Server`
|
||||
info!("listening on http://{}", &addr);
|
||||
axum::Server::bind(&addr)
|
||||
.serve(app.into_make_service())
|
||||
let listener = tokio::net::TcpListener::bind(&addr).await.unwrap();
|
||||
axum::serve(listener, app.into_make_service())
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
|
|
@ -15,7 +15,6 @@ console_log = "1.0.0"
|
|||
console_error_panic_hook = "0.1.7"
|
||||
serde = { version = "1.0.152", features = ["derive"] }
|
||||
futures = "0.3.25"
|
||||
cfg-if = "1.0.0"
|
||||
leptos = { path = "../../leptos", features = ["nightly"] }
|
||||
leptos_actix = { path = "../../integrations/actix", optional = true }
|
||||
leptos_meta = { path = "../../meta", features = ["nightly"] }
|
||||
|
|
Binary file not shown.
|
@ -1,18 +1,11 @@
|
|||
use cfg_if::cfg_if;
|
||||
pub mod todo;
|
||||
|
||||
// Needs to be in lib.rs AFAIK because wasm-bindgen needs us to be compiling a lib. I may be wrong.
|
||||
cfg_if! {
|
||||
if #[cfg(feature = "hydrate")] {
|
||||
use wasm_bindgen::prelude::wasm_bindgen;
|
||||
use crate::todo::*;
|
||||
#[cfg(feature = "hydrate")]
|
||||
#[wasm_bindgen::prelude::wasm_bindgen]
|
||||
pub fn hydrate() {
|
||||
use crate::todo::*;
|
||||
console_error_panic_hook::set_once();
|
||||
_ = console_log::init_with_level(log::Level::Debug);
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn hydrate() {
|
||||
console_error_panic_hook::set_once();
|
||||
_ = console_log::init_with_level(log::Level::Debug);
|
||||
|
||||
leptos::mount_to_body(TodoApp);
|
||||
}
|
||||
}
|
||||
leptos::mount_to_body(TodoApp);
|
||||
}
|
||||
|
|
|
@ -1,63 +1,61 @@
|
|||
use cfg_if::cfg_if;
|
||||
mod todo;
|
||||
|
||||
// boilerplate to run in different modes
|
||||
cfg_if! {
|
||||
// server-only stuff
|
||||
if #[cfg(feature = "ssr")] {
|
||||
use actix_files::{Files};
|
||||
use actix_web::*;
|
||||
use crate::todo::*;
|
||||
use leptos::*;
|
||||
use leptos_actix::{generate_route_list, LeptosRoutes};
|
||||
#[cfg(feature = "ssr")]
|
||||
mod ssr {
|
||||
pub use crate::todo::*;
|
||||
pub use actix_files::Files;
|
||||
pub use actix_web::*;
|
||||
pub use leptos::*;
|
||||
pub use leptos_actix::{generate_route_list, LeptosRoutes};
|
||||
|
||||
#[get("/style.css")]
|
||||
async fn css() -> impl Responder {
|
||||
actix_files::NamedFile::open_async("./style.css").await
|
||||
}
|
||||
|
||||
#[actix_web::main]
|
||||
async fn main() -> std::io::Result<()> {
|
||||
let mut conn = db().await.expect("couldn't connect to DB");
|
||||
sqlx::migrate!()
|
||||
.run(&mut conn)
|
||||
.await
|
||||
.expect("could not run SQLx migrations");
|
||||
|
||||
// Explicit server function registration is no longer required
|
||||
// on the main branch. On 0.3.0 and earlier, uncomment the lines
|
||||
// below to register the server functions.
|
||||
// _ = GetTodos::register();
|
||||
// _ = AddTodo::register();
|
||||
// _ = DeleteTodo::register();
|
||||
|
||||
// Setting this to None means we'll be using cargo-leptos and its env vars.
|
||||
let conf = get_configuration(None).await.unwrap();
|
||||
|
||||
let addr = conf.leptos_options.site_addr;
|
||||
|
||||
// Generate the list of routes in your Leptos App
|
||||
let routes = generate_route_list(TodoApp);
|
||||
|
||||
HttpServer::new(move || {
|
||||
let leptos_options = &conf.leptos_options;
|
||||
let site_root = &leptos_options.site_root;
|
||||
let routes = &routes;
|
||||
|
||||
App::new()
|
||||
.service(css)
|
||||
.route("/api/{tail:.*}", leptos_actix::handle_server_fns())
|
||||
.leptos_routes(leptos_options.to_owned(), routes.to_owned(), TodoApp)
|
||||
.service(Files::new("/", site_root))
|
||||
//.wrap(middleware::Compress::default())
|
||||
})
|
||||
.bind(addr)?
|
||||
.run()
|
||||
.await
|
||||
}
|
||||
} else {
|
||||
fn main() {
|
||||
// no client-side main function
|
||||
}
|
||||
#[get("/style.css")]
|
||||
pub async fn css() -> impl Responder {
|
||||
actix_files::NamedFile::open_async("./style.css").await
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
#[actix_web::main]
|
||||
async fn main() -> std::io::Result<()> {
|
||||
use self::{ssr::*, todo::ssr::*};
|
||||
|
||||
let mut conn = db().await.expect("couldn't connect to DB");
|
||||
sqlx::migrate!()
|
||||
.run(&mut conn)
|
||||
.await
|
||||
.expect("could not run SQLx migrations");
|
||||
|
||||
// Explicit server function registration is no longer required
|
||||
// on the main branch. On 0.3.0 and earlier, uncomment the lines
|
||||
// below to register the server functions.
|
||||
// _ = GetTodos::register();
|
||||
// _ = AddTodo::register();
|
||||
// _ = DeleteTodo::register();
|
||||
|
||||
// Setting this to None means we'll be using cargo-leptos and its env vars.
|
||||
let conf = get_configuration(None).await.unwrap();
|
||||
|
||||
let addr = conf.leptos_options.site_addr;
|
||||
|
||||
// Generate the list of routes in your Leptos App
|
||||
let routes = generate_route_list(TodoApp);
|
||||
|
||||
HttpServer::new(move || {
|
||||
let leptos_options = &conf.leptos_options;
|
||||
let site_root = &leptos_options.site_root;
|
||||
let routes = &routes;
|
||||
|
||||
App::new()
|
||||
.service(css)
|
||||
.leptos_routes(
|
||||
leptos_options.to_owned(),
|
||||
routes.to_owned(),
|
||||
TodoApp,
|
||||
)
|
||||
.service(Files::new("/", site_root))
|
||||
//.wrap(middleware::Compress::default())
|
||||
})
|
||||
.bind(addr)?
|
||||
.run()
|
||||
.await
|
||||
}
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
use cfg_if::cfg_if;
|
||||
use leptos::*;
|
||||
use leptos_meta::*;
|
||||
use leptos_router::*;
|
||||
|
@ -12,21 +11,25 @@ pub struct Todo {
|
|||
completed: bool,
|
||||
}
|
||||
|
||||
cfg_if! {
|
||||
if #[cfg(feature = "ssr")] {
|
||||
use sqlx::{Connection, SqliteConnection};
|
||||
#[cfg(feature = "ssr")]
|
||||
pub mod ssr {
|
||||
pub use actix_web::HttpRequest;
|
||||
pub use leptos::ServerFnError;
|
||||
pub use sqlx::{Connection, SqliteConnection};
|
||||
|
||||
pub async fn db() -> Result<SqliteConnection, ServerFnError> {
|
||||
Ok(SqliteConnection::connect("sqlite:Todos.db").await?)
|
||||
}
|
||||
pub async fn db() -> Result<SqliteConnection, ServerFnError> {
|
||||
Ok(SqliteConnection::connect("sqlite:Todos.db").await?)
|
||||
}
|
||||
}
|
||||
|
||||
/// Server functions can be given doc comments.
|
||||
#[server(GetTodos, "/api")]
|
||||
/// This is an example of a server function using an alternative CBOR encoding. Both the function arguments being sent
|
||||
/// to the server and the server response will be encoded with CBOR. Good for binary data that doesn't encode well via the default methods
|
||||
#[server(encoding = "Cbor")]
|
||||
pub async fn get_todos() -> Result<Vec<Todo>, ServerFnError> {
|
||||
use self::ssr::*;
|
||||
|
||||
// this is just an example of how to access server context injected in the handlers
|
||||
let req = use_context::<actix_web::HttpRequest>();
|
||||
let req = use_context::<HttpRequest>();
|
||||
|
||||
if let Some(req) = req {
|
||||
println!("req.path = {:#?}", req.path());
|
||||
|
@ -44,10 +47,11 @@ pub async fn get_todos() -> Result<Vec<Todo>, ServerFnError> {
|
|||
|
||||
Ok(todos)
|
||||
}
|
||||
// This is an example of leptos's server functions using an alternative CBOR encoding. Both the function arguments being sent
|
||||
// to the server and the server response will be encoded with CBOR. Good for binary data that doesn't encode well via the default methods
|
||||
#[server(AddTodo, "/api", "Cbor")]
|
||||
|
||||
#[server]
|
||||
pub async fn add_todo(title: String) -> Result<(), ServerFnError> {
|
||||
use self::ssr::*;
|
||||
|
||||
let mut conn = db().await?;
|
||||
|
||||
// fake API delay
|
||||
|
@ -66,6 +70,8 @@ pub async fn add_todo(title: String) -> Result<(), ServerFnError> {
|
|||
// The struct name and path prefix arguments are optional.
|
||||
#[server]
|
||||
pub async fn delete_todo(id: u16) -> Result<(), ServerFnError> {
|
||||
use self::ssr::*;
|
||||
|
||||
let mut conn = db().await?;
|
||||
|
||||
Ok(sqlx::query("DELETE FROM todos WHERE id = $1")
|
||||
|
@ -78,8 +84,8 @@ pub async fn delete_todo(id: u16) -> Result<(), ServerFnError> {
|
|||
#[component]
|
||||
pub fn TodoApp() -> impl IntoView {
|
||||
provide_meta_context();
|
||||
view! {
|
||||
|
||||
view! {
|
||||
<Link rel="shortcut icon" type_="image/ico" href="/favicon.ico"/>
|
||||
<Stylesheet id="leptos" href="/pkg/todo_app_sqlite.css"/>
|
||||
<Router>
|
||||
|
|
|
@ -7,27 +7,27 @@ edition = "2021"
|
|||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[dependencies]
|
||||
console_log = "1.0.0"
|
||||
console_error_panic_hook = "0.1.7"
|
||||
futures = "0.3.25"
|
||||
cfg-if = "1.0.0"
|
||||
console_log = "1.0"
|
||||
console_error_panic_hook = "0.1"
|
||||
futures = "0.3"
|
||||
http = "1.0"
|
||||
leptos = { path = "../../leptos", features = ["nightly"] }
|
||||
server_fn = { path = "../../server_fn", features = ["serde-lite"] }
|
||||
leptos_axum = { path = "../../integrations/axum", optional = true }
|
||||
leptos_meta = { path = "../../meta", features = ["nightly"] }
|
||||
leptos_router = { path = "../../router", features = ["nightly"] }
|
||||
log = "0.4.17"
|
||||
simple_logger = "4.0.0"
|
||||
log = "0.4"
|
||||
simple_logger = "4.0"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
axum = { version = "0.6.1", optional = true }
|
||||
tower = { version = "0.4.13", optional = true }
|
||||
tower-http = { version = "0.4", features = ["fs"], optional = true }
|
||||
tokio = { version = "1.22.0", features = ["full"], optional = true }
|
||||
http = { version = "0.2.11" }
|
||||
sqlx = { version = "0.6.2", features = [
|
||||
axum = { version = "0.7", optional = true }
|
||||
tower = { version = "0.4", optional = true }
|
||||
tower-http = { version = "0.5", features = ["fs"], optional = true }
|
||||
tokio = { version = "1", features = ["full"], optional = true }
|
||||
sqlx = { version = "0.7", features = [
|
||||
"runtime-tokio-rustls",
|
||||
"sqlite",
|
||||
], optional = true }
|
||||
thiserror = "1.0.38"
|
||||
thiserror = "1.0"
|
||||
wasm-bindgen = "0.2"
|
||||
|
||||
[features]
|
||||
|
|
Binary file not shown.
|
@ -1,5 +1,4 @@
|
|||
use crate::errors::TodoAppError;
|
||||
use cfg_if::cfg_if;
|
||||
use leptos::{Errors, *};
|
||||
#[cfg(feature = "ssr")]
|
||||
use leptos_axum::ResponseOptions;
|
||||
|
@ -29,13 +28,12 @@ pub fn ErrorTemplate(
|
|||
|
||||
// Only the response code for the first error is actually sent from the server
|
||||
// this may be customized by the specific application
|
||||
cfg_if! {
|
||||
if #[cfg(feature="ssr")]{
|
||||
#[cfg(feature = "ssr")]
|
||||
{
|
||||
let response = use_context::<ResponseOptions>();
|
||||
if let Some(response) = response{
|
||||
response.set_status(errors[0].status_code());
|
||||
if let Some(response) = response {
|
||||
response.set_status(errors[0].status_code());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
view! {
|
||||
|
@ -50,7 +48,6 @@ pub fn ErrorTemplate(
|
|||
let error_string = error.1.to_string();
|
||||
let error_code= error.1.status_code();
|
||||
view! {
|
||||
|
||||
<h2>{error_code.to_string()}</h2>
|
||||
<p>"Error: " {error_string}</p>
|
||||
}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue