feat: basic project restructure

This commit is contained in:
Sharang Parnerkar
2026-02-16 21:23:25 +01:00
parent f102d96c09
commit 37478ba8f9
44 changed files with 3159 additions and 654 deletions

122
src/app.rs Normal file
View File

@@ -0,0 +1,122 @@
use crate::{components::*, pages::*};
use dioxus::prelude::*;
#[derive(Debug, Clone, Routable, PartialEq)]
#[rustfmt::skip]
pub enum Route {
#[layout(Navbar)]
#[route("/")]
OverviewPage {},
#[route("/login?:redirect_url")]
Login { redirect_url: String },
#[route("/blog/:id")]
Blog { id: i32 },
}
const FAVICON: Asset = asset!("/assets/favicon.ico");
const MAIN_CSS: Asset = asset!("/assets/main.css");
const HEADER_SVG: Asset = asset!("/assets/header.svg");
const TAILWIND_CSS: Asset = asset!("/assets/tailwind.css");
#[component]
pub fn App() -> Element {
rsx! {
document::Link { rel: "icon", href: FAVICON }
document::Link { rel: "stylesheet", href: MAIN_CSS }
document::Link { rel: "stylesheet", href: TAILWIND_CSS }
Router::<Route> {}
}
}
#[component]
pub fn Hero() -> Element {
rsx! {
div { id: "hero",
img { src: HEADER_SVG, id: "header" }
div { id: "links",
a { href: "https://dioxuslabs.com/learn/0.7/", "📚 Learn Dioxus" }
a { href: "https://dioxuslabs.com/awesome", "🚀 Awesome Dioxus" }
a { href: "https://github.com/dioxus-community/", "📡 Community Libraries" }
a { href: "https://github.com/DioxusLabs/sdk", "⚙️ Dioxus Development Kit" }
a { href: "https://marketplace.visualstudio.com/items?itemName=DioxusLabs.dioxus",
"💫 VSCode Extension"
}
a { href: "https://discord.gg/XgGxMSkvUM", "👋 Community Discord" }
}
}
}
}
/// Home page
#[component]
fn Home() -> Element {
rsx! {
Hero {}
Echo {}
}
}
/// Blog page
#[component]
pub fn Blog(id: i32) -> Element {
rsx! {
div { id: "blog",
// Content
h1 { "This is blog #{id}!" }
p {
"In blog #{id}, we show how the Dioxus router works and how URL parameters can be passed as props to our route components."
}
// Navigation links
Link { to: Route::Blog { id: id - 1 }, "Previous" }
span { " <---> " }
Link { to: Route::Blog { id: id + 1 }, "Next" }
}
}
}
/// Shared navbar component.
#[component]
fn Navbar() -> Element {
rsx! {
div { id: "navbar",
Link { to: Route::OverviewPage {}, "Home" }
Link { to: Route::Blog { id: 1 }, "Blog" }
}
Outlet::<Route> {}
}
}
/// Echo component that demonstrates fullstack server functions.
#[component]
fn Echo() -> Element {
let mut response = use_signal(|| String::new());
rsx! {
div { id: "echo",
h4 { "ServerFn Echo" }
input {
placeholder: "Type here to echo...",
oninput: move |event| async move {
let data = echo_server(event.value()).await.unwrap();
response.set(data);
},
}
if !response().is_empty() {
p {
"Server echoed: "
i { "{response}" }
}
}
}
}
}
/// Echo the user input on the server.
#[post("/api/echo")]
async fn echo_server(input: String) -> Result<String, ServerFnError> {
Ok(input)
}

15
src/components/login.rs Normal file
View File

@@ -0,0 +1,15 @@
use crate::Route;
use dioxus::prelude::*;
#[component]
pub fn Login(redirect_url: String) -> Element {
let navigator = use_navigator();
use_effect(move || {
let target = format!("/auth?redirect_url={}", redirect_url);
navigator.push(NavigationTarget::<Route>::External(target));
});
rsx!(
div { class: "text-center p-6", "Redirecting to secure login page…" }
)
}

2
src/components/mod.rs Normal file
View File

@@ -0,0 +1,2 @@
mod login;
pub use login::*;

109
src/infrastructure/auth.rs Normal file
View File

@@ -0,0 +1,109 @@
use super::error::{Error, Result};
use axum::Extension;
use axum::{
extract::FromRequestParts,
http::request::Parts,
response::{IntoResponse, Redirect, Response},
};
use url::form_urlencoded;
pub struct KeycloakVariables {
pub base_url: String,
pub realm: String,
pub client_id: String,
pub client_secret: String,
pub enable_test_user: bool,
}
/// Session data available to the backend when the user is logged in
#[derive(Debug, serde::Serialize, serde::Deserialize)]
pub struct LoggedInData {
pub id: String,
// ID Token value associated with the authenticated session.
pub token_id: String,
pub username: String,
pub avatar_url: Option<String>,
}
/// Used for extracting in the server functions.
/// If the `data` is `Some`, the user is logged in.
pub struct UserSession {
data: Option<LoggedInData>,
}
impl UserSession {
/// Get the [`LoggedInData`].
///
/// Raises a [`Error::UserNotLoggedIn`] error if the user is not logged in.
pub fn data(self) -> Result<LoggedInData> {
self.data.ok_or(Error::UserNotLoggedIn)
}
}
const LOGGED_IN_USER_SESSION_KEY: &str = "logged_in_data";
impl<S: std::marker::Sync + std::marker::Send> FromRequestParts<S> for UserSession {
type Rejection = Error;
async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self> {
let session = parts
.extensions
.get::<tower_sessions::Session>()
.cloned()
.ok_or(Error::AuthSessionLayerNotFound(
"Auth Session Layer not found".to_string(),
))?;
let data: Option<LoggedInData> = session
.get::<LoggedInData>(LOGGED_IN_USER_SESSION_KEY)
.await?;
Ok(Self { data })
}
}
/// Helper function to log the user in by setting the session data
pub async fn login(session: &tower_sessions::Session, data: &LoggedInData) -> Result<()> {
session.insert(LOGGED_IN_USER_SESSION_KEY, data).await?;
Ok(())
}
/// Handler to run when the user wants to logout
#[axum::debug_handler]
pub async fn logout(
state: Extension<super::server_state::ServerState>,
session: tower_sessions::Session,
) -> Result<Response> {
let dashboard_base_url = "http://localhost:8000";
let redirect_uri = format!("{dashboard_base_url}/");
let encoded_redirect_uri: String =
form_urlencoded::byte_serialize(redirect_uri.as_bytes()).collect();
// clear the session value for this session
if let Some(login_data) = session
.remove::<LoggedInData>(LOGGED_IN_USER_SESSION_KEY)
.await?
{
let kc_base_url = &state.keycloak_variables.base_url;
let kc_realm = &state.keycloak_variables.realm;
let kc_client_id = &state.keycloak_variables.client_id;
// Needed for running locally.
// This will not panic on production and it will return the original so we can keep it
let routed_kc_base_url = kc_base_url.replace("keycloak", "localhost");
let token_id = login_data.token_id;
// redirect to Keycloak logout endpoint
let logout_url = format!(
"{routed_kc_base_url}/realms/{kc_realm}/protocol/openid-connect/logout\
?post_logout_redirect_uri={encoded_redirect_uri}\
&client_id={kc_client_id}\
&id_token_hint={token_id}"
);
Ok(Redirect::to(&logout_url).into_response())
} else {
// No id_token in session; just redirect to homepage
Ok(Redirect::to(&redirect_uri).into_response())
}
}

49
src/infrastructure/db.rs Normal file
View File

@@ -0,0 +1,49 @@
use super::error::Result;
use super::user::{KeyCloakSub, UserEntity};
use mongodb::{bson::doc, Client, Collection};
pub struct Database {
client: Client,
}
impl Database {
pub async fn new(client: Client) -> Self {
Self { client }
}
}
/// Impl of project related DB actions
impl Database {}
/// Impl of user-related actions
impl Database {
async fn users_collection(&self) -> Collection<UserEntity> {
self.client
.database("dashboard")
.collection::<UserEntity>("users")
}
pub async fn get_user_by_kc_sub(&self, kc_sub: KeyCloakSub) -> Result<Option<UserEntity>> {
let c = self.users_collection().await;
let result = c
.find_one(doc! {
"kc_sub" : kc_sub.0
})
.await?;
Ok(result)
}
pub async fn get_user_by_id(&self, user_id: &str) -> Result<Option<UserEntity>> {
let c = self.users_collection().await;
let user_id: mongodb::bson::oid::ObjectId = user_id.parse()?;
let filter = doc! { "_id" : user_id };
let result = c.find_one(filter).await?;
Ok(result)
}
pub async fn insert_user(&self, user: &UserEntity) -> Result<()> {
let c = self.users_collection().await;
let _ = c.insert_one(user).await?;
Ok(())
}
}

View File

@@ -0,0 +1,78 @@
use axum::response::{IntoResponse, Redirect, Response};
use reqwest::StatusCode;
use crate::Route;
pub type Result<T> = core::result::Result<T, Error>;
#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error("{0}")]
NotFound(String),
#[error("{0}")]
BadRequest(String),
#[error("ReqwestError: {0}")]
ReqwestError(#[from] reqwest::Error),
#[error("ServerStateError: {0}")]
ServerStateError(String),
#[error("SessionError: {0}")]
SessionError(#[from] tower_sessions::session::Error),
#[error("AuthSessionLayerNotFound: {0}")]
AuthSessionLayerNotFound(String),
#[error("UserNotLoggedIn")]
UserNotLoggedIn,
#[error("MongoDbError: {0}")]
MongoDbError(#[from] mongodb::error::Error),
#[error("MongoBsonError: {0}")]
MongoBsonError(#[from] mongodb::bson::ser::Error),
#[error("MongoObjectIdParseError: {0}")]
MongoObjectIdParseError(#[from] mongodb::bson::oid::Error),
#[error("IoError: {0}")]
IoError(#[from] std::io::Error),
#[error("GeneralError: {0}")]
GeneralError(String),
#[error("SerdeError: {0}")]
SerdeError(#[from] serde_json::Error),
#[error("Forbidden: {0}")]
Forbidden(String),
}
impl IntoResponse for Error {
#[tracing::instrument]
fn into_response(self) -> Response {
let message = self.to_string();
tracing::error!("Converting Error to Reponse: {message}");
match self {
Error::NotFound(_) => (StatusCode::NOT_FOUND, message).into_response(),
Error::BadRequest(_) => (StatusCode::BAD_REQUEST, message).into_response(),
// ideally we would like to redirect with the original URL as the target, but we do not have access to it here
Error::UserNotLoggedIn => Redirect::to(
&Route::Login {
redirect_url: Route::OverviewPage {}.to_string(),
}
.to_string(),
)
.into_response(),
Error::Forbidden(_) => (StatusCode::FORBIDDEN, message).into_response(),
// INTERNAL_SERVER_ERROR variants
_ => {
tracing::error!("Internal Server Error: {:?}", message);
(StatusCode::INTERNAL_SERVER_ERROR, message).into_response()
}
}
}
}

302
src/infrastructure/login.rs Normal file
View File

@@ -0,0 +1,302 @@
use super::error::Result;
use super::user::{KeyCloakSub, UserEntity};
use crate::Route;
use axum::{
extract::Query,
response::{IntoResponse, Redirect, Response},
Extension,
};
use reqwest::StatusCode;
use tracing::{info, warn};
use url::form_urlencoded;
#[derive(serde::Deserialize)]
pub struct CallbackCode {
code: Option<String>,
error: Option<String>,
}
const LOGIN_REDIRECT_URL_SESSION_KEY: &str = "login.redirect.url";
const TEST_USER_SUB: KeyCloakSub = KeyCloakSub(String::new());
#[derive(serde::Deserialize)]
pub struct LoginRedirectQuery {
redirect_url: Option<String>,
}
/// Handler that redirects the user to the login page of Keycloack.
#[axum::debug_handler]
pub async fn redirect_to_keycloack_login(
state: Extension<super::server_state::ServerState>,
user_session: super::auth::UserSession,
session: tower_sessions::Session,
query: Query<LoginRedirectQuery>,
) -> Result<Response> {
// check if already logged in before redirecting again
if user_session.data().is_ok() {
return Ok(Redirect::to(&Route::OverviewPage {}.to_string()).into_response());
}
if let Some(url) = &query.redirect_url {
if !url.is_empty() {
session.insert(LOGIN_REDIRECT_URL_SESSION_KEY, &url).await?;
}
}
// if this is a test user then skip login
if state.keycloak_variables.enable_test_user {
return login_test_user(state, session).await;
}
let kc_base_url = &state.keycloak_variables.base_url;
let kc_realm = &state.keycloak_variables.realm;
let kc_client_id = &state.keycloak_variables.client_id;
let redirect_uri = format!("http://localhost:8000/auth/callback");
let encoded_redirect_uri: String =
form_urlencoded::byte_serialize(redirect_uri.as_bytes()).collect();
// Needed for running locally.
// This will not panic on production and it will return the original so we can keep it
let routed_kc_base_url = kc_base_url.replace("keycloak", "localhost");
Ok(Redirect::to(
format!("{routed_kc_base_url}/realms/{kc_realm}/protocol/openid-connect/auth?client_id={kc_client_id}&response_type=code&scope=openid%20profile%20email&redirect_uri={encoded_redirect_uri}").as_str())
.into_response())
}
/// Helper function that automatically logs the user in as a test user.
async fn login_test_user(
state: Extension<super::server_state::ServerState>,
session: tower_sessions::Session,
) -> Result<Response> {
let user = state.db.get_user_by_kc_sub(TEST_USER_SUB).await?;
// if we do not have a test user already, create one
let user = if let Some(user) = user {
info!("Existing test user logged in");
user
} else {
info!("Test User not found, inserting ...");
let user = UserEntity {
_id: mongodb::bson::oid::ObjectId::new(),
created_at: mongodb::bson::DateTime::now(),
kc_sub: TEST_USER_SUB,
email: "exampleuser@domain.com".to_string(),
};
state.db.insert_user(&user).await?;
user
};
info!("Test User successfuly logged in: {:?}", user);
let data = super::auth::LoggedInData {
id: user._id.to_string(),
token_id: String::new(),
username: "tester".to_string(),
avatar_url: None,
};
super::auth::login(&session, &data).await?;
// redirect to the URL stored in the session if available
let redirect_url = session
.remove::<String>(LOGIN_REDIRECT_URL_SESSION_KEY)
.await?
.unwrap_or_else(|| Route::OverviewPage {}.to_string());
Ok(Redirect::to(&redirect_url).into_response())
}
/// Handler function executed once KC redirects back to us. Creates database entries if
/// needed and initializes the user session to mark the user as "logged in".
#[axum::debug_handler]
pub async fn handle_login_callback(
state: Extension<super::server_state::ServerState>,
session: tower_sessions::Session,
Query(params): Query<CallbackCode>,
) -> Result<Response> {
// now make sure the user actually authorized the app and that there was no error
let Some(code) = params.code else {
warn!("Code was not provided, error: {:?}", params.error);
return Ok(Redirect::to(&Route::OverviewPage {}.to_string()).into_response());
};
// if on dev environment we get the internal kc url
let kc_base_url = std::env::var("KEYCLOAK_ADMIN_URL")
.unwrap_or_else(|_| state.keycloak_variables.base_url.clone());
let kc_realm = &state.keycloak_variables.realm;
let kc_client_id = &state.keycloak_variables.client_id;
let kc_client_secret = &state.keycloak_variables.client_secret;
let redirect_uri = format!("http://localhost:8000/auth/callback");
// exchange the code for an access token
let token = exchange_code(
&code,
&kc_base_url,
kc_realm,
kc_client_id,
kc_client_secret,
redirect_uri.as_str(),
)
.await?;
// use the access token to get the user information
let user_info = get_user_info(&token, &kc_base_url, kc_realm).await?;
// Check if the user is a member of the organization (only on dev and demo environments)
let base_url = state.keycloak_variables.base_url.clone();
let is_for_devs = base_url.contains("dev") || base_url.contains("demo");
if is_for_devs {
let Some(github_login) = user_info.github_login.as_ref() else {
return Err(crate::infrastructure::error::Error::Forbidden(
"GitHub login not available.".to_string(),
));
};
if !is_org_member(github_login).await? {
return Err(crate::infrastructure::error::Error::Forbidden(
"You are not a member of the organization.".to_string(),
));
}
}
// now check if we have a user already
let kc_sub = KeyCloakSub(user_info.sub);
let user = state.db.get_user_by_kc_sub(kc_sub.clone()).await?;
// if we do not have a user already, create one
let user = if let Some(user) = user {
info!("Existing user logged in");
user
} else {
info!("User not found, creating ...");
let user = UserEntity {
_id: mongodb::bson::oid::ObjectId::new(),
created_at: mongodb::bson::DateTime::now(),
kc_sub,
email: user_info.email.clone(),
};
state.db.insert_user(&user).await?;
user
};
info!("User successfuly logged in");
// we now have access token and information about the user that just logged in, as well as an
// existing or newly created user database entity.
// Store information in session storage that we want (eg name and avatar url + databae id) to make the user "logged in"!
// Redirect the user somewhere
let data = super::auth::LoggedInData {
id: user._id.to_string(),
token_id: token.id_token,
username: user_info.preferred_username,
avatar_url: user_info.picture,
};
super::auth::login(&session, &data).await?;
// redirect to the URL stored in the session if available
let redirect_url = session
.remove::<String>(LOGIN_REDIRECT_URL_SESSION_KEY)
.await?
.unwrap_or_else(|| Route::OverviewPage {}.to_string());
Ok(Redirect::to(&redirect_url).into_response())
}
#[derive(serde::Deserialize)]
#[allow(dead_code)] // not all fields are currently used
struct AccessToken {
access_token: String,
expires_in: u64,
refresh_token: String,
refresh_expires_in: u64,
id_token: String,
}
/// Exchange KC code for an access token
async fn exchange_code(
code: &str,
kc_base_url: &str,
kc_realm: &str,
kc_client_id: &str,
kc_client_secret: &str,
redirect_uri: &str,
) -> Result<AccessToken> {
let res = reqwest::Client::new()
.post(format!(
"{kc_base_url}/realms/{kc_realm}/protocol/openid-connect/token",
))
.form(&[
("grant_type", "authorization_code"),
("client_id", kc_client_id),
("client_secret", kc_client_secret),
("code", code),
("redirect_uri", redirect_uri),
])
.send()
.await?;
let res: AccessToken = res.json().await?;
Ok(res)
}
/// Query the openid-connect endpoint to get the user info by using the access token.
async fn get_user_info(token: &AccessToken, kc_base_url: &str, kc_realm: &str) -> Result<UserInfo> {
let client = reqwest::Client::new();
let url = format!("{kc_base_url}/realms/{kc_realm}/protocol/openid-connect/userinfo");
let mut request = client.get(&url).bearer_auth(token.access_token.clone());
// If KEYCLOAK_ADMIN_URL is NOT set (i.e. we're on the local Keycloak),
// add the HOST header for local testing.
if std::env::var("KEYCLOAK_ADMIN_URL").is_err() {
request = request.header("HOST", "localhost:8888");
}
let res = request.send().await?;
let res: UserInfo = res.json().await?;
Ok(res)
}
/// Contains selected fields from the user information call to KC
/// https://openid.net/specs/openid-connect-core-1_0.html#UserInfo
#[derive(serde::Deserialize)]
#[allow(dead_code)] // not all fields are currently used
struct UserInfo {
sub: String, // subject element of the ID Token
name: String,
given_name: String,
family_name: String,
preferred_username: String,
email: String,
picture: Option<String>,
github_login: Option<String>,
}
/// Check if a user is a member of the organization
const GITHUB_ORG: &str = "etospheres-labs";
async fn is_org_member(username: &str) -> Result<bool> {
let url = format!("https://api.github.com/orgs/{GITHUB_ORG}/members/{username}");
let client = reqwest::Client::new();
let response = client
.get(&url)
.header("Accept", "application/vnd.github+json") // GitHub requires a User-Agent header.
.header("User-Agent", "etopay-app")
.send()
.await?;
match response.status() {
StatusCode::NO_CONTENT => Ok(true),
status => {
tracing::warn!(
"{}: User '{}' is not a member of the organization",
status.as_str(),
username
);
Ok(false)
}
}
}

10
src/infrastructure/mod.rs Normal file
View File

@@ -0,0 +1,10 @@
#![cfg(feature = "server")]
mod login;
pub mod auth;
pub mod db;
pub mod error;
pub mod server;
pub mod server_state;
pub mod user;

View File

@@ -0,0 +1,105 @@
use super::error::Error;
use super::server_state::ServerState;
use crate::infrastructure::{auth::KeycloakVariables, server_state::ServerStateInner};
use axum::{routing::*, Extension};
use dioxus::dioxus_core::Element;
use dioxus::prelude::*;
use dioxus_logger::tracing::info;
use reqwest::{
header::{ACCEPT, AUTHORIZATION, CONTENT_TYPE},
Method,
};
use time::Duration;
use tower_http::cors::{Any, CorsLayer};
use tower_sessions::{
cookie::{Key, SameSite},
Expiry, MemoryStore, SessionManagerLayer,
};
pub fn server_start(app_fn: fn() -> Element) -> Result<(), Error> {
dotenvy::dotenv().ok();
tokio::runtime::Runtime::new()?.block_on(async move {
info!("Connecting to the database ...");
let mongodb_uri = get_env_variable("MONGODB_URI");
let client = mongodb::Client::with_uri_str(mongodb_uri).await?;
let db = super::db::Database::new(client).await;
info!("Connected");
let keycloak_variables: KeycloakVariables = KeycloakVariables {
base_url: get_env_variable("BASE_URL_AUTH"),
realm: get_env_variable("KC_REALM"),
client_id: get_env_variable("KC_CLIENT_ID"),
client_secret: get_env_variable("KC_CLIENT_SECRET"),
enable_test_user: std::env::var("ENABLE_TEST_USER").is_ok_and(|v| v == "yes"),
};
let state: ServerState = ServerStateInner {
db,
keycloak_variables: Box::leak(Box::new(keycloak_variables)),
}
.into();
// This uses `tower-sessions` to establish a layer that will provide the session
// as a request extension.
let key = Key::generate(); // This is only used for demonstration purposes; provide a proper
// cryptographic key in a real application.
let session_store = MemoryStore::default();
let session_layer = SessionManagerLayer::new(session_store)
// only allow session cookie in HTTPS connections (also works on localhost)
.with_secure(true)
.with_expiry(Expiry::OnInactivity(Duration::days(1)))
// Allow the session cookie to be sent when request originates from outside our
// domain. Required for the browser to pass the cookie when returning from github auth page.
.with_same_site(SameSite::Lax)
.with_signed(key);
let cors = CorsLayer::new()
// allow `GET` and `POST` when accessing the resource
.allow_methods([Method::GET, Method::POST, Method::PATCH, Method::DELETE])
// .allow_credentials(true)
.allow_headers([AUTHORIZATION, ACCEPT, CONTENT_TYPE])
// allow requests from any origin
.allow_origin(Any);
// Build our application web api router.
let web_api_router = Router::new()
// .route("/webhook/gitlab", post(super::gitlab::webhook_handler))
.route("/auth", get(super::login::redirect_to_keycloack_login))
.route("/auth/logout", get(super::auth::logout))
.route("/auth/callback", get(super::login::handle_login_callback))
// Server side render the application, serve static assets, and register the server functions.
.serve_dioxus_application(ServeConfig::default(), app_fn)
.layer(Extension(state))
.layer(session_layer)
.layer(cors)
.layer(tower_http::trace::TraceLayer::new_for_http());
// Start it.
let addr = dioxus_cli_config::fullstack_address_or_localhost();
info!("Server address: {}", addr);
let listener = tokio::net::TcpListener::bind(&addr).await?;
axum::serve(listener, web_api_router.into_make_service()).await?;
Ok(())
})
}
/// Tries to load the value from an environment as String.
///
/// # Arguments
///
/// * `key` - the environment variable key to try to load
///
/// # Panics
///
/// Panics if the environment variable does not exist.
fn get_env_variable(key: &str) -> String {
std::env::var(key).unwrap_or_else(|_| {
tracing::error!("{key} environment variable not set. {key} must be set!");
panic!("Environment variable {key} not present")
})
}

View File

@@ -0,0 +1,55 @@
//! Implements a [`ServerState`] that is available in the dioxus server functions
//! as well as in axum handlers.
//! Taken from https://github.com/dxps/dioxus_playground/tree/44a4ddb223e6afe50ef195e61aa2b7182762c7da/dioxus-05-fullstack-routing-axum-pgdb
use super::auth::KeycloakVariables;
use super::error::{Error, Result};
use axum::http;
use std::ops::Deref;
use std::sync::Arc;
/// This is stored as an "extension" object in the axum webserver
/// We can get it in the dioxus server functions using
/// ```rust
/// let state: crate::infrastructure::server_state::ServerState = extract().await?;
/// ```
#[derive(Clone)]
pub struct ServerState(Arc<ServerStateInner>);
impl Deref for ServerState {
type Target = ServerStateInner;
fn deref(&self) -> &Self::Target {
&self.0
}
}
pub struct ServerStateInner {
pub db: crate::infrastructure::db::Database,
pub keycloak_variables: &'static KeycloakVariables,
}
impl From<ServerStateInner> for ServerState {
fn from(value: ServerStateInner) -> Self {
Self(Arc::new(value))
}
}
impl<S> axum::extract::FromRequestParts<S> for ServerState
where
S: std::marker::Sync + std::marker::Send,
{
type Rejection = Error;
async fn from_request_parts(parts: &mut http::request::Parts, _: &S) -> Result<Self> {
parts
.extensions
.get::<ServerState>()
.cloned()
.ok_or(Error::ServerStateError(
"ServerState extension should exist".to_string(),
))
}
}

View File

@@ -0,0 +1,21 @@
use serde::{Deserialize, Serialize};
/// Wraps a `String` to store the sub from KC
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct KeyCloakSub(pub String);
/// database entity to store our users
#[derive(Debug, Serialize, Deserialize)]
pub struct UserEntity {
/// Our unique id of the user, for now this is just the mongodb assigned id
pub _id: mongodb::bson::oid::ObjectId,
/// Time the user was created
pub created_at: mongodb::bson::DateTime,
/// KC subject element of the ID Token
pub kc_sub: KeyCloakSub,
/// User email as provided during signup with the identity provider
pub email: String,
}

8
src/lib.rs Normal file
View File

@@ -0,0 +1,8 @@
mod app;
mod components;
pub mod infrastructure;
mod pages;
pub use app::*;
pub use components::*;
pub use pages::*;

2
src/pages/mod.rs Normal file
View File

@@ -0,0 +1,2 @@
mod overview;
pub use overview::*;

8
src/pages/overview.rs Normal file
View File

@@ -0,0 +1,8 @@
use dioxus::prelude::*;
#[component]
pub fn OverviewPage() -> Element {
rsx! {
h1 { "Hello" }
}
}