From 2eb9f699aae32efcb2501872c0d079b8e46d46bb Mon Sep 17 00:00:00 2001 From: Sharang Parnerkar Date: Mon, 16 Feb 2026 21:23:25 +0100 Subject: [PATCH] feat: basic project restructure --- packages/api/Cargo.toml | 10 - packages/api/README.md | 13 - packages/api/src/lib.rs | 8 - packages/web/Cargo.toml | 13 - packages/web/README.md | 30 -- packages/web/assets/blog.css | 8 - packages/web/assets/dx-components-theme.css | 87 ----- packages/web/assets/favicon.ico | Bin 132770 -> 0 bytes packages/web/assets/main.css | 6 - packages/web/src/components/mod.rs | 2 - .../web/src/components/toast/component.rs | 15 - packages/web/src/components/toast/mod.rs | 2 - packages/web/src/components/toast/style.css | 185 ----------- packages/web/src/main.rs | 45 --- packages/web/src/views/blog.rs | 30 -- packages/web/src/views/home.rs | 6 - packages/web/src/views/mod.rs | 5 - src/infrastructure/db.rs | 49 +++ src/infrastructure/login.rs | 302 ++++++++++++++++++ src/infrastructure/server_state.rs | 55 ++++ src/infrastructure/user.rs | 21 ++ 21 files changed, 427 insertions(+), 465 deletions(-) delete mode 100644 packages/api/Cargo.toml delete mode 100644 packages/api/README.md delete mode 100644 packages/api/src/lib.rs delete mode 100644 packages/web/Cargo.toml delete mode 100644 packages/web/README.md delete mode 100644 packages/web/assets/blog.css delete mode 100644 packages/web/assets/dx-components-theme.css delete mode 100644 packages/web/assets/favicon.ico delete mode 100644 packages/web/assets/main.css delete mode 100644 packages/web/src/components/mod.rs delete mode 100644 packages/web/src/components/toast/component.rs delete mode 100644 packages/web/src/components/toast/mod.rs delete mode 100644 packages/web/src/components/toast/style.css delete mode 100644 packages/web/src/main.rs delete mode 100644 packages/web/src/views/blog.rs delete mode 100644 packages/web/src/views/home.rs delete mode 100644 packages/web/src/views/mod.rs create mode 100644 src/infrastructure/db.rs create mode 100644 src/infrastructure/login.rs create mode 100644 src/infrastructure/server_state.rs create mode 100644 src/infrastructure/user.rs diff --git a/packages/api/Cargo.toml b/packages/api/Cargo.toml deleted file mode 100644 index bca3c11..0000000 --- a/packages/api/Cargo.toml +++ /dev/null @@ -1,10 +0,0 @@ -[package] -name = "api" -version = "0.1.0" -edition = "2021" - -[dependencies] -dioxus = { workspace = true, features = ["fullstack"] } - -[features] -server = ["dioxus/server"] diff --git a/packages/api/README.md b/packages/api/README.md deleted file mode 100644 index 0bd19eb..0000000 --- a/packages/api/README.md +++ /dev/null @@ -1,13 +0,0 @@ -# API - -This crate contains all shared fullstack server functions. This is a great place to place any server-only logic you would like to expose in multiple platforms like a method that accesses your database or a method that sends an email. - -This crate will be built twice: -1. Once for the server build with the `dioxus/server` feature enabled -2. Once for the client build with the client feature disabled - -During the server build, the server functions will be collected and hosted on a public API for the client to call. During the client build, the server functions will be compiled into the client build. - -## Dependencies - -Most server dependencies (like sqlx and tokio) will not compile on client platforms like WASM. To avoid building server dependencies on the client, you should add platform specific dependencies under the `server` feature in the [Cargo.toml](../Cargo.toml) file. More details about managing server only dependencies can be found in the [Dioxus guide](https://dioxuslabs.com/learn/0.7/guides/fullstack/managing_dependencies#adding-server-only-dependencies). diff --git a/packages/api/src/lib.rs b/packages/api/src/lib.rs deleted file mode 100644 index e507148..0000000 --- a/packages/api/src/lib.rs +++ /dev/null @@ -1,8 +0,0 @@ -//! This crate contains all shared fullstack server functions. -use dioxus::prelude::*; - -/// Echo the user input on the server. -#[post("/api/echo")] -pub async fn echo(input: String) -> Result { - Ok(input) -} diff --git a/packages/web/Cargo.toml b/packages/web/Cargo.toml deleted file mode 100644 index 4a46205..0000000 --- a/packages/web/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "web" -version = "0.1.0" -edition = "2021" - -[dependencies] -dioxus = { workspace = true, features = ["router", "fullstack"] } -dioxus-primitives = { git = "https://github.com/DioxusLabs/components", version = "0.0.1", default-features = false } - -[features] -default = [] -web = ["dioxus/web"] -server = ["dioxus/server"] diff --git a/packages/web/README.md b/packages/web/README.md deleted file mode 100644 index f533f8d..0000000 --- a/packages/web/README.md +++ /dev/null @@ -1,30 +0,0 @@ -# Development - -The web crate defines the entrypoint for the web app along with any assets, components and dependencies that are specific to web builds. The web crate starts out something like this: - -``` -web/ -├─ assets/ # Assets used by the web app - Any platform specific assets should go in this folder -├─ src/ -│ ├─ main.rs # The entrypoint for the web app.It also defines the routes for the web platform -│ ├─ views/ # The views each route will render in the web version of the app -│ │ ├─ mod.rs # Defines the module for the views route and re-exports the components for each route -│ │ ├─ blog.rs # The component that will render at the /blog/:id route -│ │ ├─ home.rs # The component that will render at the / route -├─ Cargo.toml # The web crate's Cargo.toml - This should include all web specific dependencies -``` - -## Dependencies -Since you have fullstack enabled, the web crate will be built two times: -1. Once for the server build with the `server` feature enabled -2. Once for the client build with the `web` feature enabled - -You should make all web specific dependencies optional and only enabled in the `web` feature. This will ensure that the server builds don't pull in web specific dependencies which cuts down on build times significantly. - -### Serving Your Web App - -You can start your web app with the following command: - -```bash -dx serve -``` diff --git a/packages/web/assets/blog.css b/packages/web/assets/blog.css deleted file mode 100644 index f27f060..0000000 --- a/packages/web/assets/blog.css +++ /dev/null @@ -1,8 +0,0 @@ -#blog { - margin-top: 50px; -} - -#blog a { - color: #ffffff; - margin-top: 50px; -} \ No newline at end of file diff --git a/packages/web/assets/dx-components-theme.css b/packages/web/assets/dx-components-theme.css deleted file mode 100644 index c55ca4a..0000000 --- a/packages/web/assets/dx-components-theme.css +++ /dev/null @@ -1,87 +0,0 @@ -/* This file contains the global styles for the styled dioxus components. You only - * need to import this file once in your project root. - */ -@import url("https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap"); - -body { - color: var(--secondary-color-4); - font-family: Inter, sans-serif; - font-optical-sizing: auto; - font-style: normal; - font-weight: 400; -} - -html[data-theme="dark"] { - --dark: initial; - --light: ; -} - -html[data-theme="light"] { - --dark: ; - --light: initial; -} - -@media (prefers-color-scheme: dark) { - :root { - --dark: initial; - --light: ; - } -} - -@media (prefers-color-scheme: light) { - :root { - --dark: ; - --light: initial; - } -} - -:root { - /* Primary colors */ - --primary-color: var(--dark, #000) var(--light, #fff); - --primary-color-1: var(--dark, #0e0e0e) var(--light, #fbfbfb); - --primary-color-2: var(--dark, #0a0a0a) var(--light, #fff); - --primary-color-3: var(--dark, #141313) var(--light, #f8f8f8); - --primary-color-4: var(--dark, #1a1a1a) var(--light, #f8f8f8); - --primary-color-5: var(--dark, #262626) var(--light, #f5f5f5); - --primary-color-6: var(--dark, #232323) var(--light, #e5e5e5); - --primary-color-7: var(--dark, #3e3e3e) var(--light, #b0b0b0); - - /* Secondary colors */ - --secondary-color: var(--dark, #fff) var(--light, #000); - --secondary-color-1: var(--dark, #fafafa) var(--light, #000); - --secondary-color-2: var(--dark, #e6e6e6) var(--light, #0d0d0d); - --secondary-color-3: var(--dark, #dcdcdc) var(--light, #2b2b2b); - --secondary-color-4: var(--dark, #d4d4d4) var(--light, #111); - --secondary-color-5: var(--dark, #a1a1a1) var(--light, #848484); - --secondary-color-6: var(--dark, #5d5d5d) var(--light, #d0d0d0); - - /* Highlight colors */ - --focused-border-color: var(--dark, #2b7fff) var(--light, #2b7fff); - --primary-success-color: var(--dark, #02271c) var(--light, #ecfdf5); - --secondary-success-color: var(--dark, #b6fae3) var(--light, #10b981); - --primary-warning-color: var(--dark, #342203) var(--light, #fffbeb); - --secondary-warning-color: var(--dark, #feeac7) var(--light, #f59e0b); - --primary-error-color: var(--dark, #a22e2e) var(--light, #dc2626); - --secondary-error-color: var(--dark, #9b1c1c) var(--light, #ef4444); - --contrast-error-color: var(--dark, var(--secondary-color-3)) var(--light, var(--primary-color)); - --primary-info-color: var(--dark, var(--primary-color-5)) var(--light, var(--primary-color)); - --secondary-info-color: var(--dark, var(--primary-color-7)) var(--light, var(--secondary-color-3)); -} - -/* Modern browsers with `scrollbar-*` support */ -@supports (scrollbar-width: auto) { - :not(:hover) { - scrollbar-color: rgb(0 0 0 / 0%) rgb(0 0 0 / 0%); - } - - :hover { - scrollbar-color: var(--secondary-color-2) rgb(0 0 0 / 0%); - } -} - -/* Legacy browsers with `::-webkit-scrollbar-*` support */ -@supports selector(::-webkit-scrollbar) { - :root::-webkit-scrollbar-track { - background: transparent; - } -} diff --git a/packages/web/assets/favicon.ico b/packages/web/assets/favicon.ico deleted file mode 100644 index eed0c09735ab94e724c486a053c367cf7ee3d694..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 132770 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 diff --git a/packages/web/assets/main.css b/packages/web/assets/main.css deleted file mode 100644 index ef6e674..0000000 --- a/packages/web/assets/main.css +++ /dev/null @@ -1,6 +0,0 @@ -body { - background-color: #0f1116; - color: #ffffff; - font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; - margin: 20px; -} \ No newline at end of file diff --git a/packages/web/src/components/mod.rs b/packages/web/src/components/mod.rs deleted file mode 100644 index d7137e2..0000000 --- a/packages/web/src/components/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -// AUTOGENERATED Components module -pub mod toast; diff --git a/packages/web/src/components/toast/component.rs b/packages/web/src/components/toast/component.rs deleted file mode 100644 index 4b6878f..0000000 --- a/packages/web/src/components/toast/component.rs +++ /dev/null @@ -1,15 +0,0 @@ -use dioxus::prelude::*; -use dioxus_primitives::toast::{self, ToastProviderProps}; - -#[component] -pub fn ToastProvider(props: ToastProviderProps) -> Element { - rsx! { - document::Link { rel: "stylesheet", href: asset!("./style.css") } - toast::ToastProvider { - default_duration: props.default_duration, - max_toasts: props.max_toasts, - render_toast: props.render_toast, - {props.children} - } - } -} diff --git a/packages/web/src/components/toast/mod.rs b/packages/web/src/components/toast/mod.rs deleted file mode 100644 index 9a8ae55..0000000 --- a/packages/web/src/components/toast/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -mod component; -pub use component::*; \ No newline at end of file diff --git a/packages/web/src/components/toast/style.css b/packages/web/src/components/toast/style.css deleted file mode 100644 index 7bf994a..0000000 --- a/packages/web/src/components/toast/style.css +++ /dev/null @@ -1,185 +0,0 @@ -.toast-container { - position: fixed; - z-index: 9999; - right: 20px; - bottom: 20px; - max-width: 350px; -} - -.toast-list { - display: flex; - flex-direction: column-reverse; - padding: 0; - margin: 0; - gap: 0.75rem; -} - -.toast-item { - display: flex; -} - -.toast { - z-index: calc(var(--toast-count) - var(--toast-index)); - display: flex; - overflow: hidden; - width: 18rem; - height: 4rem; - box-sizing: border-box; - align-items: center; - justify-content: space-between; - padding: 12px 16px; - border: 1px solid var(--light, var(--primary-color-6)) - var(--dark, var(--primary-color-7)); - border-radius: 0.5rem; - margin-top: -4rem; - box-shadow: 0 4px 12px rgb(0 0 0 / 15%); - filter: var(--light, none) - var( - --dark, - brightness(calc(0.5 + 0.5 * (1 - ((var(--toast-index) + 1) / 4)))) - ); - opacity: calc(1 - var(--toast-hidden)); - transform: scale( - calc(100% - var(--toast-index) * 5%), - calc(100% - var(--toast-index) * 2%) - ); - transition: transform 0.2s ease, margin-top 0.2s ease, opacity 0.2s ease; - - --toast-hidden: calc(min(max(0, var(--toast-index) - 2), 1)); -} - -.toast-container:not(:hover, :focus-within) - .toast[data-toast-even]:not([data-top]) { - animation: slide-up-even 0.2s ease-out; -} - -.toast-container:not(:hover, :focus-within) - .toast[data-toast-odd]:not([data-top]) { - animation: slide-up-odd 0.2s ease-out; -} - -@keyframes slide-up-even { - from { - transform: translateY(0.5rem) - scale( - calc(100% - var(--toast-index) * 5%), - calc(100% - var(--toast-index) * 2%) - ); - } - - to { - transform: translateY(0) - scale( - calc(100% - var(--toast-index) * 5%), - calc(100% - var(--toast-index) * 2%) - ); - } -} - -@keyframes slide-up-odd { - from { - transform: translateY(0.5rem) - scale( - calc(100% - var(--toast-index) * 5%), - calc(100% - var(--toast-index) * 2%) - ); - } - - to { - transform: translateY(0) - scale( - calc(100% - var(--toast-index) * 5%), - calc(100% - var(--toast-index) * 2%) - ); - } -} - -.toast[data-top] { - animation: slide-in 0.2s ease-out; -} - -.toast-container:hover .toast[data-top], -.toast-container:focus-within .toast[data-top] { - animation: slide-in 0 ease-out; -} - -@keyframes slide-in { - from { - opacity: 0; - transform: translateY(100%) - scale( - calc(110% - var(--toast-index) * 5%), - calc(110% - var(--toast-index) * 2%) - ); - } - - to { - opacity: 1; - transform: translateY(0) - scale( - calc(100% - var(--toast-index) * 5%), - calc(100% - var(--toast-index) * 2%) - ); - } -} - -.toast-container:hover .toast, -.toast-container:focus-within .toast { - margin-top: var(--toast-padding); - filter: brightness(1); - opacity: 1; - transform: scale(calc(100%)); -} - -.toast[data-type="success"] { - background-color: var(--primary-success-color); - color: var(--secondary-success-color); -} - -.toast[data-type="error"] { - background-color: var(--primary-error-color); - color: var(--contrast-error-color); -} - -.toast[data-type="warning"] { - background-color: var(--primary-warning-color); - color: var(--secondary-warning-color); -} - -.toast[data-type="info"] { - background-color: var(--primary-info-color); - color: var(--secondary-info-color); -} - -.toast-content { - flex: 1; - margin-right: 8px; - transition: filter 0.2s ease; -} - -.toast-title { - margin-bottom: 4px; - color: var(--secondary-color-4); - font-weight: 600; -} - -.toast-description { - color: var(--secondary-color-3); - font-size: 0.875rem; -} - -.toast-close { - align-self: flex-start; - padding: 0; - border: none; - margin: 0; - background: none; - color: var(--secondary-color-3); - cursor: pointer; - font-size: 18px; - line-height: 1; -} - -.toast-close:hover { - color: var(--secondary-color-1); -} diff --git a/packages/web/src/main.rs b/packages/web/src/main.rs deleted file mode 100644 index 54b2443..0000000 --- a/packages/web/src/main.rs +++ /dev/null @@ -1,45 +0,0 @@ -use dioxus::prelude::*; - -use views::{Blog, Home}; - -mod views; - -#[derive(Debug, Clone, Routable, PartialEq)] -#[rustfmt::skip] -enum Route { - #[layout(WebNavbar)] - #[route("/")] - Home {}, - #[route("/blog/:id")] - Blog { id: i32 }, -} - -const FAVICON: Asset = asset!("/assets/favicon.ico"); -const MAIN_CSS: Asset = asset!("/assets/main.css"); - -fn main() { - dioxus::launch(App); -} - -#[component] -fn App() -> Element { - // Build cool things ✌️ - - rsx! { - // Global app resources - document::Link { rel: "icon", href: FAVICON } - document::Link { rel: "stylesheet", href: MAIN_CSS } - - Router:: {} - } -} - -/// A web-specific Router around the shared `Navbar` component -/// which allows us to use the web-specific `Route` enum. -#[component] -fn WebNavbar() -> Element { - rsx! { - - Outlet:: {} - } -} diff --git a/packages/web/src/views/blog.rs b/packages/web/src/views/blog.rs deleted file mode 100644 index c114f5e..0000000 --- a/packages/web/src/views/blog.rs +++ /dev/null @@ -1,30 +0,0 @@ -use crate::Route; -use dioxus::prelude::*; - -const BLOG_CSS: Asset = asset!("/assets/blog.css"); - -#[component] -pub fn Blog(id: i32) -> Element { - rsx! { - document::Link { rel: "stylesheet", href: BLOG_CSS} - - 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" - } - } - } -} diff --git a/packages/web/src/views/home.rs b/packages/web/src/views/home.rs deleted file mode 100644 index fb016b2..0000000 --- a/packages/web/src/views/home.rs +++ /dev/null @@ -1,6 +0,0 @@ -use dioxus::prelude::*; - -#[component] -pub fn Home() -> Element { - rsx! {} -} diff --git a/packages/web/src/views/mod.rs b/packages/web/src/views/mod.rs deleted file mode 100644 index b3fe26b..0000000 --- a/packages/web/src/views/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -mod home; -pub use home::Home; - -mod blog; -pub use blog::Blog; diff --git a/src/infrastructure/db.rs b/src/infrastructure/db.rs new file mode 100644 index 0000000..0c52dca --- /dev/null +++ b/src/infrastructure/db.rs @@ -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 { + self.client + .database("dashboard") + .collection::("users") + } + + pub async fn get_user_by_kc_sub(&self, kc_sub: KeyCloakSub) -> Result> { + 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> { + 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(()) + } +} diff --git a/src/infrastructure/login.rs b/src/infrastructure/login.rs new file mode 100644 index 0000000..6f75aac --- /dev/null +++ b/src/infrastructure/login.rs @@ -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, + error: Option, +} + +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, +} + +/// Handler that redirects the user to the login page of Keycloack. +#[axum::debug_handler] +pub async fn redirect_to_keycloack_login( + state: Extension, + user_session: super::auth::UserSession, + session: tower_sessions::Session, + query: Query, +) -> Result { + // 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, + session: tower_sessions::Session, +) -> Result { + 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::(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, + session: tower_sessions::Session, + Query(params): Query, +) -> Result { + // 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::(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 { + 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 { + 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, + github_login: Option, +} + +/// Check if a user is a member of the organization +const GITHUB_ORG: &str = "etospheres-labs"; +async fn is_org_member(username: &str) -> Result { + 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) + } + } +} diff --git a/src/infrastructure/server_state.rs b/src/infrastructure/server_state.rs new file mode 100644 index 0000000..5c3f017 --- /dev/null +++ b/src/infrastructure/server_state.rs @@ -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); + +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 for ServerState { + fn from(value: ServerStateInner) -> Self { + Self(Arc::new(value)) + } +} + +impl axum::extract::FromRequestParts 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 { + parts + .extensions + .get::() + .cloned() + .ok_or(Error::ServerStateError( + "ServerState extension should exist".to_string(), + )) + } +} diff --git a/src/infrastructure/user.rs b/src/infrastructure/user.rs new file mode 100644 index 0000000..c26ffd1 --- /dev/null +++ b/src/infrastructure/user.rs @@ -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, +}