Chore: hoist example projects for discoverability (#2959)
* hoist example projects, remove oidc * move over example projects from the separate repo * Update ecommerce site to the latest version of dioxus * update wifi scanner to 0.5 * drop the chatbot example; might be restored in a separate PR --------- Co-authored-by: Evan Almloff <evanalmloff@gmail.com>
This commit is contained in:
1 parent
a9f87384f8
commit
983fcfc616
94 files changed
+2774
-7322
No files matched your search
@@ -1,180 +0,0 @@
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: 'Roboto', sans-serif;
|
||||
user-select: none;
|
||||
transition: .2s all;
|
||||
}
|
||||
|
||||
body {
|
||||
padding-top: 77px;
|
||||
}
|
||||
|
||||
|
||||
/* header {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 10;
|
||||
padding: 20px;
|
||||
background-color: #2196F3;
|
||||
color: white;
|
||||
}
|
||||
header h1 {
|
||||
float: left;
|
||||
font-size: 20px;
|
||||
font-weight: 400;
|
||||
}
|
||||
header .material-icons {
|
||||
float: right;
|
||||
cursor: pointer;
|
||||
}
|
||||
header .icon-menu {
|
||||
float: left;
|
||||
margin-right: 20px;
|
||||
} */
|
||||
|
||||
main {
|
||||
padding: 20px 50px;
|
||||
}
|
||||
|
||||
.folder * {
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
.folder {
|
||||
float: left;
|
||||
width: 100px;
|
||||
height: 152px;
|
||||
/* //padding: 20px; */
|
||||
margin-right: 50px;
|
||||
margin-bottom: 70px;
|
||||
border-radius: 2px;
|
||||
/* //overflow: hidden; */
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.folder:hover h1 {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.folder:hover p.cooltip {
|
||||
opacity: 1;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.folder * {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.folder i {
|
||||
margin: 0;
|
||||
font-size: 100px;
|
||||
color: #607D8B;
|
||||
}
|
||||
|
||||
.folder h1 {
|
||||
position: relative;
|
||||
display: block;
|
||||
top: -37px;
|
||||
font-size: 20px;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.folder p.cooltip {
|
||||
position: relative;
|
||||
top: 5px;
|
||||
left: -50%;
|
||||
margin-left: 35px;
|
||||
background: #212121;
|
||||
font-size: 15px;
|
||||
color: white;
|
||||
border-radius: 4px;
|
||||
padding: 10px 20px;
|
||||
padding-right: 30px;
|
||||
width: 100px;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.folder p.cooltip:before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
display: block;
|
||||
top: -4px;
|
||||
left: 50%;
|
||||
margin-left: -5px;
|
||||
height: 10px;
|
||||
width: 10px;
|
||||
border-radius: 2px;
|
||||
background-color: #212121;
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
||||
div.properties {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 10;
|
||||
width: 300px;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
div.properties:before {
|
||||
content: '';
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 300px;
|
||||
bottom: 0;
|
||||
background-color: #212121;
|
||||
opacity: .5;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
div.properties img {
|
||||
position: relative;
|
||||
top: -1px;
|
||||
left: -1px;
|
||||
width: 110%;
|
||||
height: 200px;
|
||||
filter: blur(2px);
|
||||
}
|
||||
|
||||
div.properties h1 {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
margin-left: 20px;
|
||||
color: white;
|
||||
}
|
||||
|
||||
header {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
padding: 20px;
|
||||
background-color: #2196F3;
|
||||
color: white;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
header h1 {
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
header span {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
header i {
|
||||
margin: 0 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
header i:nth-child(1) {
|
||||
margin: 0 20px;
|
||||
}
|
||||
@@ -1,129 +0,0 @@
|
||||
//! Example: File Explorer
|
||||
//!
|
||||
//! This is a fun little desktop application that lets you explore the file system.
|
||||
//!
|
||||
//! This example is interesting because it's mixing filesystem operations and GUI, which is typically hard for UI to do.
|
||||
//! We store the state entirely in a single signal, making the explorer logic fairly easy to reason about.
|
||||
|
||||
use dioxus::desktop::{Config, WindowBuilder};
|
||||
use dioxus::prelude::*;
|
||||
|
||||
fn main() {
|
||||
LaunchBuilder::desktop()
|
||||
.with_cfg(Config::new().with_window(WindowBuilder::new().with_resizable(true)))
|
||||
.launch(app)
|
||||
}
|
||||
|
||||
fn app() -> Element {
|
||||
let mut files = use_signal(Files::new);
|
||||
|
||||
rsx! {
|
||||
head::Link {
|
||||
rel: "stylesheet",
|
||||
href: asset!("./examples/assets/fileexplorer.css")
|
||||
}
|
||||
div {
|
||||
head::Link { href: "https://fonts.googleapis.com/icon?family=Material+Icons", rel: "stylesheet" }
|
||||
header {
|
||||
i { class: "material-icons icon-menu", "menu" }
|
||||
h1 { "Files: " {files.read().current()} }
|
||||
span { }
|
||||
i { class: "material-icons", onclick: move |_| files.write().go_up(), "logout" }
|
||||
}
|
||||
main {
|
||||
for (dir_id, path) in files.read().path_names.iter().enumerate() {
|
||||
{
|
||||
let path_end = path.split('/').last().unwrap_or(path.as_str());
|
||||
rsx! {
|
||||
div { class: "folder", key: "{path}",
|
||||
i { class: "material-icons",
|
||||
onclick: move |_| files.write().enter_dir(dir_id),
|
||||
if path_end.contains('.') {
|
||||
"description"
|
||||
} else {
|
||||
"folder"
|
||||
}
|
||||
p { class: "cooltip", "0 folders / 0 files" }
|
||||
}
|
||||
h1 { "{path_end}" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(err) = files.read().err.as_ref() {
|
||||
div {
|
||||
code { "{err}" }
|
||||
button { onclick: move |_| files.write().clear_err(), "x" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A simple little struct to hold the file explorer state
|
||||
///
|
||||
/// We don't use any fancy signals or memoization here - Dioxus is so fast that even a file explorer can be done with a
|
||||
/// single signal.
|
||||
struct Files {
|
||||
path_stack: Vec<String>,
|
||||
path_names: Vec<String>,
|
||||
err: Option<String>,
|
||||
}
|
||||
|
||||
impl Files {
|
||||
fn new() -> Self {
|
||||
let mut files = Self {
|
||||
path_stack: vec!["./".to_string()],
|
||||
path_names: vec![],
|
||||
err: None,
|
||||
};
|
||||
|
||||
files.reload_path_list();
|
||||
|
||||
files
|
||||
}
|
||||
|
||||
fn reload_path_list(&mut self) {
|
||||
let cur_path = self.path_stack.last().unwrap();
|
||||
let paths = match std::fs::read_dir(cur_path) {
|
||||
Ok(e) => e,
|
||||
Err(err) => {
|
||||
let err = format!("An error occurred: {err:?}");
|
||||
self.err = Some(err);
|
||||
self.path_stack.pop();
|
||||
return;
|
||||
}
|
||||
};
|
||||
let collected = paths.collect::<Vec<_>>();
|
||||
|
||||
// clear the current state
|
||||
self.clear_err();
|
||||
self.path_names.clear();
|
||||
|
||||
for path in collected {
|
||||
self.path_names
|
||||
.push(path.unwrap().path().display().to_string());
|
||||
}
|
||||
}
|
||||
|
||||
fn go_up(&mut self) {
|
||||
if self.path_stack.len() > 1 {
|
||||
self.path_stack.pop();
|
||||
}
|
||||
self.reload_path_list();
|
||||
}
|
||||
|
||||
fn enter_dir(&mut self, dir_id: usize) {
|
||||
let path = &self.path_names[dir_id];
|
||||
self.path_stack.push(path.clone());
|
||||
self.reload_path_list();
|
||||
}
|
||||
|
||||
fn current(&self) -> &str {
|
||||
self.path_stack.last().unwrap()
|
||||
}
|
||||
fn clear_err(&mut self) {
|
||||
self.err = None;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
dist
|
||||
target
|
||||
static
|
||||
.dioxus
|
||||
@@ -0,0 +1,47 @@
|
||||
[package]
|
||||
name = "fullstack-auth-example"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
dioxus-web = { workspace = true, features = ["hydrate"], optional = true }
|
||||
dioxus = { features = ["fullstack"], workspace = true }
|
||||
dioxus-fullstack = { workspace = true }
|
||||
axum = { workspace = true, optional = true }
|
||||
tokio = { workspace = true, features = ["full"], optional = true }
|
||||
tower-http = { workspace = true, features = ["auth"], optional = true }
|
||||
simple_logger = { version = "4.2.0", optional = true }
|
||||
async-trait = { version = "0.1.71", optional = true }
|
||||
sqlx = { version = "0.7.0", features = [
|
||||
"macros",
|
||||
"migrate",
|
||||
"postgres",
|
||||
"sqlite",
|
||||
"_unstable-all-types",
|
||||
"tls-native-tls",
|
||||
"runtime-tokio",
|
||||
], optional = true }
|
||||
http = { workspace = true, optional = true }
|
||||
tower = { workspace = true, optional = true }
|
||||
|
||||
serde = "1.0.159"
|
||||
execute = "0.2.12"
|
||||
anyhow = "1.0.71"
|
||||
|
||||
[dependencies.axum_session]
|
||||
workspace = true
|
||||
features = ["sqlite-rustls"]
|
||||
optional = true
|
||||
|
||||
[dependencies.axum_session_auth]
|
||||
workspace = true
|
||||
features = ["sqlite-rustls"]
|
||||
optional = true
|
||||
|
||||
[features]
|
||||
default = []
|
||||
server = ["axum", "tokio", "dioxus-fullstack/axum", "tower-http", "simple_logger", "async-trait", "sqlx", "axum_session", "axum_session_auth", "http", "tower"]
|
||||
web = ["dioxus-web"]
|
||||
@@ -0,0 +1,262 @@
|
||||
use async_trait::async_trait;
|
||||
use axum::{
|
||||
http::Method,
|
||||
response::{IntoResponse, Response},
|
||||
routing::get,
|
||||
Router,
|
||||
};
|
||||
use axum_session::{SessionConfig, SessionLayer, SessionSqlitePool, SessionStore};
|
||||
use axum_session_auth::*;
|
||||
use core::pin::Pin;
|
||||
use dioxus_fullstack::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::sqlite::{SqliteConnectOptions, SqlitePool, SqlitePoolOptions};
|
||||
use std::error::Error;
|
||||
use std::future::Future;
|
||||
use std::{collections::HashSet, net::SocketAddr, str::FromStr};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct User {
|
||||
pub id: i32,
|
||||
pub anonymous: bool,
|
||||
pub username: String,
|
||||
pub permissions: HashSet<String>,
|
||||
}
|
||||
|
||||
#[derive(sqlx::FromRow, Clone)]
|
||||
pub struct SqlPermissionTokens {
|
||||
pub token: String,
|
||||
}
|
||||
|
||||
impl Default for User {
|
||||
fn default() -> Self {
|
||||
let mut permissions = HashSet::new();
|
||||
|
||||
permissions.insert("Category::View".to_owned());
|
||||
|
||||
Self {
|
||||
id: 1,
|
||||
anonymous: true,
|
||||
username: "Guest".into(),
|
||||
permissions,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[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_user(userid, pool)
|
||||
.await
|
||||
.ok_or_else(|| anyhow::anyhow!("Could not load user"))
|
||||
}
|
||||
|
||||
fn is_authenticated(&self) -> bool {
|
||||
!self.anonymous
|
||||
}
|
||||
|
||||
fn is_active(&self) -> bool {
|
||||
!self.anonymous
|
||||
}
|
||||
|
||||
fn is_anonymous(&self) -> bool {
|
||||
self.anonymous
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl HasPermission<SqlitePool> for User {
|
||||
async fn has(&self, perm: &str, _pool: &Option<&SqlitePool>) -> bool {
|
||||
self.permissions.contains(perm)
|
||||
}
|
||||
}
|
||||
|
||||
impl User {
|
||||
pub async fn get_user(id: i64, pool: &SqlitePool) -> Option<Self> {
|
||||
let sqluser = sqlx::query_as::<_, SqlUser>("SELECT * FROM users WHERE id = $1")
|
||||
.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 modifying them.
|
||||
let sql_user_perms = sqlx::query_as::<_, SqlPermissionTokens>(
|
||||
"SELECT token FROM user_permissions WHERE user_id = $1;",
|
||||
)
|
||||
.bind(id)
|
||||
.fetch_all(pool)
|
||||
.await
|
||||
.ok()?;
|
||||
|
||||
Some(sqluser.into_user(Some(sql_user_perms)))
|
||||
}
|
||||
|
||||
pub async fn create_user_tables(pool: &SqlitePool) {
|
||||
sqlx::query(
|
||||
r#"
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
"id" INTEGER PRIMARY KEY,
|
||||
"anonymous" BOOLEAN NOT NULL,
|
||||
"username" VARCHAR(256) NOT NULL
|
||||
)
|
||||
"#,
|
||||
)
|
||||
.execute(pool)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
sqlx::query(
|
||||
r#"
|
||||
CREATE TABLE IF NOT EXISTS user_permissions (
|
||||
"user_id" INTEGER NOT NULL,
|
||||
"token" VARCHAR(256) NOT NULL
|
||||
)
|
||||
"#,
|
||||
)
|
||||
.execute(pool)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
sqlx::query(
|
||||
r#"
|
||||
INSERT INTO users
|
||||
(id, anonymous, username) SELECT 1, true, 'Guest'
|
||||
ON CONFLICT(id) DO UPDATE SET
|
||||
anonymous = EXCLUDED.anonymous,
|
||||
username = EXCLUDED.username
|
||||
"#,
|
||||
)
|
||||
.execute(pool)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
sqlx::query(
|
||||
r#"
|
||||
INSERT INTO users
|
||||
(id, anonymous, username) SELECT 2, false, 'Test'
|
||||
ON CONFLICT(id) DO UPDATE SET
|
||||
anonymous = EXCLUDED.anonymous,
|
||||
username = EXCLUDED.username
|
||||
"#,
|
||||
)
|
||||
.execute(pool)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
sqlx::query(
|
||||
r#"
|
||||
INSERT INTO user_permissions
|
||||
(user_id, token) SELECT 2, 'Category::View'
|
||||
"#,
|
||||
)
|
||||
.execute(pool)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(sqlx::FromRow, Clone)]
|
||||
pub struct SqlUser {
|
||||
pub id: i32,
|
||||
pub anonymous: bool,
|
||||
pub username: String,
|
||||
}
|
||||
|
||||
impl SqlUser {
|
||||
pub fn into_user(self, sql_user_perms: Option<Vec<SqlPermissionTokens>>) -> User {
|
||||
User {
|
||||
id: self.id,
|
||||
anonymous: self.anonymous,
|
||||
username: self.username,
|
||||
permissions: if let Some(user_perms) = sql_user_perms {
|
||||
user_perms
|
||||
.into_iter()
|
||||
.map(|x| x.token)
|
||||
.collect::<HashSet<String>>()
|
||||
} else {
|
||||
HashSet::<String>::new()
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn connect_to_database() -> SqlitePool {
|
||||
let connect_opts = SqliteConnectOptions::from_str("sqlite::memory:").unwrap();
|
||||
|
||||
SqlitePoolOptions::new()
|
||||
.max_connections(5)
|
||||
.connect_with(connect_opts)
|
||||
.await
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
pub struct Session(
|
||||
pub axum_session_auth::AuthSession<
|
||||
crate::auth::User,
|
||||
i64,
|
||||
axum_session_auth::SessionSqlitePool,
|
||||
sqlx::SqlitePool,
|
||||
>,
|
||||
);
|
||||
|
||||
impl std::ops::Deref for Session {
|
||||
type Target = axum_session_auth::AuthSession<
|
||||
crate::auth::User,
|
||||
i64,
|
||||
axum_session_auth::SessionSqlitePool,
|
||||
sqlx::SqlitePool,
|
||||
>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::DerefMut for Session {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.0
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct AuthSessionLayerNotFound;
|
||||
|
||||
impl std::fmt::Display for AuthSessionLayerNotFound {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "AuthSessionLayer was not found")
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for AuthSessionLayerNotFound {}
|
||||
|
||||
impl IntoResponse for AuthSessionLayerNotFound {
|
||||
fn into_response(self) -> Response {
|
||||
(
|
||||
http::status::StatusCode::INTERNAL_SERVER_ERROR,
|
||||
"AuthSessionLayer was not found",
|
||||
)
|
||||
.into_response()
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<S: std::marker::Sync + std::marker::Send> axum::extract::FromRequestParts<S> for Session {
|
||||
type Rejection = AuthSessionLayerNotFound;
|
||||
|
||||
async fn from_request_parts(
|
||||
parts: &mut http::request::Parts,
|
||||
state: &S,
|
||||
) -> Result<Self, Self::Rejection> {
|
||||
axum_session_auth::AuthSession::<
|
||||
crate::auth::User,
|
||||
i64,
|
||||
axum_session_auth::SessionSqlitePool,
|
||||
sqlx::SqlitePool,
|
||||
>::from_request_parts(parts, state)
|
||||
.await
|
||||
.map(Session)
|
||||
.map_err(|_| AuthSessionLayerNotFound)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
//! Run with:
|
||||
//!
|
||||
//! ```sh
|
||||
//! dx build --features web
|
||||
//! cargo run --features server
|
||||
//! ```
|
||||
|
||||
#![allow(non_snake_case, unused)]
|
||||
|
||||
#[cfg(feature = "server")]
|
||||
mod auth;
|
||||
|
||||
use dioxus::prelude::*;
|
||||
use dioxus_fullstack::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
fn main() {
|
||||
#[cfg(feature = "web")]
|
||||
// Hydrate the application on the client
|
||||
dioxus_web::launch::launch_cfg(app, dioxus_web::Config::new().hydrate(true));
|
||||
|
||||
#[cfg(feature = "server")]
|
||||
{
|
||||
use crate::auth::*;
|
||||
use axum::routing::*;
|
||||
use axum_session::SessionConfig;
|
||||
use axum_session::SessionStore;
|
||||
use axum_session_auth::AuthConfig;
|
||||
use axum_session_auth::SessionSqlitePool;
|
||||
simple_logger::SimpleLogger::new().init().unwrap();
|
||||
tokio::runtime::Runtime::new()
|
||||
.unwrap()
|
||||
.block_on(async move {
|
||||
let pool = connect_to_database().await;
|
||||
|
||||
//This Defaults as normal Cookies.
|
||||
//To enable Private cookies for integrity, and authenticity please check the next Example.
|
||||
let session_config = SessionConfig::default().with_table_name("test_table");
|
||||
let auth_config = AuthConfig::<i64>::default().with_anonymous_user_id(Some(1));
|
||||
let session_store = SessionStore::<SessionSqlitePool>::new(
|
||||
Some(pool.clone().into()),
|
||||
session_config,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
User::create_user_tables(&pool).await;
|
||||
|
||||
// build our application with some routes
|
||||
let app = Router::new()
|
||||
// Server side render the application, serve static assets, and register server functions
|
||||
.serve_dioxus_application(ServeConfig::new().unwrap(), app)
|
||||
.layer(
|
||||
axum_session_auth::AuthSessionLayer::<
|
||||
crate::auth::User,
|
||||
i64,
|
||||
axum_session_auth::SessionSqlitePool,
|
||||
sqlx::SqlitePool,
|
||||
>::new(Some(pool))
|
||||
.with_config(auth_config),
|
||||
)
|
||||
.layer(axum_session::SessionLayer::new(session_store));
|
||||
|
||||
// run it
|
||||
let addr = std::net::SocketAddr::from(([127, 0, 0, 1], 3000));
|
||||
let listener = tokio::net::TcpListener::bind(&addr).await.unwrap();
|
||||
|
||||
axum::serve(listener, app.into_make_service())
|
||||
.await
|
||||
.unwrap();
|
||||
});
|
||||
}
|
||||
}
|
||||
//
|
||||
fn app() -> Element {
|
||||
let mut user_name = use_signal(|| "?".to_string());
|
||||
let mut permissions = use_signal(|| "?".to_string());
|
||||
|
||||
rsx! {
|
||||
div {
|
||||
button { onclick: move |_| {
|
||||
async move {
|
||||
login().await.unwrap();
|
||||
}
|
||||
},
|
||||
"Login Test User"
|
||||
}
|
||||
}
|
||||
div {
|
||||
button {
|
||||
onclick: move |_| async move {
|
||||
if let Ok(data) = get_user_name().await {
|
||||
user_name.set(data);
|
||||
}
|
||||
},
|
||||
"Get User Name"
|
||||
}
|
||||
"User name: {user_name}"
|
||||
}
|
||||
div {
|
||||
button {
|
||||
onclick: move |_| async move {
|
||||
if let Ok(data) = get_permissions().await {
|
||||
permissions.set(data);
|
||||
}
|
||||
},
|
||||
"Get Permissions"
|
||||
}
|
||||
"Permissions: {permissions}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[server(GetUserName)]
|
||||
pub async fn get_user_name() -> Result<String, ServerFnError> {
|
||||
let session: crate::auth::Session = extract().await?;
|
||||
Ok(session.0.current_user.unwrap().username.to_string())
|
||||
}
|
||||
|
||||
#[server(Login)]
|
||||
pub async fn login() -> Result<(), ServerFnError> {
|
||||
let auth: crate::auth::Session = extract().await?;
|
||||
auth.login_user(2);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[server(Permissions)]
|
||||
pub async fn get_permissions() -> Result<String, ServerFnError> {
|
||||
let method: axum::http::Method = extract().await?;
|
||||
let auth: crate::auth::Session = extract().await?;
|
||||
let current_user = auth.current_user.clone().unwrap_or_default();
|
||||
|
||||
// lets check permissions only and not worry about if they are anon or not
|
||||
if !axum_session_auth::Auth::<crate::auth::User, i64, sqlx::SqlitePool>::build(
|
||||
[axum::http::Method::POST],
|
||||
false,
|
||||
)
|
||||
.requires(axum_session_auth::Rights::any([
|
||||
axum_session_auth::Rights::permission("Category::View"),
|
||||
axum_session_auth::Rights::permission("Admin::View"),
|
||||
]))
|
||||
.validate(¤t_user, &method, None)
|
||||
.await
|
||||
{
|
||||
return Ok(format!(
|
||||
"User {}, Does not have permissions needed to view this page please login",
|
||||
current_user.username
|
||||
));
|
||||
}
|
||||
|
||||
Ok(format!(
|
||||
"User has Permissions needed. Here are the Users permissions: {:?}",
|
||||
current_user.permissions
|
||||
))
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
dist
|
||||
target
|
||||
static
|
||||
.dioxus
|
||||
@@ -0,0 +1,14 @@
|
||||
[package]
|
||||
name = "fullstack-desktop-example"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
dioxus = { workspace = true, features = ["launch", "fullstack"] }
|
||||
serde = "1.0.159"
|
||||
|
||||
[features]
|
||||
default = []
|
||||
server = ["dioxus/server"]
|
||||
desktop = ["dioxus/desktop"]
|
||||
@@ -0,0 +1,43 @@
|
||||
#![allow(non_snake_case)]
|
||||
use dioxus::prelude::*;
|
||||
|
||||
fn main() {
|
||||
// Set the url of the server where server functions are hosted.
|
||||
#[cfg(not(feature = "server"))]
|
||||
dioxus::fullstack::prelude::server_fn::client::set_server_url("http://127.0.0.1:8080");
|
||||
launch(app);
|
||||
}
|
||||
|
||||
pub fn app() -> Element {
|
||||
let mut count = use_signal(|| 0);
|
||||
let mut text = use_signal(|| "...".to_string());
|
||||
|
||||
rsx! {
|
||||
h1 { "High-Five counter: {count}" }
|
||||
button { onclick: move |_| count += 1, "Up high!" }
|
||||
button { onclick: move |_| count -= 1, "Down low!" }
|
||||
button {
|
||||
onclick: move |_| async move {
|
||||
if let Ok(data) = get_server_data().await {
|
||||
println!("Client received: {}", data);
|
||||
text.set(data.clone());
|
||||
post_server_data(data).await.unwrap();
|
||||
}
|
||||
},
|
||||
"Run a server function"
|
||||
}
|
||||
"Server said: {text}"
|
||||
}
|
||||
}
|
||||
|
||||
#[server(PostServerData)]
|
||||
async fn post_server_data(data: String) -> Result<(), ServerFnError> {
|
||||
println!("Server received: {}", data);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[server(GetServerData)]
|
||||
async fn get_server_data() -> Result<String, ServerFnError> {
|
||||
Ok("Hello from the server!".to_string())
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
dist
|
||||
target
|
||||
static
|
||||
.dioxus
|
||||
@@ -0,0 +1,21 @@
|
||||
[package]
|
||||
name = "fullstack-hello-world-example"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
dioxus = { workspace = true, features = ["fullstack"]}
|
||||
serde = "1.0.159"
|
||||
simple_logger = "4.2.0"
|
||||
tracing-wasm = "0.2.1"
|
||||
tracing = { workspace = true }
|
||||
tracing-subscriber = "0.3.17"
|
||||
reqwest = { workspace = true }
|
||||
|
||||
[features]
|
||||
default = []
|
||||
server = ["dioxus/axum"]
|
||||
web = ["dioxus/web"]
|
||||
@@ -0,0 +1,54 @@
|
||||
//! Run with:
|
||||
//!
|
||||
//! ```sh
|
||||
//! dx serve --platform fullstack
|
||||
//! ```
|
||||
|
||||
#![allow(non_snake_case, unused)]
|
||||
use dioxus::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
fn app() -> Element {
|
||||
let mut count = use_signal(|| 0);
|
||||
let mut text = use_signal(|| "...".to_string());
|
||||
let server_future = use_server_future(get_server_data)?;
|
||||
|
||||
rsx! {
|
||||
h1 { "High-Five counter: {count}" }
|
||||
button { onclick: move |_| count += 1, "Up high!" }
|
||||
button { onclick: move |_| count -= 1, "Down low!" }
|
||||
button {
|
||||
onclick: move |_| async move {
|
||||
if let Ok(data) = get_server_data().await {
|
||||
println!("Client received: {}", data);
|
||||
text.set(data.clone());
|
||||
post_server_data(data).await.unwrap();
|
||||
}
|
||||
},
|
||||
"Run a server function!"
|
||||
}
|
||||
"Server said: {text}"
|
||||
}
|
||||
}
|
||||
|
||||
#[server]
|
||||
async fn post_server_data(data: String) -> Result<(), ServerFnError> {
|
||||
println!("Server received: {}", data);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[server]
|
||||
async fn get_server_data() -> Result<String, ServerFnError> {
|
||||
Ok(reqwest::get("https://httpbin.org/ip").await?.text().await?)
|
||||
}
|
||||
|
||||
fn main() {
|
||||
#[cfg(feature = "web")]
|
||||
tracing_wasm::set_as_global_default();
|
||||
|
||||
#[cfg(feature = "server")]
|
||||
tracing_subscriber::fmt::init();
|
||||
|
||||
launch(app);
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
dist
|
||||
target
|
||||
static
|
||||
.dioxus
|
||||
@@ -0,0 +1,19 @@
|
||||
[package]
|
||||
name = "fullstack-router-example"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
dioxus = { workspace = true, features = ["fullstack", "router"] }
|
||||
axum = { workspace = true, optional = true }
|
||||
tokio = {workspace = true, features = ["full"], optional = true }
|
||||
serde = { version = "1.0.159", features = ["derive"] }
|
||||
|
||||
[features]
|
||||
default = []
|
||||
server = ["axum", "dioxus/axum"]
|
||||
web = ["dioxus/web"]
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
//! Run with:
|
||||
//!
|
||||
//! ```sh
|
||||
//! dx serve --platform fullstack
|
||||
//! ```
|
||||
|
||||
use dioxus::prelude::*;
|
||||
|
||||
fn main() {
|
||||
LaunchBuilder::fullstack()
|
||||
.with_cfg(server_only!(ServeConfig::builder().incremental(
|
||||
IncrementalRendererConfig::default()
|
||||
.invalidate_after(std::time::Duration::from_secs(120)),
|
||||
)))
|
||||
.launch(app);
|
||||
}
|
||||
|
||||
fn app() -> Element {
|
||||
rsx! { Router::<Route> {} }
|
||||
}
|
||||
|
||||
#[derive(Clone, Routable, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
enum Route {
|
||||
#[route("/")]
|
||||
Home {},
|
||||
|
||||
#[route("/blog/:id/")]
|
||||
Blog { id: i32 },
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn Blog(id: i32) -> Element {
|
||||
rsx! {
|
||||
Link { to: Route::Home {}, "Go to counter" }
|
||||
table {
|
||||
tbody {
|
||||
for _ in 0..id {
|
||||
tr {
|
||||
for _ in 0..id {
|
||||
td { "hello world!" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn Home() -> Element {
|
||||
let mut count = use_signal(|| 0);
|
||||
let mut text = use_signal(|| "...".to_string());
|
||||
|
||||
rsx! {
|
||||
Link { to: Route::Blog { id: count() }, "Go to blog" }
|
||||
div {
|
||||
h1 { "High-Five counter: {count}" }
|
||||
button { onclick: move |_| count += 1, "Up high!" }
|
||||
button { onclick: move |_| count -= 1, "Down low!" }
|
||||
button {
|
||||
onclick: move |_| async move {
|
||||
if let Ok(data) = get_server_data().await {
|
||||
println!("Client received: {}", data);
|
||||
text.set(data.clone());
|
||||
post_server_data(data).await.unwrap();
|
||||
}
|
||||
},
|
||||
"Run server function!"
|
||||
}
|
||||
"Server said: {text}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[server(PostServerData)]
|
||||
async fn post_server_data(data: String) -> Result<(), ServerFnError> {
|
||||
println!("Server received: {}", data);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[server(GetServerData)]
|
||||
async fn get_server_data() -> Result<String, ServerFnError> {
|
||||
Ok("Hello from the server!".to_string())
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
dist
|
||||
target
|
||||
static
|
||||
.dioxus
|
||||
@@ -0,0 +1,24 @@
|
||||
[package]
|
||||
name = "fullstack-streaming-example"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
dioxus = { workspace = true, features = ["fullstack"] }
|
||||
serde = "1.0.159"
|
||||
simple_logger = "4.2.0"
|
||||
tracing-wasm = "0.2.1"
|
||||
tracing.workspace = true
|
||||
tracing-subscriber = "0.3.17"
|
||||
futures = "0.3.30"
|
||||
tokio = { workspace = true, optional = true }
|
||||
futures-util.workspace = true
|
||||
once_cell = "1.19.0"
|
||||
|
||||
[features]
|
||||
default = []
|
||||
server = ["dioxus/axum", "tokio"]
|
||||
web = ["dioxus/web"]
|
||||
@@ -0,0 +1,41 @@
|
||||
use dioxus::prelude::*;
|
||||
use futures::StreamExt;
|
||||
use server_fn::codec::{StreamingText, TextStream};
|
||||
|
||||
fn app() -> Element {
|
||||
let mut response = use_signal(String::new);
|
||||
|
||||
rsx! {
|
||||
button {
|
||||
onclick: move |_| async move {
|
||||
response.write().clear();
|
||||
if let Ok(stream) = test_stream().await {
|
||||
response.write().push_str("Stream started\n");
|
||||
let mut stream = stream.into_inner();
|
||||
while let Some(Ok(text)) = stream.next().await {
|
||||
response.write().push_str(&text);
|
||||
}
|
||||
}
|
||||
},
|
||||
"Start stream"
|
||||
}
|
||||
"{response}"
|
||||
}
|
||||
}
|
||||
|
||||
#[server(output = StreamingText)]
|
||||
pub async fn test_stream() -> Result<TextStream, ServerFnError> {
|
||||
let (tx, rx) = futures::channel::mpsc::unbounded();
|
||||
tokio::spawn(async move {
|
||||
loop {
|
||||
tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
|
||||
let _ = tx.unbounded_send(Ok("Hello, world!".to_string()));
|
||||
}
|
||||
});
|
||||
|
||||
Ok(TextStream::new(rx))
|
||||
}
|
||||
|
||||
fn main() {
|
||||
launch(app)
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
[env]
|
||||
DIOXUS_FRONT_ISSUER_URL = "TODO"
|
||||
DIOXUS_FRONT_CLIENT_ID = "TODO"
|
||||
DIOXUS_FRONT_CLIENT_SECRET = "TODO"
|
||||
DIOXUS_FRONT_URL = "http://localhost:8080"
|
||||
@@ -1,3 +0,0 @@
|
||||
/target
|
||||
/dist
|
||||
.env
|
||||
Generated
-6372
File diff suppressed because it is too large.
Load diff
@@ -1,36 +0,0 @@
|
||||
[package]
|
||||
name = "openid_auth_demo"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.86"
|
||||
console_error_panic_hook = "0.1"
|
||||
dioxus = { path = "../../packages/dioxus", default_features = true, features = [
|
||||
"router",
|
||||
"signals",
|
||||
], version = "*" }
|
||||
dioxus-logger = "0.5.1"
|
||||
dioxus-sdk = { git = "https://github.com/Dioxuslabs/sdk", features = [
|
||||
"storage",
|
||||
] }
|
||||
form_urlencoded = "1.2.1"
|
||||
log = "0.4"
|
||||
openidconnect = "3.5.0"
|
||||
serde = { version = "1.0.203", features = ["derive"] }
|
||||
uuid = "1.8"
|
||||
|
||||
[features]
|
||||
default = ["web"]
|
||||
server = ["dioxus/axum"]
|
||||
web = ["dioxus/web"]
|
||||
desktop = ["dioxus/desktop"]
|
||||
fullstack = ["dioxus/fullstack"]
|
||||
|
||||
# since we're using dioxus from local path, inform dioxus-sdk to use it as well
|
||||
[patch.crates-io]
|
||||
dioxus = { path = "../../packages/dioxus" }
|
||||
dioxus-signals = { path = "../../packages/signals" }
|
||||
@@ -1,35 +0,0 @@
|
||||
[application]
|
||||
|
||||
# dioxus project name
|
||||
name = "OpenID Connect authentication demo"
|
||||
|
||||
# default platform
|
||||
# you can also use `dioxus serve/build --platform XXX` to use other platform
|
||||
# value: web | desktop
|
||||
default_platform = "web"
|
||||
|
||||
# Web `build` & `serve` dist path
|
||||
out_dir = "dist"
|
||||
|
||||
# resource (static) file folder
|
||||
asset_dir = "public"
|
||||
|
||||
# hot reload by default
|
||||
hot_reload = true
|
||||
|
||||
[web.app]
|
||||
|
||||
# HTML title tag content
|
||||
title = "OpenID Connect authentication demo"
|
||||
|
||||
[web.watcher]
|
||||
|
||||
index_on_404 = true
|
||||
|
||||
watch_path = ["src"]
|
||||
|
||||
[application.plugins]
|
||||
|
||||
available = true
|
||||
|
||||
required = []
|
||||
@@ -1,12 +0,0 @@
|
||||
# OpenID Connect example to show how to authenticate an user
|
||||
|
||||
The environment variables in [`.cargo/config.toml`](./.cargo/config.toml) must be set in order for this example to work.
|
||||
|
||||
Once they are set, you can run `dx serve --platform web` or `dx serve --platform desktop`.
|
||||
|
||||
### Environment variables summary
|
||||
|
||||
- `DIOXUS_FRONT_ISSUER_URL`: The openid-connect's issuer url
|
||||
- `DIOXUS_FRONT_CLIENT_ID`: The openid-connect's client id
|
||||
- `DIOXUS_FRONT_CLIENT_SECRET`: The openid-connect's client secret
|
||||
- `DIOXUS_FRONT_URL`: The url the frontend is supposed to be running on, it could be for example `http://localhost:8080`
|
||||
@@ -1,2 +0,0 @@
|
||||
pub const DIOXUS_FRONT_AUTH_TOKEN: &str = "auth_token";
|
||||
pub const DIOXUS_FRONT_AUTH_REQUEST: &str = "auth_request";
|
||||
@@ -1,36 +0,0 @@
|
||||
#![allow(non_snake_case)]
|
||||
use dioxus::prelude::*;
|
||||
use dioxus_logger::tracing::Level;
|
||||
use router::Route;
|
||||
|
||||
use crate::oidc::ClientState;
|
||||
use crate::storage::{use_auth_request_provider, use_auth_token_provider};
|
||||
|
||||
pub(crate) mod constants;
|
||||
pub(crate) mod model;
|
||||
pub(crate) mod oidc;
|
||||
pub(crate) mod props;
|
||||
pub(crate) mod router;
|
||||
pub(crate) mod storage;
|
||||
pub(crate) mod views;
|
||||
|
||||
pub static CLIENT: GlobalSignal<ClientState> = Signal::global(ClientState::default);
|
||||
|
||||
pub static DIOXUS_FRONT_ISSUER_URL: &str = env!("DIOXUS_FRONT_ISSUER_URL");
|
||||
pub static DIOXUS_FRONT_CLIENT_ID: &str = env!("DIOXUS_FRONT_CLIENT_ID");
|
||||
pub static DIOXUS_FRONT_CLIENT_SECRET: &str = env!("DIOXUS_FRONT_CLIENT_SECRET");
|
||||
pub static DIOXUS_FRONT_URL: &str = env!("DIOXUS_FRONT_URL");
|
||||
|
||||
fn App() -> Element {
|
||||
use_auth_request_provider();
|
||||
use_auth_token_provider();
|
||||
rsx! { Router::<Route> {} }
|
||||
}
|
||||
|
||||
fn main() {
|
||||
dioxus_logger::init(Level::DEBUG).expect("failed to init logger");
|
||||
dioxus_sdk::set_dir!();
|
||||
console_error_panic_hook::set_once();
|
||||
log::info!("starting app");
|
||||
launch(App);
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
pub(crate) mod user;
|
||||
@@ -1,7 +0,0 @@
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(PartialEq)]
|
||||
pub struct User {
|
||||
pub id: Uuid,
|
||||
pub name: String,
|
||||
}
|
||||
@@ -1,127 +0,0 @@
|
||||
use anyhow::Result;
|
||||
use openidconnect::{
|
||||
core::{CoreClient, CoreIdToken, CoreResponseType, CoreTokenResponse},
|
||||
reqwest::async_http_client,
|
||||
url::Url,
|
||||
AuthenticationFlow, AuthorizationCode, ClaimsVerificationError, ClientId, ClientSecret,
|
||||
CsrfToken, IssuerUrl, LogoutRequest, Nonce, ProviderMetadataWithLogout, RedirectUrl,
|
||||
RefreshToken,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{props::client::ClientProps, DIOXUS_FRONT_CLIENT_ID};
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct ClientState {
|
||||
pub oidc_client: Option<ClientProps>,
|
||||
}
|
||||
|
||||
/// State that holds the nonce and authorization url and the nonce generated to log in an user
|
||||
#[derive(Clone, PartialEq, Deserialize, Serialize, Default)]
|
||||
pub struct AuthRequestState {
|
||||
pub auth_request: Option<AuthRequest>,
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Deserialize, Serialize)]
|
||||
pub struct AuthRequest {
|
||||
pub nonce: Nonce,
|
||||
pub authorize_url: String,
|
||||
}
|
||||
|
||||
/// State the tokens returned once the user is authenticated
|
||||
#[derive(Debug, Deserialize, Serialize, Default, Clone)]
|
||||
pub struct AuthTokenState {
|
||||
/// Token used to identify the user
|
||||
pub id_token: Option<CoreIdToken>,
|
||||
/// Token used to refresh the tokens if they expire
|
||||
pub refresh_token: Option<RefreshToken>,
|
||||
}
|
||||
|
||||
impl PartialEq for AuthTokenState {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.id_token == other.id_token
|
||||
&& self.refresh_token.as_ref().map(|t| t.secret().clone())
|
||||
== other.refresh_token.as_ref().map(|t| t.secret().clone())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn email(
|
||||
client: CoreClient,
|
||||
id_token: CoreIdToken,
|
||||
nonce: Nonce,
|
||||
) -> Result<String, ClaimsVerificationError> {
|
||||
match id_token.claims(&client.id_token_verifier(), &nonce) {
|
||||
Ok(claims) => Ok(claims.clone().email().unwrap().to_string()),
|
||||
Err(error) => Err(error),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn authorize_url(client: CoreClient) -> AuthRequest {
|
||||
let (authorize_url, _csrf_state, nonce) = client
|
||||
.authorize_url(
|
||||
AuthenticationFlow::<CoreResponseType>::AuthorizationCode,
|
||||
CsrfToken::new_random,
|
||||
Nonce::new_random,
|
||||
)
|
||||
.add_scope(openidconnect::Scope::new("email".to_string()))
|
||||
.add_scope(openidconnect::Scope::new("profile".to_string()))
|
||||
.url();
|
||||
AuthRequest {
|
||||
authorize_url: authorize_url.to_string(),
|
||||
nonce,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn init_provider_metadata() -> Result<ProviderMetadataWithLogout> {
|
||||
let issuer_url = IssuerUrl::new(crate::DIOXUS_FRONT_ISSUER_URL.to_string())?;
|
||||
Ok(ProviderMetadataWithLogout::discover_async(issuer_url, async_http_client).await?)
|
||||
}
|
||||
|
||||
pub async fn init_oidc_client() -> Result<(ClientId, CoreClient)> {
|
||||
let client_id = ClientId::new(crate::DIOXUS_FRONT_CLIENT_ID.to_string());
|
||||
let provider_metadata = init_provider_metadata().await?;
|
||||
let client_secret = Some(ClientSecret::new(
|
||||
crate::DIOXUS_FRONT_CLIENT_SECRET.to_string(),
|
||||
));
|
||||
let redirect_url = RedirectUrl::new(format!("{}/login", crate::DIOXUS_FRONT_URL))?;
|
||||
|
||||
Ok((
|
||||
client_id.clone(),
|
||||
CoreClient::from_provider_metadata(provider_metadata, client_id, client_secret)
|
||||
.set_redirect_uri(redirect_url),
|
||||
))
|
||||
}
|
||||
|
||||
///TODO: Add pkce_pacifier
|
||||
pub async fn token_response(oidc_client: CoreClient, code: String) -> Result<CoreTokenResponse> {
|
||||
// let (pkce_challenge, pkce_verifier) = PkceCodeChallenge::new_random_sha256();
|
||||
Ok(oidc_client
|
||||
.exchange_code(AuthorizationCode::new(code.clone()))
|
||||
// .set_pkce_verifier(pkce_verifier)
|
||||
.request_async(async_http_client)
|
||||
.await?)
|
||||
}
|
||||
|
||||
pub async fn exchange_refresh_token(
|
||||
oidc_client: CoreClient,
|
||||
refresh_token: RefreshToken,
|
||||
) -> Result<CoreTokenResponse> {
|
||||
Ok(oidc_client
|
||||
.exchange_refresh_token(&refresh_token)
|
||||
.request_async(async_http_client)
|
||||
.await?)
|
||||
}
|
||||
|
||||
pub async fn log_out_url(id_token_hint: CoreIdToken) -> Result<Url> {
|
||||
let provider_metadata = init_provider_metadata().await?;
|
||||
let end_session_url = provider_metadata
|
||||
.additional_metadata()
|
||||
.clone()
|
||||
.end_session_endpoint
|
||||
.unwrap();
|
||||
let logout_request: LogoutRequest = LogoutRequest::from(end_session_url);
|
||||
Ok(logout_request
|
||||
.set_client_id(ClientId::new(DIOXUS_FRONT_CLIENT_ID.to_string()))
|
||||
.set_id_token_hint(&id_token_hint)
|
||||
.http_get_url())
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
use dioxus::prelude::*;
|
||||
use openidconnect::{core::CoreClient, ClientId};
|
||||
|
||||
#[derive(Props, Clone, Debug)]
|
||||
pub struct ClientProps {
|
||||
pub client: CoreClient,
|
||||
pub client_id: ClientId,
|
||||
}
|
||||
|
||||
impl PartialEq for ClientProps {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.client_id == other.client_id
|
||||
}
|
||||
}
|
||||
|
||||
impl ClientProps {
|
||||
pub fn new(client_id: ClientId, client: CoreClient) -> Self {
|
||||
ClientProps { client_id, client }
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
pub(crate) mod client;
|
||||
@@ -1,16 +0,0 @@
|
||||
use crate::views::{header::AuthHeader, home::Home, login::Login, not_found::NotFound};
|
||||
use dioxus::prelude::*;
|
||||
|
||||
#[derive(Routable, Clone)]
|
||||
pub enum Route {
|
||||
#[layout(AuthHeader)]
|
||||
#[route("/")]
|
||||
Home {},
|
||||
|
||||
// https://dioxuslabs.com/learn/0.4/router/reference/routes#query-segments
|
||||
#[route("/login?:query_string")]
|
||||
Login { query_string: String },
|
||||
#[end_layout]
|
||||
#[route("/:..route")]
|
||||
NotFound { route: Vec<String> },
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
use dioxus::prelude::*;
|
||||
use dioxus_sdk::storage::*;
|
||||
|
||||
use crate::{
|
||||
constants::{DIOXUS_FRONT_AUTH_REQUEST, DIOXUS_FRONT_AUTH_TOKEN},
|
||||
oidc::{AuthRequestState, AuthTokenState},
|
||||
};
|
||||
|
||||
pub fn use_auth_token_provider() {
|
||||
let stored_token =
|
||||
use_storage::<LocalStorage, _>(DIOXUS_FRONT_AUTH_TOKEN.to_owned(), AuthTokenState::default);
|
||||
|
||||
use_context_provider(move || stored_token);
|
||||
}
|
||||
|
||||
pub fn use_auth_token() -> Signal<AuthTokenState> {
|
||||
use_context()
|
||||
}
|
||||
|
||||
pub fn use_auth_request_provider() {
|
||||
let stored_req = use_storage::<LocalStorage, _>(
|
||||
DIOXUS_FRONT_AUTH_REQUEST.to_owned(),
|
||||
AuthRequestState::default,
|
||||
);
|
||||
|
||||
use_context_provider(move || stored_req);
|
||||
}
|
||||
|
||||
pub fn use_auth_request() -> Signal<AuthRequestState> {
|
||||
use_context()
|
||||
}
|
||||
|
||||
pub fn auth_request() -> Signal<AuthRequestState> {
|
||||
consume_context()
|
||||
}
|
||||
@@ -1,221 +0,0 @@
|
||||
use crate::storage::{auth_request, use_auth_request, use_auth_token};
|
||||
use crate::{
|
||||
oidc::{
|
||||
authorize_url, email, exchange_refresh_token, init_oidc_client, log_out_url,
|
||||
AuthRequestState, AuthTokenState, ClientState,
|
||||
},
|
||||
props::client::ClientProps,
|
||||
router::Route,
|
||||
CLIENT,
|
||||
};
|
||||
use anyhow::Result;
|
||||
use dioxus::prelude::*;
|
||||
use dioxus::router::prelude::{Link, Outlet};
|
||||
use openidconnect::{url::Url, OAuth2TokenResponse, TokenResponse};
|
||||
|
||||
#[component]
|
||||
pub fn LogOut() -> Element {
|
||||
let mut auth_token = use_auth_token();
|
||||
let log_out_url_state = use_signal(|| None::<Option<Result<Url>>>);
|
||||
match auth_token().id_token {
|
||||
Some(id_token) => match &*log_out_url_state.read() {
|
||||
Some(log_out_url_result) => match log_out_url_result {
|
||||
Some(uri) => match uri {
|
||||
Ok(uri) => {
|
||||
rsx! {
|
||||
Link {
|
||||
onclick: move |_| {
|
||||
auth_token.take();
|
||||
},
|
||||
to: uri.to_string(),
|
||||
"Log out"
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(error) => {
|
||||
rsx! { div { "Failed to load disconnection url: {error:?}" } }
|
||||
}
|
||||
},
|
||||
None => {
|
||||
rsx! { div { "Loading... Please wait" } }
|
||||
}
|
||||
},
|
||||
None => {
|
||||
let logout_url_task = move || {
|
||||
spawn({
|
||||
let mut log_out_url_state = log_out_url_state.to_owned();
|
||||
async move {
|
||||
let logout_url = log_out_url(id_token).await;
|
||||
let logout_url_option = Some(logout_url);
|
||||
log_out_url_state.set(Some(logout_url_option));
|
||||
}
|
||||
})
|
||||
};
|
||||
logout_url_task();
|
||||
rsx! { div { "Loading log out url... Please wait" } }
|
||||
}
|
||||
},
|
||||
None => {
|
||||
rsx! {{}}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn RefreshToken(props: ClientProps) -> Element {
|
||||
let mut auth_token = use_auth_token();
|
||||
match auth_token().refresh_token {
|
||||
Some(refresh_token) => {
|
||||
rsx! { div {
|
||||
onmounted: {
|
||||
move |_| {
|
||||
let client = props.client.clone();
|
||||
let refresh_token = refresh_token.clone();
|
||||
async move {
|
||||
let exchange_refresh_token =
|
||||
exchange_refresh_token(client, refresh_token).await;
|
||||
match exchange_refresh_token {
|
||||
Ok(response_token) => {
|
||||
auth_token.set(AuthTokenState {
|
||||
id_token: response_token.id_token().cloned(),
|
||||
refresh_token: response_token.refresh_token().cloned(),
|
||||
});
|
||||
}
|
||||
Err(_error) => {
|
||||
auth_token.take();
|
||||
auth_request().take();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Refreshing session, please wait"
|
||||
} }
|
||||
}
|
||||
None => {
|
||||
rsx! { div { "Id token expired and no refresh token found" } }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn LoadClient() -> Element {
|
||||
let init_client_future = use_resource(move || async move { init_oidc_client().await });
|
||||
match &*init_client_future.read_unchecked() {
|
||||
Some(Ok((client_id, client))) => rsx! {
|
||||
div {
|
||||
onmounted: {
|
||||
let client_id = client_id.clone();
|
||||
let client = client.clone();
|
||||
move |_| {
|
||||
*CLIENT.write() = ClientState {
|
||||
oidc_client: Some(ClientProps::new(client_id.clone(), client.clone())),
|
||||
};
|
||||
}
|
||||
},
|
||||
"Client successfully loaded"
|
||||
}
|
||||
Outlet::<Route> {}
|
||||
},
|
||||
Some(Err(error)) => {
|
||||
log::info! {"Failed to load client: {:?}", error};
|
||||
rsx! {
|
||||
div { "Failed to load client: {error:?}" }
|
||||
Outlet::<Route> {}
|
||||
}
|
||||
}
|
||||
None => {
|
||||
rsx! {
|
||||
div {
|
||||
div { "Loading client, please wait" }
|
||||
Outlet::<Route> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn AuthHeader() -> Element {
|
||||
let client = CLIENT.read().oidc_client.clone();
|
||||
let mut auth_request = use_auth_request();
|
||||
let auth_token = use_auth_token();
|
||||
match (client, auth_request(), auth_token()) {
|
||||
// We have everything we need to attempt to authenticate the user
|
||||
(Some(client_props), current_auth_request, current_auth_token) => {
|
||||
match current_auth_request.auth_request {
|
||||
Some(new_auth_request) => {
|
||||
match current_auth_token.id_token {
|
||||
Some(id_token) => {
|
||||
match email(
|
||||
client_props.client.clone(),
|
||||
id_token.clone(),
|
||||
new_auth_request.nonce.clone(),
|
||||
) {
|
||||
Ok(email) => {
|
||||
rsx! {
|
||||
div {
|
||||
div { {email} }
|
||||
LogOut {}
|
||||
Outlet::<Route> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Id token failed to be decoded
|
||||
Err(error) => match error {
|
||||
// Id token failed to be decoded because it expired, we refresh it
|
||||
openidconnect::ClaimsVerificationError::Expired(_message) => {
|
||||
log::info!("Token expired");
|
||||
rsx! {
|
||||
div {
|
||||
RefreshToken { client_id: client_props.client_id, client: client_props.client }
|
||||
Outlet::<Route> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Other issue with token decoding
|
||||
_ => {
|
||||
log::info!("Other issue with token");
|
||||
rsx! {
|
||||
div {
|
||||
div { "{error}" }
|
||||
Outlet::<Route> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
// User is not logged in
|
||||
None => {
|
||||
rsx! {
|
||||
div {
|
||||
Link { to: new_auth_request.authorize_url.clone(), "Log in" }
|
||||
Outlet::<Route> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
None => {
|
||||
rsx! { div {
|
||||
onmounted: {
|
||||
let client = client_props.client;
|
||||
move |_| {
|
||||
let new_auth_request = authorize_url(client.clone());
|
||||
auth_request.set(AuthRequestState {
|
||||
auth_request: Some(new_auth_request),
|
||||
});
|
||||
}
|
||||
},
|
||||
"Loading nonce"
|
||||
} }
|
||||
}
|
||||
}
|
||||
}
|
||||
// Client is not initialized yet, we need it for everything
|
||||
(None, _, _) => {
|
||||
rsx! { LoadClient {} }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
use dioxus::prelude::*;
|
||||
|
||||
pub fn Home() -> Element {
|
||||
rsx! { div { "Hello world" } }
|
||||
}
|
||||
@@ -1,86 +0,0 @@
|
||||
use crate::{
|
||||
oidc::{token_response, AuthTokenState},
|
||||
router::Route,
|
||||
storage::{auth_request, use_auth_token},
|
||||
CLIENT,
|
||||
};
|
||||
use dioxus::prelude::*;
|
||||
use dioxus::router::prelude::Link;
|
||||
use openidconnect::{OAuth2TokenResponse, TokenResponse};
|
||||
|
||||
#[component]
|
||||
pub fn Login(query_string: String) -> Element {
|
||||
let client = CLIENT.read().oidc_client.clone();
|
||||
let mut auth_token = use_auth_token();
|
||||
let current_auth_token = auth_token();
|
||||
match client {
|
||||
Some(client_props) => {
|
||||
match (
|
||||
current_auth_token.id_token,
|
||||
current_auth_token.refresh_token,
|
||||
) {
|
||||
(Some(_id_token), Some(_refresh_token)) => {
|
||||
rsx! {
|
||||
div { "Sign in successful" }
|
||||
Link { to: Route::Home {}, "Go back home" }
|
||||
}
|
||||
}
|
||||
// If the refresh token is set but not the id_token, there was an error, we just go back home and reset their value
|
||||
(None, Some(_)) | (Some(_), None) => {
|
||||
rsx! {
|
||||
div { "Error while attempting to log in" }
|
||||
Link {
|
||||
to: Route::Home {},
|
||||
onclick: move |_| {
|
||||
auth_token.take();
|
||||
auth_request().take();
|
||||
},
|
||||
"Go back home"
|
||||
}
|
||||
}
|
||||
}
|
||||
(None, None) => {
|
||||
let mut query_pairs = form_urlencoded::parse(query_string.as_bytes());
|
||||
let code_pair = query_pairs.find(|(key, _value)| key == "code");
|
||||
match code_pair {
|
||||
Some((_key, code)) => {
|
||||
let code = code.to_string();
|
||||
rsx! { div {
|
||||
onmounted: {
|
||||
move |_| {
|
||||
let auth_code = code.to_string();
|
||||
let client_props = client_props.clone();
|
||||
async move {
|
||||
let token_response_result =
|
||||
token_response(client_props.client, auth_code).await;
|
||||
match token_response_result {
|
||||
Ok(token_response) => {
|
||||
let id_token = token_response.id_token().unwrap();
|
||||
auth_token.set(AuthTokenState {
|
||||
id_token: Some(id_token.clone()),
|
||||
refresh_token: token_response
|
||||
.refresh_token()
|
||||
.cloned(),
|
||||
});
|
||||
}
|
||||
Err(error) => {
|
||||
log::warn! {"{error}"};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}}
|
||||
}
|
||||
None => {
|
||||
rsx! { div { "No code provided" } }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
rsx! {{}}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
pub(crate) mod header;
|
||||
pub(crate) mod home;
|
||||
pub(crate) mod login;
|
||||
pub(crate) mod not_found;
|
||||
@@ -1,10 +0,0 @@
|
||||
use dioxus::prelude::*;
|
||||
|
||||
#[component]
|
||||
pub fn NotFound(route: Vec<String>) -> Element {
|
||||
rsx! {
|
||||
div{
|
||||
{route.join("")}
|
||||
}
|
||||
}
|
||||
}
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -0,0 +1,4 @@
|
||||
dist
|
||||
target
|
||||
docs
|
||||
.dioxus
|
||||
@@ -0,0 +1,17 @@
|
||||
[package]
|
||||
name = "github-pages-static-generation"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
dioxus = { workspace = true, features = ["static-generation", "router"] }
|
||||
tower = { workspace = true, features = ["util"] }
|
||||
tracing-subscriber = "0.3.18"
|
||||
|
||||
[features]
|
||||
default = []
|
||||
server = ["dioxus/axum"]
|
||||
web = ["dioxus/web"]
|
||||
@@ -0,0 +1,70 @@
|
||||
//! You can use the `github_pages` method to set up a preset for github pages.
|
||||
//! This will output your files in the `/docs` directory and set up a `404.html` file.
|
||||
|
||||
#![allow(unused)]
|
||||
use dioxus::prelude::*;
|
||||
|
||||
// Generate all routes and output them to the static path
|
||||
fn main() {
|
||||
LaunchBuilder::new()
|
||||
.with_cfg(dioxus::static_site_generation::Config::new().github_pages())
|
||||
.launch(|| {
|
||||
rsx! {
|
||||
Router::<Route> {}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[derive(Clone, Routable, Debug, PartialEq)]
|
||||
enum Route {
|
||||
#[route("/")]
|
||||
Home {},
|
||||
|
||||
#[route("/blog")]
|
||||
Blog,
|
||||
|
||||
// You must include a catch all route to handle 404s
|
||||
#[route("/:..route")]
|
||||
PageNotFound { route: Vec<String> },
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn Blog() -> Element {
|
||||
rsx! {
|
||||
Link { to: Route::Home {}, "Go to counter" }
|
||||
table {
|
||||
tbody {
|
||||
for _ in 0..100 {
|
||||
tr {
|
||||
for _ in 0..100 {
|
||||
td { "hello world!" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn Home() -> Element {
|
||||
let mut count = use_signal(|| 0);
|
||||
|
||||
rsx! {
|
||||
Link { to: Route::Blog {}, "Go to blog" }
|
||||
div {
|
||||
h1 { "High-Five counter: {count}" }
|
||||
button { onclick: move |_| count += 1, "Up high!" }
|
||||
button { onclick: move |_| count -= 1, "Down low!" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn PageNotFound(route: Vec<String>) -> Element {
|
||||
rsx! {
|
||||
h1 { "Page not found" }
|
||||
p { "We are terribly sorry, but the page you requested doesn't exist." }
|
||||
pre { color: "red", "log:\nattempted to navigate to: {route:?}" }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
dist
|
||||
target
|
||||
static
|
||||
.dioxus
|
||||
@@ -0,0 +1,16 @@
|
||||
[package]
|
||||
name = "router-static-generation"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
dioxus = { workspace = true, features = ["static-generation", "router"] }
|
||||
tracing-subscriber = "0.3.18"
|
||||
|
||||
[features]
|
||||
default = []
|
||||
server = ["dioxus/axum"]
|
||||
web = ["dioxus/web"]
|
||||
@@ -0,0 +1,54 @@
|
||||
//! Static generation works out of the box with the router. Just add a router anywhere in your app and it will generate any static routes for you!
|
||||
|
||||
#![allow(unused)]
|
||||
use dioxus::prelude::*;
|
||||
|
||||
// Generate all routes and output them to the static path
|
||||
fn main() {
|
||||
launch(|| {
|
||||
rsx! {
|
||||
Router::<Route> {}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[derive(Clone, Routable, Debug, PartialEq)]
|
||||
enum Route {
|
||||
#[route("/")]
|
||||
Home {},
|
||||
|
||||
#[route("/blog")]
|
||||
Blog,
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn Blog() -> Element {
|
||||
rsx! {
|
||||
Link { to: Route::Home {}, "Go to counter" }
|
||||
table {
|
||||
tbody {
|
||||
for _ in 0..100 {
|
||||
tr {
|
||||
for _ in 0..100 {
|
||||
td { "hello!" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn Home() -> Element {
|
||||
let mut count = use_signal(|| 0);
|
||||
|
||||
rsx! {
|
||||
Link { to: Route::Blog {}, "Go to blog" }
|
||||
div {
|
||||
h1 { "High-Five counter: {count}" }
|
||||
button { onclick: move |_| count += 1, "Up high!" }
|
||||
button { onclick: move |_| count -= 1, "Down low!" }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
dist
|
||||
target
|
||||
static
|
||||
.dioxus
|
||||
@@ -0,0 +1,16 @@
|
||||
[package]
|
||||
name = "simple-static-generation"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
dioxus = { workspace = true, features = ["static-generation"] }
|
||||
tracing-subscriber = "0.3.18"
|
||||
|
||||
[features]
|
||||
default = []
|
||||
server = ["dioxus/axum"]
|
||||
web = ["dioxus/web"]
|
||||
@@ -0,0 +1,17 @@
|
||||
//! Static generation lets you pre-render your entire app to static files and then hydrate it on the client.
|
||||
use dioxus::prelude::*;
|
||||
|
||||
// Generate all routes and output them to the static path
|
||||
fn main() {
|
||||
launch(app);
|
||||
}
|
||||
|
||||
fn app() -> Element {
|
||||
let mut count = use_signal(|| 0);
|
||||
|
||||
rsx! {
|
||||
h1 { "High-Five counter: {count}" }
|
||||
button { onclick: move |_| count += 1, "Up high!" }
|
||||
button { onclick: move |_| count -= 1, "Down low!" }
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user