1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
//! Session auth for Dioxus fullstack apps on SurrealDB (the `auth`
//! feature): sessions stored in the database, a signed-in user on every
//! request, and a guard that denies by default.
//!
//! # The guard
//!
//! Every request needs a signed-in user unless it is for:
//!
//! - a static asset ([`is_static_asset`]),
//! - a page marked `#[public]` in the app's [`PublicRoutes`] enum, or
//! - a server function marked [`public`](crate::public):
//!
//! ```ignore
//! /// The splash asks this before it knows whether anyone is signed in.
//! #[g3_kit::public]
//! #[get("/api/v1/is_signed_in", ctx: SessionContext)]
//! pub async fn is_signed_in() -> Result<bool> {
//! Ok(!ctx.session_user.anonymous)
//! }
//! ```
//!
//! Forgetting `#[public]` is the safe mistake: a signed-out caller gets a
//! `401`, which the page can see and act on. A signed-out page load is
//! redirected to the splash instead ([`is_document_navigation`]).
//!
//! Pages are marked the same way, on the route enum:
//!
//! ```ignore
//! #[derive(Clone, Routable, PartialEq, PublicRoutes)]
//! enum Route {
//! #[redirect("/:..segments", |segments: Vec<String>| Route::Splash {})]
//! #[public]
//! #[route("/")]
//! Splash {},
//! #[nest("/games/:game_id")]
//! #[public]
//! #[route("/join")]
//! JoinGame { game_id: String },
//! #[end_nest]
//! #[route("/home")]
//! Home {},
//! }
//! ```
//!
//! # Setup
//!
//! ```toml
//! [dependencies]
//! g3-kit = { version = "0.1", features = ["auth"] }
//!
//! [features]
//! server = ["dioxus/server", "g3-kit/server"]
//! ```
//!
//! Load [`SESSIONS_SCHEMA`] into the database (or copy it into the app's
//! schema), describe the account table with [`AuthUser`], and build the
//! router, innermost layer first:
//!
//! ```ignore
//! pub enum AppUser {}
//! impl AuthUser for AppUser {}
//!
//! // Panics at startup if the splash isn't `#[public]`: the redirect would loop.
//! let guard = AuthGuard::for_routes(Route::Splash {});
//!
//! let session_store = SessionStore::new(
//! Some(SurrealSessionPool::new(Arc::clone(&db))),
//! SessionConfig::default().with_secure(!cfg!(debug_assertions)),
//! )
//! .await?;
//!
//! dioxus::server::router(App)
//! .layer(Extension(Arc::clone(&db)))
//! .layer(from_fn_with_state(guard, require_session::<AppUser, Client>))
//! .layer(AuthSessionLayer::<AppUser, Client>::new(Some(Arc::clone(&db))))
//! .layer(SessionLayer::new(session_store))
//! ```
//!
//! # What the guard does not cover
//!
//! A server function called during server-side rendering runs directly,
//! without any middleware, so the guard only applies to HTTP requests. A
//! public page that renders a private server function would run it for a
//! signed-out visitor. Functions that act on "the current user" should still
//! check `session_user.anonymous` rather than trust that they were guarded.
pub use ;
pub use ;
pub use PublicRoutes;
pub use SurrealSessionPool;
pub use ;
/// The `sessions` table [`SurrealSessionPool`] reads and writes.
pub const SESSIONS_SCHEMA: &str = include_str!;