Compare commits

...

153 Commits

Author SHA1 Message Date
Greg Johnston
9de34b74cf 0.6.2 2024-01-26 18:07:04 -05:00
Greg Johnston
1b5961edaa fix: fix type inference on extract() functions (#2233) 2024-01-26 17:54:42 -05:00
Greg Johnston
26d1aee9ad Update README.md framework comparisons (#2232) 2024-01-26 17:01:26 -05:00
benwis
2bf09384df 0.6.1
Signed-off-by: benwis <ben@celcyon.com>
2024-01-26 12:32:14 -08:00
benwis
ac12e1a411 0.6.0
Signed-off-by: benwis <ben@celcyon.com>
2024-01-26 11:54:07 -08:00
Greg Johnston
b367b68a43 fix: use #[server(default)] to pass use default values for a field (#2231) 2024-01-26 14:46:31 -05:00
Greg Johnston
1f9dad421f fix: allow paths to ServerFnError type (#2230) 2024-01-26 11:32:43 -08:00
Greg Johnston
4648fb2cfc Update README.md 2024-01-25 20:34:15 -05:00
Saber Haj Rabiee
817ec045f7 chore: fix import of quote::quote (#2227) 2024-01-25 17:48:41 -05:00
Greg Johnston
ca3806e6bc v0.6.0-rc1 2024-01-24 21:35:14 -05:00
Greg Johnston
936c2077c3 Merge pull request #2222 from leptos-rs/2221
fix: `.refetch()` should not include any tracked reads
2024-01-24 20:44:51 -05:00
Greg Johnston
b3b18875c6 chore: allow unknown lints 2024-01-24 19:43:28 -05:00
Greg Johnston
5cbab48713 chore: avoid possible false positive in cargo check 2024-01-24 19:38:16 -05:00
Greg Johnston
5a8880dd2e fix: correctly track in the effect that creates the resource 2024-01-24 16:18:45 -05:00
Greg Johnston
ea6c957f3d fix: .refetch() should not track any reads (closes #2221) 2024-01-23 11:08:50 -05:00
Greg Johnston
694e5f1cb3 fix: cast to correct type on Memo::try_with_untracked 2024-01-23 11:08:33 -05:00
Greg Johnston
fce2c727ab feat: add support for custom encodings to #[server] macro (#2216) (closes #2210) 2024-01-21 16:14:02 -05:00
Greg Johnston
7d1ce45a57 chore: minimize features activated with leptos_axum's default feature (#1846) (#2213)
- `leptos_axum` default feature:
  - remove `tokio/full`, `axum/macros`
  - add `tokio/fs`, `tokio/sync`
- example `leptos-tailwind-axum`:
  - enable `tokio`'s `rt-multi-thread` and `macros` features
- example `ssr_modes_axum`:
  - enable `tokio`'s `rt-multi-thread` and `macros` features

Co-authored-by: Paul Nettleton <paulnett7@hotmail.com>
2024-01-21 15:22:46 -05:00
Niklas Eicker
997b99081b change: for static routes, remove .static and provide additional context for static_params closures (#2207) 2024-01-21 13:33:05 -05:00
Chris
d33e57d4b7 feat: Default for LeptosOptions, ConfFile (#2208)
Co-authored-by: chrisp60 <gh@cperry.me>
2024-01-21 13:26:10 -05:00
Greg Johnston
b450f0fd10 fix: don't enable tracing feature on leptos by default (#2211) 2024-01-20 17:58:22 -05:00
Greg Johnston
c84c6ee8cd Merge pull request #2158 from leptos-rs/leptos_v0.6 2024-01-20 15:58:25 -05:00
Greg Johnston
567644df8f clarify docs here 2024-01-20 14:29:22 -05:00
Greg Johnston
39f5481b8c clean up in docs and rename Axum extract() to match Actix extract() 2024-01-20 14:29:08 -05:00
Greg Johnston
c88bfbe0a0 tweak sets of features for CI 2024-01-20 14:18:25 -05:00
Greg Johnston
40da1fe94e clippy 2024-01-20 14:16:13 -05:00
Greg Johnston
8df46fcdb9 examples: use old Axum version of hackernews_js_fetch until supported by axum-js-fetch 2024-01-20 12:39:16 -05:00
Greg Johnston
b4a1d90327 clean up for CI 2024-01-20 12:32:51 -05:00
Chris
d746f83387 docs: View::render_to_string panic (#2200)
Co-authored-by: chrisp60 <gh@cperry.me>
2024-01-19 17:07:39 -08:00
Greg Johnston
2092c40bc7 missing derives 2024-01-19 18:21:57 -05:00
Greg Johnston
70ec0c2d0a update sso example 2024-01-19 18:02:22 -05:00
Greg Johnston
eb45d05f3b clippy 2024-01-19 17:43:05 -05:00
Greg Johnston
f19def9541 clippy 2024-01-19 16:55:16 -05:00
Greg Johnston
ddda785045 fix multipart support 2024-01-19 16:52:41 -05:00
Greg Johnston
26d9d75cf2 cargo fmt 2024-01-19 15:56:57 -05:00
Greg Johnston
46e7abf9ba allow custom req/res/client types 2024-01-19 15:48:14 -05:00
Greg Johnston
1b1e02729e clean up examples 2024-01-19 15:17:17 -05:00
Greg Johnston
fdd576535a clean up examples 2024-01-19 15:14:39 -05:00
Greg Johnston
2a9e502893 fix rkyv deserialization 2024-01-19 15:03:21 -05:00
Greg Johnston
a519859a66 Revert "use &[u8] instead of Bytes for requests"
This reverts commit e179db1d42.
2024-01-19 14:37:03 -05:00
Greg Johnston
25120c0e9f fix streaming requests and clarify in docs 2024-01-19 14:17:26 -05:00
Greg Johnston
94cb4c0ec3 remove pavex work (now in pavex branch) 2024-01-19 14:17:26 -05:00
Greg Johnston
f9cd8539e4 add missing PartialEq/Eq implementations on ServerFnError (closes #2198) 2024-01-19 14:17:26 -05:00
Greg Johnston
14072457d0 clean up docs (closes #2197) 2024-01-19 14:17:26 -05:00
Greg Johnston
e179db1d42 use &[u8] instead of Bytes for requests 2024-01-19 14:17:26 -05:00
Greg Johnston
2fa60103b4 share inventory collect across types 2024-01-19 14:17:26 -05:00
Greg Johnston
a3a15f244d expose all fields of ServerFnTraitObj via methods 2024-01-19 14:17:26 -05:00
Greg Johnston
0df5dfeaf8 weak dependency on Cargo.toml 2024-01-19 14:17:26 -05:00
Greg Johnston
3f22906053 fix warning 2024-01-19 14:17:26 -05:00
Greg Johnston
33ad30515d serde-lite support should be enabled directly on server_fn 2024-01-19 14:17:26 -05:00
Greg Johnston
c5bab09423 partial support for streaming requests (doesn't actually work in the browser) 2024-01-19 14:17:26 -05:00
Greg Johnston
320179bc04 remove misleading warning 2024-01-19 14:17:26 -05:00
Greg Johnston
5065bed594 example of middleware that can run before and/or after server fn 2024-01-19 14:17:26 -05:00
Greg Johnston
22b4537f27 fix version numbers 2024-01-19 14:17:26 -05:00
Greg Johnston
8d23d5136a add package metadata 2024-01-19 14:17:25 -05:00
Greg Johnston
c7fac64054 fix merge error 2024-01-19 14:17:25 -05:00
Greg Johnston
047235e7c1 clippy 2024-01-19 14:17:25 -05:00
Greg Johnston
7a086ad159 update version number 2024-01-19 14:17:25 -05:00
Greg Johnston
bb923b3f9b erroneous hyphen 2024-01-19 14:16:59 -05:00
Greg Johnston
6a8c26a820 streaming example with filesystem watcher 2024-01-19 14:16:59 -05:00
Greg Johnston
21f8085851 add streaming/file watcher example 2024-01-19 14:16:59 -05:00
Greg Johnston
9a5a102ce3 add middleware to kitchen-sink example 2024-01-19 14:16:59 -05:00
Greg Johnston
4d602c21f8 example with custom errors 2024-01-19 14:16:59 -05:00
Greg Johnston
7d114c7414 file upload example 2024-01-19 14:16:58 -05:00
Greg Johnston
1f017a2ade hm custom encodings have orphan rule issues 2024-01-19 14:16:58 -05:00
Greg Johnston
35e8e74dcf get rkyv working and work on custom encoding example 2024-01-19 14:16:58 -05:00
Markus Kohlhase
4366d786ac Update login example (CSR only) (#2155) 2024-01-19 14:16:58 -05:00
Ari Seyhun
1777a4057a fix!: remove clone in Cow<'static, str> IntoView impl (#1946) 2024-01-19 14:16:58 -05:00
Greg Johnston
0571ebbc36 working on example 2024-01-19 14:16:58 -05:00
Greg Johnston
06c478b7cb feature-gate the form redirect stuff, and clear old errors from query 2024-01-19 14:16:58 -05:00
Greg Johnston
90ba3529e9 working on Axum version 2024-01-19 14:16:58 -05:00
Greg Johnston
13a2691806 working on server fn example 2024-01-19 14:16:58 -05:00
Greg Johnston
1ad7ee8a03 generalize error redirect behavior across integrations 2024-01-19 14:16:58 -05:00
Greg Johnston
88fee243a8 support setting server URL on either platform 2024-01-19 14:16:58 -05:00
Greg Johnston
5e08253521 get both client and server side working 2024-01-19 14:16:58 -05:00
Greg Johnston
cc6f65cd83 initial version of server action error handling without JS 2024-01-19 14:16:58 -05:00
Greg Johnston
9488114801 docs 2024-01-19 14:16:58 -05:00
Greg Johnston
b0cdeab906 remove old code 2024-01-19 14:16:58 -05:00
Greg Johnston
def4be80b2 docs 2024-01-19 14:16:58 -05:00
Greg Johnston
15b04a8a85 more docs 2024-01-19 14:16:58 -05:00
Greg Johnston
0a9cdba22e getting started on docs 2024-01-19 14:16:58 -05:00
Greg Johnston
1d1de4ac38 remove cfg-if from all examples 2024-01-19 14:16:58 -05:00
Greg Johnston
31b2b9e94c remove explicit handle_server_fns in most cases because it's now included in .leptos_routes() 2024-01-19 14:16:58 -05:00
Greg Johnston
8f07818687 nicer formatting, remove cfg-if 2024-01-19 14:16:58 -05:00
Greg Johnston
a5cbfa0aad remove viz integration (see #2177) 2024-01-19 14:16:58 -05:00
Greg Johnston
6c8e704fb3 smh 2024-01-19 14:16:58 -05:00
Greg Johnston
81fb5160e5 missing makefiles 2024-01-19 14:16:58 -05:00
Greg Johnston
2af0d3d781 update session_auth_axum 2024-01-19 14:16:58 -05:00
Greg Johnston
7f532cda70 update todo_app_sqlite_csrs 2024-01-19 14:16:58 -05:00
Greg Johnston
c7941f7639 clippy 2024-01-19 14:16:58 -05:00
Greg Johnston
61148026d1 allow type paths for input/output, and properly namespace built-in encodings 2024-01-19 14:16:58 -05:00
Greg Johnston
738eeefe73 chore: clear warnings 2024-01-19 14:16:18 -05:00
Greg Johnston
be084a5d1d remove list of magic identifiers, use rust-analyzer to help with imports instead 2024-01-19 14:16:18 -05:00
Greg Johnston
f5c007df7b use server fns directly in ActionForm and MultiActionForm 2024-01-19 14:16:18 -05:00
Rakshith Ravi
a1bd84f3dc feat: add serde-lite codec for server functions (#2168) 2024-01-19 14:16:18 -05:00
Rakshith Ravi
f6ce82c9d1 Fixed tests for server_fn (#2167)
* Fixed server_fn tests

* Changed type_name to TypeId

* Fixed handling of leading slashes for server_fn endpoint
2024-01-19 14:16:18 -05:00
Greg Johnston
853c080707 add missing server fn registration 2024-01-19 14:16:18 -05:00
Greg Johnston
f6b95e40f4 make sure endpoint names begin with a / 2024-01-19 14:16:18 -05:00
Greg Johnston
db1497b9c2 set version, input, etc. correctly 2024-01-19 14:16:18 -05:00
Greg Johnston
f53ac1a4ae remove unused var 2024-01-19 14:16:18 -05:00
Greg Johnston
5e6f4403ca set up redirects in Actix 2024-01-19 14:16:18 -05:00
Greg Johnston
4e3f1c834c handle client-side and server-side redirects correctly (in Axum) 2024-01-19 14:16:18 -05:00
Greg Johnston
566df034ff actually use server functions in ActionForm 2024-01-19 14:16:17 -05:00
Greg Johnston
fd97e2e027 Restore the previous full functionality of Form 2024-01-19 14:16:17 -05:00
Greg Johnston
c8fbee18c8 finished Actix support? 2024-01-19 14:16:17 -05:00
Greg Johnston
e1a9856ca9 more Actix work 2024-01-19 14:16:17 -05:00
Greg Johnston
60efaefff4 start Actix work 2024-01-19 14:16:17 -05:00
Greg Johnston
db4158f5c3 clear up warnings 2024-01-19 14:16:17 -05:00
Greg Johnston
af62d2e900 automatically include server function handler in .leptos_router() 2024-01-19 14:16:17 -05:00
Greg Johnston
c3e3ce7878 changes to get todo_app_sqlite_axum example working 2024-01-19 14:16:17 -05:00
Greg Johnston
dec17fc65b fix server actions and server multi actions 2024-01-19 14:16:03 -05:00
Greg Johnston
2dbc5899f3 cargo fmt 2024-01-19 14:16:03 -05:00
Greg Johnston
dd368a845c @ealmloff changes to reexport actix/axum 2024-01-19 14:16:03 -05:00
Greg Johnston
9c258219dd fix Actix implementation with middleware 2024-01-19 14:16:03 -05:00
Greg Johnston
6a1685936b fix rkyv 2024-01-19 14:16:03 -05:00
Greg Johnston
7d45e6bb13 clean up my mistake 2024-01-19 14:16:03 -05:00
Greg Johnston
8fae76828e FromStr-based lightweight ServerFnError deserialization 2024-01-19 14:16:03 -05:00
Greg Johnston
d5b9e84f36 properly gate inventory 2024-01-19 14:16:03 -05:00
benwis
197edebd51 Made some progress, started work on pavex integration as well 2024-01-19 14:16:03 -05:00
benwis
2a5c855595 It starts to compile! 2024-01-19 14:16:03 -05:00
benwis
c9627bfeb4 Setup folder structure as before. Got a cyclical dependency though 2024-01-19 14:16:03 -05:00
benwis
c7422cd96e First commit, checkpoint for cyclical dependency error 2024-01-19 14:15:51 -05:00
Daniel Santana
cadd217078 Update integration with support for axum 0.7 (#2082)
* chore: update to axum 0.7

Removed http, since it's included in axum, and replaced hyper by http-body-util, which is a smaller.

* chore: update samples to work with nre axum

Missing sessions_axum_auth, pending PR merge.

* chore: all dependencies update to axum 0.7

* chore: cargo fmt

* chore: fix doctests

* chore: Fix example that in reality doesn't use axum.

Fixed anyway.

* chore: more examples support for axum 0.7

* Small tweak
2024-01-19 14:13:55 -05:00
Greg Johnston
0c4cf5471d v0.5.7 2024-01-19 13:03:44 -05:00
Greg Johnston
dd0c349554 examples: update axum-session because old version was yanked (#2205) 2024-01-19 12:54:08 -05:00
Greg Johnston
dd5a0ae094 Merge pull request #2203 from leptos-rs/2201
fix: routing regressions caused by `trailing_slash` support
2024-01-19 12:14:20 -05:00
Greg Johnston
5cacb57283 chore: new clippy warnings 2024-01-19 11:14:36 -05:00
Greg Johnston
b356d3cd28 ci: add regression test for #2190 2024-01-19 10:29:30 -05:00
Greg Johnston
ae1de88916 Revert "Better handling for trailing slashes. (#2154) (#2172)"
This reverts commit 1eaf886481.
2024-01-19 10:27:38 -05:00
Greg Johnston
67dd188358 ci: add regression test for matching main page correctly in router example 2024-01-19 10:26:58 -05:00
Joseph Cruz
1d4772251a fix: ci stopped detecting leptos or example changes (#2194) 2024-01-17 18:58:21 -05:00
benwis
98f18e7c31 release: version 0.5.6 2024-01-16 16:57:07 -08:00
Greg Johnston
3f34d9bb23 fix: doc comments breaking server functions (closes #2190) (#2191) 2024-01-16 15:31:29 -05:00
Greg Johnston
44db400f6b chore: fix spin-sdk dependency 2024-01-15 17:11:50 -05:00
Greg Johnston
3a5730800c v0.5.5 2024-01-15 17:10:49 -05:00
Sam Judelson
041b86e6e5 sso_auth_session example (#2117)
* example draft

* fmt

* db delete

* db

* oops clippy

* clippy 2

* - nightly ?

* fmt

* - cargo all features?
2024-01-15 10:48:22 -08:00
Greg Johnston
d71feada7e change: change all tracing levels to trace to reduce verbosity (#2176) 2024-01-13 08:19:12 -08:00
Cody Casterline
1eaf886481 Better handling for trailing slashes. (#2154) (#2172)
* retain trailing slashes in paths but leave matching trail-slash-insensitive

* fix: Allow trailing slashes to remain in leptos_path.

* Better handling for trailing slashes. (#2154)

This adds a trailing_slash option to <Router> and <Route>.

By default, this option is backward compatible with current Leptos
behavior, but users can opt into two new modes for handling trailing
slashes.

* cargo fmt

* Fix redirect routes for wildcard patterns.

* Clippy fixies

* (Re)Reduce the scope of PossibleBranchContext's internals.

* Test real code, not copied code.

* Test TrailingSlash redirects.

* Fixes and more tests for matching "" && "/".

This path is the exception to the rule and *should* be treated
as equivalent regardless of its trailing slash.

* cargo fmt

---------

Co-authored-by: Tadas Dailyda <tadas@dailyda.com>
2024-01-11 13:51:31 -08:00
dependabot[bot]
c7da3998ba chore(deps): bump tj-actions/changed-files in /.github/workflows (#2180)
Bumps [tj-actions/changed-files](https://github.com/tj-actions/changed-files) from 39 to 41.
- [Release notes](https://github.com/tj-actions/changed-files/releases)
- [Changelog](https://github.com/tj-actions/changed-files/blob/main/HISTORY.md)
- [Commits](https://github.com/tj-actions/changed-files/compare/v39...v41)

---
updated-dependencies:
- dependency-name: tj-actions/changed-files
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-11 16:04:21 -05:00
Luxalpa
10bbeea697 fix: remember router state (fixes #2164) (#2165) 2024-01-10 19:51:37 -05:00
webmstk
2d70229608 chore: add rust-toolchain.toml to examples (closes #2151) (#2161) 2024-01-07 15:29:54 -05:00
Paul Hansen
bbcef811f4 chore: remove dead code in actix tailwind example (#2160) 2024-01-07 15:29:15 -05:00
Simon Bihel
f5bf539148 fix: allow multiple SSRed Leptos apps on same server (closes #1806) (#2135) 2024-01-07 15:28:50 -05:00
Greg Johnston
855a3c65c0 chore: remove newly-detected unused tuple fields (#2169) 2024-01-06 18:25:20 -05:00
SleeplessOne1917
16cf3c4eaf feat: make IntoAttribute more flexible for Option<T: IntoAttribute> (#2130) 2023-12-30 13:10:07 -05:00
Greg Johnston
da533ad4e0 chore: clarify cargo-make and examples (see #2141) (#2150) 2023-12-30 08:14:23 -05:00
JackSpagnoli
b4a9db51be chore: fix broken tailwind example repo link (#2138) 2023-12-28 20:07:49 -05:00
Greg Johnston
15946c6136 fix: emit original token stream when there are syntax errors in #[component] or #[island] function signature (closes #2133) (#2134) 2023-12-25 14:30:31 -05:00
Greg Johnston
9f4c480725 docs: specify form for component generics (#2129) 2023-12-23 07:36:28 -05:00
Greg Johnston
74f9c3cd82 fix: <Transition/> fallback should show if client-rendered (closes #2123) (#2128) 2023-12-22 17:10:09 -05:00
martin frances
34fb39c976 chore: fix lifetime on example (#2125)
```bash
cd leptos/leptos_macro/example
cargo clippy
```

warning: the following explicit lifetimes could be elided: 'a
  --> src/lib.rs:41:20
   |
41 | fn TestMutCallback<'a, F>(mut callback: F, value: &'a str) -> impl IntoView
   |                    ^^                              ^^
   |
   = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#needless_lifetimes
   = note: `#[warn(clippy::needless_lifetimes)]` on by default
help: elide the lifetimes
   |
41 - fn TestMutCallback<'a, F>(mut callback: F, value: &'a str) -> impl IntoView
41 + fn TestMutCallback<F>(mut callback: F, value: &str) -> impl IntoView
   |

warning: `example` (lib) generated 1 warning (run `cargo clippy --fix --lib -p example` to apply 1 suggestion)
2023-12-20 19:10:59 -05:00
martin frances
ea80a21a54 chore: remove duplicate itertools versions (#2124)
The root cause is the family of leptos modules requiring both versions 0.10.5 and 0.11.0
This PR will fix that. ( Also needs a bump to 0.12.0 )

```
warning: multiple versions for dependency `itertools`: 0.10.5, 0.11.0
  |
  = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#multiple_crate_versions
note: the lint level is defined here
 --> src/lib.rs:4:9
  |
4 | #![warn(clippy::cargo)]
  |         ^^^^^^^^^^^^^
  = note: `#[warn(clippy::multiple_crate_versions)]` implied by `#[warn(clippy::cargo)]`
```
2023-12-20 19:10:00 -05:00
Greg Johnston
33590d487b chore: clean up warnings causing CI issues (#2119) 2023-12-18 08:15:46 -05:00
251 changed files with 9372 additions and 6557 deletions

View File

@@ -22,7 +22,6 @@ jobs:
[
integrations/actix,
integrations/axum,
integrations/viz,
integrations/utils,
leptos,
leptos_config,

View File

@@ -26,7 +26,7 @@ jobs:
- name: Get example project directories that changed
id: changed-dirs
uses: tj-actions/changed-files@v39
uses: tj-actions/changed-files@v41
with:
dir_names: true
dir_names_max_depth: "2"

View File

@@ -21,10 +21,10 @@ jobs:
- name: Get example files that changed
id: changed-files
uses: tj-actions/changed-files@v39
uses: tj-actions/changed-files@v41
with:
files: |
examples
examples/**
!examples/cargo-make
!examples/gtk
!examples/Makefile.toml

View File

@@ -19,21 +19,21 @@ jobs:
- name: Get source files that changed
id: changed-source
uses: tj-actions/changed-files@v39
uses: tj-actions/changed-files@v41
with:
files: |
integrations
leptos
leptos_config
leptos_dom
leptos_hot_reload
leptos_macro
leptos_reactive
leptos_server
meta
router
server_fn
server_fn_macro
integrations/**
leptos/**
leptos_config/**
leptos_dom/**
leptos_hot_reload/**
leptos_macro/**
leptos_reactive/**
leptos_server/**
meta/**
router/**
server_fn/**
server_fn_macro/**
- name: List source files that changed
run: echo '${{ steps.changed-source.outputs.all_changed_files }}'

3
.gitignore vendored
View File

@@ -10,4 +10,5 @@ Cargo.lock
.direnv
.envrc
.vscode
.vscode
vendor

View File

@@ -16,7 +16,6 @@ members = [
# integrations
"integrations/actix",
"integrations/axum",
"integrations/viz",
"integrations/utils",
# libraries
@@ -26,22 +25,22 @@ members = [
exclude = ["benchmarks", "examples"]
[workspace.package]
version = "0.5.4"
version = "0.6.2"
[workspace.dependencies]
leptos = { path = "./leptos", version = "0.5.4" }
leptos_dom = { path = "./leptos_dom", version = "0.5.4" }
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.5.4" }
leptos_macro = { path = "./leptos_macro", version = "0.5.4" }
leptos_reactive = { path = "./leptos_reactive", version = "0.5.4" }
leptos_server = { path = "./leptos_server", version = "0.5.4" }
server_fn = { path = "./server_fn", version = "0.5.4" }
server_fn_macro = { path = "./server_fn_macro", version = "0.5.4" }
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", version = "0.5.4" }
leptos_config = { path = "./leptos_config", version = "0.5.4" }
leptos_router = { path = "./router", version = "0.5.4" }
leptos_meta = { path = "./meta", version = "0.5.4" }
leptos_integration_utils = { path = "./integrations/utils", version = "0.5.4" }
leptos = { path = "./leptos", version = "0.6.2" }
leptos_dom = { path = "./leptos_dom", version = "0.6.2" }
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.6.2" }
leptos_macro = { path = "./leptos_macro", version = "0.6.2" }
leptos_reactive = { path = "./leptos_reactive", version = "0.6.2" }
leptos_server = { path = "./leptos_server", version = "0.6.2" }
server_fn = { path = "./server_fn", version = "0.6.2" }
server_fn_macro = { path = "./server_fn_macro", version = "0.6.2" }
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", version = "0.6" }
leptos_config = { path = "./leptos_config", version = "0.6.2" }
leptos_router = { path = "./router", version = "0.6.2" }
leptos_meta = { path = "./meta", version = "0.6.2" }
leptos_integration_utils = { path = "./integrations/utils", version = "0.6.2" }
[profile.release]
codegen-units = 1

View File

@@ -40,6 +40,25 @@ pub fn SimpleCounter(initial_value: i32) -> impl IntoView {
}
}
// we also support a builder syntax rather than the JSX-like `view` macro
#[component]
pub fn SimpleCounterWithBuilder(initial_value: i32) -> impl IntoView {
use leptos::html::*;
let (value, set_value) = create_signal(initial_value);
let clear = move |_| set_value(0);
let decrement = move |_| set_value.update(|value| *value -= 1);
let increment = move |_| set_value.update(|value| *value += 1);
// the `view` macro above expands to this builder syntax
div().child((
button().on(ev::click, clear).child("Clear"),
button().on(ev::click, decrement).child("-1"),
span().child(("Value: ", value, "!")),
button().on(ev::click, increment).child("+1")
))
}
// Easy to use with Trunk (trunkrs.dev) or with a simple wasm-bindgen setup
pub fn main() {
mount_to_body(|| view! {
@@ -140,35 +159,27 @@ Sure! Obviously the `view` macro is for generating DOM nodes but you can use the
I've put together a [very simple GTK example](https://github.com/leptos-rs/leptos/blob/main/examples/gtk/src/main.rs) so you can see what I mean.
### How is this different from Yew/Dioxus?
The new rendering approach being developed for 0.7 supports “universal rendering,” i.e., it can use any rendering library that supports a small set of 6-8 functions. (This is intended as a layer over typical retained-mode, OOP-style GUI toolkits like the DOM, GTK, etc.) That future rendering work will allow creating native UI in a way that is much more similar to the declarative approach used by the web framework.
On the surface level, these libraries may seem similar. Yew is, of course, the most mature Rust library for web UI development and has a huge ecosystem. Dioxus is similar in many ways, being heavily inspired by React. Here are some conceptual differences between Leptos and these frameworks:
### How is this different from Yew?
Yew is the most-used library for Rust web UI development, but there are several differences between Yew and Leptos, in philosophy, approach, and performance.
- **VDOM vs. fine-grained:** Yew is built on the virtual DOM (VDOM) model: state changes cause components to re-render, generating a new virtual DOM tree. Yew diffs this against the previous VDOM, and applies those patches to the actual DOM. Component functions rerun whenever state changes. Leptos takes an entirely different approach. Components run once, creating (and returning) actual DOM nodes and setting up a reactive system to update those DOM nodes.
- **Performance:** This has huge performance implications: Leptos is simply much faster at both creating and updating the UI than Yew is. (Dioxus has made huge advances in performance with its recent 0.3 release, and is now roughly on par with Leptos.)
- **Mental model:** Adopting fine-grained reactivity also tends to simplify the mental model. There are no surprising component re-renders because there are no re-renders. You can call functions, create timeouts, etc. within the body of your component functions because they wont be re-run. You dont need to think about manual dependency tracking for effects; fine-grained reactivity tracks dependencies automatically.
- **Performance:** This has huge performance implications: Leptos is simply much faster at both creating and updating the UI than Yew is.
- **Server integration:** Yew was created in an era in which browser-rendered single-page apps (SPAs) were the dominant paradigm. While Leptos supports client-side rendering, it also focuses on integrating with the server side of your application via server functions and multiple modes of serving HTML, including out-of-order streaming.
### How is this different from Sycamore?
- ### How is this different from Dioxus?
Conceptually, these two frameworks are very similar: because both are built on fine-grained reactivity, most apps will end up looking very similar between the two, and Sycamore or Leptos apps will both look a lot like SolidJS apps, in the same way that Yew or Dioxus can look a lot like React.
Like Leptos, Dioxus is a framework for building UIs using web technologies. However, there are significant differences in approach and features.
There are some practical differences that make a significant difference:
- **VDOM vs. fine-grained:** While Dioxus has a performant virtual DOM (VDOM), it still uses coarse-grained/component-scoped reactivity: changing a stateful value reruns the component function and diffs the old UI against the new one. Leptos components use a different mental model, creating (and returning) actual DOM nodes and setting up a reactive system to update those DOM nodes.
- **Web vs. desktop priorities:** Dioxus uses Leptos server functions in its fullstack mode, but does not have the same `<Suspense>`-based support for things like streaming HTML rendering, or share the same focus on holistic web performance. Leptos tends to prioritize holistic web performance (streaming HTML rendering, smaller WASM binary sizes, etc.), whereas Dioxus has an unparalleled experience when building desktop apps, because your application logic runs as a native Rust binary.
- **Templating:** Leptos uses a JSX-like template format (built on [syn-rsx](https://github.com/stoically/syn-rsx)) for its `view` macro. Sycamore offers the choice of its own templating DSL or a builder syntax.
- **Server integration:** Leptos provides primitives that encourage HTML streaming and allow for easy async integration and RPC calls, even without WASM enabled, making it easy to opt into integrations between your frontend and backend code without pushing you toward any particular metaframework patterns.
- **Read-write segregation:** Leptos, like Solid, encourages read-write segregation between signal getters and setters, so you end up accessing signals with tuples like `let (count, set_count) = create_signal(0);` _(If you prefer or if it's more convenient for your API, you can use [`create_rw_signal`](https://docs.rs/leptos/latest/leptos/fn.create_rw_signal.html) to give a unified read/write signal.)_
- **Signals are functions:** In Leptos, you can call a signal to access it rather than calling a specific method (so, `count()` instead of `count.get()`) This creates a more consistent mental model: accessing a reactive value is always a matter of calling a function. For example:
- ### How is this different from Sycamore?
```rust
let (count, set_count) = create_signal(0); // a signal
let double_count = move || count() * 2; // a derived signal
let memoized_count = create_memo(move |_| count() * 3); // a memo
// all are accessed by calling them
assert_eq!(count(), 0);
assert_eq!(double_count(), 0);
assert_eq!(memoized_count(), 0);
// this function can accept any of those signals
fn do_work_on_signal(my_signal: impl Fn() -> i32) { ... }
```
Sycamore and Leptos are both heavily influenced by SolidJS. At this point, Leptos has a larger community and ecosystem and is more actively developed. Other differences:
- **Signals and scopes are `'static`:** Both Leptos and Sycamore ease the pain of moving signals in closures (in particular, event listeners) by making them `Copy`, to avoid the `{ let count = count.clone(); move |_| ... }` that's very familiar in Rust UI code. Sycamore does this by using bump allocation to tie the lifetimes of its signals to its scopes: since references are `Copy`, `&'a Signal<T>` can be moved into a closure. Leptos does this by using arena allocation and passing around indices: types like `ReadSignal<T>`, `WriteSignal<T>`, and `Memo<T>` are actually wrappers for indices into an arena. This means that both scopes and signals are both `Copy` and `'static` in Leptos, which means that they can be moved easily into closures without adding lifetime complexity.
- **Templating DSLs:** Sycamore uses a custom templating language for its views, while Leptos uses a JSX-like template format.
- **`'static` signals:** One of Leptoss main innovations was the creation of `Copy + 'static` signals, which have excellent ergonomics. Sycamore is in the process of adopting the same pattern, but this is not yet released.
- **Perseus vs. server functions:** The Perseus metaframework provides an opinionated way to build Sycamore apps that include server functionality. Leptos instead provides primitives like server functions in the core of the framework.

View File

@@ -21,6 +21,7 @@ CARGO_MAKE_CRATE_WORKSPACE_MEMBERS = [
"login_with_token_csr_only",
"parent_child",
"router",
"server_fns_axum",
"session_auth_axum",
"slots",
"ssr_modes",
@@ -32,7 +33,6 @@ CARGO_MAKE_CRATE_WORKSPACE_MEMBERS = [
"timer",
"todo_app_sqlite",
"todo_app_sqlite_axum",
"todo_app_sqlite_viz",
"todomvc",
]

View File

@@ -4,30 +4,31 @@
The examples in this directory are all built and tested against the current `main` branch.
To the extent that new features have been released or breaking changes have been made since the previous release, the examples are compatible with the `main` branch but not the current release.
To see the examples as they were at the time of the `0.5.0` release, [click here](https://github.com/leptos-rs/leptos/tree/v0.5.0/examples).
## Cargo Make
[Cargo Make](https://sagiegurari.github.io/cargo-make/) is used to build, test, and run examples.
Here are the highlights.
- Extendable custom task files are located in the [cargo-make](./cargo-make/) directory
- Running a task will automatically install `cargo` dependencies
- Each `Makefile.toml` file must extend the [cargo-make/main.toml](./cargo-make/main.toml) file
- [cargo-make](./cargo-make/) files that end in `*-test.toml` configure web testing strategies
- Run `cargo make test-report` to learn which examples have web tests
To the extent that new features have been released or breaking changes have been made since the previous release, the examples are compatible with the `main` branch and not the current release.
## Getting Started
The simplest way to get started with any example is to use the “quick start” command found in the README for each example. Most of the examples use either [`trunk`](https://trunkrs.dev/) (a simple build system and dev server for client-side-rendered apps) or [`cargo-leptos`](https://github.com/leptos-rs/cargo-leptos) (a build system for server-rendered and client-hydrated apps).
## Using Cargo Make
You can also run any of the examples using [`cargo-make`](https://github.com/sagiegurari/cargo-make). Note that this is completely optional. We use it for CI, and it can be convenient for running the examples, but is not required.
Follow these steps to get any example up and running.
1. `cd` to the example root directory
2. Run `cargo make ci` to setup and test the example
3. Run `cargo make start` to run the example
4. Open the client URL in the console output (<http://127.0.0.1:8080> or <http://127.0.0.1:3000> by default)
5. Run `cargo make stop` to end any processes started by `cargo make start`.
Here are a few additional notes:
- Extendable custom task files are located in the [cargo-make](./cargo-make/) directory
- Running a task will automatically install `cargo` dependencies
- Each `Makefile.toml` file must extend the [cargo-make/main.toml](./cargo-make/main.toml) file
- [cargo-make](./cargo-make/) files that end in `*-test.toml` configure web testing strategies
- Run `cargo make test-report` to learn which examples have web tests
## Prerequisites

View File

@@ -0,0 +1,90 @@
[package]
name = "action-form-error-handling"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib", "rlib"]
[dependencies]
actix-files = { version = "0.6", optional = true }
actix-web = { version = "4", optional = true, features = ["macros"] }
console_error_panic_hook = "0.1"
cfg-if = "1"
http = { version = "0.2", optional = true }
leptos = { path = "../../leptos" }
leptos_meta = { path = "../../meta" }
leptos_actix = { path = "../../integrations/actix", optional = true }
leptos_router = { path = "../../router" }
wasm-bindgen = "0.2"
serde = { version = "1", features = ["derive"] }
[features]
csr = ["leptos/csr", "leptos_meta/csr", "leptos_router/csr"]
hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate"]
ssr = [
"dep:actix-files",
"dep:actix-web",
"dep:leptos_actix",
"leptos/ssr",
"leptos_meta/ssr",
"leptos_router/ssr",
]
# Defines a size-optimized profile for the WASM bundle in release mode
[profile.wasm-release]
inherits = "release"
opt-level = 'z'
lto = true
codegen-units = 1
panic = "abort"
[package.metadata.leptos]
# The name used by wasm-bindgen/cargo-leptos for the JS/WASM bundle. Defaults to the crate name
output-name = "leptos_start"
# The site root folder is where cargo-leptos generate all output. WARNING: all content of this folder will be erased on a rebuild. Use it in your server setup.
site-root = "target/site"
# The site-root relative folder where all compiled output (JS, WASM and CSS) is written
# Defaults to pkg
site-pkg-dir = "pkg"
# [Optional] The source CSS file. If it ends with .sass or .scss then it will be compiled by dart-sass into CSS. The CSS is optimized by Lightning CSS before being written to <site-root>/<site-pkg>/app.css
style-file = "style/main.scss"
# The IP and port (ex: 127.0.0.1:3000) where the server serves the content. Use it in your server setup.
site-addr = "127.0.0.1:3000"
# The port to use for automatic reload monitoring
reload-port = 3001
# [Optional] Command to use when running end2end tests. It will run in the end2end dir.
# [Windows] for non-WSL use "npx.cmd playwright test"
# This binary name can be checked in Powershell with Get-Command npx
end2end-cmd = "npx playwright test"
end2end-dir = "end2end"
# The browserlist query used for optimizing the CSS.
browserquery = "defaults"
# Set by cargo-leptos watch when building with that tool. Controls whether autoreload JS will be included in the head
watch = false
# The environment Leptos will run in, usually either "DEV" or "PROD"
env = "DEV"
# The features to use when compiling the bin target
#
# Optional. Can be over-ridden with the command line parameter --bin-features
bin-features = ["ssr"]
# If the --no-default-features flag should be used when compiling the bin target
#
# Optional. Defaults to false.
bin-default-features = false
# The features to use when compiling the lib target
#
# Optional. Can be over-ridden with the command line parameter --lib-features
lib-features = ["hydrate"]
# If the --no-default-features flag should be used when compiling the lib target
#
# Optional. Defaults to false.
lib-default-features = false
# The profile to use for the lib target when compiling for release
#
# Optional. Defaults to "release".
lib-profile-release = "wasm-release"

View File

@@ -0,0 +1,8 @@
extend = [
{ path = "../cargo-make/main.toml" },
{ path = "../cargo-make/cargo-leptos.toml" },
]
[env]
CLIENT_PROCESS_NAME = "action_form_error_handling"

View File

@@ -0,0 +1,68 @@
<picture>
<source srcset="https://raw.githubusercontent.com/leptos-rs/leptos/main/docs/logos/Leptos_logo_Solid_White.svg" media="(prefers-color-scheme: dark)">
<img src="https://raw.githubusercontent.com/leptos-rs/leptos/main/docs/logos/Leptos_logo_RGB.svg" alt="Leptos Logo">
</picture>
# Leptos Starter Template
This is a template for use with the [Leptos](https://github.com/leptos-rs/leptos) web framework and the [cargo-leptos](https://github.com/akesson/cargo-leptos) tool.
## Creating your template repo
If you don't have `cargo-leptos` installed you can install it with
`cargo install cargo-leptos`
Then run
`cargo leptos new --git leptos-rs/start`
to generate a new project template (you will be prompted to enter a project name).
`cd {projectname}`
to go to your newly created project.
Of course, you should explore around the project structure, but the best place to start with your application code is in `src/app.rs`.
## Running your project
`cargo leptos watch`
By default, you can access your local project at `http://localhost:3000`
## Installing Additional Tools
By default, `cargo-leptos` uses `nightly` Rust, `cargo-generate`, and `sass`. If you run into any trouble, you may need to install one or more of these tools.
1. `rustup toolchain install nightly --allow-downgrade` - make sure you have Rust nightly
2. `rustup target add wasm32-unknown-unknown` - add the ability to compile Rust to WebAssembly
3. `cargo install cargo-generate` - install `cargo-generate` binary (should be installed automatically in future)
4. `npm install -g sass` - install `dart-sass` (should be optional in future)
## Executing a Server on a Remote Machine Without the Toolchain
After running a `cargo leptos build --release` the minimum files needed are:
1. The server binary located in `target/server/release`
2. The `site` directory and all files within located in `target/site`
Copy these files to your remote server. The directory structure should be:
```text
leptos_start
site/
```
Set the following environment variables (updating for your project as needed):
```sh
export LEPTOS_OUTPUT_NAME="leptos_start"
export LEPTOS_SITE_ROOT="site"
export LEPTOS_SITE_PKG_DIR="pkg"
export LEPTOS_SITE_ADDR="127.0.0.1:3000"
export LEPTOS_RELOAD_PORT="3001"
```
Finally, run the server binary.
## Notes about CSR and Trunk:
Although it is not recommended, you can also run your project without server integration using the feature `csr` and `trunk serve`:
`trunk serve --open --features csr`
This may be useful for integrating external tools which require a static site, e.g. `tauri`.

View File

@@ -0,0 +1,97 @@
use leptos::*;
use leptos_meta::*;
use leptos_router::*;
#[component]
pub fn App() -> impl IntoView {
// Provides context that manages stylesheets, titles, meta tags, etc.
provide_meta_context();
view! {
// injects a stylesheet into the document <head>
// id=leptos means cargo-leptos will hot-reload this stylesheet
<Stylesheet id="leptos" href="/pkg/leptos_start.css"/>
// sets the document title
<Title text="Welcome to Leptos"/>
// content for this welcome page
<Router>
<main id="app">
<Routes>
<Route path="" view=HomePage/>
<Route path="/*any" view=NotFound/>
</Routes>
</main>
</Router>
}
}
#[server]
async fn do_something(
should_error: Option<String>,
) -> Result<String, ServerFnError> {
if should_error.is_none() {
Ok(String::from("Successful submit"))
} else {
Err(ServerFnError::ServerError(String::from(
"You got an error!",
)))
}
}
/// Renders the home page of your application.
#[component]
fn HomePage() -> impl IntoView {
let do_something_action = Action::<DoSomething, _>::server();
let value = Signal::derive(move || {
do_something_action
.value()
.get()
.unwrap_or_else(|| Ok(String::new()))
});
Effect::new_isomorphic(move |_| {
logging::log!("Got value = {:?}", value.get());
});
view! {
<h1>"Test the action form!"</h1>
<ErrorBoundary fallback=move |error| format!("{:#?}", error
.get()
.into_iter()
.next()
.unwrap()
.1.into_inner()
.to_string())
>
{value}
<ActionForm action=do_something_action class="form">
<label>Should error: <input type="checkbox" name="should_error"/></label>
<button type="submit">Submit</button>
</ActionForm>
</ErrorBoundary>
}
}
/// 404 - Not Found
#[component]
fn NotFound() -> impl IntoView {
// set an HTTP status code 404
// this is feature gated because it can only be done during
// initial server-side rendering
// if you navigate to the 404 page subsequently, the status
// code will not be set because there is not a new HTTP request
// to the server
#[cfg(feature = "ssr")]
{
// this can be done inline because it's synchronous
// if it were async, we'd use a server function
let resp = expect_context::<leptos_actix::ResponseOptions>();
resp.set_status(actix_web::http::StatusCode::NOT_FOUND);
}
view! {
<h1>"Not Found"</h1>
}
}

View File

@@ -0,0 +1,18 @@
pub mod app;
use cfg_if::cfg_if;
cfg_if! {
if #[cfg(feature = "hydrate")] {
use wasm_bindgen::prelude::wasm_bindgen;
#[wasm_bindgen]
pub fn hydrate() {
use app::*;
console_error_panic_hook::set_once();
leptos::mount_to_body(App);
}
}
}

View File

@@ -0,0 +1,53 @@
#[cfg(feature = "ssr")]
#[actix_web::main]
async fn main() -> std::io::Result<()> {
use action_form_error_handling::app::*;
use actix_files::Files;
use actix_web::*;
use leptos::*;
use leptos_actix::{generate_route_list, LeptosRoutes};
let conf = get_configuration(None).await.unwrap();
let addr = conf.leptos_options.site_addr;
// Generate the list of routes in your Leptos App
let routes = generate_route_list(App);
println!("listening on http://{}", &addr);
HttpServer::new(move || {
let leptos_options = &conf.leptos_options;
let site_root = &leptos_options.site_root;
App::new()
.route("/api/{tail:.*}", leptos_actix::handle_server_fns())
// serve JS/WASM/CSS from `pkg`
.service(Files::new("/pkg", format!("{site_root}/pkg")))
.leptos_routes(leptos_options.to_owned(), routes.to_owned(), App)
.app_data(web::Data::new(leptos_options.to_owned()))
//.wrap(middleware::Compress::default())
})
.bind(&addr)?
.run()
.await
}
#[cfg(not(any(feature = "ssr", feature = "csr")))]
pub fn main() {
// no client-side main function
// unless we want this to work with e.g., Trunk for pure client-side testing
// see lib.rs for hydration function instead
// see optional feature `csr` instead
}
#[cfg(all(not(feature = "ssr"), feature = "csr"))]
pub fn main() {
// a client-side main function is required for using `trunk serve`
// prefer using `cargo leptos serve` instead
// to run: `trunk serve --open --features csr`
use action_form_error_handling::app::*;
use leptos::*;
use wasm_bindgen::prelude::wasm_bindgen;
console_error_panic_hook::set_once();
leptos::mount_to_body(App);
}

View File

@@ -0,0 +1,15 @@
body {
font-family: sans-serif;
text-align: center;
}
#app {
text-align: center;
}
.form {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.5rem;
}

View File

@@ -0,0 +1,2 @@
[toolchain]
channel = "nightly"

View File

@@ -17,7 +17,6 @@ broadcaster = "1"
console_log = "1"
console_error_panic_hook = "0.1"
futures = "0.3"
cfg-if = "1"
lazy_static = "1"
leptos = { path = "../../leptos" }
leptos_actix = { path = "../../integrations/actix", optional = true }

View File

@@ -1,34 +1,35 @@
use cfg_if::cfg_if;
use leptos::*;
use leptos_meta::*;
use leptos_router::*;
#[cfg(feature = "ssr")]
use tracing::instrument;
cfg_if! {
if #[cfg(feature = "ssr")] {
use std::sync::atomic::{AtomicI32, Ordering};
use broadcaster::BroadcastChannel;
use once_cell::sync::OnceCell;
#[cfg(feature = "ssr")]
pub mod ssr_imports {
pub use broadcaster::BroadcastChannel;
pub use once_cell::sync::OnceCell;
pub use std::sync::atomic::{AtomicI32, Ordering};
static COUNT: AtomicI32 = AtomicI32::new(0);
pub static COUNT: AtomicI32 = AtomicI32::new(0);
lazy_static::lazy_static! {
pub static ref COUNT_CHANNEL: BroadcastChannel<i32> = BroadcastChannel::new();
}
lazy_static::lazy_static! {
pub static ref COUNT_CHANNEL: BroadcastChannel<i32> = BroadcastChannel::new();
}
static LOG_INIT: OnceCell<()> = OnceCell::new();
fn init_logging() {
LOG_INIT.get_or_init(|| {
simple_logger::SimpleLogger::new().env().init().unwrap();
});
}
static LOG_INIT: OnceCell<()> = OnceCell::new();
pub fn init_logging() {
LOG_INIT.get_or_init(|| {
simple_logger::SimpleLogger::new().env().init().unwrap();
});
}
}
#[server]
#[cfg_attr(feature = "ssr", instrument)]
pub async fn get_server_count() -> Result<i32, ServerFnError> {
use ssr_imports::*;
Ok(COUNT.load(Ordering::Relaxed))
}
@@ -38,6 +39,8 @@ pub async fn adjust_server_count(
delta: i32,
msg: String,
) -> Result<i32, ServerFnError> {
use ssr_imports::*;
let new = COUNT.load(Ordering::Relaxed) + delta;
COUNT.store(new, Ordering::Relaxed);
_ = COUNT_CHANNEL.send(&new).await;
@@ -48,6 +51,8 @@ pub async fn adjust_server_count(
#[server]
#[cfg_attr(feature = "ssr", instrument)]
pub async fn clear_server_count() -> Result<i32, ServerFnError> {
use ssr_imports::*;
COUNT.store(0, Ordering::Relaxed);
_ = COUNT_CHANNEL.send(&0).await;
Ok(0)
@@ -55,7 +60,7 @@ pub async fn clear_server_count() -> Result<i32, ServerFnError> {
#[component]
pub fn Counters() -> impl IntoView {
#[cfg(feature = "ssr")]
init_logging();
ssr_imports::init_logging();
provide_meta_context();
view! {
@@ -113,9 +118,9 @@ pub fn Counters() -> impl IntoView {
// This is the typical pattern for a CRUD app
#[component]
pub fn Counter() -> impl IntoView {
let dec = create_action(|_| adjust_server_count(-1, "decing".into()));
let inc = create_action(|_| adjust_server_count(1, "incing".into()));
let clear = create_action(|_| clear_server_count());
let dec = create_action(|_: &()| adjust_server_count(-1, "decing".into()));
let inc = create_action(|_: &()| adjust_server_count(1, "incing".into()));
let clear = create_action(|_: &()| clear_server_count());
let counter = create_resource(
move || {
(
@@ -217,9 +222,10 @@ pub fn FormCounter() -> impl IntoView {
#[component]
pub fn MultiuserCounter() -> impl IntoView {
let dec =
create_action(|_| adjust_server_count(-1, "dec dec goose".into()));
let inc = create_action(|_| adjust_server_count(1, "inc inc moose".into()));
let clear = create_action(|_| clear_server_count());
create_action(|_: &()| adjust_server_count(-1, "dec dec goose".into()));
let inc =
create_action(|_: &()| adjust_server_count(1, "inc inc moose".into()));
let clear = create_action(|_: &()| clear_server_count());
#[cfg(not(feature = "ssr"))]
let multiplayer_value = {

View File

@@ -1,21 +1,13 @@
use cfg_if::cfg_if;
pub mod counters;
// Needs to be in lib.rs AFAIK because wasm-bindgen needs us to be compiling a lib. I may be wrong.
cfg_if! {
if #[cfg(feature = "hydrate")] {
use leptos::*;
use wasm_bindgen::prelude::wasm_bindgen;
use crate::counters::*;
#[cfg(feature = "hydrate")]
#[wasm_bindgen::prelude::wasm_bindgen]
pub fn hydrate() {
use crate::counters::*;
use leptos::*;
#[wasm_bindgen]
pub fn hydrate() {
_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();
_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();
mount_to_body(|| {
view! { <Counters/> }
});
}
}
mount_to_body(Counters);
}

View File

@@ -1,72 +1,54 @@
use cfg_if::cfg_if;
mod counters;
// boilerplate to run in different modes
cfg_if! {
// server-only stuff
if #[cfg(feature = "ssr")] {
use leptos::*;
use actix_files::{Files};
use actix_web::*;
use crate::counters::*;
use leptos_actix::{generate_route_list, LeptosRoutes};
use crate::counters::*;
use actix_files::Files;
use actix_web::*;
use leptos::*;
use leptos_actix::{generate_route_list, LeptosRoutes};
#[get("/api/events")]
async fn counter_events() -> impl Responder {
use futures::StreamExt;
#[get("/api/events")]
async fn counter_events() -> impl Responder {
use crate::counters::ssr_imports::*;
use futures::StreamExt;
let stream =
futures::stream::once(async { crate::counters::get_server_count().await.unwrap_or(0) })
.chain(COUNT_CHANNEL.clone())
.map(|value| {
Ok(web::Bytes::from(format!(
"event: message\ndata: {value}\n\n"
))) as Result<web::Bytes>
});
HttpResponse::Ok()
.insert_header(("Content-Type", "text/event-stream"))
.streaming(stream)
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
// Explicit server function registration is no longer required
// on the main branch. On 0.3.0 and earlier, uncomment the lines
// below to register the server functions.
// _ = GetServerCount::register();
// _ = AdjustServerCount::register();
// _ = ClearServerCount::register();
// Setting this to None means we'll be using cargo-leptos and its env vars.
// when not using cargo-leptos None must be replaced with Some("Cargo.toml")
let conf = get_configuration(None).await.unwrap();
let addr = conf.leptos_options.site_addr;
let routes = generate_route_list(|| view! { <Counters/> });
HttpServer::new(move || {
let leptos_options = &conf.leptos_options;
let site_root = &leptos_options.site_root;
App::new()
.service(counter_events)
.route("/api/{tail:.*}", leptos_actix::handle_server_fns())
.leptos_routes(leptos_options.to_owned(), routes.to_owned(), || view! { <Counters/> })
.service(Files::new("/", site_root))
//.wrap(middleware::Compress::default())
})
.bind(&addr)?
.run()
.await
}
}
// client-only main for Trunk
else {
pub fn main() {
// isomorphic counters cannot work in a Client-Side-Rendered only
// app as a server is required to maintain state
}
}
let stream = futures::stream::once(async {
crate::counters::get_server_count().await.unwrap_or(0)
})
.chain(COUNT_CHANNEL.clone())
.map(|value| {
Ok(web::Bytes::from(format!(
"event: message\ndata: {value}\n\n"
))) as Result<web::Bytes>
});
HttpResponse::Ok()
.insert_header(("Content-Type", "text/event-stream"))
.streaming(stream)
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
// Setting this to None means we'll be using cargo-leptos and its env vars.
// when not using cargo-leptos None must be replaced with Some("Cargo.toml")
let conf = get_configuration(None).await.unwrap();
let addr = conf.leptos_options.site_addr;
let routes = generate_route_list(Counters);
HttpServer::new(move || {
let leptos_options = &conf.leptos_options;
let site_root = &leptos_options.site_root;
App::new()
.service(counter_events)
.leptos_routes(
leptos_options.to_owned(),
routes.to_owned(),
Counters,
)
.service(Files::new("/", site_root))
//.wrap(middleware::Compress::default())
})
.bind(&addr)?
.run()
.await
}

View File

@@ -0,0 +1,2 @@
[toolchain]
channel = "nightly"

View File

@@ -1,3 +1,2 @@
[toolchain]
channel = "nightly"

View File

@@ -1,3 +1,2 @@
[toolchain]
channel = "nightly"

View File

@@ -0,0 +1,2 @@
[toolchain]
channel = "nightly"

View File

@@ -7,22 +7,21 @@ edition = "2021"
crate-type = ["cdylib", "rlib"]
[dependencies]
console_log = "1.0.0"
console_error_panic_hook = "0.1.7"
cfg-if = "1.0.0"
console_log = "1.0"
console_error_panic_hook = "0.1"
leptos = { path = "../../leptos", features = ["nightly"] }
leptos_axum = { path = "../../integrations/axum", optional = true }
leptos_meta = { path = "../../meta" }
leptos_router = { path = "../../router" }
log = "0.4.17"
log = "0.4"
serde = { version = "1", features = ["derive"] }
simple_logger = "4.0.0"
axum = { version = "0.6.1", optional = true }
tower = { version = "0.4.13", optional = true }
tower-http = { version = "0.4", features = ["fs"], optional = true }
tokio = { version = "1.22.0", features = ["full"], optional = true }
http = { version = "0.2.8" }
thiserror = "1.0.38"
simple_logger = "4.0"
axum = { version = "0.7", optional = true }
tower = { version = "0.4", optional = true }
tower-http = { version = "0.5", features = ["fs"], optional = true }
tokio = { version = "1", features = ["full"], optional = true }
http = { version = "1.0" }
thiserror = "1.0"
wasm-bindgen = "0.2"
[features]

View File

@@ -5,3 +5,7 @@ This example demonstrates how Leptos Errors can work with an Axum backend on a s
## Getting Started
See the [Examples README](../README.md) for setup and run instructions.
## Quick Start
Run `cargo leptos watch` to run this example.

View File

@@ -0,0 +1,2 @@
[toolchain]
channel = "nightly"

View File

@@ -1,5 +1,4 @@
use crate::errors::AppError;
use cfg_if::cfg_if;
use leptos::{logging::log, Errors, *};
#[cfg(feature = "ssr")]
use leptos_axum::ResponseOptions;
@@ -30,12 +29,13 @@ pub fn ErrorTemplate(
// Only the response code for the first error is actually sent from the server
// this may be customized by the specific application
cfg_if! { if #[cfg(feature="ssr")] {
#[cfg(feature = "ssr")]
{
let response = use_context::<ResponseOptions>();
if let Some(response) = response {
response.set_status(errors[0].status_code());
}
}}
}
view! {
<h1>{if errors.len() > 1 {"Errors"} else {"Error"}}</h1>

View File

@@ -1,43 +1,48 @@
use cfg_if::cfg_if;
use crate::landing::App;
use axum::{
body::Body,
extract::State,
http::{Request, Response, StatusCode, Uri},
response::{IntoResponse, Response as AxumResponse},
};
use leptos::{view, LeptosOptions};
use tower::ServiceExt;
use tower_http::services::ServeDir;
cfg_if! { if #[cfg(feature = "ssr")] {
use axum::{
body::{boxed, Body, BoxBody},
extract::State,
response::IntoResponse,
http::{Request, Response, StatusCode, Uri},
};
use axum::response::Response as AxumResponse;
use tower::ServiceExt;
use tower_http::services::ServeDir;
use leptos::{LeptosOptions, view};
use crate::landing::App;
pub async fn file_and_error_handler(
uri: Uri,
State(options): State<LeptosOptions>,
req: Request<Body>,
) -> AxumResponse {
let root = options.site_root.clone();
let res = get_static_file(uri.clone(), &root).await.unwrap();
pub async fn file_and_error_handler(uri: Uri, State(options): State<LeptosOptions>, req: Request<Body>) -> AxumResponse {
let root = options.site_root.clone();
let res = get_static_file(uri.clone(), &root).await.unwrap();
if res.status() == StatusCode::OK {
res.into_response()
} else{
let handler = leptos_axum::render_app_to_stream(
options.to_owned(),
move || view!{ <App/> }
);
handler(req).await.into_response()
}
if res.status() == StatusCode::OK {
res.into_response()
} else {
let handler = leptos_axum::render_app_to_stream(
options.to_owned(),
move || view! { <App/> },
);
handler(req).await.into_response()
}
}
async fn get_static_file(uri: Uri, root: &str) -> Result<Response<BoxBody>, (StatusCode, String)> {
let req = Request::builder().uri(uri.clone()).body(Body::empty()).unwrap();
// `ServeDir` implements `tower::Service` so we can call it with `tower::ServiceExt::oneshot`
// This path is relative to the cargo root
match ServeDir::new(root).oneshot(req).await {
Ok(res) => Ok(res.map(boxed)),
Err(err) => Err((
StatusCode::INTERNAL_SERVER_ERROR,
format!("Something went wrong: {err}"),
)),
}
async fn get_static_file(
uri: Uri,
root: &str,
) -> Result<Response<Body>, (StatusCode, String)> {
let req = Request::builder()
.uri(uri.clone())
.body(Body::empty())
.unwrap();
// `ServeDir` implements `tower::Service` so we can call it with `tower::ServiceExt::oneshot`
// This path is relative to the cargo root
match ServeDir::new(root).oneshot(req).await {
Ok(res) => Ok(res.into_response()),
Err(err) => Err((
StatusCode::INTERNAL_SERVER_ERROR,
format!("Something went wrong: {err}"),
)),
}
}}
}

View File

@@ -1,24 +1,21 @@
use cfg_if::cfg_if;
pub mod error_template;
pub mod errors;
#[cfg(feature = "ssr")]
pub mod fallback;
pub mod landing;
// Needs to be in lib.rs AFAIK because wasm-bindgen needs us to be compiling a lib. I may be wrong.
cfg_if! {
if #[cfg(feature = "hydrate")] {
use leptos::*;
use wasm_bindgen::prelude::wasm_bindgen;
use crate::landing::*;
use wasm_bindgen::prelude::wasm_bindgen;
#[wasm_bindgen]
pub fn hydrate() {
_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();
#[cfg(feature = "hydrate")]
#[wasm_bindgen]
pub fn hydrate() {
use crate::landing::*;
use leptos::*;
leptos::mount_to_body(|| {
view! { <App/> }
});
}
}
_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();
leptos::mount_to_body(|| {
view! { <App/> }
});
}

View File

@@ -1,41 +1,39 @@
use cfg_if::cfg_if;
cfg_if! { if #[cfg(feature = "ssr")] {
use crate::fallback::file_and_error_handler;
use crate::landing::*;
use axum::body::Body as AxumBody;
use axum::{
extract::{State, Path},
#[cfg(feature = "ssr")]
mod ssr_imports {
pub use axum::{
body::Body as AxumBody,
extract::{Path, State},
http::Request,
response::{IntoResponse, Response},
routing::{get, post},
routing::get,
Router,
};
use errors_axum::*;
use leptos::{logging::log, *};
use leptos_axum::{generate_route_list, LeptosRoutes};
}}
pub use errors_axum::{fallback::*, landing::App};
pub use leptos::{logging::log, *};
pub use leptos_axum::{generate_route_list, LeptosRoutes};
//Define a handler to test extractor with state
#[cfg(feature = "ssr")]
async fn custom_handler(
Path(id): Path<String>,
State(options): State<LeptosOptions>,
req: Request<AxumBody>,
) -> Response {
let handler = leptos_axum::render_app_to_stream_with_context(
options.clone(),
move || {
provide_context(id.clone());
},
App,
);
handler(req).await.into_response()
// This custom handler lets us provide Axum State via context
pub async fn custom_handler(
Path(id): Path<String>,
State(options): State<LeptosOptions>,
req: Request<AxumBody>,
) -> Response {
let handler = leptos_axum::render_app_to_stream_with_context(
options.clone(),
move || {
provide_context(id.clone());
},
App,
);
handler(req).await.into_response()
}
}
#[cfg(feature = "ssr")]
#[tokio::main]
async fn main() {
use ssr_imports::*;
simple_logger::init_with_level(log::Level::Debug)
.expect("couldn't initialize logging");
@@ -52,7 +50,6 @@ async fn main() {
// build our application with a route
let app = Router::new()
.route("/api/*fn_name", post(leptos_axum::handle_server_fns))
.route("/special/:id", get(custom_handler))
.leptos_routes(&leptos_options, routes, App)
.fallback(file_and_error_handler)
@@ -61,8 +58,8 @@ async fn main() {
// run our app with hyper
// `axum::Server` is a re-export of `hyper::Server`
log!("listening on http://{}", &addr);
axum::Server::bind(&addr)
.serve(app.into_make_service())
let listener = tokio::net::TcpListener::bind(&addr).await.unwrap();
axum::serve(listener, app.into_make_service())
.await
.unwrap();
}
@@ -71,5 +68,5 @@ async fn main() {
#[cfg(not(feature = "ssr"))]
pub fn main() {
// This example cannot be built as a trunk standalone CSR-only app.
// The server is needed to demonstrate the error statuses.
// The server is needed to demonstrate the error statuses.
}

View File

@@ -0,0 +1,2 @@
[toolchain]
channel = "nightly"

View File

@@ -15,7 +15,6 @@ actix-files = { version = "0.6", optional = true }
actix-web = { version = "4", optional = true, features = ["macros"] }
console_log = "1"
console_error_panic_hook = "0.1"
cfg-if = "1"
leptos = { path = "../../leptos", features = ["nightly"] }
leptos_meta = { path = "../../meta", features = ["nightly"] }
leptos_actix = { path = "../../integrations/actix", optional = true }

View File

@@ -0,0 +1,2 @@
[toolchain]
channel = "nightly"

View File

@@ -1,4 +1,3 @@
use cfg_if::cfg_if;
use leptos::*;
use leptos_meta::*;
use leptos_router::*;
@@ -33,16 +32,10 @@ pub fn App() -> impl IntoView {
}
}
// Needs to be in lib.rs AFAIK because wasm-bindgen needs us to be compiling a lib. I may be wrong.
cfg_if! {
if #[cfg(feature = "hydrate")] {
use wasm_bindgen::prelude::wasm_bindgen;
#[wasm_bindgen]
pub fn hydrate() {
_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();
leptos::mount_to_body(App);
}
}
#[cfg(feature = "hydrate")]
#[wasm_bindgen::prelude::wasm_bindgen]
pub fn hydrate() {
_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();
leptos::mount_to_body(App);
}

View File

@@ -1,56 +1,56 @@
use cfg_if::cfg_if;
use leptos::*;
// server-only stuff
#[cfg(feature = "ssr")]
mod ssr_imports {
pub use actix_files::Files;
pub use actix_web::*;
pub use hackernews::App;
pub use leptos_actix::{generate_route_list, LeptosRoutes};
// boilerplate to run in different modes
cfg_if! {
// server-only stuff
if #[cfg(feature = "ssr")] {
use actix_files::{Files};
use actix_web::*;
use hackernews::{App};
use leptos_actix::{LeptosRoutes, generate_route_list};
#[get("/style.css")]
async fn css() -> impl Responder {
actix_files::NamedFile::open_async("./style.css").await
}
#[get("/favicon.ico")]
async fn favicon() -> impl Responder {
actix_files::NamedFile::open_async("./target/site//favicon.ico").await
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
// Setting this to None means we'll be using cargo-leptos and its env vars.
let conf = get_configuration(None).await.unwrap();
let addr = conf.leptos_options.site_addr;
// Generate the list of routes in your Leptos App
let routes = generate_route_list(App);
HttpServer::new(move || {
let leptos_options = &conf.leptos_options;
let site_root = &leptos_options.site_root;
App::new()
.service(css)
.service(favicon)
.route("/api/{tail:.*}", leptos_actix::handle_server_fns())
.leptos_routes(leptos_options.to_owned(), routes.to_owned(), App)
.service(Files::new("/", site_root))
//.wrap(middleware::Compress::default())
})
.bind(&addr)?
.run()
.await
}
} else {
fn main() {
use hackernews::{App};
_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();
mount_to_body(App)
}
#[get("/style.css")]
pub async fn css() -> impl Responder {
actix_files::NamedFile::open_async("./style.css").await
}
#[get("/favicon.ico")]
pub async fn favicon() -> impl Responder {
actix_files::NamedFile::open_async("./target/site//favicon.ico").await
}
}
#[cfg(feature = "ssr")]
#[actix_web::main]
async fn main() -> std::io::Result<()> {
use leptos::get_configuration;
use ssr_imports::*;
// Setting this to None means we'll be using cargo-leptos and its env vars.
let conf = get_configuration(None).await.unwrap();
let addr = conf.leptos_options.site_addr;
// Generate the list of routes in your Leptos App
let routes = generate_route_list(App);
HttpServer::new(move || {
let leptos_options = &conf.leptos_options;
let site_root = &leptos_options.site_root;
App::new()
.service(css)
.service(favicon)
.leptos_routes(leptos_options.to_owned(), routes.to_owned(), App)
.service(Files::new("/", site_root))
//.wrap(middleware::Compress::default())
})
.bind(&addr)?
.run()
.await
}
// CSR-only setup
#[cfg(not(feature = "ssr"))]
fn main() {
use hackernews::App;
_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();
mount_to_body(App)
}

View File

@@ -11,24 +11,23 @@ codegen-units = 1
lto = true
[dependencies]
console_log = "1.0.0"
console_error_panic_hook = "0.1.7"
cfg-if = "1.0.0"
console_log = "1.0"
console_error_panic_hook = "0.1"
leptos = { path = "../../leptos", features = ["nightly"] }
leptos_axum = { path = "../../integrations/axum", optional = true }
leptos_meta = { path = "../../meta", features = ["nightly"] }
leptos_router = { path = "../../router", features = ["nightly"] }
log = "0.4.17"
simple_logger = "4.0.0"
serde = { version = "1.0.148", features = ["derive"] }
log = "0.4"
simple_logger = "4.0"
serde = { version = "1.0", features = ["derive"] }
tracing = "0.1"
gloo-net = { version = "0.2.5", features = ["http"] }
reqwest = { version = "0.11.13", features = ["json"] }
axum = { version = "0.6.1", optional = true }
tower = { version = "0.4.13", optional = true }
tower-http = { version = "0.4", features = ["fs"], optional = true }
tokio = { version = "1.22.0", features = ["full"], optional = true }
http = { version = "0.2.11", optional = true }
gloo-net = { version = "0.4", features = ["http"] }
reqwest = { version = "0.11", features = ["json"] }
axum = { version = "0.7", optional = true }
tower = { version = "0.4", optional = true }
tower-http = { version = "0.5", features = ["fs"], optional = true }
tokio = { version = "1", features = ["full"], optional = true }
http = { version = "1.0", optional = true }
web-sys = { version = "0.3", features = ["AbortController", "AbortSignal"] }
wasm-bindgen = "0.2"

View File

@@ -0,0 +1,2 @@
[toolchain]
channel = "nightly"

View File

@@ -1,44 +1,48 @@
use cfg_if::cfg_if;
use crate::error_template::error_template;
use axum::{
body::Body,
extract::State,
http::{Request, Response, StatusCode, Uri},
response::{IntoResponse, Response as AxumResponse},
};
use leptos::LeptosOptions;
use tower::ServiceExt;
use tower_http::services::ServeDir;
cfg_if! {
if #[cfg(feature = "ssr")] {
use axum::{
body::{boxed, Body, BoxBody},
extract::State,
response::IntoResponse,
http::{Request, Response, StatusCode, Uri},
};
use axum::response::Response as AxumResponse;
use tower::ServiceExt;
use tower_http::services::ServeDir;
use leptos::{LeptosOptions};
use crate::error_template::error_template;
pub async fn file_and_error_handler(
uri: Uri,
State(options): State<LeptosOptions>,
req: Request<Body>,
) -> AxumResponse {
let root = options.site_root.clone();
let res = get_static_file(uri.clone(), &root).await.unwrap();
pub async fn file_and_error_handler(uri: Uri, State(options): State<LeptosOptions>, req: Request<Body>) -> AxumResponse {
let root = options.site_root.clone();
let res = get_static_file(uri.clone(), &root).await.unwrap();
if res.status() == StatusCode::OK {
res.into_response()
} else{
let handler = leptos_axum::render_app_to_stream(options.to_owned(), || error_template( None));
handler(req).await.into_response()
}
if res.status() == StatusCode::OK {
res.into_response()
} else {
let handler =
leptos_axum::render_app_to_stream(options.to_owned(), || {
error_template(None)
});
handler(req).await.into_response()
}
}
async fn get_static_file(uri: Uri, root: &str) -> Result<Response<BoxBody>, (StatusCode, String)> {
let req = Request::builder().uri(uri.clone()).body(Body::empty()).unwrap();
// `ServeDir` implements `tower::Service` so we can call it with `tower::ServiceExt::oneshot`
// This path is relative to the cargo root
match ServeDir::new(root).oneshot(req).await {
Ok(res) => Ok(res.map(boxed)),
Err(err) => Err((
StatusCode::INTERNAL_SERVER_ERROR,
format!("Something went wrong: {}", err),
)),
}
async fn get_static_file(
uri: Uri,
root: &str,
) -> Result<Response<Body>, (StatusCode, String)> {
let req = Request::builder()
.uri(uri.clone())
.body(Body::empty())
.unwrap();
// `ServeDir` implements `tower::Service` so we can call it with `tower::ServiceExt::oneshot`
// This path is relative to the cargo root
match ServeDir::new(root).oneshot(req).await {
Ok(res) => Ok(res.into_response()),
Err(err) => Err((
StatusCode::INTERNAL_SERVER_ERROR,
format!("Something went wrong: {}", err),
)),
}
}
}

View File

@@ -1,63 +1,68 @@
use cfg_if::cfg_if;
use axum::{
body::Body,
http::{Request, Response, StatusCode, Uri},
response::IntoResponse,
};
use tower::ServiceExt;
use tower_http::services::ServeDir;
cfg_if! {
if #[cfg(feature = "ssr")] {
use axum::{
body::{boxed, Body, BoxBody},
http::{Request, Response, StatusCode, Uri},
};
use tower::ServiceExt;
use tower_http::services::ServeDir;
pub async fn file_handler(
uri: Uri,
) -> Result<Response<Body>, (StatusCode, String)> {
let res = get_static_file(uri.clone(), "/pkg").await?;
pub async fn file_handler(uri: Uri) -> Result<Response<BoxBody>, (StatusCode, String)> {
let res = get_static_file(uri.clone(), "/pkg").await?;
if res.status() == StatusCode::NOT_FOUND {
// try with `.html`
// TODO: handle if the Uri has query parameters
match format!("{}.html", uri).parse() {
Ok(uri_html) => get_static_file(uri_html, "/pkg").await,
Err(_) => Err((StatusCode::INTERNAL_SERVER_ERROR, "Invalid URI".to_string())),
}
} else {
Ok(res)
if res.status() == StatusCode::NOT_FOUND {
// try with `.html`
// TODO: handle if the Uri has query parameters
match format!("{}.html", uri).parse() {
Ok(uri_html) => get_static_file(uri_html, "/pkg").await,
Err(_) => Err((
StatusCode::INTERNAL_SERVER_ERROR,
"Invalid URI".to_string(),
)),
}
}
pub async fn get_static_file_handler(uri: Uri) -> Result<Response<BoxBody>, (StatusCode, String)> {
let res = get_static_file(uri.clone(), "/static").await?;
if res.status() == StatusCode::NOT_FOUND {
Err((StatusCode::INTERNAL_SERVER_ERROR, "Invalid URI".to_string()))
} else {
Ok(res)
}
}
async fn get_static_file(uri: Uri, base: &str) -> Result<Response<BoxBody>, (StatusCode, String)> {
let req = Request::builder().uri(&uri).body(Body::empty()).unwrap();
// `ServeDir` implements `tower::Service` so we can call it with `tower::ServiceExt::oneshot`
// When run normally, the root should be the crate root
if base == "/static" {
match ServeDir::new("./static").oneshot(req).await {
Ok(res) => Ok(res.map(boxed)),
Err(err) => Err((
StatusCode::INTERNAL_SERVER_ERROR,
format!("Something went wrong: {}", err),
))
}
} else if base == "/pkg" {
match ServeDir::new("./pkg").oneshot(req).await {
Ok(res) => Ok(res.map(boxed)),
Err(err) => Err((
StatusCode::INTERNAL_SERVER_ERROR,
format!("Something went wrong: {}", err),
)),
}
} else{
Err((StatusCode::NOT_FOUND, "Not Found".to_string()))
}
}
} else {
Ok(res)
}
}
pub async fn get_static_file_handler(
uri: Uri,
) -> Result<Response<Body>, (StatusCode, String)> {
let res = get_static_file(uri.clone(), "/static").await?;
if res.status() == StatusCode::NOT_FOUND {
Err((StatusCode::INTERNAL_SERVER_ERROR, "Invalid URI".to_string()))
} else {
Ok(res)
}
}
async fn get_static_file(
uri: Uri,
base: &str,
) -> Result<Response<Body>, (StatusCode, String)> {
let req = Request::builder().uri(&uri).body(Body::empty()).unwrap();
// `ServeDir` implements `tower::Service` so we can call it with `tower::ServiceExt::oneshot`
// When run normally, the root should be the crate root
if base == "/static" {
match ServeDir::new("./static").oneshot(req).await {
Ok(res) => Ok(res.into_response()),
Err(err) => Err((
StatusCode::INTERNAL_SERVER_ERROR,
format!("Something went wrong: {}", err),
)),
}
} else if base == "/pkg" {
match ServeDir::new("./pkg").oneshot(req).await {
Ok(res) => Ok(res.into_response()),
Err(err) => Err((
StatusCode::INTERNAL_SERVER_ERROR,
format!("Something went wrong: {}", err),
)),
}
} else {
Err((StatusCode::NOT_FOUND, "Not Found".to_string()))
}
}

View File

@@ -1,10 +1,11 @@
use cfg_if::cfg_if;
use leptos::{component, view, IntoView};
use leptos_meta::*;
use leptos_router::*;
mod api;
pub mod error_template;
#[cfg(feature = "ssr")]
pub mod fallback;
#[cfg(feature = "ssr")]
pub mod handlers;
mod routes;
use routes::{nav::*, stories::*, story::*, users::*};
@@ -12,38 +13,28 @@ use routes::{nav::*, stories::*, story::*, users::*};
#[component]
pub fn App() -> impl IntoView {
provide_meta_context();
view! {
<>
<Link rel="shortcut icon" type_="image/ico" href="/favicon.ico"/>
<Stylesheet id="leptos" href="/pkg/hackernews_axum.css"/>
<Meta name="description" content="Leptos implementation of a HackerNews demo."/>
<Router>
<Nav />
<main>
<Routes>
<Route path="users/:id" view=User/>
<Route path="stories/:id" view=Story/>
<Route path=":stories?" view=Stories/>
</Routes>
</main>
</Router>
</>
<Link rel="shortcut icon" type_="image/ico" href="/favicon.ico"/>
<Stylesheet id="leptos" href="/pkg/hackernews_axum.css"/>
<Meta name="description" content="Leptos implementation of a HackerNews demo."/>
<Router>
<Nav />
<main>
<Routes>
<Route path="users/:id" view=User/>
<Route path="stories/:id" view=Story/>
<Route path=":stories?" view=Stories/>
</Routes>
</main>
</Router>
}
}
// Needs to be in lib.rs AFAIK because wasm-bindgen needs us to be compiling a lib. I may be wrong.
cfg_if! {
if #[cfg(feature = "hydrate")] {
use wasm_bindgen::prelude::wasm_bindgen;
#[wasm_bindgen]
pub fn hydrate() {
_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();
leptos::mount_to_body(move || {
view! { <App/> }
});
}
}
#[cfg(feature = "hydrate")]
#[wasm_bindgen::prelude::wasm_bindgen]
pub fn hydrate() {
_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();
leptos::mount_to_body(App);
}

View File

@@ -1,54 +1,41 @@
use cfg_if::cfg_if;
use leptos::{logging::log, *};
// boilerplate to run in different modes
cfg_if! {
if #[cfg(feature = "ssr")] {
use axum::{
Router,
routing::get,
};
#[cfg(feature = "ssr")]
#[tokio::main]
async fn main() {
use axum::{routing::get, Router};
use hackernews_axum::{fallback::file_and_error_handler, *};
use leptos::get_configuration;
use leptos_axum::{generate_route_list, LeptosRoutes};
use hackernews_axum::fallback::file_and_error_handler;
#[tokio::main]
async fn main() {
use hackernews_axum::*;
let conf = get_configuration(Some("Cargo.toml")).await.unwrap();
let leptos_options = conf.leptos_options;
let addr = leptos_options.site_addr;
let routes = generate_route_list(App);
let conf = get_configuration(Some("Cargo.toml")).await.unwrap();
let leptos_options = conf.leptos_options;
let addr = leptos_options.site_addr;
let routes = generate_route_list(App);
simple_logger::init_with_level(log::Level::Debug)
.expect("couldn't initialize logging");
simple_logger::init_with_level(log::Level::Debug).expect("couldn't initialize logging");
// build our application with a route
let app = Router::new()
// build our application with a route
let app = Router::new()
.route("/favicon.ico", get(file_and_error_handler))
.leptos_routes(&leptos_options, routes, || view! { <App/> } )
.leptos_routes(&leptos_options, routes, App)
.fallback(file_and_error_handler)
.with_state(leptos_options);
// run our app with hyper
// `axum::Server` is a re-export of `hyper::Server`
log!("listening on {}", addr);
axum::Server::bind(&addr)
.serve(app.into_make_service())
.await
.unwrap();
}
// run our app with hyper
// `axum::Server` is a re-export of `hyper::Server`
println!("listening on {}", addr);
let listener = tokio::net::TcpListener::bind(&addr).await.unwrap();
axum::serve(listener, app.into_make_service())
.await
.unwrap();
}
// client-only stuff for Trunk
else {
use hackernews_axum::*;
// client-only stuff for Trunk
#[cfg(not(feature = "ssr"))]
pub fn main() {
use hackernews_axum::*;
pub fn main() {
_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();
mount_to_body(|| {
view! { <App/> }
});
}
}
_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();
mount_to_body(App);
}

View File

@@ -11,9 +11,8 @@ codegen-units = 1
lto = true
[dependencies]
console_log = "1.0.0"
console_error_panic_hook = "0.1.7"
cfg-if = "1.0.0"
console_log = "1.0"
console_error_panic_hook = "0.1"
leptos = { path = "../../leptos", features = [
"nightly",
"experimental-islands",
@@ -23,20 +22,20 @@ leptos_axum = { path = "../../integrations/axum", optional = true, features = [
] }
leptos_meta = { path = "../../meta", features = ["nightly"] }
leptos_router = { path = "../../router", features = ["nightly"] }
log = "0.4.17"
simple_logger = "4.0.0"
serde = { version = "1.0.148", features = ["derive"] }
log = "0.4"
simple_logger = "4.0"
serde = { version = "1.0", features = ["derive"] }
tracing = "0.1"
gloo-net = { version = "0.2.5", features = ["http"] }
reqwest = { version = "0.11.13", features = ["json"] }
axum = { version = "0.6.1", optional = true, features = ["http2"] }
tower = { version = "0.4.13", optional = true }
tower-http = { version = "0.4", features = [
gloo-net = { version = "0.4", features = ["http"] }
reqwest = { version = "0.11", features = ["json"] }
axum = { version = "0.7", optional = true, features = ["http2"] }
tower = { version = "0.4", optional = true }
tower-http = { version = "0.5", features = [
"fs",
"compression-br",
], optional = true }
tokio = { version = "1.22.0", features = ["full"], optional = true }
http = { version = "0.2.11", optional = true }
tokio = { version = "1", features = ["full"], optional = true }
http = { version = "1.0", optional = true }
web-sys = { version = "0.3", features = ["AbortController", "AbortSignal"] }
wasm-bindgen = "0.2"
lazy_static = "1.4.0"

View File

@@ -0,0 +1,2 @@
[toolchain]
channel = "nightly"

View File

@@ -1,44 +1,48 @@
use cfg_if::cfg_if;
use crate::error_template::error_template;
use axum::{
body::Body,
extract::State,
http::{Request, Response, StatusCode, Uri},
response::{IntoResponse, Response as AxumResponse},
};
use leptos::LeptosOptions;
use tower::ServiceExt;
use tower_http::services::ServeDir;
cfg_if! {
if #[cfg(feature = "ssr")] {
use axum::{
body::{boxed, Body, BoxBody},
extract::State,
response::IntoResponse,
http::{Request, Response, StatusCode, Uri},
};
use axum::response::Response as AxumResponse;
use tower::ServiceExt;
use tower_http::services::ServeDir;
use leptos::{LeptosOptions};
use crate::error_template::error_template;
pub async fn file_and_error_handler(
uri: Uri,
State(options): State<LeptosOptions>,
req: Request<Body>,
) -> AxumResponse {
let root = options.site_root.clone();
let res = get_static_file(uri.clone(), &root).await.unwrap();
pub async fn file_and_error_handler(uri: Uri, State(options): State<LeptosOptions>, req: Request<Body>) -> AxumResponse {
let root = options.site_root.clone();
let res = get_static_file(uri.clone(), &root).await.unwrap();
if res.status() == StatusCode::OK {
res.into_response()
} else{
let handler = leptos_axum::render_app_to_stream(options.to_owned(), || error_template( None));
handler(req).await.into_response()
}
if res.status() == StatusCode::OK {
res.into_response()
} else {
let handler =
leptos_axum::render_app_to_stream(options.to_owned(), || {
error_template(None)
});
handler(req).await.into_response()
}
}
async fn get_static_file(uri: Uri, root: &str) -> Result<Response<BoxBody>, (StatusCode, String)> {
let req = Request::builder().uri(uri.clone()).body(Body::empty()).unwrap();
// `ServeDir` implements `tower::Service` so we can call it with `tower::ServiceExt::oneshot`
// This path is relative to the cargo root
match ServeDir::new(root).oneshot(req).await {
Ok(res) => Ok(res.map(boxed)),
Err(err) => Err((
StatusCode::INTERNAL_SERVER_ERROR,
format!("Something went wrong: {}", err),
)),
}
async fn get_static_file(
uri: Uri,
root: &str,
) -> Result<Response<Body>, (StatusCode, String)> {
let req = Request::builder()
.uri(uri.clone())
.body(Body::empty())
.unwrap();
// `ServeDir` implements `tower::Service` so we can call it with `tower::ServiceExt::oneshot`
// This path is relative to the cargo root
match ServeDir::new(root).oneshot(req).await {
Ok(res) => Ok(res.into_response()),
Err(err) => Err((
StatusCode::INTERNAL_SERVER_ERROR,
format!("Something went wrong: {}", err),
)),
}
}
}

View File

@@ -1,63 +0,0 @@
use cfg_if::cfg_if;
cfg_if! {
if #[cfg(feature = "ssr")] {
use axum::{
body::{boxed, Body, BoxBody},
http::{Request, Response, StatusCode, Uri},
};
use tower::ServiceExt;
use tower_http::services::ServeDir;
pub async fn file_handler(uri: Uri) -> Result<Response<BoxBody>, (StatusCode, String)> {
let res = get_static_file(uri.clone(), "/pkg").await?;
if res.status() == StatusCode::NOT_FOUND {
// try with `.html`
// TODO: handle if the Uri has query parameters
match format!("{}.html", uri).parse() {
Ok(uri_html) => get_static_file(uri_html, "/pkg").await,
Err(_) => Err((StatusCode::INTERNAL_SERVER_ERROR, "Invalid URI".to_string())),
}
} else {
Ok(res)
}
}
pub async fn get_static_file_handler(uri: Uri) -> Result<Response<BoxBody>, (StatusCode, String)> {
let res = get_static_file(uri.clone(), "/static").await?;
if res.status() == StatusCode::NOT_FOUND {
Err((StatusCode::INTERNAL_SERVER_ERROR, "Invalid URI".to_string()))
} else {
Ok(res)
}
}
async fn get_static_file(uri: Uri, base: &str) -> Result<Response<BoxBody>, (StatusCode, String)> {
let req = Request::builder().uri(&uri).body(Body::empty()).unwrap();
// `ServeDir` implements `tower::Service` so we can call it with `tower::ServiceExt::oneshot`
// When run normally, the root should be the crate root
if base == "/static" {
match ServeDir::new("./static").oneshot(req).await {
Ok(res) => Ok(res.map(boxed)),
Err(err) => Err((
StatusCode::INTERNAL_SERVER_ERROR,
format!("Something went wrong: {}", err),
))
}
} else if base == "/pkg" {
match ServeDir::new("./pkg").oneshot(req).await {
Ok(res) => Ok(res.map(boxed)),
Err(err) => Err((
StatusCode::INTERNAL_SERVER_ERROR,
format!("Something went wrong: {}", err),
)),
}
} else{
Err((StatusCode::NOT_FOUND, "Not Found".to_string()))
}
}
}
}

View File

@@ -1,11 +1,11 @@
#![feature(lazy_cell)]
use cfg_if::cfg_if;
use leptos::*;
use leptos_meta::*;
use leptos_router::*;
mod api;
pub mod error_template;
#[cfg(feature = "ssr")]
pub mod fallback;
mod routes;
use routes::{nav::*, stories::*, story::*, users::*};
@@ -31,16 +31,10 @@ pub fn App() -> impl IntoView {
}
}
// Needs to be in lib.rs AFAIK because wasm-bindgen needs us to be compiling a lib. I may be wrong.
cfg_if! {
if #[cfg(feature = "hydrate")] {
use wasm_bindgen::prelude::wasm_bindgen;
#[wasm_bindgen]
pub fn hydrate() {
#[cfg(debug_assertions)]
console_error_panic_hook::set_once();
leptos::leptos_dom::HydrationCtx::stop_hydrating();
}
}
#[cfg(feature = "hydrate")]
#[wasm_bindgen::prelude::wasm_bindgen]
pub fn hydrate() {
#[cfg(debug_assertions)]
console_error_panic_hook::set_once();
leptos::leptos_dom::HydrationCtx::stop_hydrating();
}

View File

@@ -1,16 +1,11 @@
#[cfg(feature = "ssr")]
mod ssr_imports {
pub use axum::{routing::get, Router};
pub use hackernews_islands::fallback::file_and_error_handler;
pub use leptos::*;
pub use leptos_axum::{generate_route_list, LeptosRoutes};
}
#[cfg(feature = "ssr")]
#[tokio::main]
async fn main() {
pub use axum::{routing::get, Router};
pub use hackernews_islands::fallback::file_and_error_handler;
use hackernews_islands::*;
use ssr_imports::*;
pub use leptos::get_configuration;
pub use leptos_axum::{generate_route_list, LeptosRoutes};
let conf = get_configuration(Some("Cargo.toml")).await.unwrap();
let leptos_options = conf.leptos_options;
@@ -26,9 +21,9 @@ async fn main() {
// run our app with hyper
// `axum::Server` is a re-export of `hyper::Server`
logging::log!("listening on {}", addr);
axum::Server::bind(&addr)
.serve(app.into_make_service())
println!("listening on {}", addr);
let listener = tokio::net::TcpListener::bind(&addr).await.unwrap();
axum::serve(listener, app.into_make_service())
.await
.unwrap();
}
@@ -40,7 +35,5 @@ pub fn main() {
use leptos::*;
_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();
mount_to_body(|| {
view! { <App/> }
});
mount_to_body(App);
}

View File

@@ -14,10 +14,10 @@ lto = true
console_log = "1.0.0"
console_error_panic_hook = "0.1.7"
cfg-if = "1.0.0"
leptos = { path = "../../leptos", features = ["nightly"] }
leptos_axum = { path = "../../integrations/axum", default-features = false, optional = true }
leptos_meta = { path = "../../meta", features = ["nightly"] }
leptos_router = { path = "../../router", features = ["nightly"] }
leptos = { version = "0.5", features = ["nightly"] }
leptos_axum = { version = "0.5", default-features = false, optional = true }
leptos_meta = { version = "0.5", features = ["nightly"] }
leptos_router = { version = "0.5", features = ["nightly"] }
log = "0.4.17"
simple_logger = "4.0.0"
serde = { version = "1.0.148", features = ["derive"] }

View File

@@ -2,6 +2,8 @@
This example uses the basic Hacker News example as its basis, but shows how to run the server side as WASM running in a JS environment. In this example, Deno is used as the runtime.
**NOTE**: This example is slightly out of date pending an update to [`axum-js-fetch`](https://github.com/seanaye/axum-js-fetch/), which was waiting on a version of `gloo-net` that uses `http` 1.0. It still works with Leptos 0.5 and Axum 0.6, but not with the versions of Leptos (0.6 and later) that support Axum 1.0.
## Server Side Rendering with Deno
To run the Deno version, run

View File

@@ -1,39 +1,43 @@
use cfg_if::cfg_if;
use crate::error_template::error_template;
use axum::{
body::Body,
extract::State,
http::{Request, Response, StatusCode, Uri},
response::{IntoResponse, Response as AxumResponse},
};
//use tower::ServiceExt;
use leptos::LeptosOptions;
cfg_if! {
if #[cfg(feature = "ssr")] {
use axum::{
body::{Body, BoxBody},
extract::State,
response::IntoResponse,
http::{Request, Response, StatusCode, Uri},
};
use axum::response::Response as AxumResponse;
//use tower::ServiceExt;
use leptos::{LeptosOptions};
use crate::error_template::error_template;
pub async fn file_and_error_handler(
uri: Uri,
State(options): State<LeptosOptions>,
req: Request<Body>,
) -> AxumResponse {
let root = options.site_root.clone();
let res = get_static_file(uri.clone(), &root).await.unwrap();
pub async fn file_and_error_handler(uri: Uri, State(options): State<LeptosOptions>, req: Request<Body>) -> AxumResponse {
let root = options.site_root.clone();
let res = get_static_file(uri.clone(), &root).await.unwrap();
if res.status() == StatusCode::OK {
res.into_response()
} else{
let handler = leptos_axum::render_app_to_stream(options.to_owned(), || error_template(None));
handler(req).await.into_response()
}
if res.status() == StatusCode::OK {
res.into_response()
} else {
let handler =
leptos_axum::render_app_to_stream(options.to_owned(), || {
error_template(None)
});
handler(req).await.into_response()
}
async fn get_static_file(uri: Uri, root: &str) -> Result<Response<BoxBody>, (StatusCode, String)> {
let req = Request::builder().uri(uri.clone()).body(Body::empty()).unwrap();
// `ServeDir` implements `tower::Service` so we can call it with `tower::ServiceExt::oneshot`
// This path is relative to the cargo root
_ = req;
_ = root;
todo!()
}
}
async fn get_static_file(
uri: Uri,
root: &str,
) -> Result<Response<Body>, (StatusCode, String)> {
let req = Request::builder()
.uri(uri.clone())
.body(Body::empty())
.unwrap();
// `ServeDir` implements `tower::Service` so we can call it with `tower::ServiceExt::oneshot`
// This path is relative to the cargo root
_ = req;
_ = root;
todo!()
}

View File

@@ -1,9 +1,9 @@
use cfg_if::cfg_if;
use leptos::{component, view, IntoView};
use leptos_meta::*;
use leptos_router::*;
mod api;
pub mod error_template;
#[cfg(feature = "ssr")]
pub mod fallback;
mod routes;
use routes::{nav::*, stories::*, story::*, users::*};
@@ -29,25 +29,22 @@ pub fn App() -> impl IntoView {
}
}
cfg_if! {
if #[cfg(feature = "hydrate")] {
#[wasm_bindgen]
pub fn hydrate() {
_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();
leptos::mount_to_body(move || {
view! { <App/> }
});
}
} else if #[cfg(feature = "ssr")] {
#[cfg(feature = "hydrate")]
#[wasm_bindgen]
pub fn hydrate() {
_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();
leptos::mount_to_body(App);
}
use axum::{
Router,
routing::post
};
use leptos_axum::{generate_route_list, LeptosRoutes};
#[cfg(feature = "ssr")]
mod ssr_imports {
use crate::App;
use axum::{routing::post, Router};
use leptos::*;
use leptos_axum::{generate_route_list, LeptosRoutes};
use log::{info, Level};
use wasm_bindgen::prelude::wasm_bindgen;
#[wasm_bindgen]
pub struct Handler(axum_js_fetch::App);
@@ -55,17 +52,20 @@ cfg_if! {
#[wasm_bindgen]
impl Handler {
pub async fn new() -> Self {
console_log::init_with_level(Level::Debug);
_ = console_log::init_with_level(Level::Debug);
console_error_panic_hook::set_once();
let leptos_options = LeptosOptions::builder().output_name("client").site_pkg_dir("pkg").build();
let leptos_options = LeptosOptions::builder()
.output_name("client")
.site_pkg_dir("pkg")
.build();
let routes = generate_route_list(App);
// build our application with a route
let app: axum::Router<(), axum::body::Body> = Router::new()
.leptos_routes(&leptos_options, routes, || view! { <App/> } )
.route("/api/*fn_name", post(leptos_axum::handle_server_fns))
.with_state(leptos_options);
.leptos_routes(&leptos_options, routes, || view! { <App/> })
.route("/api/*fn_name", post(leptos_axum::handle_server_fns))
.with_state(leptos_options);
info!("creating handler instance");
@@ -77,4 +77,3 @@ cfg_if! {
}
}
}
}

View File

@@ -0,0 +1,2 @@
[toolchain]
channel = "nightly"

View File

@@ -14,7 +14,7 @@ leptos_router = { path = "../../../router", features = ["csr"] }
log = "0.4"
console_error_panic_hook = "0.1"
console_log = "1"
gloo-net = "0.2"
gloo-storage = "0.2"
gloo-net = "0.5"
gloo-storage = "0.3"
serde = "1.0"
thiserror = "1.0"

View File

@@ -1,5 +1,5 @@
use api_boundary::*;
use gloo_net::http::{Request, Response};
use gloo_net::http::{Request, RequestBuilder, Response};
use serde::de::DeserializeOwned;
use thiserror::Error;
@@ -41,7 +41,7 @@ impl AuthorizedApi {
fn auth_header_value(&self) -> String {
format!("Bearer {}", self.token.token)
}
async fn send<T>(&self, req: Request) -> Result<T>
async fn send<T>(&self, req: RequestBuilder) -> Result<T>
where
T: DeserializeOwned,
{

View File

@@ -5,14 +5,18 @@ edition = "2021"
publish = false
[dependencies]
api-boundary = "=0.0.0"
anyhow = "1.0"
api-boundary = "*"
axum = { version = "0.6", features = ["headers"] }
axum = "0.7"
axum-extra = { version = "0.9.2", features = ["typed-header"] }
env_logger = "0.10"
log = "0.4"
mailparse = "0.14"
pwhash = "1.0"
thiserror = "1.0"
tokio = { version = "1.25", features = ["macros", "rt-multi-thread"] }
tower-http = { version = "0.4", features = ["cors"] }
uuid = { version = "1.3", features = ["v4"] }
tokio = { version = "1.35", features = ["macros", "rt-multi-thread"] }
tower-http = { version = "0.5", features = ["cors"] }
uuid = { version = "1.6", features = ["v4"] }
parking_lot = "0.12.1"
headers = "0.4.0"

View File

@@ -1,38 +1,36 @@
use mailparse::addrparse;
use pwhash::bcrypt;
use std::{collections::HashMap, str::FromStr, sync::RwLock};
use std::{collections::HashMap, str::FromStr};
use thiserror::Error;
use uuid::Uuid;
#[derive(Default)]
pub struct AppState {
users: RwLock<HashMap<EmailAddress, Password>>,
tokens: RwLock<HashMap<Uuid, EmailAddress>>,
users: HashMap<EmailAddress, Password>,
tokens: HashMap<Uuid, EmailAddress>,
}
impl AppState {
pub fn create_user(
&self,
&mut self,
credentials: Credentials,
) -> Result<(), CreateUserError> {
let Credentials { email, password } = credentials;
let user_exists = self.users.read().unwrap().get(&email).is_some();
let user_exists = self.users.get(&email).is_some();
if user_exists {
return Err(CreateUserError::UserExists);
}
self.users.write().unwrap().insert(email, password);
self.users.insert(email, password);
Ok(())
}
pub fn login(
&self,
&mut self,
email: EmailAddress,
password: &str,
) -> Result<Uuid, LoginError> {
let valid_credentials = self
.users
.read()
.unwrap()
.get(&email)
.map(|hashed_password| hashed_password.verify(password))
.unwrap_or(false);
@@ -40,16 +38,16 @@ impl AppState {
Err(LoginError::InvalidEmailOrPassword)
} else {
let token = Uuid::new_v4();
self.tokens.write().unwrap().insert(token, email);
self.tokens.insert(token, email);
Ok(token)
}
}
pub fn logout(&self, token: &str) -> Result<(), LogoutError> {
pub fn logout(&mut self, token: &str) -> Result<(), LogoutError> {
let token = token
.parse::<Uuid>()
.map_err(|_| LogoutError::NotLoggedIn)?;
self.tokens.write().unwrap().remove(&token);
self.tokens.remove(&token);
Ok(())
}
@@ -62,8 +60,6 @@ impl AppState {
.map_err(|_| AuthError::NotAuthorized)
.and_then(|token| {
self.tokens
.read()
.unwrap()
.get(&token)
.cloned()
.map(|email| CurrentUser { email, token })

View File

@@ -1,13 +1,16 @@
use api_boundary as json;
use axum::{
extract::{State, TypedHeader},
headers::{authorization::Bearer, Authorization},
extract::State,
http::Method,
response::Json,
routing::{get, post},
Router,
};
use std::{env, sync::Arc};
use axum_extra::TypedHeader;
use headers::{authorization::Bearer, Authorization};
use parking_lot::RwLock;
use std::{env, net::SocketAddr, sync::Arc};
use tokio::net::TcpListener;
use tower_http::cors::{Any, CorsLayer};
mod adapters;
@@ -32,7 +35,7 @@ async fn main() -> anyhow::Result<()> {
}
env_logger::init();
let shared_state = Arc::new(AppState::default());
let shared_state = Arc::new(RwLock::new(AppState::default()));
let cors_layer = CorsLayer::new()
.allow_methods([Method::GET, Method::POST])
@@ -46,11 +49,10 @@ async fn main() -> anyhow::Result<()> {
.route_layer(cors_layer)
.with_state(shared_state);
let addr = "0.0.0.0:3000".parse().unwrap();
log::info!("Listen on {addr}");
axum::Server::bind(&addr)
.serve(app.into_make_service())
.await?;
let addr = "0.0.0.0:3000".parse::<SocketAddr>()?;
log::info!("Start listening on http://{addr}");
let listener = TcpListener::bind(addr).await?;
axum::serve(listener, app.into_make_service()).await?;
Ok(())
}
@@ -73,40 +75,43 @@ enum Error {
}
async fn create_user(
State(state): State<Arc<AppState>>,
State(state): State<Arc<RwLock<AppState>>>,
Json(credentials): Json<json::Credentials>,
) -> Result<()> {
let credentials = Credentials::try_from(credentials)?;
state.create_user(credentials)?;
state.write().create_user(credentials)?;
Ok(Json(()))
}
async fn login(
State(state): State<Arc<AppState>>,
State(state): State<Arc<RwLock<AppState>>>,
Json(credentials): Json<json::Credentials>,
) -> Result<json::ApiToken> {
let json::Credentials { email, password } = credentials;
log::debug!("{email} tries to login");
let email = email.parse().map_err(|_|
// Here we don't want to leak detailed info.
LoginError::InvalidEmailOrPassword)?;
let token = state.login(email, &password).map(|s| s.to_string())?;
// Here we don't want to leak detailed info.
LoginError::InvalidEmailOrPassword)?;
let token = state
.write()
.login(email, &password)
.map(|s| s.to_string())?;
Ok(Json(json::ApiToken { token }))
}
async fn logout(
State(state): State<Arc<AppState>>,
State(state): State<Arc<RwLock<AppState>>>,
TypedHeader(auth): TypedHeader<Authorization<Bearer>>,
) -> Result<()> {
state.logout(auth.token())?;
state.write().logout(auth.token())?;
Ok(Json(()))
}
async fn get_user_info(
State(state): State<Arc<AppState>>,
State(state): State<Arc<RwLock<AppState>>>,
TypedHeader(auth): TypedHeader<Authorization<Bearer>>,
) -> Result<json::UserInfo> {
let user = state.authorize_user(auth.token())?;
let user = state.read().authorize_user(auth.token())?;
let CurrentUser { email, .. } = user;
Ok(Json(json::UserInfo {
email: email.into_string(),

View File

@@ -1,3 +1,2 @@
[toolchain]
channel = "nightly"

View File

@@ -5,6 +5,10 @@ test.describe("Test Router example", () => {
await page.goto("/");
});
test("Starts on correct home page", async({ page }) => {
await expect(page.getByText("Select a contact.")).toBeVisible();
});
const links = [
{ label: "Bill Smith", url: "/0" },
{ label: "Tim Jones", url: "/1" },

View File

@@ -0,0 +1,2 @@
[toolchain]
channel = "nightly"

View File

@@ -1,5 +1,5 @@
[package]
name = "todo_app_sqlite_viz"
name = "server_fns_axum"
version = "0.1.0"
edition = "2021"
@@ -7,48 +7,52 @@ edition = "2021"
crate-type = ["cdylib", "rlib"]
[dependencies]
console_log = "1.0.0"
console_error_panic_hook = "0.1.7"
futures = "0.3.25"
cfg-if = "1.0.0"
leptos = { path = "../../leptos" }
leptos_viz = { path = "../../integrations/viz", optional = true }
console_log = "1.0"
console_error_panic_hook = "0.1"
futures = "0.3"
http = "1.0"
leptos = { path = "../../leptos", features = ["nightly"] }
leptos_axum = { path = "../../integrations/axum", optional = true }
leptos_meta = { path = "../../meta", features = ["nightly"] }
leptos_router = { path = "../../router", features = ["nightly"] }
leptos_reactive = { path = "../../leptos_reactive", features = ["nightly"] }
log = "0.4.17"
simple_logger = "4.0.0"
server_fn = { path = "../../server_fn", features = ["serde-lite", "rkyv", "multipart" ]}
log = "0.4"
simple_logger = "4.0"
serde = { version = "1", features = ["derive"] }
viz = { version = "0.4.8", features = ["serve"], optional = true }
tokio = { version = "1.25.0", features = ["full"], optional = true }
http = { version = "0.2.11" }
sqlx = { version = "0.6.2", features = [
"runtime-tokio-rustls",
"sqlite",
], optional = true }
thiserror = "1.0.38"
axum = { version = "0.7", optional = true }
tower = { version = "0.4", optional = true }
tower-http = { version = "0.5", features = ["fs", "tracing", "trace"], optional = true }
tokio = { version = "1", features = ["full"], optional = true }
thiserror = "1.0"
wasm-bindgen = "0.2"
serde_toml = "0.0.1"
toml = "0.8.8"
web-sys = { version = "0.3.67", features = ["FileList", "File"] }
strum = { version = "0.25.0", features = ["strum_macros", "derive"] }
notify = { version = "6.1.1", optional = true }
pin-project-lite = "0.2.13"
[features]
csr = ["leptos/csr", "leptos_meta/csr", "leptos_router/csr"]
hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate"]
ssr = [
"dep:viz",
"dep:tokio",
"dep:sqlx",
"leptos/ssr",
"leptos_meta/ssr",
"leptos_router/ssr",
"dep:leptos_viz",
"dep:axum",
"dep:tower",
"dep:tower-http",
"dep:tokio",
"leptos/ssr",
"leptos_meta/ssr",
"leptos_router/ssr",
"dep:leptos_axum",
"dep:notify"
]
[package.metadata.cargo-all-features]
denylist = ["viz", "tokio", "sqlx", "leptos_viz"]
denylist = ["axum", "tower", "tower-http", "tokio", "leptos_axum"]
skip_feature_sets = [["csr", "ssr"], ["csr", "hydrate"], ["ssr", "hydrate"]]
[package.metadata.leptos]
# The name used by wasm-bindgen/cargo-leptos for the JS/WASM bundle. Defaults to the crate name
output-name = "todo_app_sqlite_viz"
output-name = "server_fns_axum"
# The site root folder is where cargo-leptos generate all output. WARNING: all content of this folder will be erased on a rebuild. Use it in your server setup.
site-root = "target/site"
# The site-root relative folder where all compiled output (JS, WASM and CSS) is written
@@ -63,7 +67,8 @@ site-addr = "127.0.0.1:3000"
# The port to use for automatic reload monitoring
reload-port = 3001
# [Optional] Command to use when running end2end tests. It will run in the end2end dir.
end2end-cmd = "npx playwright test"
end2end-cmd = "cargo make test-ui"
end2end-dir = "e2e"
# The browserlist query used for optimizing the CSS.
browserquery = "defaults"
# Set by cargo-leptos watch when building with tha tool. Controls whether autoreload JS will be included in the head

View File

@@ -5,4 +5,4 @@ extend = [
[env]
CLIENT_PROCESS_NAME = "todo_app_sqlite_viz"
CLIENT_PROCESS_NAME = "server_fns_axum"

View File

@@ -0,0 +1,19 @@
# Leptos Todo App Sqlite with Axum
This example creates a basic todo app with an Axum backend that uses Leptos' server functions to call sqlx from the client and seamlessly run it on the server.
## Getting Started
See the [Examples README](../README.md) for setup and run instructions.
## E2E Testing
See the [E2E README](./e2e/README.md) for more information about the testing strategy.
## Rendering
See the [SSR Notes](../SSR_NOTES.md) for more information about Server Side Rendering.
## Quick Start
Run `cargo leptos watch` to run this example.

View File

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -0,0 +1,2 @@
[toolchain]
channel = "nightly"

View File

@@ -0,0 +1,634 @@
use futures::StreamExt;
use http::Method;
use leptos::{html::Input, *};
use leptos_meta::{provide_meta_context, Link, Meta, Stylesheet};
use leptos_router::{ActionForm, Route, Router, Routes};
use serde::{de::DeserializeOwned, Deserialize, Serialize};
use server_fn::{
codec::{
Encoding, FromReq, FromRes, GetUrl, IntoReq, IntoRes, MultipartData,
MultipartFormData, Rkyv, SerdeLite, StreamingText, TextStream,
},
request::{ClientReq, Req},
response::{ClientRes, Res},
};
#[cfg(feature = "ssr")]
use std::sync::{
atomic::{AtomicU8, Ordering},
Mutex,
};
use strum::{Display, EnumString};
use wasm_bindgen::JsCast;
use web_sys::{FormData, HtmlFormElement, SubmitEvent};
#[component]
pub fn TodoApp() -> impl IntoView {
provide_meta_context();
view! {
<Meta name="color-scheme" content="dark light"/>
<Link rel="shortcut icon" type_="image/ico" href="/favicon.ico"/>
<Stylesheet id="leptos" href="/pkg/server_fns_axum.css"/>
<Router>
<header>
<h1>"Server Function Demo"</h1>
</header>
<main>
<Routes>
<Route path="" view=HomePage/>
</Routes>
</main>
</Router>
}
}
#[component]
pub fn HomePage() -> impl IntoView {
view! {
<h2>"Some Simple Server Functions"</h2>
<SpawnLocal/>
<WithAnAction/>
<WithActionForm/>
<h2>"Custom Error Types"</h2>
<CustomErrorTypes/>
<h2>"Alternative Encodings"</h2>
<ServerFnArgumentExample/>
<RkyvExample/>
<FileUpload/>
<FileWatcher/>
<CustomEncoding/>
}
}
/// A server function is really just an API call to your server. But it provides a plain async
/// function as a wrapper around that. This means you can call it like any other async code, just
/// by spawning a task with `spawn_local`.
///
/// In reality, you usually want to use a resource to load data from the server or an action to
/// mutate data on the server. But a simple `spawn_local` can make it more obvious what's going on.
#[component]
pub fn SpawnLocal() -> impl IntoView {
/// A basic server function can be called like any other async function.
///
/// You can define a server function at any scope. This one, for example, is only available
/// inside the SpawnLocal component. **However**, note that all server functions are publicly
/// available API endpoints: This scoping means you can only call this server function
/// from inside this component, but it is still available at its URL to any caller, from within
/// your app or elsewhere.
#[server]
pub async fn shouting_text(input: String) -> Result<String, ServerFnError> {
// insert a simulated wait
tokio::time::sleep(std::time::Duration::from_millis(250)).await;
Ok(input.to_ascii_uppercase())
}
let input_ref = NodeRef::<Input>::new();
let (shout_result, set_shout_result) =
create_signal("Click me".to_string());
view! {
<h3>Using <code>spawn_local</code></h3>
<p>
"You can call a server function by using "<code>"spawn_local"</code> " in an event listener. "
"Clicking this button should alert with the uppercase version of the input."
</p>
<input node_ref=input_ref placeholder="Type something here."/>
<button
on:click=move |_| {
let value = input_ref.get().unwrap().value();
spawn_local(async move {
let uppercase_text = shouting_text(value).await.unwrap_or_else(|e| e.to_string());
set_shout_result(uppercase_text);
});
}
>
{shout_result}
</button>
}
}
/// Pretend this is a database and we're storing some rows in memory!
/// This exists only on the server.
#[cfg(feature = "ssr")]
static ROWS: Mutex<Vec<String>> = Mutex::new(Vec::new());
/// Imagine this server function mutates some state on the server, like a database row.
/// Every third time, it will return an error.
///
/// This kind of mutation is often best handled by an Action.
/// Remember, if you're loading data, use a resource; if you're running an occasional action,
/// use an action.
#[server]
pub async fn add_row(text: String) -> Result<usize, ServerFnError> {
static N: AtomicU8 = AtomicU8::new(0);
// insert a simulated wait
tokio::time::sleep(std::time::Duration::from_millis(250)).await;
let nth_run = N.fetch_add(1, Ordering::Relaxed);
// this will print on the server, like any server function
println!("Adding {text:?} to the database!");
if nth_run % 3 == 2 {
Err(ServerFnError::new("Oh no! Couldn't add to database!"))
} else {
let mut rows = ROWS.lock().unwrap();
rows.push(text);
Ok(rows.len())
}
}
/// Simply returns the number of rows.
#[server]
pub async fn get_rows() -> Result<usize, ServerFnError> {
// insert a simulated wait
tokio::time::sleep(std::time::Duration::from_millis(250)).await;
Ok(ROWS.lock().unwrap().len())
}
/// An action abstracts over the process of spawning a future and setting a signal when it
/// resolves. Its .input() signal holds the most recent argument while it's still pending,
/// and its .value() signal holds the most recent result. Its .version() signal can be fed
/// into a resource, telling it to refetch whenever the action has successfully resolved.
///
/// This makes actions useful for mutations, i.e., some server function that invalidates
/// loaded previously loaded from another server function.
#[component]
pub fn WithAnAction() -> impl IntoView {
let input_ref = NodeRef::<Input>::new();
// a server action can be created by using the server function's type name as a generic
// the type name defaults to the PascalCased function name
let action = create_server_action::<AddRow>();
// this resource will hold the total number of rows
// passing it action.version() means it will refetch whenever the action resolves successfully
let row_count = create_resource(action.version(), |_| get_rows());
view! {
<h3>Using <code>create_action</code></h3>
<p>
"Some server functions are conceptually \"mutations,\", which change something on the server. "
"These often work well as actions."
</p>
<input node_ref=input_ref placeholder="Type something here."/>
<button
on:click=move |_| {
let text = input_ref.get().unwrap().value();
action.dispatch(text.into());
// note: technically, this `action` takes `AddRow` (the server fn type) as its
// argument
//
// however, for any one-argument server functions, `From<_>` is implemented between
// the server function type and the type of this single argument
}
>
Submit
</button>
<p>You submitted: {move || format!("{:?}", action.input().get())}</p>
<p>The result was: {move || format!("{:?}", action.value().get())}</p>
<Transition>
<p>Total rows: {row_count}</p>
</Transition>
}
}
/// An <ActionForm/> lets you do the same thing as dispatching an action, but automates the
/// creation of the dispatched argument struct using a <form>. This means it also gracefully
/// degrades well when JS/WASM are not available.
///
/// Try turning off WASM in your browser. The form still works, and successfully displays the error
/// message if the server function returns an error. Otherwise, it loads the new resource data.
#[component]
pub fn WithActionForm() -> impl IntoView {
let action = create_server_action::<AddRow>();
let row_count = create_resource(action.version(), |_| get_rows());
view! {
<h3>Using <code>"<ActionForm/>"</code></h3>
<p>
<code>"<ActionForm/>"</code> "lets you use an HTML " <code>"<form>"</code>
"to call a server function in a way that gracefully degrades."
</p>
<ActionForm action>
<input
// the `name` of the input corresponds to the argument name
name="text"
placeholder="Type something here."
/>
<button> Submit </button>
</ActionForm>
<p>You submitted: {move || format!("{:?}", action.input().get())}</p>
<p>The result was: {move || format!("{:?}", action.value().get())}</p>
<Transition>archive underaligned: need alignment 4 but have alignment 1
<p>Total rows: {row_count}</p>
</Transition>
}
}
/// The plain `#[server]` macro gives sensible defaults for the settings needed to create a server
/// function, but those settings can also be customized. For example, you can set a specific unique
/// path rather than the hashed path, or you can choose a different combination of input and output
/// encodings.
///
/// Arguments to the server macro can be specified as named key-value pairs, like `name = value`.
#[server(
// this server function will be exposed at /api2/custom_path
prefix = "/api2",
endpoint = "custom_path",
// it will take its arguments as a URL-encoded GET request (useful for caching)
input = GetUrl,
// it will return its output using SerdeLite
// (this needs to be enabled with the `serde-lite` feature on the `server_fn` crate
output = SerdeLite,
)]
// You can use the `#[middleware]` macro to add appropriate middleware
// In this case, any `tower::Layer` that takes services of `Request<Body>` will work
#[middleware(crate::middleware::LoggingLayer)]
pub async fn length_of_input(input: String) -> Result<usize, ServerFnError> {
println!("2. Running server function.");
// insert a simulated wait
tokio::time::sleep(std::time::Duration::from_millis(250)).await;
Ok(input.len())
}
#[component]
pub fn ServerFnArgumentExample() -> impl IntoView {
let input_ref = NodeRef::<Input>::new();
let (result, set_result) = create_signal(0);
view! {
<h3>Custom arguments to the <code>#[server]</code> " macro"</h3>
<p>
This example shows how to specify additional behavior including
<ul>
<li>Specific server function <strong>paths</strong></li>
<li>Mixing and matching input and output <strong>encodings</strong></li>
<li>Adding custom <strong>middleware</strong> on a per-server-fn basis</li>
</ul>
</p>
<input node_ref=input_ref placeholder="Type something here."/>
<button
on:click=move |_| {
let value = input_ref.get().unwrap().value();
spawn_local(async move {
let length = length_of_input(value).await.unwrap_or(0);
set_result(length);
});
}
>
Click to see length
</button>
<p>Length is {result}</p>
}
}
/// `server_fn` supports a wide variety of input and output encodings, each of which can be
/// referred to as a PascalCased struct name
/// - Toml
/// - Cbor
/// - Rkyv
/// - etc.
#[server(
input = Rkyv,
output = Rkyv
)]
pub async fn rkyv_example(input: String) -> Result<String, ServerFnError> {
// insert a simulated wait
tokio::time::sleep(std::time::Duration::from_millis(250)).await;
Ok(input.to_ascii_uppercase())
}
#[component]
pub fn RkyvExample() -> impl IntoView {
let input_ref = NodeRef::<Input>::new();
let (input, set_input) = create_signal(String::new());
let rkyv_result = create_resource(input, rkyv_example);
view! {
<h3>Using <code>rkyv</code> encoding</h3>
<p>
</p>
<input node_ref=input_ref placeholder="Type something here."/>
<button
on:click=move |_| {
let value = input_ref.get().unwrap().value();
set_input(value);
}
>
Click to see length
</button>
<p>{input}</p>
<Transition>
{rkyv_result}
</Transition>
}
}
#[component]
pub fn FileUpload() -> impl IntoView {
/// A simple file upload function, which does just returns the length of the file.
///
/// On the server, this uses the `multer` crate, which provides a streaming API.
#[server(
input = MultipartFormData,
)]
pub async fn file_length(
data: MultipartData,
) -> Result<usize, ServerFnError> {
// `.into_inner()` returns the inner `multer` stream
// it is `None` if we call this on the client, but always `Some(_)` on the server, so is safe to
// unwrap
let mut data = data.into_inner().unwrap();
// this will just measure the total number of bytes uploaded
let mut count = 0;
while let Ok(Some(mut field)) = data.next_field().await {
println!("\n[NEXT FIELD]\n");
let name = field.name().unwrap_or_default().to_string();
println!(" [NAME] {name}");
while let Ok(Some(chunk)) = field.chunk().await {
let len = chunk.len();
count += len;
println!(" [CHUNK] {len}");
// in a real server function, you'd do something like saving the file here
}
}
Ok(count)
}
let upload_action = create_action(|data: &FormData| {
let data = data.clone();
// `MultipartData` implements `From<FormData>`
file_length(data.into())
});
view! {
<h3>File Upload</h3>
<p>Uploading files is fairly easy using multipart form data.</p>
<form on:submit=move |ev: SubmitEvent| {
ev.prevent_default();
let target = ev.target().unwrap().unchecked_into::<HtmlFormElement>();
let form_data = FormData::new_with_form(&target).unwrap();
upload_action.dispatch(form_data);
}>
<input type="file" name="file_to_upload"/>
<input type="submit"/>
</form>
<p>
{move || if upload_action.input().get().is_none() && upload_action.value().get().is_none() {
"Upload a file.".to_string()
} else if upload_action.pending().get() {
"Uploading...".to_string()
} else if let Some(Ok(value)) = upload_action.value().get() {
value.to_string()
} else {
format!("{:?}", upload_action.value().get())
}}
</p>
}
}
#[component]
pub fn FileWatcher() -> impl IntoView {
#[server(input = GetUrl, output = StreamingText)]
pub async fn watched_files() -> Result<TextStream, ServerFnError> {
use notify::{
Config, Error, Event, RecommendedWatcher, RecursiveMode, Watcher,
};
use std::path::Path;
let (tx, rx) = futures::channel::mpsc::unbounded();
let mut watcher = RecommendedWatcher::new(
move |res: Result<Event, Error>| {
if let Ok(ev) = res {
if let Some(path) = ev.paths.last() {
let filename = path
.file_name()
.unwrap()
.to_str()
.unwrap()
.to_string();
_ = tx.unbounded_send(filename); //res);
}
}
},
Config::default(),
)?;
watcher
.watch(Path::new("./watched_files"), RecursiveMode::Recursive)?;
std::mem::forget(watcher);
Ok(TextStream::from(rx))
}
let (files, set_files) = create_signal(Vec::new());
create_effect(move |_| {
spawn_local(async move {
while let Some(res) =
watched_files().await.unwrap().into_inner().next().await
{
if let Ok(filename) = res {
set_files.update(|n| n.push(filename));
}
}
});
});
view! {
<h3>Watching files and returning a streaming response</h3>
<p>Files changed since you loaded the page:</p>
<ul>
{move || files.get().into_iter().map(|file| view! { <li><code>{file}</code></li> }).collect::<Vec<_>>()}
</ul>
<p><em>Add or remove some text files in the <code>watched_files</code> directory and see the list of changes here.</em></p>
}
}
/// The `ServerFnError` type is generic over a custom error type, which defaults to `NoCustomError`
/// for backwards compatibility and to support the most common use case.
///
/// A custom error type should implement `FromStr` and `Display`, which allows it to be converted
/// into and from a string easily to be sent over the network. It does *not* need to implement
/// `Serialize` and `Deserialize`, although these can be used to generate the `FromStr`/`Display`
/// implementations if you'd like. However, it's much lighter weight to use something like `strum`
/// simply to generate those trait implementations.
#[server]
pub async fn ascii_uppercase(
text: String,
) -> Result<String, ServerFnError<InvalidArgument>> {
if text.len() < 5 {
Err(InvalidArgument::TooShort.into())
} else if text.len() > 15 {
Err(InvalidArgument::TooLong.into())
} else if text.is_ascii() {
Ok(text.to_ascii_uppercase())
} else {
Err(InvalidArgument::NotAscii.into())
}
}
// The EnumString and Display derive macros are provided by strum
#[derive(Debug, Clone, EnumString, Display)]
pub enum InvalidArgument {
TooShort,
TooLong,
NotAscii,
}
#[component]
pub fn CustomErrorTypes() -> impl IntoView {
let input_ref = NodeRef::<Input>::new();
let (result, set_result) = create_signal(None);
view! {
<h3>Using custom error types</h3>
<p>
"Server functions can use a custom error type that is preserved across the network boundary."
</p>
<p>
"Try typing a message that is between 5 and 15 characters of ASCII text below. Then try breaking \
the rules!"
</p>
<input node_ref=input_ref placeholder="Type something here."/>
<button
on:click=move |_| {
let value = input_ref.get().unwrap().value();
spawn_local(async move {
let data = ascii_uppercase(value).await;
set_result(Some(data));
});
}
>
"Submit"
</button>
<p>
{move || format!("{:?}", result.get())}
</p>
}
}
/// Server function encodings are just types that implement a few traits.
/// This means that you can implement your own encodings, by implementing those traits!
///
/// Here, we'll create a custom encoding that serializes and deserializes the server fn
/// using TOML. Why would you ever want to do this? I don't know, but you can!
pub struct Toml;
/// A newtype wrapper around server fn data that will be TOML-encoded.
///
/// This is needed because of Rust rules around implementing foreign traits for foreign types.
/// It will be fed into the `custom = ` argument to the server fn below.
#[derive(Serialize, Deserialize)]
pub struct TomlEncoded<T>(T);
impl Encoding for Toml {
const CONTENT_TYPE: &'static str = "application/toml";
const METHOD: Method = Method::POST;
}
impl<T, Request, Err> IntoReq<Toml, Request, Err> for TomlEncoded<T>
where
Request: ClientReq<Err>,
T: Serialize,
{
fn into_req(
self,
path: &str,
accepts: &str,
) -> Result<Request, ServerFnError<Err>> {
let data = toml::to_string(&self.0)
.map_err(|e| ServerFnError::Serialization(e.to_string()))?;
Request::try_new_post(path, Toml::CONTENT_TYPE, accepts, data)
}
}
impl<T, Request, Err> FromReq<Toml, Request, Err> for TomlEncoded<T>
where
Request: Req<Err> + Send,
T: DeserializeOwned,
{
async fn from_req(req: Request) -> Result<Self, ServerFnError<Err>> {
let string_data = req.try_into_string().await?;
toml::from_str::<T>(&string_data)
.map(TomlEncoded)
.map_err(|e| ServerFnError::Args(e.to_string()))
}
}
impl<T, Response, Err> IntoRes<Toml, Response, Err> for TomlEncoded<T>
where
Response: Res<Err>,
T: Serialize + Send,
{
async fn into_res(self) -> Result<Response, ServerFnError<Err>> {
let data = toml::to_string(&self.0)
.map_err(|e| ServerFnError::Serialization(e.to_string()))?;
Response::try_from_string(Toml::CONTENT_TYPE, data)
}
}
impl<T, Response, Err> FromRes<Toml, Response, Err> for TomlEncoded<T>
where
Response: ClientRes<Err> + Send,
T: DeserializeOwned,
{
async fn from_res(res: Response) -> Result<Self, ServerFnError<Err>> {
let data = res.try_into_string().await?;
toml::from_str(&data)
.map(TomlEncoded)
.map_err(|e| ServerFnError::Deserialization(e.to_string()))
}
}
#[derive(Serialize, Deserialize)]
pub struct WhyNotResult {
original: String,
modified: String,
}
#[server(
input = Toml,
output = Toml,
custom = TomlEncoded
)]
pub async fn why_not(
original: String,
addition: String,
) -> Result<TomlEncoded<WhyNotResult>, ServerFnError> {
// insert a simulated wait
tokio::time::sleep(std::time::Duration::from_millis(250)).await;
Ok(TomlEncoded(WhyNotResult {
modified: format!("{original}{addition}"),
original,
}))
}
#[component]
pub fn CustomEncoding() -> impl IntoView {
let input_ref = NodeRef::<Input>::new();
let (result, set_result) = create_signal("foo".to_string());
view! {
<h3>Custom encodings</h3>
<p>
"This example creates a custom encoding that sends server fn data using TOML. Why? Well... why not?"
</p>
<input node_ref=input_ref placeholder="Type something here."/>
<button
on:click=move |_| {
let value = input_ref.get().unwrap().value();
spawn_local(async move {
let new_value = why_not(value, ", but in TOML!!!".to_string()).await.unwrap();
set_result(new_value.0.modified);
});
}
>
Submit
</button>
<p>{result}</p>
}
}

View File

@@ -1,8 +1,7 @@
use crate::errors::TodoAppError;
use cfg_if::cfg_if;
use leptos::{Errors, *};
#[cfg(feature = "ssr")]
use leptos_viz::ResponseOptions;
use leptos_axum::ResponseOptions;
// A basic function to display errors served by the error boundaries. Feel free to do more complicated things
// here than just displaying them
@@ -24,18 +23,17 @@ pub fn ErrorTemplate(
let errors: Vec<TodoAppError> = errors
.get()
.into_iter()
.filter_map(|(_k, v)| v.downcast_ref::<TodoAppError>().cloned())
.filter_map(|(_, v)| v.downcast_ref::<TodoAppError>().cloned())
.collect();
// Only the response code for the first error is actually sent from the server
// this may be customized by the specific application
cfg_if! {
if #[cfg(feature="ssr")]{
#[cfg(feature = "ssr")]
{
let response = use_context::<ResponseOptions>();
if let Some(response) = response{
response.set_status(errors[0].status_code());
if let Some(response) = response {
response.set_status(errors[0].status_code());
}
}
}
view! {
@@ -46,11 +44,10 @@ pub fn ErrorTemplate(
// a unique key for each item as a reference
key=|(index, _error)| *index
// renders each item to a view
children= move |error| {
children=move |error| {
let error_string = error.1.to_string();
let error_code= error.1.status_code();
view! {
<h2>{error_code.to_string()}</h2>
<p>"Error: " {error_string}</p>
}

View File

@@ -0,0 +1,50 @@
use crate::{error_template::ErrorTemplate, errors::TodoAppError};
use axum::{
body::Body,
extract::State,
http::{Request, Response, StatusCode, Uri},
response::{IntoResponse, Response as AxumResponse},
};
use leptos::{view, Errors, LeptosOptions};
use tower::ServiceExt;
use tower_http::services::ServeDir;
pub async fn file_and_error_handler(
uri: Uri,
State(options): State<LeptosOptions>,
req: Request<Body>,
) -> AxumResponse {
let root = options.site_root.clone();
let res = get_static_file(uri.clone(), &root).await.unwrap();
if res.status() == StatusCode::OK {
res.into_response()
} else {
let mut errors = Errors::default();
errors.insert_with_default_key(TodoAppError::NotFound);
let handler = leptos_axum::render_app_to_stream(
options.to_owned(),
move || view! {<ErrorTemplate outside_errors=errors.clone()/>},
);
handler(req).await.into_response()
}
}
async fn get_static_file(
uri: Uri,
root: &str,
) -> Result<Response<Body>, (StatusCode, String)> {
let req = Request::builder()
.uri(uri.clone())
.body(Body::empty())
.unwrap();
// `ServeDir` implements `tower::Service` so we can call it with `tower::ServiceExt::oneshot`
// This path is relative to the cargo root
match ServeDir::new(root).oneshot(req).await {
Ok(res) => Ok(res.into_response()),
Err(err) => Err((
StatusCode::INTERNAL_SERVER_ERROR,
format!("Something went wrong: {err}"),
)),
}
}

View File

@@ -0,0 +1,17 @@
pub mod app;
pub mod error_template;
pub mod errors;
#[cfg(feature = "ssr")]
pub mod fallback;
#[cfg(feature = "ssr")]
pub mod middleware;
#[wasm_bindgen::prelude::wasm_bindgen]
pub fn hydrate() {
use crate::app::TodoApp;
_ = console_log::init_with_level(log::Level::Error);
console_error_panic_hook::set_once();
leptos::mount_to_body(TodoApp);
}

View File

@@ -0,0 +1,31 @@
use crate::{app::*, fallback::file_and_error_handler};
use axum::Router;
use leptos::{get_configuration, logging};
use leptos_axum::{generate_route_list, LeptosRoutes};
use server_fns_axum::*;
#[tokio::main]
async fn main() {
simple_logger::init_with_level(log::Level::Error)
.expect("couldn't initialize logging");
// Setting this to None means we'll be using cargo-leptos and its env vars
let conf = get_configuration(None).await.unwrap();
let leptos_options = conf.leptos_options;
let addr = leptos_options.site_addr;
let routes = generate_route_list(TodoApp);
// build our application with a route
let app = Router::new()
.leptos_routes(&leptos_options, routes, TodoApp)
.fallback(file_and_error_handler)
.with_state(leptos_options);
// run our app with hyper
// `axum::Server` is a re-export of `hyper::Server`
let listener = tokio::net::TcpListener::bind(&addr).await.unwrap();
logging::log!("listening on http://{}", &addr);
axum::serve(listener, app.into_make_service())
.await
.unwrap();
}

View File

@@ -0,0 +1,72 @@
use axum::body::Body;
use http::Request;
use pin_project_lite::pin_project;
use std::{
future::Future,
pin::Pin,
task::{Context, Poll},
};
use tower::{Layer, Service};
pub struct LoggingLayer;
impl<S> Layer<S> for LoggingLayer {
type Service = LoggingService<S>;
fn layer(&self, inner: S) -> Self::Service {
LoggingService { inner }
}
}
pub struct LoggingService<T> {
inner: T,
}
impl<T> Service<Request<Body>> for LoggingService<T>
where
T: Service<Request<Body>>,
{
type Response = T::Response;
type Error = T::Error;
type Future = LoggingServiceFuture<T::Future>;
fn poll_ready(
&mut self,
cx: &mut Context<'_>,
) -> Poll<Result<(), Self::Error>> {
self.inner.poll_ready(cx)
}
fn call(&mut self, req: Request<Body>) -> Self::Future {
println!("1. Running my middleware!");
LoggingServiceFuture {
inner: self.inner.call(req),
}
}
}
pin_project! {
pub struct LoggingServiceFuture<T> {
#[pin]
inner: T,
}
}
impl<T> Future for LoggingServiceFuture<T>
where
T: Future,
{
type Output = T::Output;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let this = self.project();
match this.inner.poll(cx) {
Poll::Pending => Poll::Pending,
Poll::Ready(output) => {
println!("3. Running my middleware!");
Poll::Ready(output)
}
}
}
}

View File

@@ -1,3 +1,3 @@
.pending {
color: purple;
}
}

View File

@@ -7,40 +7,40 @@ edition = "2021"
crate-type = ["cdylib", "rlib"]
[dependencies]
anyhow = "1.0.66"
console_log = "1.0.0"
rand = { version = "0.8.5", features = ["min_const_gen"], optional = true }
console_error_panic_hook = "0.1.7"
futures = "0.3.25"
cfg-if = "1.0.0"
anyhow = "1.0"
console_log = "1.0"
rand = { version = "0.8", features = ["min_const_gen"], optional = true }
console_error_panic_hook = "0.1"
futures = "0.3"
leptos = { path = "../../leptos", features = ["nightly"] }
leptos_meta = { path = "../../meta", features = ["nightly"] }
leptos_axum = { path = "../../integrations/axum", optional = true }
leptos_router = { path = "../../router", features = ["nightly"] }
log = "0.4.17"
simple_logger = "4.0.0"
serde = { version = "1.0.148", features = ["derive"] }
axum = { version = "0.6.1", optional = true, features=["macros"] }
tower = { version = "0.4.13", optional = true }
tower-http = { version = "0.4", features = ["fs"], optional = true }
tokio = { version = "1.22.0", features = ["full"], optional = true }
http = { version = "0.2.11" }
log = "0.4"
simple_logger = "4.0"
serde = { version = "1.0", features = ["derive"] }
axum = { version = "0.7", optional = true, features=["macros"] }
tower = { version = "0.4", optional = true }
tower-http = { version = "0.5", features = ["fs"], optional = true }
tokio = { version = "1", features = ["full"], optional = true }
http = { version = "1.0" }
sqlx = { version = "0.7.2", features = [
"runtime-tokio-rustls",
"sqlite",
], optional = true }
thiserror = "1.0.38"
thiserror = "1.0"
wasm-bindgen = "0.2"
axum_session_auth = { version = "0.9.0", features = [
axum_session_auth = { version = "0.10", features = [
"sqlite-rustls",
], optional = true }
axum_session = { version = "0.9.0", features = [
axum_session = { version = "0.10", features = [
"sqlite-rustls",
], optional = true }
bcrypt = { version = "0.14", optional = true }
async-trait = { version = "0.1.64", optional = true }
bcrypt = { version = "0.15", optional = true }
async-trait = { version = "0.1", optional = true }
[features]
default = ["ssr"]
hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate"]
ssr = [
"dep:axum",

Binary file not shown.

View File

@@ -5,11 +5,11 @@
"systems": "systems"
},
"locked": {
"lastModified": 1681202837,
"narHash": "sha256-H+Rh19JDwRtpVPAWp64F+rlEtxUWBAQW28eAi3SRSzg=",
"lastModified": 1701680307,
"narHash": "sha256-kAuep2h5ajznlPMD9rnQyffWG8EM/C73lejGofXvdM8=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "cfacdce06f30d2b68473a46042957675eebb3401",
"rev": "4022d587cbbfd70fe950c1e2083a02621806a725",
"type": "github"
},
"original": {
@@ -38,11 +38,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1672580127,
"narHash": "sha256-3lW3xZslREhJogoOkjeZtlBtvFMyxHku7I/9IVehhT8=",
"lastModified": 1685573264,
"narHash": "sha256-Zffu01pONhs/pqH07cjlF10NnMDLok8ix5Uk4rhOnZQ=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "0874168639713f547c05947c76124f78441ea46c",
"rev": "380be19fbd2d9079f677978361792cb25e8a3635",
"type": "github"
},
"original": {
@@ -67,11 +67,11 @@
]
},
"locked": {
"lastModified": 1681525152,
"narHash": "sha256-KzI+ILcmU03iFWtB+ysPqtNmp8TP8v1BBReTuPP8MJY=",
"lastModified": 1703902408,
"narHash": "sha256-qXdWvu+tlgNjeoz8yQMRKSom6QyRROfgpmeOhwbujqw=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "b6f8d87208336d7cb85003b2e439fc707c38f92a",
"rev": "319f57cd2c34348c55970a4bf2b35afe82088681",
"type": "github"
},
"original": {

View File

@@ -0,0 +1,2 @@
[toolchain]
channel = "nightly"

View File

@@ -1,17 +1,7 @@
use cfg_if::cfg_if;
use leptos::*;
use serde::{Deserialize, Serialize};
use std::collections::HashSet;
cfg_if! {
if #[cfg(feature = "ssr")] {
use sqlx::SqlitePool;
use axum_session_auth::{SessionSqlitePool, Authentication, HasPermission};
use bcrypt::{hash, verify, DEFAULT_COST};
use crate::todo::{pool, auth};
pub type AuthSession = axum_session_auth::AuthSession<User, i64, SessionSqlitePool, SqlitePool>;
}}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct User {
pub id: i64,
@@ -33,17 +23,33 @@ impl Default for User {
}
}
cfg_if! {
if #[cfg(feature = "ssr")] {
use async_trait::async_trait;
#[cfg(feature = "ssr")]
pub mod ssr {
pub use super::User;
pub use axum_session_auth::{
Authentication, HasPermission, SessionSqlitePool,
};
pub use sqlx::SqlitePool;
pub use std::collections::HashSet;
pub type AuthSession = axum_session_auth::AuthSession<
User,
i64,
SessionSqlitePool,
SqlitePool,
>;
pub use crate::todo::ssr::{auth, pool};
pub use async_trait::async_trait;
pub use bcrypt::{hash, verify, DEFAULT_COST};
impl User {
pub async fn get(id: i64, pool: &SqlitePool) -> Option<Self> {
let sqluser = sqlx::query_as::<_, SqlUser>("SELECT * FROM users WHERE id = ?")
.bind(id)
.fetch_one(pool)
.await
.ok()?;
let sqluser = sqlx::query_as::<_, SqlUser>(
"SELECT * FROM users WHERE id = ?",
)
.bind(id)
.fetch_one(pool)
.await
.ok()?;
//lets just get all the tokens the user can use, we will only use the full permissions if modifing them.
let sql_user_perms = sqlx::query_as::<_, SqlPermissionTokens>(
@@ -57,12 +63,17 @@ if #[cfg(feature = "ssr")] {
Some(sqluser.into_user(Some(sql_user_perms)))
}
pub async fn get_from_username(name: String, pool: &SqlitePool) -> Option<Self> {
let sqluser = sqlx::query_as::<_, SqlUser>("SELECT * FROM users WHERE username = ?")
.bind(name)
.fetch_one(pool)
.await
.ok()?;
pub async fn get_from_username(
name: String,
pool: &SqlitePool,
) -> Option<Self> {
let sqluser = sqlx::query_as::<_, SqlUser>(
"SELECT * FROM users WHERE username = ?",
)
.bind(name)
.fetch_one(pool)
.await
.ok()?;
//lets just get all the tokens the user can use, we will only use the full permissions if modifing them.
let sql_user_perms = sqlx::query_as::<_, SqlPermissionTokens>(
@@ -84,7 +95,10 @@ if #[cfg(feature = "ssr")] {
#[async_trait]
impl Authentication<User, i64, SqlitePool> for User {
async fn load_user(userid: i64, pool: Option<&SqlitePool>) -> Result<User, anyhow::Error> {
async fn load_user(
userid: i64,
pool: Option<&SqlitePool>,
) -> Result<User, anyhow::Error> {
let pool = pool.unwrap();
User::get(userid, pool)
@@ -120,7 +134,10 @@ if #[cfg(feature = "ssr")] {
}
impl SqlUser {
pub fn into_user(self, sql_user_perms: Option<Vec<SqlPermissionTokens>>) -> User {
pub fn into_user(
self,
sql_user_perms: Option<Vec<SqlPermissionTokens>>,
) -> User {
User {
id: self.id,
username: self.username,
@@ -137,15 +154,16 @@ if #[cfg(feature = "ssr")] {
}
}
}
}
#[server(Foo, "/api")]
#[server]
pub async fn foo() -> Result<String, ServerFnError> {
Ok(String::from("Bar!"))
}
#[server(GetUser, "/api")]
#[server]
pub async fn get_user() -> Result<Option<User>, ServerFnError> {
use crate::todo::ssr::auth;
let auth = auth()?;
Ok(auth.current_user)
@@ -157,14 +175,14 @@ pub async fn login(
password: String,
remember: Option<String>,
) -> Result<(), ServerFnError> {
use self::ssr::*;
let pool = pool()?;
let auth = auth()?;
let user: User = User::get_from_username(username, &pool)
.await
.ok_or_else(|| {
ServerFnError::ServerError("User does not exist.".into())
})?;
.ok_or_else(|| ServerFnError::new("User does not exist."))?;
match verify(password, &user.password)? {
true => {
@@ -186,6 +204,8 @@ pub async fn signup(
password_confirmation: String,
remember: Option<String>,
) -> Result<(), ServerFnError> {
use self::ssr::*;
let pool = pool()?;
let auth = auth()?;
@@ -207,9 +227,7 @@ pub async fn signup(
User::get_from_username(username, &pool)
.await
.ok_or_else(|| {
ServerFnError::ServerError(
"Signup failed: User does not exist.".into(),
)
ServerFnError::new("Signup failed: User does not exist.")
})?;
auth.login_user(user.id);
@@ -222,6 +240,8 @@ pub async fn signup(
#[server(Logout, "/api")]
pub async fn logout() -> Result<(), ServerFnError> {
use self::ssr::*;
let auth = auth()?;
auth.logout_user();

View File

@@ -1,5 +1,4 @@
use crate::errors::TodoAppError;
use cfg_if::cfg_if;
use leptos::{Errors, *};
#[cfg(feature = "ssr")]
use leptos_axum::ResponseOptions;
@@ -29,13 +28,12 @@ pub fn ErrorTemplate(
// Only the response code for the first error is actually sent from the server
// this may be customized by the specific application
cfg_if! {
if #[cfg(feature="ssr")]{
#[cfg(feature = "ssr")]
{
let response = use_context::<ResponseOptions>();
if let Some(response) = response{
response.set_status(errors[0].status_code());
if let Some(response) = response {
response.set_status(errors[0].status_code());
}
}
}
view! {

View File

@@ -1,47 +1,50 @@
use cfg_if::cfg_if;
use crate::{error_template::ErrorTemplate, errors::TodoAppError};
use axum::{
body::Body,
extract::State,
http::{Request, Response, StatusCode, Uri},
response::{IntoResponse, Response as AxumResponse},
};
use leptos::{view, Errors, LeptosOptions};
use tower::ServiceExt;
use tower_http::services::ServeDir;
cfg_if! {
if #[cfg(feature = "ssr")] {
use axum::{
body::{boxed, Body, BoxBody},
extract::State,
response::IntoResponse,
http::{Request, Response, StatusCode, Uri},
};
use axum::response::Response as AxumResponse;
use tower::ServiceExt;
use tower_http::services::ServeDir;
use leptos::{LeptosOptions, Errors, view};
use crate::error_template::ErrorTemplate;
use crate::errors::TodoAppError;
pub async fn file_and_error_handler(
uri: Uri,
State(options): State<LeptosOptions>,
req: Request<Body>,
) -> AxumResponse {
let root = options.site_root.clone();
let res = get_static_file(uri.clone(), &root).await.unwrap();
pub async fn file_and_error_handler(uri: Uri, State(options): State<LeptosOptions>, req: Request<Body>) -> AxumResponse {
let root = options.site_root.clone();
let res = get_static_file(uri.clone(), &root).await.unwrap();
if res.status() == StatusCode::OK {
res.into_response()
} else{
let mut errors = Errors::default();
errors.insert_with_default_key(TodoAppError::NotFound);
let handler = leptos_axum::render_app_to_stream(options.to_owned(), move || view!{<ErrorTemplate outside_errors=errors.clone()/>});
handler(req).await.into_response()
}
if res.status() == StatusCode::OK {
res.into_response()
} else {
let mut errors = Errors::default();
errors.insert_with_default_key(TodoAppError::NotFound);
let handler = leptos_axum::render_app_to_stream(
options.to_owned(),
move || view! {<ErrorTemplate outside_errors=errors.clone()/>},
);
handler(req).await.into_response()
}
}
async fn get_static_file(uri: Uri, root: &str) -> Result<Response<BoxBody>, (StatusCode, String)> {
let req = Request::builder().uri(uri.clone()).body(Body::empty()).unwrap();
// `ServeDir` implements `tower::Service` so we can call it with `tower::ServiceExt::oneshot`
// This path is relative to the cargo root
match ServeDir::new(root).oneshot(req).await {
Ok(res) => Ok(res.map(boxed)),
Err(err) => Err((
StatusCode::INTERNAL_SERVER_ERROR,
format!("Something went wrong: {err}"),
)),
}
async fn get_static_file(
uri: Uri,
root: &str,
) -> Result<Response<Body>, (StatusCode, String)> {
let req = Request::builder()
.uri(uri.clone())
.body(Body::empty())
.unwrap();
// `ServeDir` implements `tower::Service` so we can call it with `tower::ServiceExt::oneshot`
// This path is relative to the cargo root
match ServeDir::new(root).oneshot(req).await {
Ok(res) => Ok(res.into_response()),
Err(err) => Err((
StatusCode::INTERNAL_SERVER_ERROR,
format!("Something went wrong: {err}"),
)),
}
}
}

View File

@@ -1,27 +1,18 @@
use cfg_if::cfg_if;
pub mod auth;
pub mod error_template;
pub mod errors;
#[cfg(feature = "ssr")]
pub mod fallback;
#[cfg(feature = "ssr")]
pub mod state;
pub mod todo;
// Needs to be in lib.rs AFAIK because wasm-bindgen needs us to be compiling a lib. I may be wrong.
cfg_if! {
if #[cfg(feature = "hydrate")] {
use wasm_bindgen::prelude::wasm_bindgen;
use crate::todo::*;
use leptos::view;
#[cfg(feature = "hydrate")]
#[wasm_bindgen::prelude::wasm_bindgen]
pub fn hydrate() {
use crate::todo::*;
_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();
#[wasm_bindgen]
pub fn hydrate() {
_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();
leptos::mount_to_body(|| {
view! { <TodoApp/> }
});
}
}
leptos::mount_to_body(TodoApp);
}

View File

@@ -1,117 +1,131 @@
use cfg_if::cfg_if;
use axum::{
body::Body as AxumBody,
extract::{Path, State},
http::Request,
response::{IntoResponse, Response},
routing::get,
Router,
};
use axum_session::{SessionConfig, SessionLayer, SessionStore};
use axum_session_auth::{AuthConfig, AuthSessionLayer, SessionSqlitePool};
use leptos::{get_configuration, logging::log, provide_context};
use leptos_axum::{
generate_route_list, handle_server_fns_with_context, LeptosRoutes,
};
use session_auth_axum::{
auth::{ssr::AuthSession, User},
fallback::file_and_error_handler,
state::AppState,
todo::*,
};
use sqlx::{sqlite::SqlitePoolOptions, SqlitePool};
// boilerplate to run in different modes
cfg_if! {
if #[cfg(feature = "ssr")] {
use axum::{
response::{Response, IntoResponse},
routing::get,
extract::{Path, State, RawQuery},
http::{Request, header::HeaderMap},
body::Body as AxumBody,
Router,
};
use session_auth_axum::todo::*;
use session_auth_axum::auth::*;
use session_auth_axum::state::AppState;
use session_auth_axum::fallback::file_and_error_handler;
use leptos_axum::{generate_route_list, LeptosRoutes, handle_server_fns_with_context};
use leptos::{logging::log, provide_context, get_configuration};
use sqlx::{SqlitePool, sqlite::SqlitePoolOptions};
use axum_session::{SessionConfig, SessionLayer, SessionStore};
use axum_session_auth::{AuthSessionLayer, AuthConfig, SessionSqlitePool};
async fn server_fn_handler(
State(app_state): State<AppState>,
auth_session: AuthSession,
path: Path<String>,
request: Request<AxumBody>,
) -> impl IntoResponse {
log!("{:?}", path);
async fn server_fn_handler(State(app_state): State<AppState>, auth_session: AuthSession, path: Path<String>, headers: HeaderMap, raw_query: RawQuery,
request: Request<AxumBody>) -> impl IntoResponse {
log!("{:?}", path);
handle_server_fns_with_context(path, headers, raw_query, move || {
handle_server_fns_with_context(
move || {
provide_context(auth_session.clone());
provide_context(app_state.pool.clone());
}, request).await
},
request,
)
.await
}
async fn leptos_routes_handler(
auth_session: AuthSession,
State(app_state): State<AppState>,
req: Request<AxumBody>,
) -> Response {
let handler = leptos_axum::render_route_with_context(
app_state.leptos_options.clone(),
app_state.routes.clone(),
move || {
provide_context(auth_session.clone());
provide_context(app_state.pool.clone());
},
TodoApp,
);
handler(req).await.into_response()
}
#[tokio::main]
async fn main() {
simple_logger::init_with_level(log::Level::Info)
.expect("couldn't initialize logging");
let pool = SqlitePoolOptions::new()
.connect("sqlite:Todos.db")
.await
.expect("Could not make pool.");
// Auth section
let session_config =
SessionConfig::default().with_table_name("axum_sessions");
let auth_config = AuthConfig::<i64>::default();
let session_store = SessionStore::<SessionSqlitePool>::new(
Some(pool.clone().into()),
session_config,
)
.await
.unwrap();
if let Err(e) = sqlx::migrate!().run(&pool).await {
eprintln!("{e:?}");
}
async fn leptos_routes_handler(auth_session: AuthSession, State(app_state): State<AppState>, req: Request<AxumBody>) -> Response{
let handler = leptos_axum::render_route_with_context(app_state.leptos_options.clone(),
app_state.routes.clone(),
move || {
provide_context(auth_session.clone());
provide_context(app_state.pool.clone());
},
TodoApp
);
handler(req).await.into_response()
}
// Explicit server function registration is no longer required
// on the main branch. On 0.3.0 and earlier, uncomment the lines
// below to register the server functions.
// _ = GetTodos::register();
// _ = AddTodo::register();
// _ = DeleteTodo::register();
// _ = Login::register();
// _ = Logout::register();
// _ = Signup::register();
// _ = GetUser::register();
// _ = Foo::register();
#[tokio::main]
async fn main() {
simple_logger::init_with_level(log::Level::Info).expect("couldn't initialize logging");
// Setting this to None means we'll be using cargo-leptos and its env vars
let conf = get_configuration(None).await.unwrap();
let leptos_options = conf.leptos_options;
let addr = leptos_options.site_addr;
let routes = generate_route_list(TodoApp);
let pool = SqlitePoolOptions::new()
.connect("sqlite:Todos.db")
.await
.expect("Could not make pool.");
let app_state = AppState {
leptos_options,
pool: pool.clone(),
routes: routes.clone(),
};
// Auth section
let session_config = SessionConfig::default().with_table_name("axum_sessions");
let auth_config = AuthConfig::<i64>::default();
let session_store = SessionStore::<SessionSqlitePool>::new(Some(pool.clone().into()), session_config).await.unwrap();
sqlx::migrate!()
.run(&pool)
.await
.expect("could not run SQLx migrations");
// Explicit server function registration is no longer required
// on the main branch. On 0.3.0 and earlier, uncomment the lines
// below to register the server functions.
// _ = GetTodos::register();
// _ = AddTodo::register();
// _ = DeleteTodo::register();
// _ = Login::register();
// _ = Logout::register();
// _ = Signup::register();
// _ = GetUser::register();
// _ = Foo::register();
// Setting this to None means we'll be using cargo-leptos and its env vars
let conf = get_configuration(None).await.unwrap();
let leptos_options = conf.leptos_options;
let addr = leptos_options.site_addr;
let routes = generate_route_list(TodoApp);
let app_state = AppState{
leptos_options,
pool: pool.clone(),
routes: routes.clone(),
};
// build our application with a route
let app = Router::new()
.route("/api/*fn_name", get(server_fn_handler).post(server_fn_handler))
.leptos_routes_with_handler(routes, get(leptos_routes_handler) )
// build our application with a route
let app = Router::new()
.route(
"/api/*fn_name",
get(server_fn_handler).post(server_fn_handler),
)
.leptos_routes_with_handler(routes, get(leptos_routes_handler))
.fallback(file_and_error_handler)
.layer(AuthSessionLayer::<User, i64, SessionSqlitePool, SqlitePool>::new(Some(pool.clone()))
.with_config(auth_config))
.layer(
AuthSessionLayer::<User, i64, SessionSqlitePool, SqlitePool>::new(
Some(pool.clone()),
)
.with_config(auth_config),
)
.layer(SessionLayer::new(session_store))
.with_state(app_state);
// run our app with hyper
// `axum::Server` is a re-export of `hyper::Server`
log!("listening on http://{}", &addr);
axum::Server::bind(&addr)
.serve(app.into_make_service())
.await
.unwrap();
}
}
// client-only stuff for Trunk
else {
pub fn main() {
// This example cannot be built as a trunk standalone CSR-only app.
// Only the server may directly connect to the database.
}
}
// run our app with hyper
// `axum::Server` is a re-export of `hyper::Server`
log!("listening on http://{}", &addr);
let listener = tokio::net::TcpListener::bind(&addr).await.unwrap();
axum::serve(listener, app.into_make_service())
.await
.unwrap();
}

View File

@@ -1,18 +1,13 @@
use cfg_if::cfg_if;
cfg_if! {
if #[cfg(feature = "ssr")] {
use leptos::LeptosOptions;
use sqlx::SqlitePool;
use axum::extract::FromRef;
use leptos::LeptosOptions;
use leptos_router::RouteListing;
use sqlx::SqlitePool;
/// This takes advantage of Axum's SubStates feature by deriving FromRef. This is the only way to have more than one
/// item in Axum's State. Leptos requires you to have leptosOptions in your State struct for the leptos route handlers
#[derive(FromRef, Debug, Clone)]
pub struct AppState{
pub struct AppState {
pub leptos_options: LeptosOptions,
pub pool: SqlitePool,
pub routes: Vec<RouteListing>,
}
}
}

View File

@@ -1,5 +1,4 @@
use crate::{auth::*, error_template::ErrorTemplate};
use cfg_if::cfg_if;
use leptos::*;
use leptos_meta::*;
use leptos_router::*;
@@ -14,40 +13,41 @@ pub struct Todo {
completed: bool,
}
cfg_if! {
if #[cfg(feature = "ssr")] {
#[cfg(feature = "ssr")]
pub mod ssr {
use super::Todo;
use crate::auth::{ssr::AuthSession, User};
use leptos::*;
use sqlx::SqlitePool;
use sqlx::SqlitePool;
use futures::future::join_all;
pub fn pool() -> Result<SqlitePool, ServerFnError> {
use_context::<SqlitePool>()
.ok_or_else(|| ServerFnError::ServerError("Pool missing.".into()))
}
pub fn pool() -> Result<SqlitePool, ServerFnError> {
use_context::<SqlitePool>()
.ok_or_else(|| ServerFnError::ServerError("Pool missing.".into()))
}
pub fn auth() -> Result<AuthSession, ServerFnError> {
use_context::<AuthSession>().ok_or_else(|| {
ServerFnError::ServerError("Auth session missing.".into())
})
}
pub fn auth() -> Result<AuthSession, ServerFnError> {
use_context::<AuthSession>()
.ok_or_else(|| ServerFnError::ServerError("Auth session missing.".into()))
}
#[derive(sqlx::FromRow, Clone)]
pub struct SqlTodo {
id: u32,
user_id: i64,
title: String,
created_at: String,
completed: bool,
}
#[derive(sqlx::FromRow, Clone)]
pub struct SqlTodo {
id: u32,
user_id: i64,
title: String,
created_at: String,
completed: bool,
}
impl SqlTodo {
pub async fn into_todo(self, pool: &SqlitePool) -> Todo {
Todo {
id: self.id,
user: User::get(self.user_id, pool).await,
title: self.title,
created_at: self.created_at,
completed: self.completed,
}
impl SqlTodo {
pub async fn into_todo(self, pool: &SqlitePool) -> Todo {
Todo {
id: self.id,
user: User::get(self.user_id, pool).await,
title: self.title,
created_at: self.created_at,
completed: self.completed,
}
}
}
@@ -55,6 +55,9 @@ cfg_if! {
#[server(GetTodos, "/api")]
pub async fn get_todos() -> Result<Vec<Todo>, ServerFnError> {
use self::ssr::{pool, SqlTodo};
use futures::future::join_all;
let pool = pool()?;
Ok(join_all(
@@ -69,6 +72,8 @@ pub async fn get_todos() -> Result<Vec<Todo>, ServerFnError> {
#[server(AddTodo, "/api")]
pub async fn add_todo(title: String) -> Result<(), ServerFnError> {
use self::ssr::*;
let user = get_user().await?;
let pool = pool()?;
@@ -93,6 +98,8 @@ pub async fn add_todo(title: String) -> Result<(), ServerFnError> {
// The struct name and path prefix arguments are optional.
#[server]
pub async fn delete_todo(id: u16) -> Result<(), ServerFnError> {
use self::ssr::*;
let pool = pool()?;
Ok(sqlx::query("DELETE FROM todos WHERE id = $1")

View File

@@ -0,0 +1,2 @@
[toolchain]
channel = "nightly"

View File

@@ -0,0 +1,111 @@
[package]
name = "sso_auth_axum"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib", "rlib"]
[dependencies]
oauth2 = { version = "4.4.2", optional = true }
anyhow = "1.0.66"
console_log = "1.0.0"
rand = { version = "0.8.5", features = ["min_const_gen"], optional = true }
console_error_panic_hook = "0.1.7"
futures = "0.3.25"
leptos = { path = "../../leptos" }
leptos_meta = { path = "../../meta" }
leptos_axum = { path = "../../integrations/axum", optional = true }
leptos_router = { path = "../../router" }
log = "0.4.17"
simple_logger = "4.0.0"
serde = { version = "1.0.148", features = ["derive"] }
serde_json = { version = "1.0.108", optional = true }
axum = { version = "0.7", optional = true, features = ["macros"] }
tower = { version = "0.4", optional = true }
tower-http = { version = "0.5", features = ["fs"], optional = true }
tokio = { version = "1.22.0", features = ["full"], optional = true }
http = { version = "1" }
sqlx = { version = "0.7", features = [
"runtime-tokio-rustls",
"sqlite",
], optional = true }
thiserror = "1.0.38"
wasm-bindgen = "0.2"
axum_session_auth = { version = "0.12", features = [
"sqlite-rustls",
], optional = true }
axum_session = { version = "0.12", features = [
"sqlite-rustls",
], optional = true }
async-trait = { version = "0.1.64", optional = true }
reqwest = { version = "0.11", optional = true, features = ["json"] }
[features]
hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate"]
ssr = [
"dep:serde_json",
"dep:axum",
"dep:tower",
"dep:tower-http",
"dep:tokio",
"dep:reqwest",
"dep:oauth2",
"dep:axum_session_auth",
"dep:axum_session",
"dep:async-trait",
"dep:sqlx",
"dep:rand",
"leptos/ssr",
"leptos_meta/ssr",
"leptos_router/ssr",
"dep:leptos_axum",
]
[package.metadata.cargo-all-features]
denylist = ["axum", "tower", "tower-http", "tokio", "sqlx", "leptos_axum"]
skip_feature_sets = [["csr", "ssr"], ["csr", "hydrate"], ["ssr", "hydrate"]]
[package.metadata.leptos]
# The name used by wasm-bindgen/cargo-leptos for the JS/WASM bundle. Defaults to the crate name
output-name = "sso_auth_axum"
# The site root folder is where cargo-leptos generate all output. WARNING: all content of this folder will be erased on a rebuild. Use it in your server setup.
site-root = "target/site"
# The site-root relative folder where all compiled output (JS, WASM and CSS) is written
# Defaults to pkg
site-pkg-dir = "pkg"
# [Optional] The source CSS file. If it ends with .sass or .scss then it will be compiled by dart-sass into CSS. The CSS is optimized by Lightning CSS before being written to <site-root>/<site-pkg>/app.css
style-file = "./style.css"
# [Optional] Files in the asset-dir will be copied to the site-root directory
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 = "npx playwright test"
# The browserlist query used for optimizing the CSS.
browserquery = "defaults"
# Set by cargo-leptos watch when building with tha tool. Controls whether autoreload JS will be included in the head
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

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2022 Greg Johnston
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.

View File

@@ -0,0 +1,8 @@
extend = [
{ path = "../cargo-make/main.toml" },
{ path = "../cargo-make/cargo-leptos.toml" },
]
[env]
CLIENT_PROCESS_NAME = "sso_auth_axum"

View File

@@ -0,0 +1,83 @@
# Leptos SSO Authenticated Email Display App with Axum
## Overview
This project demonstrates various methods of implementing Single Sign-On (SSO) authorization using OAuth, specifically with the OAuth2 library. The primary focus is on the Authorization Code Grant flow.
### Process Flow
1. **Initiating Sign-In:** When a user clicks the 'Sign In With {THIRD PARTY SERVICE}' button, the request is sent to a server function. This function retrieves an authorization URL from the third-party service.
2. **CSRF Token Handling:** During the URL fetch, a CSRF_TOKEN is generated and confirmed by the service to mitigate Cross-Site Request Forgery attacks. Learn more about CSRF [here](https://en.wikipedia.org/wiki/Cross-site_request_forgery). This token is stored on our server.
3. **User Redirection:** Post-login, users are redirected to our server with a URL formatted as follows:
`http://your-redirect-uri.com/callback?code=AUTHORIZATION_CODE&state=CSRF_TOKEN`
Note: Additional parameters like Scope and Client_ID may be included by the service.
4. **Token Acquisition:** The 'code' parameter in the URL is not the actual service token. Instead, it's used to fetch the token. We verify the CSRF_TOKEN in the URL against our server's stored token for security.
5. **Access Token Usage:** With a valid CSRF_TOKEN, we use the AUTHORIZATION_CODE in an HTTP Request to the third-party service. The response typically includes:
- An `access token`
- An `expires_in` value (time in seconds until token expiration)
- A `refresh token` (used to renew the access token)
6. **Email Retrieval and Display:** The access token allows us to retrieve the user's email. This email is then displayed in our Email Display App.
7. **Session Management:** The `expires_in` value is sent to the client. The client uses this to set a timeout, ensuring that if the session is still active (the window hasn't been closed), it automatically triggers a token refresh when required.
## Client Side Rendering
This example cannot be built as a trunk standalone CSR-only app. Only the server may directly connect to the database.
## Server Side Rendering with cargo-leptos
cargo-leptos is now the easiest and most featureful way to build server side rendered apps with hydration. It provides automatic recompilation of client and server code, wasm optimisation, CSS minification, and more! Check out more about it [here](https://github.com/akesson/cargo-leptos)
## Env Vars
Commands that run the program, cargo leptos watch, cargo leptos serve, cargo run etc... All need the following Environment variables
G_AUTH_CLIENT_ID : This is the client ID given to you by google.
G_AUTH_SECRET : This is the secret given to you by google.
NGROK : this is the ngrok endpoint you get when you run ngrok http 3000
## Ngrok Google Set Up
After running your app, run
```bash
ngrok http 3000
```
Then use google api's and services, go to credentials, create credentials, add your app name, and use the ngrok url as the origin
and use the ngrok url with /g_auth as the redirect url. That will look like this `https://362b-24-34-20-189.ngrok-free.app/g_auth`
Save you client ID and secret given to you by google. Use them as Envars when you run the program as below
```bash
REDIRECT_URL={ngrok_redirect_url} G_AUTH_CLIENT_ID={google_credential_client_id} G_AUTH_SECRET={google_credential_secret} {your command here...}
```
1. Install cargo-leptos
```bash
cargo install --locked cargo-leptos
```
2. Build the site in watch mode, recompiling on file changes
```bash
cargo leptos watch
```
Open browser on [http://localhost:3000/](http://localhost:3000/)
3. When ready to deploy, run
```bash
cargo leptos build --release
```
## Server Side Rendering without cargo-leptos
To run it as a server side app with hydration, you'll need to have wasm-pack installed.
0. Edit the `[package.metadata.leptos]` section and set `site-root` to `"."`. You'll also want to change the path of the `<StyleSheet / >` component in the root component to point towards the CSS file in the root. This tells leptos that the WASM/JS files generated by wasm-pack are available at `./pkg` and that the CSS files are no longer processed by cargo-leptos. Building to alternative folders is not supported at this time. You'll also want to edit the call to `get_configuration()` to pass in `Some(Cargo.toml)`, so that Leptos will read the settings instead of cargo-leptos. If you do so, your file/folder names cannot include dashes.
1. Install wasm-pack
```bash
cargo install wasm-pack
```
2. Build the Webassembly used to hydrate the HTML from the server
```bash
wasm-pack build --target=web --debug --no-default-features --features=hydrate
```
3. Run the server to serve the Webassembly, JS, and HTML
```bash
cargo run --no-default-features --features=ssr
```

116
examples/sso_auth_axum/flake.lock generated Normal file
View File

@@ -0,0 +1,116 @@
{
"nodes": {
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1681202837,
"narHash": "sha256-H+Rh19JDwRtpVPAWp64F+rlEtxUWBAQW28eAi3SRSzg=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "cfacdce06f30d2b68473a46042957675eebb3401",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"flake-utils_2": {
"inputs": {
"systems": "systems_2"
},
"locked": {
"lastModified": 1681202837,
"narHash": "sha256-H+Rh19JDwRtpVPAWp64F+rlEtxUWBAQW28eAi3SRSzg=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "cfacdce06f30d2b68473a46042957675eebb3401",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1672580127,
"narHash": "sha256-3lW3xZslREhJogoOkjeZtlBtvFMyxHku7I/9IVehhT8=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "0874168639713f547c05947c76124f78441ea46c",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-22.05",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs",
"rust-overlay": "rust-overlay"
}
},
"rust-overlay": {
"inputs": {
"flake-utils": "flake-utils_2",
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1681525152,
"narHash": "sha256-KzI+ILcmU03iFWtB+ysPqtNmp8TP8v1BBReTuPP8MJY=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "b6f8d87208336d7cb85003b2e439fc707c38f92a",
"type": "github"
},
"original": {
"owner": "oxalica",
"repo": "rust-overlay",
"type": "github"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
},
"systems_2": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

View File

@@ -0,0 +1,37 @@
{
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-22.05";
inputs.rust-overlay.url = "github:oxalica/rust-overlay";
inputs.rust-overlay.inputs.nixpkgs.follows = "nixpkgs";
inputs.flake-utils.url = "github:numtide/flake-utils";
outputs = { self, nixpkgs, rust-overlay, flake-utils }:
flake-utils.lib.eachDefaultSystem (system:
let
pkgs = import nixpkgs {
inherit system;
overlays = [ (import rust-overlay) ];
};
in
with pkgs; rec {
devShells.default = mkShell {
shellHook = ''
export PKG_CONFIG_PATH="${pkgs.openssl.dev}/lib/pkgconfig";
'';
nativeBuildInputs = [
pkg-config
];
buildInputs = [
trunk
sqlite
sass
openssl
(rust-bin.nightly.latest.default.override {
extensions = [ "rust-src" ];
targets = [ "wasm32-unknown-unknown" ];
})
];
};
});
}

View File

@@ -0,0 +1,26 @@
PRAGMA foreign_keys = ON;
CREATE TABLE IF NOT EXISTS users (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
email TEXT NOT NULL UNIQUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS user_permissions (
user_id INTEGER NOT NULL,
token TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS csrf_tokens (
csrf_token TEXT NOT NULL PRIMARY KEY UNIQUE
);
CREATE TABLE IF NOT EXISTS google_tokens (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL UNIQUE,
access_secret TEXT NOT NULL,
refresh_secret TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) CONFLICT REPLACE
);

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -0,0 +1,160 @@
use serde::{Deserialize, Serialize};
use std::collections::HashSet;
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct User {
pub id: i64,
pub email: String,
pub permissions: HashSet<String>,
}
impl Default for User {
fn default() -> Self {
let permissions = HashSet::new();
Self {
id: -1,
email: "example@example.com".into(),
permissions,
}
}
}
#[cfg(feature = "ssr")]
pub mod ssr_imports {
use super::User;
pub use axum_session_auth::{
Authentication, HasPermission, SessionSqlitePool,
};
pub use sqlx::SqlitePool;
use std::collections::HashSet;
pub type AuthSession = axum_session_auth::AuthSession<
User,
i64,
SessionSqlitePool,
SqlitePool,
>;
use async_trait::async_trait;
impl User {
pub async fn get(id: i64, pool: &SqlitePool) -> Option<Self> {
let sqluser = sqlx::query_as::<_, SqlUser>(
"SELECT * FROM users WHERE id = ?",
)
.bind(id)
.fetch_one(pool)
.await
.ok()?;
//lets just get all the tokens the user can use, we will only use the full permissions if modifing them.
let sql_user_perms = sqlx::query_as::<_, SqlPermissionTokens>(
"SELECT token FROM user_permissions WHERE user_id = ?;",
)
.bind(id)
.fetch_all(pool)
.await
.ok()?;
Some(sqluser.into_user(Some(sql_user_perms)))
}
pub async fn get_from_email(
email: &str,
pool: &SqlitePool,
) -> Option<Self> {
let sqluser = sqlx::query_as::<_, SqlUser>(
"SELECT * FROM users WHERE email = ?",
)
.bind(email)
.fetch_one(pool)
.await
.ok()?;
//lets just get all the tokens the user can use, we will only use the full permissions if modifing them.
let sql_user_perms = sqlx::query_as::<_, SqlPermissionTokens>(
"SELECT token FROM user_permissions WHERE user_id = ?;",
)
.bind(sqluser.id)
.fetch_all(pool)
.await
.ok()?;
Some(sqluser.into_user(Some(sql_user_perms)))
}
}
#[derive(sqlx::FromRow, Clone)]
pub struct SqlPermissionTokens {
pub token: String,
}
#[derive(sqlx::FromRow, Clone)]
pub struct SqlCsrfToken {
pub csrf_token: String,
}
#[async_trait]
impl Authentication<User, i64, SqlitePool> for User {
async fn load_user(
userid: i64,
pool: Option<&SqlitePool>,
) -> Result<User, anyhow::Error> {
let pool = pool.unwrap();
User::get(userid, pool)
.await
.ok_or_else(|| anyhow::anyhow!("Cannot get user"))
}
fn is_authenticated(&self) -> bool {
true
}
fn is_active(&self) -> bool {
true
}
fn is_anonymous(&self) -> bool {
false
}
}
#[async_trait]
impl HasPermission<SqlitePool> for User {
async fn has(&self, perm: &str, _pool: &Option<&SqlitePool>) -> bool {
self.permissions.contains(perm)
}
}
#[derive(sqlx::FromRow, Clone)]
pub struct SqlUser {
pub id: i64,
pub email: String,
}
#[derive(sqlx::FromRow, Clone)]
pub struct SqlRefreshToken {
pub secret: String,
}
impl SqlUser {
pub fn into_user(
self,
sql_user_perms: Option<Vec<SqlPermissionTokens>>,
) -> User {
User {
id: self.id,
email: self.email,
permissions: if let Some(user_perms) = sql_user_perms {
user_perms
.into_iter()
.map(|x| x.token)
.collect::<HashSet<String>>()
} else {
HashSet::<String>::new()
},
}
}
}
}

View File

@@ -0,0 +1,23 @@
use leptos::{view, Errors, For, IntoView, RwSignal, SignalGet, View};
// A basic function to display errors served by the error boundaries. Feel free to do more complicated things
// here than just displaying them
pub fn error_template(errors: RwSignal<Errors>) -> View {
view! {
<h1>"Errors"</h1>
<For
// a function that returns the items we're iterating over; a signal is fine
each=move || errors.get()
// a unique key for each item as a reference
key=|(key, _)| key.clone()
// renders each item to a view
children= move | (_, error)| {
let error_string = error.to_string();
view! {
<p>"Error: " {error_string}</p>
}
}
/>
}
.into_view()
}

View File

@@ -0,0 +1,49 @@
use crate::error_template::error_template;
use axum::{
body::Body,
extract::State,
http::{Request, Response, StatusCode, Uri},
response::{IntoResponse, Response as AxumResponse},
};
use leptos::*;
use tower::ServiceExt;
use tower_http::services::ServeDir;
pub async fn file_and_error_handler(
uri: Uri,
State(options): State<LeptosOptions>,
req: Request<Body>,
) -> AxumResponse {
let root = options.site_root.clone();
let res = get_static_file(uri.clone(), &root).await.unwrap();
if res.status() == StatusCode::OK {
res.into_response()
} else {
leptos::logging::log!("{:?}:{}", res.status(), uri);
let handler =
leptos_axum::render_app_to_stream(options.to_owned(), || {
error_template(create_rw_signal(leptos::Errors::default()))
});
handler(req).await.into_response()
}
}
async fn get_static_file(
uri: Uri,
root: &str,
) -> Result<Response<Body>, (StatusCode, String)> {
let req = Request::builder()
.uri(uri.clone())
.body(Body::empty())
.unwrap();
// `ServeDir` implements `tower::Service` so we can call it with `tower::ServiceExt::oneshot`
// This path is relative to the cargo root
match ServeDir::new(root).oneshot(req).await {
Ok(res) => Ok(res.into_response()),
Err(err) => Err((
StatusCode::INTERNAL_SERVER_ERROR,
format!("Something went wrong: {}", err),
)),
}
}

Some files were not shown because too many files have changed in this diff Show More