Compare commits

..

10 Commits

Author SHA1 Message Date
d5eba9620c Link to favicon
All checks were successful
Push Workflows / rustfmt (push) Successful in 5s
Push Workflows / tailwind-build (push) Successful in 6s
Push Workflows / docs (push) Successful in 20s
Push Workflows / test (push) Successful in 23s
Push Workflows / clippy (push) Successful in 18s
Push Workflows / build (push) Successful in 47s
Push Workflows / nix-build (push) Successful in 6m24s
2026-07-11 12:41:03 -04:00
39afdd83a7 Add username and password form input presets
All checks were successful
Push Workflows / rustfmt (push) Successful in 5s
Push Workflows / tailwind-build (push) Successful in 6s
Push Workflows / docs (push) Successful in 31s
Push Workflows / clippy (push) Successful in 28s
Push Workflows / test (push) Successful in 35s
Push Workflows / build (push) Successful in 1m12s
Push Workflows / nix-build (push) Successful in 6m24s
2026-07-11 12:37:30 -04:00
34b887730a Enable account icons in Lucide 2026-07-11 12:37:30 -04:00
f2dc4c908e Export individual components from crate::components 2026-07-11 12:28:35 -04:00
219f1f5e7d Add open-signup to list of allowed endpoints 2026-07-11 12:26:28 -04:00
102387fdad Initialize tracing to INFO in release mode 2026-07-11 12:21:37 -04:00
87bd1b50a9 Add form component
All checks were successful
Push Workflows / rustfmt (push) Successful in 5s
Push Workflows / tailwind-build (push) Successful in 6s
Push Workflows / docs (push) Successful in 49s
Push Workflows / clippy (push) Successful in 1m19s
Push Workflows / test (push) Successful in 1m30s
Push Workflows / build (push) Successful in 2m24s
Push Workflows / nix-build (push) Successful in 5m35s
2026-07-08 21:33:04 -04:00
c8cd4b95dc Add dioxus-html 2026-07-08 21:33:04 -04:00
8348277a87 Add endpoint to check open signup 2026-07-08 20:47:58 -04:00
e7c43e7a47 Allow icon in form input component
Misc style adjustments
2026-07-08 20:44:38 -04:00
9 changed files with 165 additions and 11 deletions

1
Cargo.lock generated
View File

@@ -2419,6 +2419,7 @@ dependencies = [
"diesel-async",
"diesel_migrations",
"dioxus",
"dioxus-html",
"dotenvy",
"fred",
"getrandom 0.4.3",

View File

@@ -17,9 +17,10 @@ diesel = { version = "2.3.10", optional = true, features = ["chrono"] }
diesel-async = { version = "0.9.1", optional = true, features = ["postgres", "deadpool", "migrations"] }
diesel_migrations = { version = "2.3.2", optional = true }
dioxus = { version = "0.7.9", features = ["router", "fullstack"] }
dioxus-html = "0.7.9"
dotenvy = { version = "0.15.7", optional = true }
fred = { version = "10.1.0", optional = true }
lucide-dioxus = { version = "3.11.0", features = ["notifications"] }
lucide-dioxus = { version = "3.11.0", features = ["notifications", "account"] }
pbkdf2 = { version = "0.13.0", optional = true, features = ["getrandom", "phc"] }
rand = "0.10.1"
serde = { version = "1.0.228", features = ["derive"] }

View File

@@ -86,3 +86,9 @@ pub async fn logout() -> Result<()> {
pub async fn get_user() -> Result<Option<User>> {
Ok(auth.user.clone().map(Into::into))
}
/// Check if open signup is enabled
#[get("/api/v1/auth/open-signup", config: Extension<Config>)]
pub async fn open_signup() -> Result<bool> {
Ok(config.auth.open_signup)
}

View File

@@ -14,6 +14,7 @@ enum Route {
#[component]
pub fn App() -> Element {
rsx! {
document::Link { rel: "icon", href: LOGO_ICO }
document::Link { rel: "stylesheet", href: TAILWIND_CSS }
Router::<Route> {}
}

101
src/components/form.rs Normal file
View File

@@ -0,0 +1,101 @@
use dioxus::prelude::*;
use serde::de::DeserializeOwned;
use crate::util::error::Error;
/// A simple styled form card. Parses form fields into the provided generic type, and calls the
/// callback when submitted via button or enter key.
#[component]
pub fn Form<T>(
/// Heading above the form
title: String,
/// Form content, placed inside a `fieldset`
children: Element,
/// Text displayed on the form button
action_message: String,
/// Form-related error. Set to a parsing error on parse failure, or can be used to display any
/// submission error. Clears on each submit. Displays as a card between the inputs and action
/// button, opens a modal on click.
#[props(default)]
error: Signal<Option<Error>>,
/// Whether the form should display in a "loading" state, disables the action button and
/// displays a loading animation instead. For best UX, especially on pages the user might open
/// directly as opposed to navigate to within the app, initialize to true. This component will
/// set it to false when run. This sequence will discourage users from submitting the form
/// before the page is ready to handle it. Can also be used to indicate background work is
/// taking place in the `callback`, but this is not done automatically.
#[props(default)]
loading: Signal<bool>,
/// Content to display below the action button
#[props(default)]
extra_content: Option<Element>,
/// Form submission callback. Called with the deserialized form inputs.
onsubmit: Callback<T>,
) -> Element
where
T: DeserializeOwned + 'static,
{
// Button is initialized to loading, then set back when WASM is finished loading (use_effect
// runs only on the client)
use_effect(move || loading.set(false));
rsx! {
div {
class: "card card-xl w-full sm:w-100 sm:h-fit bg-base-200",
form {
onsubmit: move |evt| {
evt.prevent_default();
let data: T = match evt.data.parsed_values() {
Ok(data) => data,
Err(e) => {
let e = Error::message_here(e.to_string())
.with_context("Failed to parse form inputs");
error.set(Some(e));
return;
}
};
error.set(None);
onsubmit.call(data);
},
class: "card-body gap-3",
h1 {
class: "card-title",
{title}
}
fieldset {
class: "fieldset pt-0 gap-1",
{children}
}
{error().map(|e| e.as_alert())}
div {
class: "card-actions gap-4",
button {
class: "btn btn-primary btn-block",
disabled: loading(),
if loading() {
span {
class: "loading loading-dots"
}
} else {
{action_message}
}
}
{extra_content}
}
}
}
}
}

View File

@@ -9,20 +9,25 @@ pub fn FormInput(
#[props(default)] name: Option<String>,
#[props(default)] required: bool,
#[props(default)] r#type: String,
children: Element,
) -> Element {
let name = name.unwrap_or(label.to_lowercase());
rsx! {
div {
class: "group",
class: "group/field",
label {
class: "label opacity-0 not-group-has-placeholder-shown:opacity-100 transition-opacity pl-2",
p {
class: "opacity-0 not-group-has-placeholder-shown/field:opacity-70 transition-opacity pl-2",
{label.clone()}
}
label {
class: "input w-full",
{children}
input {
class: "input",
name,
required,
r#type,
@@ -30,4 +35,34 @@ pub fn FormInput(
}
}
}
}
}
#[component]
pub fn UsernameInput() -> Element {
rsx! {
FormInput {
label: "Username",
required: true,
lucide_dioxus::UserRound {
class: "opacity-50",
}
}
}
}
#[component]
pub fn PasswordInput() -> Element {
rsx! {
FormInput {
label: "Password",
required: true,
r#type: "password",
lucide_dioxus::KeyRound {
class: "opacity-50",
}
}
}
}

View File

@@ -1,2 +1,7 @@
pub mod footer;
pub mod form;
pub mod form_input;
pub use footer::*;
pub use form::*;
pub use form_input::*;

View File

@@ -16,6 +16,9 @@ use crate::app::App;
fn tracing_setup() {
#[cfg(debug_assertions)]
dioxus::logger::init(tracing::Level::DEBUG).expect("Failed to initialize tracing logger");
#[cfg(not(debug_assertions))]
dioxus::logger::init(tracing::Level::INFO).expect("Failed to initialize tracing logger");
}
#[cfg(not(feature = "server"))]

View File

@@ -13,12 +13,13 @@ const ALLOWED_PATH_PREFIX: [&str; 1] = ["/assets/"];
#[cfg(debug_assertions)]
const ALLOWED_PATH_PREFIX: [&str; 2] = ["/assets/", "/wasm/"];
const ALLOWED_PATHS: [&str; 5] = [
const ALLOWED_PATHS: [&str; 6] = [
"/login",
"/signup",
"/api/v1/auth/login",
"/api/v1/auth/signup",
"/api/v1/auth/user",
"/api/v1/auth/open-signup",
];
/// Axum middleware to redirect an unauthenticated request to /login unless it matches one of the allowed paths