+ }
+}
diff --git a/projects/meilisearch-searchbar/src/main.rs b/projects/meilisearch-searchbar/src/main.rs
new file mode 100644
index 0000000000..dfe164f956
--- /dev/null
+++ b/projects/meilisearch-searchbar/src/main.rs
@@ -0,0 +1,83 @@
+#[cfg(feature = "ssr")]
+#[tokio::main]
+async fn main() {
+ use axum::{routing::get, Extension, Router};
+ use leptos::get_configuration;
+ use leptos_axum::{generate_route_list, LeptosRoutes};
+ use meilisearch_searchbar::StockRow;
+ use meilisearch_searchbar::{fallback::file_and_error_handler, *};
+
+ // simple_logger is a lightweight alternative to tracing, when you absolutely have to trace, use tracing.
+ simple_logger::SimpleLogger::new()
+ .with_level(log::LevelFilter::Debug)
+ .init()
+ .unwrap();
+
+ let mut rdr = csv::Reader::from_path("data_set.csv").unwrap();
+
+ // Our data set doesn't have a good id for the purposes of meilisearch, Name is unique but it's not formatted correctly because it may have spaces.
+ let documents: Vec = rdr
+ .records()
+ .enumerate()
+ .map(|(i, rec)| {
+ // There's probably a better way to do this.
+ let mut record = csv::StringRecord::new();
+ record.push_field(&i.to_string());
+ for field in rec.unwrap().iter() {
+ record.push_field(field);
+ }
+ record
+ .deserialize::(None)
+ .expect(&format!("{:?}", record))
+ })
+ .collect();
+
+ // My own check. I know how long I expect it to be, if it's not this length something is wrong.
+ assert_eq!(documents.len(), 503);
+
+ let client = meilisearch_sdk::Client::new(
+ std::env::var("MEILISEARCH_URL").unwrap(),
+ std::env::var("MEILISEARCH_API_KEY").ok(),
+ );
+ // An index is where the documents are stored.
+ let task = client
+ .create_index("stock_prices", Some("id"))
+ .await
+ .unwrap();
+
+ // Meilisearch may take some time to execute the request so we are going to wait till it's completed
+ client.wait_for_task(task, None, None).await.unwrap();
+
+ let task_2 = client
+ .get_index("stock_prices")
+ .await
+ .unwrap()
+ .add_documents(&documents, Some("id"))
+ .await
+ .unwrap();
+
+ client.wait_for_task(task_2, None, None).await.unwrap();
+
+ drop(documents);
+
+ 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);
+
+ // build our application with a route
+ let app = Router::new()
+ .route("/favicon.ico", get(file_and_error_handler))
+ .leptos_routes(&leptos_options, routes, App)
+ .fallback(file_and_error_handler)
+ .layer(Extension(client))
+ .with_state(leptos_options);
+
+ // 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();
+}
diff --git a/projects/nginx-mpmc/.gitignore b/projects/nginx-mpmc/.gitignore
new file mode 100644
index 0000000000..7ededfcf71
--- /dev/null
+++ b/projects/nginx-mpmc/.gitignore
@@ -0,0 +1,3 @@
+/target
+*/target
+.vscode
diff --git a/projects/nginx-mpmc/README.md b/projects/nginx-mpmc/README.md
new file mode 100644
index 0000000000..6cb3a26fa7
--- /dev/null
+++ b/projects/nginx-mpmc/README.md
@@ -0,0 +1,34 @@
+# Nginx Multiple Server Multiple Client Example
+This example shows how multiple clients can communicate with multiple servers while being shared over a single domain i.e localhost:80 using nginx as a reverse proxy.
+
+### How to run this example
+```sh
+./run.sh
+```
+Or
+
+```sh
+./run_linux.sh
+```
+
+
+This will boot up nginx via it's docker image mapped to port 80, and the four servers. App-1, App-2, Shared-Server-1, Shared-Server-2.
+
+App-1, And App-2 are SSR rendering leptos servers.
+
+If you go to localhost (you'll get App-1), and localhost/app2 (you'll get app2).
+
+The two shared servers can be communicated with via actions and local resources, or resources (if using CSR).
+
+`create_resource` Won't work as expected, when trying to communicate to different servers. It will instead try to run the server function on the server you are serving your server side rendered content from. This will cause errors if your server function relies on state that is not present.
+
+When you are done with this example, run
+
+```sh
+./kill.sh
+```
+
+Casting ctrl-c multiple times won't close all the open programs.
+
+## Thoughts, Feedback, Criticism, Comments?
+Send me any of the above, I'm @sjud on leptos discord. I'm always looking to improve and make these projects more helpful for the community. So please let me know how I can do that. Thanks!
\ No newline at end of file
diff --git a/projects/nginx-mpmc/app-1/.gitignore b/projects/nginx-mpmc/app-1/.gitignore
new file mode 100644
index 0000000000..8cdaa33de1
--- /dev/null
+++ b/projects/nginx-mpmc/app-1/.gitignore
@@ -0,0 +1,13 @@
+# Generated by Cargo
+# will have compiled files and executables
+/target/
+pkg
+
+# These are backup files generated by rustfmt
+**/*.rs.bk
+
+# node e2e test tools and outputs
+node_modules/
+test-results/
+end2end/playwright-report/
+playwright/.cache/
diff --git a/projects/nginx-mpmc/app-1/Cargo.toml b/projects/nginx-mpmc/app-1/Cargo.toml
new file mode 100644
index 0000000000..17937d138a
--- /dev/null
+++ b/projects/nginx-mpmc/app-1/Cargo.toml
@@ -0,0 +1,120 @@
+[package]
+name = "app-1"
+version = "0.1.0"
+edition = "2021"
+
+[lib]
+crate-type = ["cdylib", "rlib"]
+
+[dependencies]
+console_error_panic_hook = "0.1"
+leptos_meta = { version = "0.6" }
+leptos_router = { version = "0.6" }
+tower = { version = "0.4", optional = true }
+tower-http = { version = "0.5", features = ["fs","trace"], optional = true }
+wasm-bindgen = "=0.2.89"
+thiserror = "1"
+tracing = { version = "0.1", optional = true }
+
+http = "1"
+
+axum = {version = "0.7",optional=true}
+leptos = "0.6"
+leptos_axum = {version = "0.6",optional=true}
+tokio = { version = "1", features = ["rt-multi-thread"], optional = true}
+shared-server = {path = "../shared-server",default-features = false}
+shared-server-2 = {path = "../shared-server-2",default-features = false}
+tracing-subscriber = {version="0.3.18",features=["env-filter"]}
+
+# 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"
+
+[features]
+hydrate = [
+ "leptos/hydrate",
+ "leptos_meta/hydrate",
+ "leptos_router/hydrate",
+ "shared-server/hydrate",
+ "shared-server-2/hydrate"
+ ]
+ssr = [
+ "shared-server/ssr",
+ "shared-server-2/ssr",
+ "dep:axum",
+ "dep:tokio",
+ "dep:tower",
+ "dep:tower-http",
+ "dep:leptos_axum",
+ "leptos/ssr",
+ "leptos_meta/ssr",
+ "leptos_router/ssr",
+ "dep:tracing",
+]
+
+
+
+[package.metadata.leptos]
+# The name used by wasm-bindgen/cargo-leptos for the JS/WASM bundle. Defaults to the crate name
+output-name = "app-1"
+
+# 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"
+
+# Assets source dir. All files found here will be copied and synchronized to site-root.
+# The assets-dir cannot have a sub directory with the same name/path as site-pkg-dir.
+#
+# Optional. Env: LEPTOS_ASSETS_DIR.
+assets-dir = "public"
+
+# The IP and port (ex: 127.0.0.1:3000) where the server serves the content. Use it in your server setup.
+# we're listening inside of a docker container, so we need to set 0.0.0.0 to let it be accessed from outside the container.
+site-addr = "127.0.0.1:3000"
+
+# The port to use for automatic reload monitoring
+reload-port = 3004
+
+# [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 //app.css
+style-file = "style/main.scss"
+
+# 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"
diff --git a/projects/nginx-mpmc/app-1/LICENSE b/projects/nginx-mpmc/app-1/LICENSE
new file mode 100644
index 0000000000..4d209962a5
--- /dev/null
+++ b/projects/nginx-mpmc/app-1/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2022
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/projects/nginx-mpmc/app-1/README.md b/projects/nginx-mpmc/app-1/README.md
new file mode 100644
index 0000000000..ac4da7932f
--- /dev/null
+++ b/projects/nginx-mpmc/app-1/README.md
@@ -0,0 +1,86 @@
+
+
+# Leptos Axum 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 using [Axum](https://github.com/tokio-rs/axum).
+
+## Creating your template repo
+
+If you don't have `cargo-leptos` installed you can install it with
+
+```bash
+cargo install cargo-leptos
+```
+
+Then run
+```bash
+cargo leptos new --git leptos-rs/start-axum
+```
+
+to generate a new project template.
+
+```bash
+cd app-1
+```
+
+to go to your newly created project.
+Feel free to explore the project structure, but the best place to start with your application code is in `src/app.rs`.
+Addtionally, Cargo.toml may need updating as new versions of the dependencies are released, especially if things are not working after a `cargo update`.
+
+## Running your project
+
+```bash
+cargo leptos watch
+```
+
+## 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
+
+## Compiling for Release
+```bash
+cargo leptos build --release
+```
+
+Will generate your server binary in target/server/release and your site package in target/site
+
+## Testing Your Project
+```bash
+cargo leptos end-to-end
+```
+
+```bash
+cargo leptos end-to-end --release
+```
+
+Cargo-leptos uses Playwright as the end-to-end test tool.
+Tests are located in end2end/tests directory.
+
+## 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
+app-1
+site/
+```
+Set the following environment variables (updating for your project as needed):
+```text
+LEPTOS_OUTPUT_NAME="app-1"
+LEPTOS_SITE_ROOT="site"
+LEPTOS_SITE_PKG_DIR="pkg"
+LEPTOS_SITE_ADDR="127.0.0.1:3000"
+LEPTOS_RELOAD_PORT="3001"
+```
+Finally, run the server binary.
diff --git a/projects/nginx-mpmc/app-1/public/favicon.ico b/projects/nginx-mpmc/app-1/public/favicon.ico
new file mode 100644
index 0000000000000000000000000000000000000000..2ba8527cb12f5f28f331b8d361eef560492d4c77
GIT binary patch
literal 15406
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
k<4FtN!5 impl IntoView {
+ // Provides context that manages stylesheets, titles, meta tags, etc.
+ provide_meta_context();
+
+ view! {
+
+
+ // injects a stylesheet into the document
+ // id=leptos means cargo-leptos will hot-reload this stylesheet
+
+
+ // sets the document title
+
+
+ // content for this welcome page
+
+ }
+ .into_view()
+ }>
+
+
+
+
+
+
+ }
+}
+
+/// Renders the home page of your application.
+#[component]
+fn HomePage() -> impl IntoView {
+ use shared_server::SharedServerFunction;
+ use shared_server_2::SharedServerFunction2;
+
+ // A local resource will wait for the client to load before attempting to initialize.
+ let hello_1 = create_local_resource(move || (), |_| shared_server::shared_server_function());
+ // this won't work : let hello_1 = create_resource(move || (), |_| shared_server::shared_server_function());
+ // A resource is initialized on the rendering server when using SSR.
+
+ let hello_1_action = Action::::server();
+ let hello_2_action = Action::::server();
+
+ let value_1 = create_rw_signal(String::from("waiting for update from shared server."));
+ let value_2 = create_rw_signal(String::from("waiting for update from shared server 2."));
+
+ //let hello_2 = create_resource(move || (), shared_server_2::shared_server_function);
+ create_effect(move|_|{if let Some(Ok(msg)) = hello_1_action.value().get(){value_1.set(msg)}});
+ create_effect(move|_|{if let Some(Ok(msg)) = hello_2_action.value().get(){value_2.set(msg)}});
+
+ view! {
+
App 1
+
Suspense
+ "Loading (Suspense Fallback)..."
}>
+ {move || {
+ hello_1.get().map(|data| match data {
+ Err(_) => view! {
+
+ {move || value_2.get()}
+ }
+}
diff --git a/projects/nginx-mpmc/app-1/src/error_template.rs b/projects/nginx-mpmc/app-1/src/error_template.rs
new file mode 100644
index 0000000000..1e0508da5c
--- /dev/null
+++ b/projects/nginx-mpmc/app-1/src/error_template.rs
@@ -0,0 +1,72 @@
+use http::status::StatusCode;
+use leptos::*;
+use thiserror::Error;
+
+#[derive(Clone, Debug, Error)]
+pub enum AppError {
+ #[error("Not Found")]
+ NotFound,
+}
+
+impl AppError {
+ pub fn status_code(&self) -> StatusCode {
+ match self {
+ AppError::NotFound => StatusCode::NOT_FOUND,
+ }
+ }
+}
+
+// A basic function to display errors served by the error boundaries.
+// Feel free to do more complicated things here than just displaying the error.
+#[component]
+pub fn ErrorTemplate(
+ #[prop(optional)] outside_errors: Option,
+ #[prop(optional)] errors: Option>,
+) -> impl IntoView {
+ let errors = match outside_errors {
+ Some(e) => create_rw_signal(e),
+ None => match errors {
+ Some(e) => e,
+ None => panic!("No Errors found and we expected errors!"),
+ },
+ };
+ // Get Errors from Signal
+ let errors = errors.get_untracked();
+
+ // Downcast lets us take a type that implements `std::error::Error`
+ let errors: Vec = errors
+ .into_iter()
+ .filter_map(|(_k, v)| v.downcast_ref::().cloned())
+ .collect();
+ println!("Errors: {errors:#?}");
+
+ // Only the response code for the first error is actually sent from the server
+ // this may be customized by the specific application
+ #[cfg(feature = "ssr")]
+ {
+ use leptos_axum::ResponseOptions;
+ let response = use_context::();
+ if let Some(response) = response {
+ response.set_status(errors[0].status_code());
+ }
+ }
+
+ view! {
+
{if errors.len() > 1 {"Errors"} else {"Error"}}
+ {error_code.to_string()}
+
"Error: " {error_string}
+ }
+ }
+ />
+ }
+}
diff --git a/projects/nginx-mpmc/app-1/src/fileserv.rs b/projects/nginx-mpmc/app-1/src/fileserv.rs
new file mode 100644
index 0000000000..e7692f6486
--- /dev/null
+++ b/projects/nginx-mpmc/app-1/src/fileserv.rs
@@ -0,0 +1,43 @@
+use axum::{
+ body::Body,
+ 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::app::App;
+
+pub async fn file_and_error_handler(uri: Uri, State(options): State, req: Request) -> AxumResponse {
+ let root = options.site_root.clone();
+ tracing::debug!("APP 1");
+ 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(), App);
+ handler(req).await.into_response()
+ }
+}
+
+async fn get_static_file(
+ uri: Uri,
+ root: &str,
+) -> Result, (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}"),
+ )),
+ }
+}
diff --git a/projects/nginx-mpmc/app-1/src/lib.rs b/projects/nginx-mpmc/app-1/src/lib.rs
new file mode 100644
index 0000000000..ac3668783c
--- /dev/null
+++ b/projects/nginx-mpmc/app-1/src/lib.rs
@@ -0,0 +1,12 @@
+pub mod app;
+pub mod error_template;
+#[cfg(feature = "ssr")]
+pub mod fileserv;
+
+#[cfg(feature = "hydrate")]
+#[wasm_bindgen::prelude::wasm_bindgen]
+pub fn hydrate() {
+ use crate::app::*;
+ console_error_panic_hook::set_once();
+ leptos::mount_to_body(App);
+}
diff --git a/projects/nginx-mpmc/app-1/src/main.rs b/projects/nginx-mpmc/app-1/src/main.rs
new file mode 100644
index 0000000000..a09ae6ce58
--- /dev/null
+++ b/projects/nginx-mpmc/app-1/src/main.rs
@@ -0,0 +1,49 @@
+#[cfg(feature = "ssr")]
+#[tokio::main]
+async fn main() {
+ use axum::Router;
+ use leptos::*;
+ use leptos_axum::{generate_route_list, LeptosRoutes};
+ use app_1::app::*;
+ use app_1::fileserv::file_and_error_handler;
+ use axum::routing::post;
+
+ tracing_subscriber::fmt()
+ .pretty()
+ .with_thread_names(true)
+ // enable everything
+ .with_max_level(tracing::Level::TRACE)
+ // sets this to be the default, global collector for this application.
+ .init();
+
+ // Setting get_configuration(None) means we'll be using cargo-leptos's env values
+ // For deployment these variables are:
+ //
+ // Alternately a file can be specified such as Some("Cargo.toml")
+ // The file would need to be included with the executable when moved to deployment
+ 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);
+
+ // build our application with a route
+ let app = Router::new()
+ .route("/api_app1/*fn_name", post(leptos_axum::handle_server_fns))
+ .leptos_routes(&leptos_options, routes, App)
+ .fallback(file_and_error_handler)
+ .layer(tower_http::trace::TraceLayer::new_for_http())
+ .with_state(leptos_options);
+
+ let listener = tokio::net::TcpListener::bind(&addr).await.unwrap();
+ logging::log!("listening on http://{}", &addr);
+ axum::serve(listener, app.into_make_service())
+ .await
+ .unwrap();
+}
+
+#[cfg(not(feature = "ssr"))]
+pub fn main() {
+ // no client-side main function
+ // unless we want this to work with e.g., Trunk for a purely client-side app
+ // see lib.rs for hydration function instead
+}
diff --git a/projects/nginx-mpmc/app-1/style/main.scss b/projects/nginx-mpmc/app-1/style/main.scss
new file mode 100644
index 0000000000..e4538e156f
--- /dev/null
+++ b/projects/nginx-mpmc/app-1/style/main.scss
@@ -0,0 +1,4 @@
+body {
+ font-family: sans-serif;
+ text-align: center;
+}
\ No newline at end of file
diff --git a/projects/nginx-mpmc/app-2/.gitignore b/projects/nginx-mpmc/app-2/.gitignore
new file mode 100644
index 0000000000..8cdaa33de1
--- /dev/null
+++ b/projects/nginx-mpmc/app-2/.gitignore
@@ -0,0 +1,13 @@
+# Generated by Cargo
+# will have compiled files and executables
+/target/
+pkg
+
+# These are backup files generated by rustfmt
+**/*.rs.bk
+
+# node e2e test tools and outputs
+node_modules/
+test-results/
+end2end/playwright-report/
+playwright/.cache/
diff --git a/projects/nginx-mpmc/app-2/Cargo.toml b/projects/nginx-mpmc/app-2/Cargo.toml
new file mode 100644
index 0000000000..3aba483da6
--- /dev/null
+++ b/projects/nginx-mpmc/app-2/Cargo.toml
@@ -0,0 +1,115 @@
+[package]
+name = "app-2"
+version = "0.1.0"
+edition = "2021"
+
+[lib]
+crate-type = ["cdylib", "rlib"]
+
+[dependencies]
+console_error_panic_hook = "0.1"
+leptos_meta = { version = "0.6" }
+leptos_router = { version = "0.6" }
+tower = { version = "0.4", optional = true }
+tower-http = { version = "0.5", features = ["fs"], optional = true }
+wasm-bindgen = "=0.2.89"
+thiserror = "1"
+tracing = { version = "0.1", optional = true }
+http = "1"
+
+axum = {version = "0.7",optional=true}
+leptos = "0.6"
+leptos_axum = {version = "0.6",optional=true}
+tokio = { version = "1", features = ["rt-multi-thread"], optional = true}
+shared-server = {path = "../shared-server",default-features = false}
+shared-server-2 = {path = "../shared-server-2",default-features = false}
+
+# 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"
+
+[features]
+hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate","shared-server/hydrate","shared-server-2/hydrate"]
+ssr = [
+ "shared-server/ssr",
+ "shared-server-2/ssr",
+ "dep:axum",
+ "dep:tokio",
+ "dep:tower",
+ "dep:tower-http",
+ "dep:leptos_axum",
+ "leptos/ssr",
+ "leptos_meta/ssr",
+ "leptos_router/ssr",
+ "dep:tracing",
+]
+
+
+[package.metadata.leptos]
+# The name used by wasm-bindgen/cargo-leptos for the JS/WASM bundle. Defaults to the crate name
+output-name = "app-2"
+
+# 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"
+
+#
+#
+### WE CHANGED THIS IN THIS EXAMPLE
+#
+#
+site-pkg-dir = "pkg2"
+
+
+# Assets source dir. All files found here will be copied and synchronized to site-root.
+# The assets-dir cannot have a sub directory with the same name/path as site-pkg-dir.
+#
+# Optional. Env: LEPTOS_ASSETS_DIR.
+assets-dir = "public"
+
+# [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 //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:3001"
+
+# The port to use for automatic reload monitoring
+reload-port = 3005
+
+
+# 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"
diff --git a/projects/nginx-mpmc/app-2/LICENSE b/projects/nginx-mpmc/app-2/LICENSE
new file mode 100644
index 0000000000..4d209962a5
--- /dev/null
+++ b/projects/nginx-mpmc/app-2/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2022
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/projects/nginx-mpmc/app-2/README.md b/projects/nginx-mpmc/app-2/README.md
new file mode 100644
index 0000000000..1f7b489715
--- /dev/null
+++ b/projects/nginx-mpmc/app-2/README.md
@@ -0,0 +1,86 @@
+
+
+# Leptos Axum 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 using [Axum](https://github.com/tokio-rs/axum).
+
+## Creating your template repo
+
+If you don't have `cargo-leptos` installed you can install it with
+
+```bash
+cargo install cargo-leptos
+```
+
+Then run
+```bash
+cargo leptos new --git leptos-rs/start-axum
+```
+
+to generate a new project template.
+
+```bash
+cd app-2
+```
+
+to go to your newly created project.
+Feel free to explore the project structure, but the best place to start with your application code is in `src/app.rs`.
+Addtionally, Cargo.toml may need updating as new versions of the dependencies are released, especially if things are not working after a `cargo update`.
+
+## Running your project
+
+```bash
+cargo leptos watch
+```
+
+## 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
+
+## Compiling for Release
+```bash
+cargo leptos build --release
+```
+
+Will generate your server binary in target/server/release and your site package in target/site
+
+## Testing Your Project
+```bash
+cargo leptos end-to-end
+```
+
+```bash
+cargo leptos end-to-end --release
+```
+
+Cargo-leptos uses Playwright as the end-to-end test tool.
+Tests are located in end2end/tests directory.
+
+## 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
+app-2
+site/
+```
+Set the following environment variables (updating for your project as needed):
+```text
+LEPTOS_OUTPUT_NAME="app-2"
+LEPTOS_SITE_ROOT="site"
+LEPTOS_SITE_PKG_DIR="pkg"
+LEPTOS_SITE_ADDR="127.0.0.1:3000"
+LEPTOS_RELOAD_PORT="3001"
+```
+Finally, run the server binary.
diff --git a/projects/nginx-mpmc/app-2/public/favicon.ico b/projects/nginx-mpmc/app-2/public/favicon.ico
new file mode 100644
index 0000000000000000000000000000000000000000..2ba8527cb12f5f28f331b8d361eef560492d4c77
GIT binary patch
literal 15406
zcmeHOd3aPs5`TblWD*3D%tXPJ#q(n!z$P=3gCjvf#a)E}a;Uf>h{pmVih!a-5LVO`
zB?JrzEFicD0wRLo0iPfO372xnkvkzFlRHB)lcTnNZ}KK@US{UKN#b8?e_zkLy1RZ=
zT~*y(-6IICgf>E_P6A)M3(wvl2qr-gx_5Ux-_uzT*6_Q&ee1v9B?vzS3&K5IhO2N5
z$9ukLN<`G>$$|GLnga~y%>f}*j%+w@(ixVUb^1_Gjoc;(?TrD3m2)RduFblVN)uy;
zQAEd^T{5>-YYH%|Kv{V^cxHMBr1Ik7Frht$imC`rqx@5*|
z+OqN!xAjqmaU=qR$uGDMa7p!W9oZ+64($4xDk^FyFQ<_9Z`(;DLnB<;LLJD1<&vnZ
zo0(>zIkQTse}qNMb6+i`th54(3pKm8;UAJ<_BULR*Z=m5FU7jiW(l+}WkHZ|e@1
z`pm;Q^pCuLUQUrnQ(hPM10pSSHQS=Bf8DqG1&!-B!oQQ|FuzLruL1w(+g<8&znyI?
zzX-}?SwUvNjEuT?7uUOy{Fb@xKklpj+jdYM^IK9}NxvLRZd{l9FHEQJ4IO~q%4I0O
zAN|*8x^nIU4Giw?f*tmNx=7H)2-Zn?J^B6SgpcW3ZXV_57Sn%Mtfr_=w|sYpAhdJT
zcKo6Z*oIOU(az~3$LOEWm9Q)dYWMA}T7L23MVGqrcA%4H)+^`+=j+Hh8CTCnnG2Rh
zgcXVW%F8$R9)6}f=NQiLPt8qt3xNUQI>Q*)H1lzk<&n?XR-f}tc&9V0H0lhGqHJ^N
zN%h(9-Of2_)!Xk{qdIkU>1%mk%I_Id1!MU*yq&&>)Q+!L^t&-2mW9Xq7g9C@*
zl&PKJ&su2L+iku?Te?Pf?k3tUK){Bj_gb&aPo8Ago^XI~mRTd(5{&^tf1)!-lSMha
z@$~ae!r(~`=p&|mMxy2EiZQ6FvXb(1avS*`Pj%$)*?vwceGKHmHnl`v&fEQ_Wh+G)
zEPQ^3&oV%}%;zF`AM|S%d>pM@1}33PN5*4SewROk_K$n^i8QjaYiRzwG8#OvVIF|{x85wH+?*P*%)woI
zR538k@=(E`V;p1UwA|fqSh`$n_t;Sz4T)`_s~pRR4lbmWWSdxa-FqLZ%fLT)Bh?iye?COx~mO1wkn5)HNMg7`8~
z25VJhz&3Z7`M>6luJrEw$Jikft+6SxyIh?)PU1?DfrKMGC
z=3T;;omE4H`PWqF8?0*dOA3o9y@~WK`S}{?tIHquEw?v`M^D%Lobpdrp%3}1=-&qk
zqAtb1px-1Fy6}E8IUg4s%8B0~P<P5C;de%@n~XnDKF@fr$a+^@$^P|>vlw($aSK2lRtLt~8tRb`I0
znfI!G?K|<5ry*gk>y56rZy0NkK6)))6Mg1=K?7yS9p+#1Ij=W*%5Rt-mlc;#MOnE9
zoi`-+6oj@)`gq2Af!B+9%J#K9V=ji2dj2<_qaLSXOCeqQ&<0zMSb$5mAi;HU=v`v<>NYk}MbD!ewYVB+N-ctzn=l&bTwv)*7
zmY<+Y@SBbtl9PPk$HTR?ln@(T92XjTRj0Mx|Mzl;lW>Su_y^~fh?8(L?oz8h!cCpb
zZG-OY=NJ3{>r*`U<(J%#zjFT-a9>u6+23H{=d(utkgqt7@^)C;pkb)fQ|Q=*8*SyT
z;otKe+f8fEp)ZacKZDn3TNzs>_Kx+g*c_mr8LBhr8GnoEmAQk#%sR52`bdbW8Ms$!0u2bdt=T-lK3JbDW`F(Urt%Ob2seiN>7U`YN}aOdIiCC;eeufJC#m3S
z9#|l2c?G@t*hH5y^76jkv)rs4H+;oiTuY5FQwRMN_7NUqeiD|b&RyxPXQz|3qC(_>
zZJMwjC4F!1m2INXqzisQ4X^w=>&(+Ecdu&~IWEMn7f*YcYI&eWI(6hI#f114%aymM
zyhlG6{q>XN7(LyGiMAS&qijR%d2rV|>AUT_sE&EKUSTCM26>aKzNxk0?K|utOcxl#
zxIOwM#O!!H+QzbX*&p=QuKe4y;bS>&StQOE5AEGg_ubk8{;1yOVAJfE_Js-lL7rr9
z)CEuFIlkApj~uV^zJK7KocjT=4B
zJP(}0x}|A7C$$5gIp>KBPZ|A#2Ew;$#g9Fk)r;Q~?G$>x<+JM)J3u>j
zi68K=I;ld`JJ?Nq+^_B?C+Q%+x#m{9JF$tbaDeNIep%=^#>KHGtg=L)>m
z_J&vaZTs2{qP!4Gdw5u5Kcf}5R4(q}Lebx%(J$7l*Q`Il#pCTM%!`y5y*-~zIVs}D
z9;t+(xmV~R65^ZQXe+<5{$QW0O8MT~a{kdFLR)nfRMA9L(YU>x*DTltN#m-2km
zC;T`cfb{c`mcx(z7o_a8bYJn8_^dz4Cq!DZ37{P6uF{@#519UWK1{>(9sZB1I^6MmNc39MJ-_|)!S8vO+O3&$MulU3Gc
z_W{N*B(yneyl-oN_MKaJ{CZ6dv-~^8uPbLSh&0jfV@EfA{2Dc!_rOyfx`R0T@LonA
z<*%O?-aa_Wm-z$s@K(ex7UhM0-?9C=PkYdk&d2n((E4>&(f4D`fOQY%CURMMyJyU`
zVeJBAId&StHjw76tnwSqZs3e0683`L{a3k9JYdg#(ZVw4J`&CkV-2LFaDE1Z?CehVy%vZx$tM3tTax8E@2;N^QTrPcI?Ob8uK!DM0_sfE6ks2M?iw
zPS4{(k-PF*-oY>S!d9;L+|xdTtLen9B2LvpL4k;#ScB<
z$NP_7j~7)5eXuoYEk*dK_rSz9yT_C4B{r~^#^o}-VQI=Y?01|$aa!a7=UEm$|DsQQ
zfLK1qmho2@)nwA?$1%T6jwO2HZ({6&;`s|OQOxI4S8*Hw=Qp!b(gNJR%SAj&wGa>^&2@x)Vj
zhd^WfzJ^b0O{E^q82Pw({uT`E`MT2WnZ02{E%t*yRPN>?W>0vU^4@Vyh4;mLj918c
z*s*papo?<}cQM{5lcgZScx}?usg{mS!KkH9U%@|^_33?{FI{1ss+8kXyFY&5M-e~f
zM$){FF;_+z3sNJ)Er~{Beux$fEl{R4|7WKcpEsGtK57f+H0DJ$hI;U;JtF>+lG@sV
zQI_;bQ^7XIJ>Bs?C32b1v;am;P4GUqAJ#zOHv}4SmV|xXX6~O9&e_~YCCpbT>s$`!
k<4FtN!5 impl IntoView {
+ // Provides context that manages stylesheets, titles, meta tags, etc.
+ provide_meta_context();
+
+ view! {
+
+
+ // injects a stylesheet into the document
+ // id=leptos means cargo-leptos will hot-reload this stylesheet
+
+
+ // sets the document title
+
+
+ // content for this welcome page
+
+ }
+ .into_view()
+ }>
+
+
+
+
+
+
+ }
+}
+
+/// Renders the home page of your application.
+#[component]
+fn HomePage() -> impl IntoView {
+ use shared_server::SharedServerFunction;
+ use shared_server_2::SharedServerFunction2;
+
+ let hello_1_action = Action::::server();
+ let hello_2_action = Action::::server();
+
+ let value_1 = create_rw_signal(String::from("waiting for update from shared server."));
+ let value_2 = create_rw_signal(String::from("waiting for update from shared server 2."));
+
+ //let hello_2 = create_resource(move || (), shared_server_2::shared_server_function);
+ create_effect(move|_|{if let Some(Ok(msg)) = hello_1_action.value().get(){value_1.set(msg)}});
+ create_effect(move|_|{if let Some(Ok(msg)) = hello_2_action.value().get(){value_2.set(msg)}});
+
+ view! {
+
App 2
+
action response from server 1
+
+ {move || value_1.get()}
+
action response from server 2
+
+ {move || value_2.get()}
+ }
+}
+
diff --git a/projects/nginx-mpmc/app-2/src/error_template.rs b/projects/nginx-mpmc/app-2/src/error_template.rs
new file mode 100644
index 0000000000..1e0508da5c
--- /dev/null
+++ b/projects/nginx-mpmc/app-2/src/error_template.rs
@@ -0,0 +1,72 @@
+use http::status::StatusCode;
+use leptos::*;
+use thiserror::Error;
+
+#[derive(Clone, Debug, Error)]
+pub enum AppError {
+ #[error("Not Found")]
+ NotFound,
+}
+
+impl AppError {
+ pub fn status_code(&self) -> StatusCode {
+ match self {
+ AppError::NotFound => StatusCode::NOT_FOUND,
+ }
+ }
+}
+
+// A basic function to display errors served by the error boundaries.
+// Feel free to do more complicated things here than just displaying the error.
+#[component]
+pub fn ErrorTemplate(
+ #[prop(optional)] outside_errors: Option,
+ #[prop(optional)] errors: Option>,
+) -> impl IntoView {
+ let errors = match outside_errors {
+ Some(e) => create_rw_signal(e),
+ None => match errors {
+ Some(e) => e,
+ None => panic!("No Errors found and we expected errors!"),
+ },
+ };
+ // Get Errors from Signal
+ let errors = errors.get_untracked();
+
+ // Downcast lets us take a type that implements `std::error::Error`
+ let errors: Vec = errors
+ .into_iter()
+ .filter_map(|(_k, v)| v.downcast_ref::().cloned())
+ .collect();
+ println!("Errors: {errors:#?}");
+
+ // Only the response code for the first error is actually sent from the server
+ // this may be customized by the specific application
+ #[cfg(feature = "ssr")]
+ {
+ use leptos_axum::ResponseOptions;
+ let response = use_context::();
+ if let Some(response) = response {
+ response.set_status(errors[0].status_code());
+ }
+ }
+
+ view! {
+
{if errors.len() > 1 {"Errors"} else {"Error"}}
+ {error_code.to_string()}
+
"Error: " {error_string}
+ }
+ }
+ />
+ }
+}
diff --git a/projects/nginx-mpmc/app-2/src/fileserv.rs b/projects/nginx-mpmc/app-2/src/fileserv.rs
new file mode 100644
index 0000000000..f4de6c70b1
--- /dev/null
+++ b/projects/nginx-mpmc/app-2/src/fileserv.rs
@@ -0,0 +1,46 @@
+use axum::{
+ body::Body,
+ 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::app::App;
+
+pub async fn file_and_error_handler(uri: Uri, State(options): State, req: Request) -> AxumResponse {
+ let root = options.site_root.clone();
+
+ tracing::debug!("APP 2");
+
+ 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(), App);
+ handler(req).await.into_response()
+ }
+}
+
+async fn get_static_file(
+ uri: Uri,
+ root: &str,
+) -> Result, (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}"),
+ )),
+ }
+}
diff --git a/projects/nginx-mpmc/app-2/src/lib.rs b/projects/nginx-mpmc/app-2/src/lib.rs
new file mode 100644
index 0000000000..ac3668783c
--- /dev/null
+++ b/projects/nginx-mpmc/app-2/src/lib.rs
@@ -0,0 +1,12 @@
+pub mod app;
+pub mod error_template;
+#[cfg(feature = "ssr")]
+pub mod fileserv;
+
+#[cfg(feature = "hydrate")]
+#[wasm_bindgen::prelude::wasm_bindgen]
+pub fn hydrate() {
+ use crate::app::*;
+ console_error_panic_hook::set_once();
+ leptos::mount_to_body(App);
+}
diff --git a/projects/nginx-mpmc/app-2/src/main.rs b/projects/nginx-mpmc/app-2/src/main.rs
new file mode 100644
index 0000000000..9578db922d
--- /dev/null
+++ b/projects/nginx-mpmc/app-2/src/main.rs
@@ -0,0 +1,41 @@
+#[cfg(feature = "ssr")]
+#[tokio::main]
+async fn main() {
+ use axum::{
+ Router,
+ routing::get,
+ };
+ use leptos::*;
+ use leptos_axum::{generate_route_list, LeptosRoutes};
+ use app_2::app::*;
+ use app_2::fileserv::file_and_error_handler;
+
+ // Setting get_configuration(None) means we'll be using cargo-leptos's env values
+ // For deployment these variables are:
+ //
+ // Alternately a file can be specified such as Some("Cargo.toml")
+ // The file would need to be included with the executable when moved to deployment
+ 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 app = Router::new()
+ .leptos_routes(&leptos_options, routes, App)
+ .fallback(file_and_error_handler)
+ .layer(tower_http::trace::TraceLayer::new_for_http())
+ .with_state(leptos_options);
+ let listener = tokio::net::TcpListener::bind(&addr).await.unwrap();
+ logging::log!("listening on http://{}", &addr);
+ axum::serve(listener, app.into_make_service())
+ .await
+ .unwrap();
+}
+
+#[cfg(not(feature = "ssr"))]
+pub fn main() {
+ // no client-side main function
+ // unless we want this to work with e.g., Trunk for a purely client-side app
+ // see lib.rs for hydration function instead
+}
diff --git a/projects/nginx-mpmc/app-2/style/main.scss b/projects/nginx-mpmc/app-2/style/main.scss
new file mode 100644
index 0000000000..e4538e156f
--- /dev/null
+++ b/projects/nginx-mpmc/app-2/style/main.scss
@@ -0,0 +1,4 @@
+body {
+ font-family: sans-serif;
+ text-align: center;
+}
\ No newline at end of file
diff --git a/projects/nginx-mpmc/kill.sh b/projects/nginx-mpmc/kill.sh
new file mode 100755
index 0000000000..4ee6a96449
--- /dev/null
+++ b/projects/nginx-mpmc/kill.sh
@@ -0,0 +1,5 @@
+lsof -ti :3000 | xargs kill && \
+lsof -ti :3001 | xargs kill && \
+lsof -ti :3002 | xargs kill && \
+lsof -ti :3003 | xargs kill && \
+lsof -ti :80 | xargs kill
\ No newline at end of file
diff --git a/projects/nginx-mpmc/nginx.conf b/projects/nginx-mpmc/nginx.conf
new file mode 100644
index 0000000000..7b89110ded
--- /dev/null
+++ b/projects/nginx-mpmc/nginx.conf
@@ -0,0 +1,43 @@
+events {
+
+}
+http {
+ # set aliases
+ upstream app_server {
+ server host.docker.internal:3000;
+ }
+ upstream app_2_server {
+ server host.docker.internal:3001;
+ }
+ upstream shared_server {
+ server host.docker.internal:3002;
+ }
+ upstream shared_server_2 {
+ server host.docker.internal:3003;
+ }
+
+ server {
+ listen 80;
+ #server_name _;
+ # /app2 will serve the client for app2, and any client can call the api by calling /app2/api
+ location /app2 {
+ proxy_pass http://app_2_server;
+ }
+ # We need to set app2 to have a different pkg directory, and to forward on that.
+ location /pkg2 {
+ proxy_pass http://app_2_server;
+ }
+ # /api_shared will call the server functions registered on shared_server
+ location /api_shared {
+ proxy_pass http://shared_server;
+ }
+ # /api_shared_2 will call the server functions registered on shared_server_2
+ location /api_shared2 {
+ proxy_pass http://shared_server_2;
+ }
+ # we will by default serve the client for app-1
+ location / {
+ proxy_pass http://app_server;
+ }
+ }
+}
\ No newline at end of file
diff --git a/projects/nginx-mpmc/nginx_linux.conf b/projects/nginx-mpmc/nginx_linux.conf
new file mode 100644
index 0000000000..7d98b3511b
--- /dev/null
+++ b/projects/nginx-mpmc/nginx_linux.conf
@@ -0,0 +1,39 @@
+events {
+
+}
+http {
+ # set aliases
+ upstream app_server {
+ server 127.0.0.1:3000;
+ }
+ upstream app_2_server {
+ server 127.0.0.1:3001;
+ }
+ upstream shared_server {
+ server 127.0.0.1:3002;
+ }
+ upstream shared_server_2 {
+ server 127.0.0.1:3003;
+ }
+
+ server {
+ listen 80;
+ #server_name _;
+ # /app2 will serve the client for app2, and any client can call the api by calling /app2/api
+ location /app2 {
+ proxy_pass http://app_2_server;
+ }
+ # /api_shared will call the server functions registered on shared_server
+ location /api_shared {
+ proxy_pass http://shared_server;
+ }
+ # /api_shared_2 will call the server functions registered on shared_server_2
+ location /api_shared2 {
+ proxy_pass http://shared_server_2;
+ }
+ # we will by default serve the client for app-1
+ location / {
+ proxy_pass http://app_server;
+ }
+ }
+}
\ No newline at end of file
diff --git a/projects/nginx-mpmc/run.sh b/projects/nginx-mpmc/run.sh
new file mode 100755
index 0000000000..ccf3231140
--- /dev/null
+++ b/projects/nginx-mpmc/run.sh
@@ -0,0 +1,9 @@
+# save pwd variable
+# append pwd to nginx.conf prefix
+# run this command with the new nginx.conf path
+(cd app-1 && cargo leptos serve) & \
+(cd app-2 && cargo leptos serve) & \
+(cd shared-server-1 && cargo run) & \
+(cd shared-server-2 && cargo run) & \
+( current_dir=$(pwd) && \
+docker run --rm -v "$current_dir"/nginx.conf:/etc/nginx/nginx.conf:ro -p 80:80 nginx)
diff --git a/projects/nginx-mpmc/run_linux.sh b/projects/nginx-mpmc/run_linux.sh
new file mode 100755
index 0000000000..8fa7074c4f
--- /dev/null
+++ b/projects/nginx-mpmc/run_linux.sh
@@ -0,0 +1,9 @@
+# save pwd variable
+# append pwd to nginx.conf prefix
+# run this command with the new nginx.conf path
+(cd app-1 && cargo leptos serve) & \
+(cd app-2 && cargo leptos serve) & \
+(cd shared-server-1 && cargo run) & \
+(cd shared-server-2 && cargo run) & \
+( current_dir=$(pwd) && \
+docker run --rm -v "$current_dir"/nginx_linux.conf:/etc/nginx/nginx.conf:ro -p 80:80 --network="host" nginx)
diff --git a/projects/nginx-mpmc/shared-server-1/.gitignore b/projects/nginx-mpmc/shared-server-1/.gitignore
new file mode 100644
index 0000000000..8cdaa33de1
--- /dev/null
+++ b/projects/nginx-mpmc/shared-server-1/.gitignore
@@ -0,0 +1,13 @@
+# Generated by Cargo
+# will have compiled files and executables
+/target/
+pkg
+
+# These are backup files generated by rustfmt
+**/*.rs.bk
+
+# node e2e test tools and outputs
+node_modules/
+test-results/
+end2end/playwright-report/
+playwright/.cache/
diff --git a/projects/nginx-mpmc/shared-server-1/Cargo.toml b/projects/nginx-mpmc/shared-server-1/Cargo.toml
new file mode 100644
index 0000000000..c6b16ee695
--- /dev/null
+++ b/projects/nginx-mpmc/shared-server-1/Cargo.toml
@@ -0,0 +1,31 @@
+[package]
+name = "shared-server-1"
+version = "0.1.0"
+edition = "2021"
+
+[lib]
+crate-type = ["cdylib", "rlib"]
+
+[dependencies]
+axum = {version = "0.7",optional=true}
+leptos = "0.6"
+leptos_axum = {version = "0.6",optional=true}
+tokio = { version = "1", features = ["rt-multi-thread"], optional = true}
+tower-http = {version = "0.5", optional = true, features=["trace"]}
+tracing = {version = "0.1.40", optional=true}
+tracing-subscriber = {version = "0.3.18", optional = true}
+
+[features]
+default = ["ssr"]
+hydrate = ["leptos/hydrate"]
+ssr = [
+ "dep:axum",
+ "dep:tokio",
+ "dep:leptos_axum",
+ "dep:tracing",
+ "dep:tracing-subscriber",
+ "dep:tower-http",
+ "leptos/ssr",
+]
+
+#We don't need cargo leptos options because we're not using cargo leptos.
\ No newline at end of file
diff --git a/projects/nginx-mpmc/shared-server-1/LICENSE b/projects/nginx-mpmc/shared-server-1/LICENSE
new file mode 100644
index 0000000000..4d209962a5
--- /dev/null
+++ b/projects/nginx-mpmc/shared-server-1/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2022
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/projects/nginx-mpmc/shared-server-1/README.md b/projects/nginx-mpmc/shared-server-1/README.md
new file mode 100644
index 0000000000..4f29ff3815
--- /dev/null
+++ b/projects/nginx-mpmc/shared-server-1/README.md
@@ -0,0 +1,86 @@
+
+
+# Leptos Axum 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 using [Axum](https://github.com/tokio-rs/axum).
+
+## Creating your template repo
+
+If you don't have `cargo-leptos` installed you can install it with
+
+```bash
+cargo install cargo-leptos
+```
+
+Then run
+```bash
+cargo leptos new --git leptos-rs/start-axum
+```
+
+to generate a new project template.
+
+```bash
+cd shared-server
+```
+
+to go to your newly created project.
+Feel free to explore the project structure, but the best place to start with your application code is in `src/app.rs`.
+Addtionally, Cargo.toml may need updating as new versions of the dependencies are released, especially if things are not working after a `cargo update`.
+
+## Running your project
+
+```bash
+cargo leptos watch
+```
+
+## 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
+
+## Compiling for Release
+```bash
+cargo leptos build --release
+```
+
+Will generate your server binary in target/server/release and your site package in target/site
+
+## Testing Your Project
+```bash
+cargo leptos end-to-end
+```
+
+```bash
+cargo leptos end-to-end --release
+```
+
+Cargo-leptos uses Playwright as the end-to-end test tool.
+Tests are located in end2end/tests directory.
+
+## 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
+shared-server
+site/
+```
+Set the following environment variables (updating for your project as needed):
+```text
+LEPTOS_OUTPUT_NAME="shared-server"
+LEPTOS_SITE_ROOT="site"
+LEPTOS_SITE_PKG_DIR="pkg"
+LEPTOS_SITE_ADDR="127.0.0.1:3000"
+LEPTOS_RELOAD_PORT="3001"
+```
+Finally, run the server binary.
diff --git a/projects/nginx-mpmc/shared-server-1/public/favicon.ico b/projects/nginx-mpmc/shared-server-1/public/favicon.ico
new file mode 100644
index 0000000000000000000000000000000000000000..2ba8527cb12f5f28f331b8d361eef560492d4c77
GIT binary patch
literal 15406
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
k<4FtN!5 Result {
+ tracing::debug!("SHARED SERVER 1");
+
+ let _ : axum::Extension = leptos_axum::extract().await?;
+ Ok("This message is from the shared server.".to_string())
+}
+
+//http://127.0.0.1:3002/api/shared/shared_server_function
+// No hydrate function on a server function only server.
\ No newline at end of file
diff --git a/projects/nginx-mpmc/shared-server-1/src/main.rs b/projects/nginx-mpmc/shared-server-1/src/main.rs
new file mode 100644
index 0000000000..6d6592a999
--- /dev/null
+++ b/projects/nginx-mpmc/shared-server-1/src/main.rs
@@ -0,0 +1,35 @@
+#[cfg(feature = "ssr")]
+#[tokio::main]
+async fn main() {
+ use axum::Router;
+ use axum::routing::post;
+
+ tracing_subscriber::fmt()
+ .pretty()
+ .with_thread_names(true)
+ // enable everything
+ .with_max_level(tracing::Level::TRACE)
+ // sets this to be the default, global collector for this application.
+ .init();
+
+ // In production you wouldn't want to use a hardcoded address like this.
+ let addr = "127.0.0.1:3002";
+ // build our application with a route
+ let app = Router::new()
+ .route("/api_shared/*fn_name", post(leptos_axum::handle_server_fns))
+ .layer(tower_http::trace::TraceLayer::new_for_http())
+ .layer(axum::Extension(shared_server::SharedServerState));
+
+ let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
+ println!("shared server listening on http://{}", addr);
+ axum::serve(listener, app.into_make_service())
+ .await
+ .unwrap();
+}
+
+#[cfg(not(feature = "ssr"))]
+pub fn main() {
+ // no client-side main function
+ // our server is SSR only, we have no client pair.
+ // We'll only ever run this with cargo run --features ssr
+}
diff --git a/projects/nginx-mpmc/shared-server-1/style/main.scss b/projects/nginx-mpmc/shared-server-1/style/main.scss
new file mode 100644
index 0000000000..e4538e156f
--- /dev/null
+++ b/projects/nginx-mpmc/shared-server-1/style/main.scss
@@ -0,0 +1,4 @@
+body {
+ font-family: sans-serif;
+ text-align: center;
+}
\ No newline at end of file
diff --git a/projects/nginx-mpmc/shared-server-2/.gitignore b/projects/nginx-mpmc/shared-server-2/.gitignore
new file mode 100644
index 0000000000..8cdaa33de1
--- /dev/null
+++ b/projects/nginx-mpmc/shared-server-2/.gitignore
@@ -0,0 +1,13 @@
+# Generated by Cargo
+# will have compiled files and executables
+/target/
+pkg
+
+# These are backup files generated by rustfmt
+**/*.rs.bk
+
+# node e2e test tools and outputs
+node_modules/
+test-results/
+end2end/playwright-report/
+playwright/.cache/
diff --git a/projects/nginx-mpmc/shared-server-2/Cargo.toml b/projects/nginx-mpmc/shared-server-2/Cargo.toml
new file mode 100644
index 0000000000..d37499fbe0
--- /dev/null
+++ b/projects/nginx-mpmc/shared-server-2/Cargo.toml
@@ -0,0 +1,31 @@
+[package]
+name = "shared-server-2"
+version = "0.1.0"
+edition = "2021"
+
+[lib]
+crate-type = ["cdylib", "rlib"]
+
+[dependencies]
+axum = {version = "0.7",optional=true}
+leptos = "0.6"
+leptos_axum = {version = "0.6",optional=true}
+tokio = { version = "1", features = ["rt-multi-thread"], optional = true}
+tower-http = {version = "0.5", optional = true, features=["trace"]}
+tracing = {version = "0.1.40", optional = true}
+tracing-subscriber = {version = "0.3.18", optional = true}
+
+[features]
+default = ["ssr"]
+hydrate = ["leptos/hydrate"]
+ssr = [
+ "dep:axum",
+ "dep:tokio",
+ "dep:leptos_axum",
+ "dep:tracing",
+ "dep:tracing-subscriber",
+ "dep:tower-http",
+ "leptos/ssr",
+]
+
+#We don't need cargo leptos options because we're not using cargo leptos.
\ No newline at end of file
diff --git a/projects/nginx-mpmc/shared-server-2/LICENSE b/projects/nginx-mpmc/shared-server-2/LICENSE
new file mode 100644
index 0000000000..4d209962a5
--- /dev/null
+++ b/projects/nginx-mpmc/shared-server-2/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2022
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/projects/nginx-mpmc/shared-server-2/README.md b/projects/nginx-mpmc/shared-server-2/README.md
new file mode 100644
index 0000000000..4f29ff3815
--- /dev/null
+++ b/projects/nginx-mpmc/shared-server-2/README.md
@@ -0,0 +1,86 @@
+
+
+# Leptos Axum 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 using [Axum](https://github.com/tokio-rs/axum).
+
+## Creating your template repo
+
+If you don't have `cargo-leptos` installed you can install it with
+
+```bash
+cargo install cargo-leptos
+```
+
+Then run
+```bash
+cargo leptos new --git leptos-rs/start-axum
+```
+
+to generate a new project template.
+
+```bash
+cd shared-server
+```
+
+to go to your newly created project.
+Feel free to explore the project structure, but the best place to start with your application code is in `src/app.rs`.
+Addtionally, Cargo.toml may need updating as new versions of the dependencies are released, especially if things are not working after a `cargo update`.
+
+## Running your project
+
+```bash
+cargo leptos watch
+```
+
+## 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
+
+## Compiling for Release
+```bash
+cargo leptos build --release
+```
+
+Will generate your server binary in target/server/release and your site package in target/site
+
+## Testing Your Project
+```bash
+cargo leptos end-to-end
+```
+
+```bash
+cargo leptos end-to-end --release
+```
+
+Cargo-leptos uses Playwright as the end-to-end test tool.
+Tests are located in end2end/tests directory.
+
+## 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
+shared-server
+site/
+```
+Set the following environment variables (updating for your project as needed):
+```text
+LEPTOS_OUTPUT_NAME="shared-server"
+LEPTOS_SITE_ROOT="site"
+LEPTOS_SITE_PKG_DIR="pkg"
+LEPTOS_SITE_ADDR="127.0.0.1:3000"
+LEPTOS_RELOAD_PORT="3001"
+```
+Finally, run the server binary.
diff --git a/projects/nginx-mpmc/shared-server-2/public/favicon.ico b/projects/nginx-mpmc/shared-server-2/public/favicon.ico
new file mode 100644
index 0000000000000000000000000000000000000000..2ba8527cb12f5f28f331b8d361eef560492d4c77
GIT binary patch
literal 15406
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
k<4FtN!5 Result {
+ tracing::debug!("SHARED SERVER 2");
+
+ let _ : axum::Extension = leptos_axum::extract().await?;
+ Ok("This message is from the shared server 2.".to_string())
+}
+
+//http://127.0.0.1:3002/api/shared/shared_server_function
+// No hydrate function on a server function only server.
\ No newline at end of file
diff --git a/projects/nginx-mpmc/shared-server-2/src/main.rs b/projects/nginx-mpmc/shared-server-2/src/main.rs
new file mode 100644
index 0000000000..244a583f38
--- /dev/null
+++ b/projects/nginx-mpmc/shared-server-2/src/main.rs
@@ -0,0 +1,26 @@
+#[cfg(feature = "ssr")]
+#[tokio::main]
+async fn main() {
+ use axum::Router;
+ use axum::routing::post;
+ // In production you wouldn't want to use a hardcoded address like this.
+ let addr = "127.0.0.1:3003";
+ // build our application with a route
+ let app = Router::new()
+ .route("/api_shared2/*fn_name", post(leptos_axum::handle_server_fns))
+ .layer(tower_http::trace::TraceLayer::new_for_http())
+ .layer(axum::Extension(shared_server_2::SharedServerState2));
+
+ let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
+ println!("shared server listening on http://{}", addr);
+ axum::serve(listener, app.into_make_service())
+ .await
+ .unwrap();
+}
+
+#[cfg(not(feature = "ssr"))]
+pub fn main() {
+ // no client-side main function
+ // our server is SSR only, we have no client pair.
+ // We'll only ever run this with cargo run --features ssr
+}
diff --git a/projects/nginx-mpmc/shared-server-2/style/main.scss b/projects/nginx-mpmc/shared-server-2/style/main.scss
new file mode 100644
index 0000000000..e4538e156f
--- /dev/null
+++ b/projects/nginx-mpmc/shared-server-2/style/main.scss
@@ -0,0 +1,4 @@
+body {
+ font-family: sans-serif;
+ text-align: center;
+}
\ No newline at end of file
diff --git a/projects/ory-kratos/.env b/projects/ory-kratos/.env
new file mode 100644
index 0000000000..f136949d29
--- /dev/null
+++ b/projects/ory-kratos/.env
@@ -0,0 +1 @@
+DATABASE_URL="sqlite:app.db"
\ No newline at end of file
diff --git a/projects/ory-kratos/.gitignore b/projects/ory-kratos/.gitignore
new file mode 100644
index 0000000000..ea38542dd7
--- /dev/null
+++ b/projects/ory-kratos/.gitignore
@@ -0,0 +1,30 @@
+# Generated by Cargo
+# will have compiled files and executables
+/target/
+pkg
+.vscode
+# These are backup files generated by rustfmt
+**/*.rs.bk
+
+e2e/target
+e2e/chromedriver_screenshot.png
+e2e/html
+e2e/network_output
+e2e/screenshots/*
+
+e2e/console_logs
+
+
+localhost+2-key.pem
+localhost+2.pem
+host.docker.internal+3-key.pem
+host.docker.internal+3.pem
+key.pem
+cert.pem
+rootCA.pem
+
+app.db
+app.db-shm
+app.db-wal
+.DS_Store
+*/.DS_Stre
\ No newline at end of file
diff --git a/projects/ory-kratos/Cargo.toml b/projects/ory-kratos/Cargo.toml
new file mode 100644
index 0000000000..1d79c3203c
--- /dev/null
+++ b/projects/ory-kratos/Cargo.toml
@@ -0,0 +1,113 @@
+[workspace]
+resolver = "2"
+members = ["app", "frontend", "ids", "server","e2e"]
+
+# need to be applied only to wasm build
+[profile.release]
+codegen-units = 1
+lto = true
+opt-level = 'z'
+
+[workspace.dependencies]
+leptos = { version = "0.6.9", features = ["nightly"] }
+leptos_meta = { version = "0.6.9", features = ["nightly"] }
+leptos_router = { version = "0.6.9", features = ["nightly"] }
+leptos_axum = { version = "0.6.9" }
+leptos-use = {version = "0.10.5"}
+
+axum = "0.7"
+axum-server = {version = "0.6", features = ["tls-rustls"]}
+axum-extra = { version = "0.9.2", features=["cookie"]}
+cfg-if = "1"
+console_error_panic_hook = "0.1.7"
+console_log = "1"
+http = "1"
+ids = {path="./ids"}
+# this goes to this personal branch because of https://github.com/ory/sdk/issues/325#issuecomment-1960834676
+ory-kratos-client = {git="https://github.com/sjud/kratos-client-rust"}
+ory-keto-client = {version = "0.11.0-alpha.0"}
+reqwest = { version = "0.11.24", features = ["json","cookies"] }
+serde = "1.0.197"
+serde_json = "1.0.114"
+sqlx = {version= "0.7.3", features=["runtime-tokio","sqlite","macros"]}
+thiserror = "1"
+time = "0.3.34"
+tokio = { version = "1.33.0", features = ["full"] }
+tower = { version = "0.4.13", features = ["full"] }
+tower-http = { version = "0.5", features = ["full"] }
+tracing = "0.1.40"
+tracing-subscriber = {version="0.3.18", features=["env-filter"]}
+url = "2.5.0"
+uuid = {version = "1.7.0", features=["v4","serde"]}
+wasm-bindgen = "0.2.92"
+web-sys = {version = "0.3.69", features=["HtmlDocument","HtmlFormElement","FormData"]}
+
+
+# See https://github.com/akesson/cargo-leptos for documentation of all the parameters.
+
+# A leptos project defines which workspace members
+# that are used together frontend (lib) & server (bin)
+[[workspace.metadata.leptos]]
+# this name is used for the wasm, js and css file names
+name = "ory-auth-example"
+
+# the package in the workspace that contains the server binary (binary crate)
+bin-package = "server"
+
+# the package in the workspace that contains the frontend wasm binary (library crate)
+lib-package = "frontend"
+
+# 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 //app.css
+style-file = "style/main.scss"
+
+# Assets source dir. All files found here will be copied and synchronized to site-root.
+# The assets-dir cannot have a sub directory with the same name/path as site-pkg-dir.
+#
+# Optional. Env: LEPTOS_ASSETS_DIR.
+assets-dir = "public"
+
+# 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.
+end2end-cmd = "cargo test --test app_suite"
+end2end-dir = "e2e"
+
+# 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 = []
+
+# 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 = []
+
+# If the --no-default-features flag should be used when compiling the lib target
+#
+# Optional. Defaults to false.
+lib-default-features = false
diff --git a/projects/ory-kratos/LICENSE b/projects/ory-kratos/LICENSE
new file mode 100644
index 0000000000..fdddb29aa4
--- /dev/null
+++ b/projects/ory-kratos/LICENSE
@@ -0,0 +1,24 @@
+This is free and unencumbered software released into the public domain.
+
+Anyone is free to copy, modify, publish, use, compile, sell, or
+distribute this software, either in source code form or as a compiled
+binary, for any purpose, commercial or non-commercial, and by any
+means.
+
+In jurisdictions that recognize copyright laws, the author or authors
+of this software dedicate any and all copyright interest in the
+software to the public domain. We make this dedication for the benefit
+of the public at large and to the detriment of our heirs and
+successors. We intend this dedication to be an overt act of
+relinquishment in perpetuity of all present and future rights to this
+software under copyright law.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
+OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
+ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+OTHER DEALINGS IN THE SOFTWARE.
+
+For more information, please refer to
diff --git a/projects/ory-kratos/README.md b/projects/ory-kratos/README.md
new file mode 100644
index 0000000000..f34f5729ac
--- /dev/null
+++ b/projects/ory-kratos/README.md
@@ -0,0 +1,106 @@
+# Leptos Ory Kratos Integration (With Axum)
+This repo used [start-axum-workspace](https://github.com/leptos-rs/start-axum-workspace/) as a base.
+
+## How to run the example.
+
+Run in different terminal windows (for the best result)
+
+```sh
+cargo leptos serve
+```
+
+```sh
+docker compose up
+```
+
+```sh
+cargo test --test app_suite
+```
+
+This will run our server, set up our compose file (MailCrab, Ory Kratos, Ory Ketos) and run the test suite that walks through logging in, registration, verification etc.
+
+The e2e testing uses [chromiumoxide](https://crates.io/crates/chromiumoxide) and does things like monitor network requests, console messages, take screenshots during the flow and produces them when any of our feature tests fail. This can be a helpful starting point in debugging. Currently it just prints the output into files in the e2e directory but it could be modified to pipe them somewhere like a tool to help with the development process.
+
+
+## High Level Overview
+
+Our project runs a leptos server alongside various Ory Kratos. Kratos provides identification, and we use it when registering users, and credentialing them.
+
+A normal flow would look something like:
+
+
+I go to the homepage,I click register
+
+
+I am redirected to the register page, the register page isn't hardcoded but is rendered by parsing the UI data structure given by Ory Kratos. The visible portions correspond to the fields we've set in our ./kratos/email.schema.json schema file, but it includes
+hidden fields (i.e a CSRF token to prevent CSRF). This project includes unstyled parsing code for the UI data structure.
+
+
+I sign up with an email and password
+
+
+Our leptos server will intercept the form data and then pass it on to the ory kratos service.
+
+
+Ory Kratos validates those inputs given the validation criteria ./kratos/email.schema.json schema file
+
+
+Ory Kratos then verifies me by sending me an email.
+
+
+In this example we catch the email with an instance of mailcrab (an email server for testing purposes we run in our docker compose)
+. You can use mailcrab locally 127.0.0.1:1080
+
+
+I look inside the email, I see a code and a link where I will input the code.
+
+
+I click through and input the code, and I am verified.
+
+
+When I go to the login page, it's rendered based on the same method as the registration page. I.e Kratos sends a UI data structure which is parsed into the UI we show the user.
+
+
+I use my password and email on the login page to login.
+
+
+Again, Our leptos server acts as the inbetween between the client and the Ory Kratos service. There were some pecularities between the CSRF token being set in the headers (which Ory Kratos updates with every step in the flow), SSR, and having the client communicate directly with Ory Kratos which lead me to use this approach where our server is the intermediary between the client and Ory Kratos.
+
+
+Ory Kratos is session based, so after it recieves valid login credentials it creates a session and returns the session token. The session token is passed via cookies with every future request. All this does is establish the identity of the caller, to perform authentication we need a way to establish permissions given an individuals identity and how that relates to the content on the website. In this example I just use tables in the database but this example could be extended to use Ory Ketos, with is to Authorization a Ory Kratos is to Identification.
+
+
+
+When given bad input in a field, Ory Kratos issues a new render UI data structure with error messages and we rerender the login page.
+
+## With regards to Ory Oathkeeper And Ory Ketos.
+
+Ory Oathkeeper is a reverse proxy that sits between your server and the client, it takes the session token, looks to see what is being requested in the request and then checks the configuration files of your Ory Services to see if such a thing is allowed. It will communicate with the Ory services on your behalf and then pass on the authorized request to the appropriate location or reject it otherwise.
+
+Ory Ketos is the authorization part of the Ory suite, Ory Kratos simplies identifies the user (this is often conflated with authorization but authorization is different). Authorization is the process of after having confirmed a user's identity provisioning services based on some permission structure. I.e Role Based Authorization, Document based permissions, etc. Ory Ketos uses a similar configuration file based set up to Ory Kratos.
+
+Instead of either of those, in this example we use an extractor to extract the session cookie and verify it with our kratos service and then perform our own checks. This is simpler to set up, more inutitive, and thus better for smaller projects. Identification is complicated, and it's nice to have it be modularized for whatever app we are building. This will save a lot of time when building multiple apps. The actual provisioning of services for most apps is much simpler, i.e database lookup tied to identification and some logic checks. Is the user preiumum? How much have they used the API compared to the maximum? Using Ory Kratos can reduce complexity and decrease your time to market, especially over multiple attempts.
+
+In production you'd have a virtual private server and you'd serve your leptos server behind Nginx, Nginx routes the calls to the Leptos Server and never to our Ory Kratos. Our Rust server handles all the communication between the client and Ory services. This is simpler from an implementation perspective then including Ory Oathkeeper and Ory Ketos. Ory Kratos/Ketos presume all api calls they recieve are valid by default, so it's best not to expose them at all to any traffic from the outside world. And when building our leptos app we'll have a clear idea about when and how these services are being communicated with when our service acts as the intermediary.
+
+## How this project is tested
+
+We use Gherkin feature files to describe the behavior of the application. We use [cucumber](https://docs.rs/cucumber/latest/cucumber/) as our test harness and match the feature files to [chromiumoxide](https://docs.rs/chromiumoxide/latest/chromiumoxide/) code to drive a local chromium application. I'm using e2e testing mostly to confirm that the service provides the value to the user, in this case just authorization testing. And that, that value proposition doesn't break when we change some middleware code that touches everything etc.
+
+The `ids` crate includes a list of static strings that we'll use in our chromiumoxide lookups and our frontend to make our testing as smooth as possible. There are other ways to do this, such as find by text, which would find the "Sign Up" text and click it etc. So these tests don't assert anything with regards to presentation, just functionality.
+
+## How to use mkcert to get a locally signed certificate (and why)
+We need to use https because we are sending cookies with the `Secure;` flag, cookies with the Secure flag can't be used
+unless delivered over https. Since we're using chromedriver for e2e testing let's use mkcert to create a cert that will allow
+https://127.0.0.1:3000/ to be a valid url.
+Install mkcert and then
+
+```sh
+mkcert -install localhost 127.0.0.1 ::1
+```
+
+Copy your cert.pem, key.pem and rootCA.pem into this crate's root.
+
+
+## Thoughts, Feedback, Criticism, Comments?
+Send me any of the above, I'm @sjud on leptos discord. I'm always looking to improve and make these projects more helpful for the community. So please let me know how I can do that. Thanks!
diff --git a/projects/ory-kratos/app/Cargo.toml b/projects/ory-kratos/app/Cargo.toml
new file mode 100644
index 0000000000..8334766071
--- /dev/null
+++ b/projects/ory-kratos/app/Cargo.toml
@@ -0,0 +1,40 @@
+[package]
+name = "app"
+version = "0.1.0"
+edition = "2021"
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[dependencies]
+leptos.workspace = true
+leptos_meta.workspace = true
+leptos_router.workspace = true
+leptos_axum = { workspace = true, optional = true }
+leptos-use.workspace = true
+
+axum = { workspace = true, optional = true }
+axum-extra = { workspace = true, optional = true }
+http.workspace = true
+cfg-if.workspace = true
+thiserror.workspace = true
+serde_json.workspace = true
+serde.workspace = true
+
+ory-kratos-client.workspace = true
+reqwest = { workspace = true, optional = true }
+time = {workspace = true, optional = true }
+tracing = { workspace = true, optional = true }
+url = { workspace = true, optional = true }
+uuid = { workspace = true}
+ids.workspace = true
+wasm-bindgen = { workspace = true, optional = true}
+web-sys = { workspace = true}
+
+sqlx = { workspace = true, optional = true}
+
+[features]
+default = []
+hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate","dep:wasm-bindgen"]
+ssr = ["leptos/ssr", "leptos_meta/ssr", "leptos_router/ssr", "dep:sqlx","leptos-use/axum","leptos-use/ssr","dep:time",
+"dep:leptos_axum","dep:axum","dep:tracing","dep:reqwest","dep:url","dep:axum-extra"]
+
diff --git a/projects/ory-kratos/app/src/auth/extractors.rs b/projects/ory-kratos/app/src/auth/extractors.rs
new file mode 100644
index 0000000000..c134aff662
--- /dev/null
+++ b/projects/ory-kratos/app/src/auth/extractors.rs
@@ -0,0 +1,90 @@
+use axum::{async_trait, extract::FromRequestParts, RequestPartsExt};
+use axum_extra::extract::CookieJar;
+use http::request::Parts;
+use ory_kratos_client::models::session::Session;
+use sqlx::SqlitePool;
+
+use crate::database_calls::UserRow;
+
+#[derive(Clone, Debug, PartialEq)]
+pub struct ExtractSession(pub Session);
+
+#[async_trait]
+impl FromRequestParts for ExtractSession
+where
+ S: Send + Sync,
+{
+ type Rejection = String;
+
+ #[tracing::instrument(err(Debug), skip_all)]
+ async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result {
+ let cookie_jar = parts.extract::().await.unwrap();
+ let csrf_cookie = cookie_jar
+ .iter()
+ .filter(|cookie| cookie.name().contains("csrf_token"))
+ .next()
+ .ok_or(
+ "Expecting a csrf_token cookie to already be set if fetching a pre-existing flow"
+ .to_string(),
+ )?;
+ let session_cookie = cookie_jar
+ .get("ory_kratos_session")
+ .ok_or("Ory Kratos Session cookie does not exist.".to_string())?;
+ let client = reqwest::ClientBuilder::new()
+ .redirect(reqwest::redirect::Policy::none())
+ .build()
+ .unwrap();
+
+ let resp = client
+ .get("http://127.0.0.1:4433/sessions/whoami")
+ .header("accept", "application/json")
+ .header(
+ "cookie",
+ format!("{}={}", csrf_cookie.name(), csrf_cookie.value()),
+ )
+ .header(
+ "cookie",
+ format!("{}={}", session_cookie.name(), session_cookie.value()),
+ )
+ .send()
+ .await
+ .map_err(|err| format!("Error sending resp to whoami err:{:#?}", err).to_string())?;
+ let session = resp
+ .json::()
+ .await
+ .map_err(|err| format!("Error getting json from body err:{:#?}", err).to_string())?;
+ Ok(Self(session))
+ }
+}
+
+#[derive(Clone, Debug, PartialEq)]
+pub struct ExtractUserRow(pub UserRow);
+
+#[async_trait]
+impl FromRequestParts for ExtractUserRow
+where
+ S: Send + Sync,
+{
+ type Rejection = String;
+
+ #[tracing::instrument(err(Debug), skip_all)]
+ async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result {
+ let identity_id = parts
+ .extract::()
+ .await?
+ .0
+ .identity
+ .ok_or("No identity")?
+ .id;
+ let pool = parts
+ .extract::>()
+ .await
+ .map_err(|err| format!("{err:#?}"))?
+ .0;
+ let user = crate::database_calls::read_user_by_identity_id(&pool, &identity_id)
+ .await
+ .map_err(|err| format!("{err:#?}"))?;
+
+ Ok(Self(user))
+ }
+}
diff --git a/projects/ory-kratos/app/src/auth/kratos_error.rs b/projects/ory-kratos/app/src/auth/kratos_error.rs
new file mode 100644
index 0000000000..e04866933d
--- /dev/null
+++ b/projects/ory-kratos/app/src/auth/kratos_error.rs
@@ -0,0 +1,79 @@
+use super::*;
+
+#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
+pub struct KratosError {
+ code: Option,
+ message: Option,
+ reason: Option,
+ debug: Option,
+}
+
+impl KratosError {
+ pub fn to_err_msg(self) -> String {
+ format!(
+ "{}\n{}\n{}\n{}\n",
+ self.code
+ .map(|code| code.to_string())
+ .unwrap_or("No Code included in error message".to_string()),
+ self.message
+ .unwrap_or("No message in Kratos Error".to_string()),
+ self.reason
+ .unwrap_or("No reason included in Kratos Error".to_string()),
+ self.debug
+ .unwrap_or("No debug included in Kratos Error".to_string())
+ )
+ }
+}
+
+impl IntoView for KratosError {
+ fn into_view(self) -> View {
+ view!{
+
{self.code.map(|code|code.to_string()).unwrap_or("No Code included in error message".to_string())}
+
{self.message.unwrap_or("No message in Kratos Error".to_string())}
+
{self.reason.unwrap_or("No reason included in Kratos Error".to_string())}
+
{self.debug.unwrap_or("No debug included in Kratos Error".to_string())}
+ }.into_view()
+ }
+}
+
+#[server]
+pub async fn fetch_error(id: String) -> Result {
+ use ory_kratos_client::models::flow_error::FlowError;
+
+ let client = reqwest::ClientBuilder::new()
+ .redirect(reqwest::redirect::Policy::none())
+ .build()?;
+ //https://www.ory.sh/docs/kratos/self-service/flows/user-facing-errors
+ let flow_error = client
+ .get("http://127.0.0.1:4433/self-service/errors")
+ .query(&[("id", id)])
+ .send()
+ .await?
+ .json::()
+ .await?;
+
+ let error = flow_error.error.ok_or(ServerFnError::new(
+ "Flow error does not contain an actual error. This is a server error.",
+ ))?;
+ Ok(serde_json::from_value::(error)?)
+}
+
+#[component]
+pub fn KratosErrorPage() -> impl IntoView {
+ let id = move || use_query_map().get().get("id").cloned().unwrap_or_default();
+ let fetch_error_resource = create_resource(move || id(), |id| fetch_error(id));
+ view! {
+
+ }>
+ { move ||
+ fetch_error_resource.get().map(|resp| match resp {
+ // kratos error isn't an error type, it's just a ui/data representation of a kratos error.
+ Ok(kratos_error) => kratos_error.into_view(),
+ // notice how we don't deconstruct i.e Err(err), this will bounce up to the error boundary
+ server_error => server_error.into_view()
+ })
+ }
+
+
+ }
+}
diff --git a/projects/ory-kratos/app/src/auth/kratos_html.rs b/projects/ory-kratos/app/src/auth/kratos_html.rs
new file mode 100644
index 0000000000..9277862148
--- /dev/null
+++ b/projects/ory-kratos/app/src/auth/kratos_html.rs
@@ -0,0 +1,129 @@
+use super::*;
+use ory_kratos_client::models::ui_node_attributes::UiNodeAttributes;
+use ory_kratos_client::models::ui_node_attributes::UiNodeAttributesTypeEnum;
+use ory_kratos_client::models::UiNode;
+use ory_kratos_client::models::UiText;
+use std::collections::HashMap;
+
+/// https://www.ory.sh/docs/kratos/concepts/ui-user-interface
+pub fn kratos_html(node: UiNode, body: RwSignal>) -> impl IntoView {
+ // the label that goes as the child of our label
+ let label_text = node.meta.label.map(|text| text.text);
+ // each node MAY have messages (i.e password is bad, email is wrong form etc)
+ let messages_html = view! {
+ {text}}
+ }
+ />
+ };
+
+ let node_html = match *node.attributes {
+ UiNodeAttributes::UiNodeInputAttributes {
+ autocomplete,
+ disabled,
+ name,
+ required,
+ _type,
+ value,
+ // this is often empty for some reason?
+ label: _label,
+ ..
+ } => {
+ let autocomplete =
+ autocomplete.map_or(String::new(), |t| serde_json::to_string(&t).unwrap());
+ let label = label_text.unwrap_or(String::from("Unlabeled Input"));
+ let required = required.unwrap_or_default();
+ let _type_str = serde_json::to_string(&_type).unwrap();
+ let name_clone = name.clone();
+ let name_clone_2 = name.clone();
+ let value = if let Some(serde_json::Value::String(value)) = value {
+ value
+ } else if value.is_none() {
+ "".to_string()
+ } else {
+ match serde_json::to_string(&value) {
+ Ok(value) => value,
+ Err(err) => {
+ leptos::logging::log!("ERROR: not value? {:?}", err);
+ "".to_string()
+ }
+ }
+ };
+ if _type == UiNodeAttributesTypeEnum::Submit {
+ body.update(|map| {
+ _ = map.insert(name.clone(), value.clone());
+ });
+ view! {
+ // will be something like value="password" name="method"
+ // or value="oidc" name="method"
+
+
+ }
+ .into_view()
+ } else if _type != UiNodeAttributesTypeEnum::Hidden {
+ let id = ids::match_name_to_id(name.clone());
+
+ view! {
+
+ }
+ .into_view()
+ } else {
+ body.update(|map| {
+ _ = map.insert(name.clone(), value.clone());
+ });
+ // this expects the identifer to be an email, but it could be telelphone etc so code is extra fragile
+ view! { }.into_view()
+ }
+ }
+ UiNodeAttributes::UiNodeAnchorAttributes { href, id, title } => {
+ let inner = title.text;
+ view! {{inner}}.into_view()
+ }
+ UiNodeAttributes::UiNodeImageAttributes {
+ height,
+ id,
+ src,
+ width,
+ } => view! {}.into_view(),
+ UiNodeAttributes::UiNodeScriptAttributes { .. } => view! {script not supported}.into_view(),
+ UiNodeAttributes::UiNodeTextAttributes {
+ id,
+ text:
+ box UiText {
+ // not sure how to make use of context yet.
+ context: _context,
+ // redundant id?
+ id: _id,
+ text,
+ // This could be, info, error, success. i.e context for msg responses on bad input etc
+ _type,
+ },
+ } => view! {
{text}
}.into_view(),
+ };
+ view! {
+ {node_html}
+ {messages_html}
+ }
+}
diff --git a/projects/ory-kratos/app/src/auth/login.rs b/projects/ory-kratos/app/src/auth/login.rs
new file mode 100644
index 0000000000..1d0b760fb0
--- /dev/null
+++ b/projects/ory-kratos/app/src/auth/login.rs
@@ -0,0 +1,217 @@
+use super::*;
+use ory_kratos_client::models::LoginFlow;
+use ory_kratos_client::models::UiContainer;
+use ory_kratos_client::models::UiText;
+use std::collections::HashMap;
+
+#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
+pub struct ViewableLoginFlow(LoginFlow);
+impl IntoView for ViewableLoginFlow {
+ fn into_view(self) -> View {
+ format!("{:?}", self).into_view()
+ }
+}
+#[tracing::instrument]
+#[server]
+pub async fn init_login() -> Result {
+ let client = reqwest::ClientBuilder::new()
+ .cookie_store(true)
+ .redirect(reqwest::redirect::Policy::none())
+ .build()?;
+ // Get the csrf_token cookie.
+ let resp = client
+ .get("http://127.0.0.1:4433/self-service/login/browser")
+ .send()
+ .await?;
+ let first_cookie = resp
+ .cookies()
+ .next()
+ .ok_or(ServerFnError::new("Expecting a first cookie"))?;
+ let csrf_token = first_cookie.value();
+ let location = resp
+ .headers()
+ .get("Location")
+ .ok_or(ServerFnError::new("expecting location in headers"))?
+ .to_str()?;
+ // Parses the url and takes first query which will be flow=FLOW_ID and we get FLOW_ID at .1
+ let location_url = url::Url::parse(location)?;
+ let id = location_url
+ .query_pairs()
+ .next()
+ .ok_or(ServerFnError::new(
+ "Expecting query in location header value",
+ ))?
+ .1;
+ let set_cookie = resp
+ .headers()
+ .get("set-cookie")
+ .ok_or(ServerFnError::new("expecting set-cookie in headers"))?
+ .to_str()?;
+ let flow = client
+ .get("http://127.0.0.1:4433/self-service/login/flows")
+ .query(&[("id", id)])
+ .header("x-csrf-token", csrf_token)
+ .send()
+ .await?
+ .json::()
+ .await?;
+ let opts = expect_context::();
+ opts.append_header(
+ axum::http::HeaderName::from_static("set-cookie"),
+ axum::http::HeaderValue::from_str(set_cookie)?,
+ );
+ Ok(LoginResponse::Flow(flow))
+}
+
+#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
+pub enum LoginResponse {
+ Flow(ViewableLoginFlow),
+ Success,
+}
+impl IntoView for LoginResponse {
+ fn into_view(self) -> View {
+ match self {
+ Self::Flow(view) => view.into_view(),
+ _ => ().into_view(),
+ }
+ }
+}
+
+#[tracing::instrument]
+#[server]
+pub async fn login(body: HashMap) -> Result {
+ use ory_kratos_client::models::error_browser_location_change_required::ErrorBrowserLocationChangeRequired;
+ use ory_kratos_client::models::generic_error::GenericError;
+ use reqwest::StatusCode;
+
+ let mut body = body;
+ let action = body
+ .remove("action")
+ .ok_or(ServerFnError::new("Can't find action on body."))?;
+ let cookie_jar = leptos_axum::extract::().await?;
+ let csrf_cookie = cookie_jar
+ .iter()
+ .filter(|cookie| cookie.name().contains("csrf_token"))
+ .next()
+ .ok_or(ServerFnError::new(
+ "Expecting a csrf_token cookie to already be set if fetching a pre-existing flow",
+ ))?;
+ let client = reqwest::ClientBuilder::new()
+ .redirect(reqwest::redirect::Policy::none())
+ .build()?;
+ let resp = client
+ .post(&action)
+ .header("content-type", "application/json")
+ .header(
+ "cookie",
+ format!("{}={}", csrf_cookie.name(), csrf_cookie.value()),
+ )
+ .body(serde_json::to_string(&body)?)
+ .send()
+ .await?;
+
+ let opts = expect_context::();
+ opts.insert_header(
+ axum::http::HeaderName::from_static("cache-control"),
+ axum::http::HeaderValue::from_str("private, no-cache, no-store, must-revalidate")?,
+ );
+ for value in resp.headers().get_all("set-cookie").iter() {
+ opts.append_header(
+ axum::http::HeaderName::from_static("set-cookie"),
+ axum::http::HeaderValue::from_str(value.to_str()?)?,
+ );
+ }
+ if resp.status() == StatusCode::BAD_REQUEST {
+ Ok(LoginResponse::Flow(resp.json::().await?))
+ } else if resp.status() == StatusCode::OK {
+ // ory_kratos_session cookie set above.
+ Ok(LoginResponse::Success)
+ } else if resp.status() == StatusCode::GONE {
+ let err = resp.json::().await?;
+ let err = format!("{:#?}", err);
+ Err(ServerFnError::new(err))
+ } else if resp.status() == StatusCode::UNPROCESSABLE_ENTITY {
+ let err = resp.json::().await?;
+ let err = format!("{:#?}", err);
+ Err(ServerFnError::new(err))
+ } else if resp.status() == StatusCode::TEMPORARY_REDIRECT {
+ let text = format!("{:#?}", resp);
+ Err(ServerFnError::new(text))
+ } else {
+ // this is a status code that isn't covered by the documentation
+ // https://www.ory.sh/docs/reference/api#tag/frontend/operation/updateLoginFlow
+ let status_code = resp.status().as_u16();
+ Err(ServerFnError::new(format!(
+ "{status_code} is not covered under the ory documentation?"
+ )))
+ }
+}
+
+#[component]
+pub fn LoginPage() -> impl IntoView {
+ let login = Action::::server();
+ let login_flow = create_local_resource(|| (), |_| async move { init_login().await });
+
+ let login_resp = create_rw_signal(None::>);
+ // after user tries to login we update the signal resp.
+ create_effect(move |_| {
+ if let Some(resp) = login.value().get() {
+ login_resp.set(Some(resp))
+ }
+ });
+ let login_flow = Signal::derive(move || {
+ if let Some(resp) = login_resp.get() {
+ Some(resp)
+ } else {
+ login_flow.get()
+ }
+ });
+ let body = create_rw_signal(HashMap::new());
+ view! {
+
+ }>
+ {
+ move ||
+ login_flow.get().map(|resp|
+ match resp {
+ Ok(resp) => {
+ match resp {
+ LoginResponse::Flow(ViewableLoginFlow(LoginFlow{ui:box UiContainer{nodes,action,messages,..},..})) => {
+ let form_inner_html = nodes.into_iter().map(|node|kratos_html(node,body)).collect_view();
+ body.update(move|map|{_=map.insert(String::from("action"),action);});
+ view!{
+
+ }.into_view()
+ },
+ LoginResponse::Success => {
+ view!{}.into_view()
+ }
+ }
+ }
+ err => err.into_view(),
+ })
+ }
+
+
+ }
+}
diff --git a/projects/ory-kratos/app/src/auth/logout.rs b/projects/ory-kratos/app/src/auth/logout.rs
new file mode 100644
index 0000000000..4d9b5fe693
--- /dev/null
+++ b/projects/ory-kratos/app/src/auth/logout.rs
@@ -0,0 +1,93 @@
+use super::*;
+
+#[tracing::instrument]
+#[server]
+pub async fn logout() -> Result<(), ServerFnError> {
+ use ory_kratos_client::models::logout_flow::LogoutFlow;
+ use ory_kratos_client::models::ErrorGeneric;
+ use reqwest::StatusCode;
+
+ let cookie_jar = leptos_axum::extract::().await?;
+ let ory_kratos_session = cookie_jar
+ .get("ory_kratos_session")
+ .ok_or(ServerFnError::new(
+ "No `ory_kratos_session` cookie found. Logout shouldn't be visible.",
+ ))?;
+ let client = reqwest::ClientBuilder::new()
+ .cookie_store(true)
+ .redirect(reqwest::redirect::Policy::none())
+ .build()?;
+ // get logout url
+ let resp = client
+ .get("http://127.0.0.1:4433/self-service/logout/browser")
+ .header(
+ "cookie",
+ format!(
+ "{}={}",ory_kratos_session.name(),ory_kratos_session.value()
+ ),
+ )
+ .send()
+ .await?;
+ let status = resp.status();
+ if status == StatusCode::NO_CONTENT || status == StatusCode::OK {
+ let LogoutFlow {
+ logout_token,
+ logout_url,
+ } = resp.json::().await?;
+ tracing::error!("token : {logout_token} url : {logout_url}");
+ let resp = client
+ .get(logout_url)
+ .query(&[("token", logout_token), ("return_to", "/".to_string())])
+ .header("accept","application/json")
+ .header(
+ "cookie",
+ format!(
+ "{}={}",
+ ory_kratos_session.name(),
+ ory_kratos_session.value()
+ ),
+ )
+ .send()
+ .await?;
+ let status = resp.status();
+ if status != StatusCode::OK && status != StatusCode::NO_CONTENT{
+ let error = resp.json::().await?;
+ return Err(ServerFnError::new(format!("{error:#?}")));
+ }
+ // set cookies to clear on the client.
+ crate::clear_cookies_inner().await?;
+ Ok(())
+ } else {
+ let location = resp
+ .headers()
+ .get("Location")
+ .ok_or(ServerFnError::new("expecting location in headers"))?
+ .to_str()?;
+ // Parses the url and takes first query which will be flow=FLOW_ID and we get FLOW_ID at .1
+ let location_url = url::Url::parse(location)?;
+ tracing::debug!("{}", location_url);
+ let id = location_url
+ .query_pairs()
+ .next()
+ .ok_or(ServerFnError::new(
+ "Expecting query in location header value",
+ ))?
+ .1;
+ let kratos_err = kratos_error::fetch_error(id.to_string()).await?;
+ //let error = resp.json::().await?;
+ Err(ServerFnError::new(kratos_err.to_err_msg()))
+ }
+}
+
+#[component]
+pub fn LogoutButton() -> impl IntoView {
+ let logout = Action::::server();
+ view! {
+
+ }
+}
diff --git a/projects/ory-kratos/app/src/auth/mod.rs b/projects/ory-kratos/app/src/auth/mod.rs
new file mode 100644
index 0000000000..d9b3a79e45
--- /dev/null
+++ b/projects/ory-kratos/app/src/auth/mod.rs
@@ -0,0 +1,25 @@
+use super::error_template::ErrorTemplate;
+use leptos::*;
+use leptos_router::*;
+use leptos_meta::*;
+pub mod kratos_html;
+use kratos_html::kratos_html;
+pub mod registration;
+pub use registration::RegistrationPage;
+pub mod verification;
+use serde::{Deserialize, Serialize};
+pub use verification::VerificationPage;
+pub mod login;
+pub use login::LoginPage;
+pub mod session;
+pub use session::HasSession;
+#[cfg(feature = "ssr")]
+pub mod extractors;
+pub mod kratos_error;
+pub use kratos_error::KratosErrorPage;
+pub mod logout;
+pub use logout::LogoutButton;
+pub mod recovery;
+pub use recovery::RecoveryPage;
+pub mod settings;
+pub use settings::SettingsPage;
diff --git a/projects/ory-kratos/app/src/auth/recovery.rs b/projects/ory-kratos/app/src/auth/recovery.rs
new file mode 100644
index 0000000000..e615a7bb5a
--- /dev/null
+++ b/projects/ory-kratos/app/src/auth/recovery.rs
@@ -0,0 +1,227 @@
+use std::collections::HashMap;
+
+use super::*;
+use ory_kratos_client::models::{
+ ContinueWith, ContinueWithSettingsUiFlow, ErrorGeneric, RecoveryFlow, UiContainer, UiText,
+};
+/*
+ User clicks recover account button and is directed to the initiate recovery page
+ On the initiate recovery page they are asked for their email
+ We send an email to them with a recovery code to recover the identity
+ and a link to the recovery page which will prompt them for the code.
+ We validate the code
+ and we then direct them to the settings page for them to change their password.
+*/
+
+#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
+pub struct ViewableRecoveryFlow(RecoveryFlow);
+// Implment IntoView, not because we want to use IntoView - but, just so we can use ErrorBoundary on the error.
+impl IntoView for ViewableRecoveryFlow {
+ fn into_view(self) -> View {
+ format!("{:?}", self).into_view()
+ }
+}
+
+pub struct ViewableContinueWith(pub Vec);
+impl IntoView for ViewableContinueWith {
+ fn into_view(self) -> View {
+ if let Some(first) = self.0.first() {
+ match first {
+ ContinueWith::ContinueWithSetOrySessionToken { ory_session_token } => todo!(),
+ ContinueWith::ContinueWithRecoveryUi { flow } => todo!(),
+ ContinueWith::ContinueWithSettingsUi {
+ flow: box ContinueWithSettingsUiFlow { id },
+ } => view! {}.into_view(),
+ ContinueWith::ContinueWithVerificationUi { flow } => todo!(),
+ }
+ } else {
+ ().into_view()
+ }
+ }
+}
+#[tracing::instrument]
+#[server]
+pub async fn init_recovery_flow() -> Result {
+ let client = reqwest::ClientBuilder::new()
+ .cookie_store(true)
+ .redirect(reqwest::redirect::Policy::none())
+ .build()?;
+ // Get the csrf_token cookie.
+ let resp = client
+ .get("http://127.0.0.1:4433/self-service/recovery/browser")
+ .header("accept", "application/json")
+ .send()
+ .await?;
+
+ let cookie = resp
+ .headers()
+ .get("set-cookie")
+ .ok_or(ServerFnError::new("Expecting a cookie"))?
+ .to_str()?;
+ let opts = expect_context::();
+ opts.append_header(
+ axum::http::HeaderName::from_static("set-cookie"),
+ axum::http::HeaderValue::from_str(cookie)?,
+ );
+ let status = resp.status();
+ if status == reqwest::StatusCode::OK {
+ let flow = resp.json::().await?;
+ Ok(ViewableRecoveryFlow(flow))
+ } else if status == reqwest::StatusCode::BAD_REQUEST {
+ let error = resp.json::().await?;
+ Err(ServerFnError::new(format!("{error:#?}")))
+ } else {
+ tracing::error!(
+ " UNHANDLED STATUS: {} \n text: {}",
+ status,
+ resp.text().await?
+ );
+ Err(ServerFnError::new("Developer made an oopsies."))
+ }
+}
+
+#[tracing::instrument(ret)]
+#[server]
+pub async fn process_recovery(
+ body: HashMap,
+) -> Result {
+ use ory_kratos_client::models::error_browser_location_change_required::ErrorBrowserLocationChangeRequired;
+ use ory_kratos_client::models::generic_error::GenericError;
+ use reqwest::StatusCode;
+
+ let mut body = body;
+ let action = body
+ .remove("action")
+ .ok_or(ServerFnError::new("Can't find action on body."))?;
+ let cookie_jar = leptos_axum::extract::().await?;
+ let csrf_cookie = cookie_jar
+ .iter()
+ .filter(|cookie| cookie.name().contains("csrf_token"))
+ .next()
+ .ok_or(ServerFnError::new(
+ "Expecting a csrf_token cookie to already be set if fetching a pre-existing flow",
+ ))?;
+ let csrf_token = csrf_cookie.value();
+ let client = reqwest::ClientBuilder::new()
+ .redirect(reqwest::redirect::Policy::none())
+ .build()?;
+ let resp = client
+ .post(&action)
+ .header("x-csrf-token", csrf_token)
+ .header("content-type", "application/json")
+ .header("accept", "application/json")
+ .header(
+ "cookie",
+ format!("{}={}", csrf_cookie.name(), csrf_cookie.value()),
+ )
+ .body(serde_json::to_string(&body)?)
+ .send()
+ .await?;
+
+ let opts = expect_context::();
+ opts.insert_header(
+ axum::http::HeaderName::from_static("cache-control"),
+ axum::http::HeaderValue::from_str("private, no-cache, no-store, must-revalidate")?,
+ );
+ for value in resp.headers().get_all("set-cookie").iter() {
+ opts.append_header(
+ axum::http::HeaderName::from_static("set-cookie"),
+ axum::http::HeaderValue::from_str(value.to_str()?)?,
+ );
+ }
+ if resp.status() == StatusCode::BAD_REQUEST || resp.status() == StatusCode::OK {
+ Ok(resp.json::().await?)
+ } else if resp.status() == StatusCode::SEE_OTHER {
+ let see_response = format!("{resp:#?}");
+ let resp_text = resp.text().await?;
+ let err = format!("Developer needs to handle 303 SEE OTHER resp : \n {see_response} \n body: \n {resp_text}");
+ Err(ServerFnError::new(err))
+ } else if resp.status() == StatusCode::GONE {
+ let err = resp.json::().await?;
+ let err = format!("{:#?}", err);
+ Err(ServerFnError::new(err))
+ } else if resp.status() == StatusCode::UNPROCESSABLE_ENTITY {
+ let err = resp.json::().await?;
+ let err = format!("{:#?}", err);
+ Err(ServerFnError::new(err))
+ } else {
+ // this is a status code that isn't covered by the documentation
+ // https://www.ory.sh/docs/reference/api#tag/frontend/operation/updateRecoveryFlow
+ let status_code = resp.status().as_u16();
+ Err(ServerFnError::new(format!(
+ "{status_code} is not covered under the ory documentation?"
+ )))
+ }
+}
+
+#[component]
+pub fn RecoveryPage() -> impl IntoView {
+ let recovery_flow = create_local_resource(|| (), |_| init_recovery_flow());
+ let recovery = Action::::server();
+
+ let recovery_resp = create_rw_signal(None::>);
+ create_effect(move |_| {
+ if let Some(resp) = recovery.value().get() {
+ recovery_resp.set(Some(resp))
+ }
+ });
+ let recovery_flow = Signal::derive(move || {
+ if let Some(resp) = recovery_resp.get() {
+ Some(resp)
+ } else {
+ recovery_flow.get()
+ }
+ });
+ let body = create_rw_signal(HashMap::new());
+ view! {
+
+ }>
+ {
+ move ||
+ recovery_flow.get().map(|resp|
+ match resp {
+ Ok(ViewableRecoveryFlow(RecoveryFlow{
+ continue_with,
+ ui:box UiContainer{nodes,action,messages,..},..})) => {
+ if let Some(continue_with) = continue_with {
+ return ViewableContinueWith(continue_with).into_view();
+ }
+ let form_inner_html = nodes.into_iter().map(|node|kratos_html(node,body)).collect_view();
+ body.update(move|map|{_=map.insert(String::from("action"),action);});
+ view!{
+
+ }.into_view()
+ },
+ err => err.into_view(),
+ })
+ }
+
+
+ }
+}
diff --git a/projects/ory-kratos/app/src/auth/registration.rs b/projects/ory-kratos/app/src/auth/registration.rs
new file mode 100644
index 0000000000..dbcb33a5a3
--- /dev/null
+++ b/projects/ory-kratos/app/src/auth/registration.rs
@@ -0,0 +1,262 @@
+use super::kratos_html;
+use super::*;
+use ory_kratos_client::models::RegistrationFlow;
+use ory_kratos_client::models::UiContainer;
+use ory_kratos_client::models::UiText;
+use std::collections::HashMap;
+
+#[cfg(feature = "ssr")]
+use reqwest::StatusCode;
+
+#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
+pub struct ViewableRegistrationFlow(RegistrationFlow);
+impl IntoView for ViewableRegistrationFlow {
+ fn into_view(self) -> View {
+ format!("{:?}", self).into_view()
+ }
+}
+#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
+pub enum RegistrationResponse {
+ Flow(ViewableRegistrationFlow),
+ Success,
+}
+impl IntoView for RegistrationResponse {
+ fn into_view(self) -> View {
+ match self {
+ Self::Flow(view) => view.into_view(),
+ _ => ().into_view(),
+ }
+ }
+}
+#[tracing::instrument]
+#[server]
+pub async fn init_registration() -> Result {
+ let client = reqwest::ClientBuilder::new()
+ .cookie_store(true)
+ .redirect(reqwest::redirect::Policy::none())
+ .build()?;
+ // Get the csrf_token cookie.
+ let resp = client
+ .get("http://127.0.0.1:4433/self-service/registration/browser")
+ .send()
+ .await?;
+ let first_cookie = resp
+ .cookies()
+ .filter(|c| c.name().contains("csrf_token"))
+ .next()
+ .ok_or(ServerFnError::new(
+ "Expecting a cookie with csrf_token in name",
+ ))?;
+ let csrf_token = first_cookie.value();
+ let location = resp
+ .headers()
+ .get("Location")
+ .ok_or(ServerFnError::new("expecting location in headers"))?
+ .to_str()?;
+ // Parses the url and takes first query which will be flow=FLOW_ID and we get FLOW_ID at .1
+ let location_url = url::Url::parse(location)?;
+ let id = location_url
+ .query_pairs()
+ .next()
+ .ok_or(ServerFnError::new(
+ "Expecting query in location header value",
+ ))?
+ .1;
+ let set_cookie = resp
+ .headers()
+ .get("set-cookie")
+ .ok_or(ServerFnError::new("expecting set-cookie in headers"))?
+ .to_str()?;
+ let resp = client
+ .get("http://127.0.0.1:4433/self-service/registration/flows")
+ .query(&[("id", id)])
+ .header("x-csrf-token", csrf_token)
+ .send()
+ .await?;
+ let flow = resp.json::().await?;
+ let opts = expect_context::();
+ opts.insert_header(
+ axum::http::HeaderName::from_static("cache-control"),
+ axum::http::HeaderValue::from_str("private, no-cache, no-store, must-revalidate")?,
+ );
+ opts.append_header(
+ axum::http::HeaderName::from_static("set-cookie"),
+ axum::http::HeaderValue::from_str(set_cookie)?,
+ );
+ Ok(RegistrationResponse::Flow(flow))
+}
+
+#[tracing::instrument(err)]
+#[server]
+pub async fn register(
+ body: HashMap,
+) -> Result {
+ use ory_kratos_client::models::error_browser_location_change_required::ErrorBrowserLocationChangeRequired;
+ use ory_kratos_client::models::generic_error::GenericError;
+ use ory_kratos_client::models::successful_native_registration::SuccessfulNativeRegistration;
+
+ let pool = leptos_axum::extract::>().await?;
+
+ let mut body = body;
+ let action = body
+ .remove("action")
+ .ok_or(ServerFnError::new("Can't find action on body."))?;
+ let email = body
+ .get("traits.email")
+ .cloned()
+ .ok_or(ServerFnError::new("Can't find traits.email on body."))?;
+ let cookie_jar = leptos_axum::extract::().await?;
+ let csrf_cookie = cookie_jar
+ .iter()
+ .filter(|cookie| cookie.name().contains("csrf_token"))
+ .next()
+ .ok_or(ServerFnError::new(
+ "Expecting a csrf_token cookie to already be set if fetching a pre-existing flow",
+ ))?;
+
+ let client = reqwest::ClientBuilder::new()
+ .redirect(reqwest::redirect::Policy::none())
+ .build()?;
+ let resp = client
+ .post(&action)
+ //.header("content-type", "application/json")
+ .header(
+ "cookie",
+ format!("{}={}", csrf_cookie.name(), csrf_cookie.value()),
+ )
+ .json(&body)
+ .send()
+ .await?;
+
+ let opts = expect_context::();
+ opts.insert_header(
+ axum::http::HeaderName::from_static("cache-control"),
+ axum::http::HeaderValue::from_str("private, no-cache, no-store, must-revalidate")?,
+ );
+ for value in resp.headers().get_all("set-cookie").iter() {
+ opts.append_header(
+ axum::http::HeaderName::from_static("set-cookie"),
+ axum::http::HeaderValue::from_str(value.to_str()?)?,
+ );
+ }
+ if resp.status() == StatusCode::BAD_REQUEST {
+ Ok(RegistrationResponse::Flow(
+ resp.json::().await?,
+ ))
+ } else if resp.status() == StatusCode::OK {
+ // get identity, session, session token
+ let SuccessfulNativeRegistration { identity, .. } =
+ resp.json::().await?;
+ let identity_id = identity.id;
+ crate::database_calls::create_user(&pool, &identity_id, &email).await?;
+ //discard all? what about session_token? I guess we aren't allowing logging in after registration without verification..
+ Ok(RegistrationResponse::Success)
+ } else if resp.status() == StatusCode::GONE {
+ let err = resp.json::().await?;
+ let err = format!("{:#?}", err);
+ Err(ServerFnError::new(err))
+ } else if resp.status() == StatusCode::UNPROCESSABLE_ENTITY {
+ let err = resp.json::().await?;
+ let err = format!("{:#?}", err);
+ Err(ServerFnError::new(err))
+ } else if resp.status() == StatusCode::TEMPORARY_REDIRECT {
+ let text = format!("{:#?}", resp);
+ Err(ServerFnError::new(text))
+ } else {
+ // this is a status code that isn't covered by the documentation
+ // https://www.ory.sh/docs/reference/api#tag/frontend/operation/updateRegistrationFlow
+ let status_code = resp.status().as_u16();
+ Err(ServerFnError::new(format!(
+ "{status_code} is not covered under the ory documentation?"
+ )))
+ }
+}
+
+#[component]
+pub fn RegistrationPage() -> impl IntoView {
+ let register = Action::::server();
+
+ // when we hit the page initiate a flow with kratos and get back data for ui renering.
+ let registration_flow =
+ create_local_resource(|| (), |_| async move { init_registration().await });
+ // Is none if user hasn't submitted data.
+ let register_resp = create_rw_signal(None::>);
+ // after user tries to register we update the signal resp.
+ create_effect(move |_| {
+ if let Some(resp) = register.value().get() {
+ register_resp.set(Some(resp))
+ }
+ });
+ // Merge our resource and our action results into a single signal.
+ // if the user hasn't tried to register yet we'll render the initial flow.
+ // if they have, we'll render the updated flow (including error messages etc).
+ let registration_flow = Signal::derive(move || {
+ if let Some(resp) = register_resp.get() {
+ Some(resp)
+ } else {
+ registration_flow.get()
+ }
+ });
+ // this is the body of our registration form, we don't know what the inputs are so it's a stand in for some
+ // json map of unknown argument length with type of string.
+ let body = create_rw_signal(HashMap::new());
+ view! {
+ // we'll render the fallback when the user hits the page for the first time
+
+ // if we get any errors, from either server functions we've merged we'll render them here.
+ }>
+ {
+ move ||
+ // this is the resource XOR the results of the register action.
+ registration_flow.get().map(|resp|{
+ match resp {
+ // TODO add Oauth using the flow args (see type docs)
+ Ok(resp) => {
+ match resp {
+ RegistrationResponse::Flow(ViewableRegistrationFlow(RegistrationFlow{ui:box UiContainer{nodes,action,messages,..},..}))
+ => {
+ let form_inner_html = nodes.into_iter().map(|node|kratos_html(node,body)).collect_view();
+ body.update(move|map|{_=map.insert(String::from("action"),action);});
+
+ view!{
+
+ }.into_view()
+
+ },
+ RegistrationResponse::Success => {
+ view!{
"Check Email for Verification"
}.into_view()
+ }
+ }
+ },
+ err => err.into_view(),
+ }
+ })
+ }
+
+
+ }
+}
diff --git a/projects/ory-kratos/app/src/auth/session.rs b/projects/ory-kratos/app/src/auth/session.rs
new file mode 100644
index 0000000000..483777eb08
--- /dev/null
+++ b/projects/ory-kratos/app/src/auth/session.rs
@@ -0,0 +1,31 @@
+use super::*;
+use ory_kratos_client::models::session::Session;
+
+#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
+pub struct ViewableSession(pub Session);
+impl IntoView for ViewableSession {
+ fn into_view(self) -> View {
+ format!("{:#?}", self).into_view()
+ }
+}
+
+#[tracing::instrument]
+#[server]
+pub async fn session_who_am_i() -> Result {
+ use self::extractors::ExtractSession;
+ let session = leptos_axum::extract::().await?.0;
+ Ok(ViewableSession(session))
+}
+
+#[component]
+pub fn HasSession() -> impl IntoView {
+ let check_session = Action::::server();
+ view! {
+
+ }
+}
diff --git a/projects/ory-kratos/app/src/auth/settings.rs b/projects/ory-kratos/app/src/auth/settings.rs
new file mode 100644
index 0000000000..043d3fab51
--- /dev/null
+++ b/projects/ory-kratos/app/src/auth/settings.rs
@@ -0,0 +1,335 @@
+use std::collections::HashMap;
+
+use super::*;
+use ory_kratos_client::models::{SettingsFlow, UiContainer, UiText};
+
+#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
+pub struct ViewableSettingsFlow(SettingsFlow);
+
+impl IntoView for ViewableSettingsFlow {
+ fn into_view(self) -> View {
+ format!("{self:#?}").into_view()
+ }
+}
+
+#[tracing::instrument(ret)]
+#[server]
+pub async fn init_settings_flow(
+ flow_id: Option,
+) -> Result {
+ use reqwest::StatusCode;
+ let cookie_jar = leptos_axum::extract::().await?;
+ let session_cookie = cookie_jar
+ .iter()
+ .filter_map(|cookie| {
+ if cookie.name().contains("ory_kratos_session") {
+ Some(format!("{}={}", cookie.name(), cookie.value()))
+ } else {
+ None
+ }
+ })
+ .next()
+ .ok_or(ServerFnError::new("Expecting session cookie"))?;
+ let csrf_token = cookie_jar
+ .iter()
+ .filter_map(|cookie| {
+ if cookie.name().contains("csrf_token") {
+ Some(format!("{}={}", cookie.name(), cookie.value()))
+ } else {
+ None
+ }
+ })
+ .next()
+ .ok_or(ServerFnError::new("Expecting csrf token cookie."))?;
+ let client = reqwest::ClientBuilder::new()
+ .cookie_store(true)
+ .redirect(reqwest::redirect::Policy::none())
+ .build()?;
+
+ let opts = expect_context::();
+
+ opts.insert_header(
+ axum::http::HeaderName::from_static("cache-control"),
+ axum::http::HeaderValue::from_str("private, no-cache, no-store, must-revalidate")?,
+ );
+ if let Some(flow_id) = flow_id {
+ // use flow id to get pre-existing session flow
+
+ let resp = client
+ .get("http://127.0.0.1:4433/self-service/settings/flows")
+ .query(&[("id", flow_id)])
+ .header("accept", "application/json")
+ .header("cookie", format!("{}; {}",csrf_token,session_cookie))
+ .send()
+ .await?;
+
+ /*let cookie = resp
+ .headers()
+ .get("set-cookie")
+ .ok_or(ServerFnError::new("Expecting a cookie"))?
+ .to_str()?;
+ tracing::error!("set cookie init {cookie}");
+ let opts = expect_context::();
+ opts.append_header(
+ axum::http::HeaderName::from_static("set-cookie"),
+ axum::http::HeaderValue::from_str(cookie)?,
+ );*/
+ // expecting 200:settingsflow ok 401,403,404,410:errorGeneric
+ let status = resp.status();
+ if status == StatusCode::OK {
+ let flow = resp.json::().await?;
+ Ok(ViewableSettingsFlow(flow))
+ } else if status == StatusCode::UNAUTHORIZED
+ || status == StatusCode::FORBIDDEN
+ || status == StatusCode::NOT_FOUND
+ || status == StatusCode::GONE
+ {
+ // 401 should really redirect to login form...
+
+ let err = resp
+ .json::()
+ .await?;
+ Err(ServerFnError::new(format!("{err:#?}")))
+ } else {
+ tracing::error!("UHHANDLED STATUS : {status}");
+ Err(ServerFnError::new("This is a helpful error message."))
+ }
+ } else {
+ // create a new flow
+
+ let resp = client
+ .get("http://127.0.0.1:4433/self-service/settings/browser")
+ .header("accept", "application/json")
+ .header("cookie", format!("{}; {}",csrf_token,session_cookie))
+ .send()
+ .await?;
+ if resp.headers().get_all("set-cookie").iter().count() == 0 {
+ tracing::error!("init set set-cookie is empty");
+ }
+ let cookie = resp
+ .headers()
+ .get("set-cookie")
+ .ok_or(ServerFnError::new("Expecting a cookie"))?
+ .to_str()?;
+ let opts = expect_context::();
+ opts.append_header(
+ axum::http::HeaderName::from_static("set-cookie"),
+ axum::http::HeaderValue::from_str(cookie)?,
+ );
+ // expecting 200:settingsflow ok 400,401,403:errorGeneric
+ let status = resp.status();
+ if status == StatusCode::OK {
+ let flow = resp.json::().await?;
+ Ok(ViewableSettingsFlow(flow))
+ } else if status == StatusCode::BAD_REQUEST
+ || status == StatusCode::UNAUTHORIZED
+ || status == StatusCode::FORBIDDEN
+ {
+ let err = resp
+ .json::()
+ .await?;
+ Err(ServerFnError::new(format!("{err:#?}")))
+ } else {
+ tracing::error!("UHHANDLED STATUS : {status}");
+ Err(ServerFnError::new("This is a helpful error message."))
+ }
+ }
+}
+
+#[tracing::instrument(ret)]
+#[server]
+pub async fn update_settings(
+ flow_id: String,
+ body: HashMap,
+) -> Result {
+ use ory_kratos_client::models::{
+ ErrorBrowserLocationChangeRequired, ErrorGeneric, GenericError,
+ };
+ use reqwest::StatusCode;
+ let session = leptos_axum::extract::().await?.0;
+ tracing::error!("{session:#?}");
+ let mut body = body;
+ let action = body
+ .remove("action")
+ .ok_or(ServerFnError::new("Can't find action on body."))?;
+
+ let cookie_jar = leptos_axum::extract::().await?;
+ let csrf_cookie = cookie_jar
+ .iter()
+ .filter(|cookie| cookie.name().contains("csrf_token"))
+ .next()
+ .ok_or(ServerFnError::new(
+ "Expecting a csrf_token cookie to already be set if fetching a pre-existing flow",
+ ))?;
+ let ory_kratos_session = cookie_jar
+ .get("ory_kratos_session")
+ .ok_or(ServerFnError::new(
+ "No `ory_kratos_session` cookie found. Logout shouldn't be visible.",
+ ))?;
+ let client = reqwest::ClientBuilder::new()
+ .redirect(reqwest::redirect::Policy::none())
+ .build()?;
+ let req = client
+ .post(&action)
+ .header("accept", "application/json")
+ .header("cookie",format!("{}={}",csrf_cookie.name(),csrf_cookie.value()))
+ .header("cookie",format!("{}={}",ory_kratos_session.name(),ory_kratos_session.value()))
+ .json(&body)
+ .build()?;
+ tracing::error!("{req:#?}");
+
+ let resp = client.execute(req).await?;
+
+ let opts = expect_context::();
+
+ opts.insert_header(
+ axum::http::HeaderName::from_static("cache-control"),
+ axum::http::HeaderValue::from_str("private, no-cache, no-store, must-revalidate")?,
+ );
+ if resp.headers().get_all("set-cookie").iter().count() == 0 {
+ tracing::error!("update set-cookie is empty");
+ }
+ for value in resp.headers().get_all("set-cookie").iter() {
+ tracing::error!("update set cookie {value:#?}");
+ opts.append_header(
+ axum::http::HeaderName::from_static("set-cookie"),
+ axum::http::HeaderValue::from_str(value.to_str()?)?,
+ );
+ }
+ // https://www.ory.sh/docs/reference/api#tag/frontend/operation/updateSettingsFlow
+ // expecting 400,200:settingsflow ok 401,403,404,410:errorGeneric 422:ErrorBrowserLocationChangeRequired
+ let status = resp.status();
+ if status == StatusCode::OK || status == StatusCode::BAD_REQUEST {
+ let flow = resp.json::().await?;
+ Ok(ViewableSettingsFlow(flow))
+ } else if status == StatusCode::UNAUTHORIZED
+ || status == StatusCode::FORBIDDEN
+ || status == StatusCode::NOT_FOUND
+ || status == StatusCode::GONE
+ {
+ /*
+ let ErrorGeneric {
+ error: box GenericError { id, message, .. },
+ } = resp.json::().await?;
+ if let Some(id) = id {
+ match id.as_str() {
+ "session_refresh_required" =>
+ /*
+ session_refresh_required: The identity requested to change something that needs a privileged session.
+ Redirect the identity to the login init endpoint with
+ query parameters ?refresh=true&return_to=,
+ or initiate a refresh login flow otherwise.
+ */
+ {}
+ "security_csrf_violation" =>
+ /*
+ Unable to fetch the flow because a CSRF violation occurred.
+ */
+ {}
+ "session_inactive" =>
+ /*
+ No Ory Session was found - sign in a user first.
+ */
+ {}
+ "security_identity_mismatch" =>
+ /*
+ The flow was interrupted with session_refresh_required
+ but apparently some other identity logged in instead.
+
+ or
+
+ The requested ?return_to address is not allowed to be used.
+ Adjust this in the configuration!
+
+ ?
+ */
+ {}
+ "browser_location_change_required" =>
+ /*
+ Usually sent when an AJAX request indicates that the browser
+ needs to open a specific URL. Most likely used in Social Sign In flows.
+ */
+ {}
+ _ => {}
+ }
+ }
+ */
+ let err = resp.json::().await?;
+ let err = format!("{err:#?}");
+ Err(ServerFnError::new(err))
+ } else if status == StatusCode::UNPROCESSABLE_ENTITY {
+ let body = resp.json::().await?;
+ tracing::error!("{body:#?}");
+ Err(ServerFnError::new("Unprocessable."))
+ } else {
+ tracing::error!("UHHANDLED STATUS : {status}");
+ Err(ServerFnError::new("This is a helpful error message."))
+ }
+}
+
+#[component]
+pub fn SettingsPage() -> impl IntoView {
+ // get flow id from url
+ // if flow id doesn't exist we create a settings flow
+ // otherwise we fetch the settings flow with the flow id
+ // we update the settings page with the ui nodes
+ // we handle update settings
+ // if we are not logged in we'll be redirect to a login page
+
+ let init_settings_flow_resource = create_local_resource(
+ // use untracked here because we don't expect the url to change after resource has been fetched.
+ || use_query_map().get_untracked().get("flow").cloned(),
+ |flow_id| init_settings_flow(flow_id),
+ );
+ let update_settings_action = Action::::server();
+ let flow = Signal::derive(move || {
+ if let Some(flow) = update_settings_action.value().get() {
+ Some(flow)
+ } else {
+ init_settings_flow_resource.get()
+ }
+ });
+ let body = create_rw_signal(HashMap::new());
+ view! {
+
+ }>
+ {
+ move || flow.get().map(|resp|
+ match resp {
+ Ok(
+ ViewableSettingsFlow(SettingsFlow{id,ui:box UiContainer{nodes,action,messages,..},..})
+ ) => {
+ let form_inner_html = nodes.into_iter().map(|node|kratos_html(node,body)).collect_view();
+ body.update(move|map|{_=map.insert(String::from("action"),action);});
+ let id = create_rw_signal(id);
+ view!{
+
+ }.into_view()
+ },
+ err => err.into_view()
+ })
+ }
+
+
+ }
+}
diff --git a/projects/ory-kratos/app/src/auth/verification.rs b/projects/ory-kratos/app/src/auth/verification.rs
new file mode 100644
index 0000000000..cf44ebc9bf
--- /dev/null
+++ b/projects/ory-kratos/app/src/auth/verification.rs
@@ -0,0 +1,162 @@
+use std::collections::HashMap;
+
+use super::*;
+use ory_kratos_client::models::{UiContainer, UiText, VerificationFlow};
+#[cfg(feature = "ssr")]
+use tracing::debug;
+
+#[derive(Clone, Debug, Serialize, Deserialize)]
+pub struct ViewableVerificationFlow(VerificationFlow);
+impl IntoView for ViewableVerificationFlow {
+ fn into_view(self) -> View {
+ format!("{:#?}", self.0).into_view()
+ }
+}
+// https://{project}.projects.oryapis.com/self-service/verification/flows?id={}
+#[tracing::instrument]
+#[server]
+pub async fn init_verification(
+ flow_id: String,
+) -> Result