registration
This commit is contained in:
@@ -5,3 +5,4 @@ pub(crate) mod admin;
|
||||
pub(crate) mod analytics;
|
||||
pub(crate) mod import_export;
|
||||
pub(crate) mod login;
|
||||
pub(crate) mod register;
|
||||
|
||||
53
web/src/pages/register/logic.rs
Normal file
53
web/src/pages/register/logic.rs
Normal file
@@ -0,0 +1,53 @@
|
||||
use axum::{
|
||||
Form,
|
||||
extract::State,
|
||||
http::{HeaderMap, StatusCode},
|
||||
response::{Html, IntoResponse, Response},
|
||||
};
|
||||
use tonic::Request;
|
||||
|
||||
use crate::{AppState, auth::RegisterRequest, ui::Nav};
|
||||
|
||||
use super::{state::RegisterInput, ui};
|
||||
|
||||
pub(crate) async fn register_page(headers: HeaderMap) -> Html<String> {
|
||||
Html(ui::render_page(Nav::new(&headers, "register")))
|
||||
}
|
||||
|
||||
pub(crate) async fn register(
|
||||
State(state): State<AppState>,
|
||||
Form(input): Form<RegisterInput>,
|
||||
) -> Response {
|
||||
// Same shaping as the client's `RegisterFormState::try_register`: trim
|
||||
// every field, upper-case the country code, and leave the checking to the
|
||||
// backend.
|
||||
let timezone = input.timezone.trim().to_string();
|
||||
let phone_country = input.phone_country.trim().to_ascii_uppercase();
|
||||
|
||||
let mut client = state.auth;
|
||||
let registered = match client
|
||||
.register(Request::new(RegisterRequest {
|
||||
username: input.username.trim().to_string(),
|
||||
email: input.email.trim().to_string(),
|
||||
password: input.password.trim().to_string(),
|
||||
password_confirmation: input.password_confirmation.trim().to_string(),
|
||||
role: input.role.trim().to_string(),
|
||||
timezone: timezone.clone(),
|
||||
phone_country: phone_country.clone(),
|
||||
}))
|
||||
.await
|
||||
{
|
||||
Ok(response) => response.into_inner(),
|
||||
Err(status) => {
|
||||
return (
|
||||
StatusCode::BAD_REQUEST,
|
||||
Html(ui::render_error(status.message())),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
|
||||
// The client reports the registered account back to the user and stays on
|
||||
// the form — registering does not sign anyone in.
|
||||
Html(ui::render_success(®istered, &timezone, &phone_country)).into_response()
|
||||
}
|
||||
20
web/src/pages/register/mod.rs
Normal file
20
web/src/pages/register/mod.rs
Normal file
@@ -0,0 +1,20 @@
|
||||
//! GET /register → register.html
|
||||
//! POST /register → registers the account and reports the outcome inline
|
||||
//!
|
||||
//! The form is the TUI client's register page (client/src/pages/register): the
|
||||
//! same seven fields, the same suggestion lists, the same handling — trim
|
||||
//! everything, upper-case the country, send it, and let the backend do the
|
||||
//! validating. Registering does not sign anyone in, exactly as in the client.
|
||||
|
||||
use axum::{Router, routing::get};
|
||||
|
||||
use crate::AppState;
|
||||
|
||||
pub(crate) mod logic;
|
||||
pub(crate) mod state;
|
||||
pub(crate) mod suggestions;
|
||||
pub(crate) mod ui;
|
||||
|
||||
pub(crate) fn router() -> Router<AppState> {
|
||||
Router::new().route("/register", get(logic::register_page).post(logic::register))
|
||||
}
|
||||
20
web/src/pages/register/state.rs
Normal file
20
web/src/pages/register/state.rs
Normal file
@@ -0,0 +1,20 @@
|
||||
/// The register form, one field per `RegisterRequest` field. Every one of them
|
||||
/// is optional at this layer: the client does not validate either, it trims and
|
||||
/// posts, and the backend answers with what is wrong.
|
||||
#[derive(serde::Deserialize)]
|
||||
pub(crate) struct RegisterInput {
|
||||
#[serde(default)]
|
||||
pub username: String,
|
||||
#[serde(default)]
|
||||
pub email: String,
|
||||
#[serde(default)]
|
||||
pub password: String,
|
||||
#[serde(default)]
|
||||
pub password_confirmation: String,
|
||||
#[serde(default)]
|
||||
pub role: String,
|
||||
#[serde(default)]
|
||||
pub timezone: String,
|
||||
#[serde(default)]
|
||||
pub phone_country: String,
|
||||
}
|
||||
39
web/src/pages/register/suggestions.rs
Normal file
39
web/src/pages/register/suggestions.rs
Normal file
@@ -0,0 +1,39 @@
|
||||
//! The three suggestion lists the register form offers, from the same sources
|
||||
//! as the TUI client's (client/src/pages/register/suggestions.rs): the four
|
||||
//! roles the database accepts, the IANA timezone database, and the ISO 3166-1
|
||||
//! alpha-2 country codes.
|
||||
//!
|
||||
//! The client filters these itself as the field is typed into; here the lists
|
||||
//! ship whole in `<datalist>` elements and the browser does the filtering.
|
||||
|
||||
pub(crate) const ROLES: &[&str] = &["admin", "moderator", "accountant", "viewer"];
|
||||
|
||||
pub(crate) fn timezones() -> Vec<String> {
|
||||
jiff::tz::db()
|
||||
.available()
|
||||
.map(|timezone| timezone.to_string())
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub(crate) fn phone_countries() -> Vec<String> {
|
||||
isocountry::CountryCode::iter_alpha2()
|
||||
.map(|code| code.alpha2().to_string())
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn the_timezone_list_is_the_iana_database() {
|
||||
assert!(timezones().contains(&"Europe/Bratislava".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_country_list_is_every_iso_alpha2_code() {
|
||||
let countries = phone_countries();
|
||||
assert_eq!(countries.len(), 249);
|
||||
assert!(countries.contains(&"SK".to_string()));
|
||||
}
|
||||
}
|
||||
54
web/src/pages/register/ui.rs
Normal file
54
web/src/pages/register/ui.rs
Normal file
@@ -0,0 +1,54 @@
|
||||
use askama::Template;
|
||||
|
||||
use crate::{
|
||||
auth::AuthResponse,
|
||||
ui::{Alert, Nav, render},
|
||||
};
|
||||
|
||||
use super::suggestions;
|
||||
|
||||
/// GET /register
|
||||
#[derive(Template)]
|
||||
#[template(path = "pages/register/register.html")]
|
||||
struct RegisterPage {
|
||||
nav: Nav,
|
||||
roles: &'static [&'static str],
|
||||
timezones: Vec<String>,
|
||||
phone_countries: Vec<String>,
|
||||
}
|
||||
|
||||
pub(crate) fn render_page(nav: Nav) -> String {
|
||||
render(&RegisterPage {
|
||||
nav,
|
||||
roles: suggestions::ROLES,
|
||||
timezones: suggestions::timezones(),
|
||||
phone_countries: suggestions::phone_countries(),
|
||||
})
|
||||
}
|
||||
|
||||
/// POST /register — the #register-status swap when the account was created.
|
||||
/// The lines are the client's success dialog, field for field.
|
||||
pub(crate) fn render_success(
|
||||
registered: &AuthResponse,
|
||||
timezone: &str,
|
||||
phone_country: &str,
|
||||
) -> String {
|
||||
let message = format!(
|
||||
"User ID: {}\nUsername: {}\nEmail: {}\nRole: {}\nTimezone: {}\nPhone country: {}",
|
||||
registered.id,
|
||||
registered.username,
|
||||
registered.email,
|
||||
registered.role,
|
||||
timezone,
|
||||
phone_country,
|
||||
);
|
||||
render(&Alert::success(
|
||||
"Registration successful. You can sign in now.",
|
||||
&message,
|
||||
))
|
||||
}
|
||||
|
||||
/// POST /register — the #register-status swap when the backend refused.
|
||||
pub(crate) fn render_error(message: &str) -> String {
|
||||
render(&Alert::error("Could not register", message))
|
||||
}
|
||||
Reference in New Issue
Block a user