From 89f8b08b5e4995b3eb94b17b56fc44353e02fb71 Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Thu, 28 Aug 2025 11:50:01 +0200 Subject: [PATCH] feat: add signup GET route --- otter/src/server/web/auth.rs | 137 ++++++++++++++++------ otter/src/web/mod.rs | 8 ++ otter/src/web/templates/views/signup.html | 11 ++ otter/src/web/templates/views/users.html | 10 ++ otter/src/web/view.rs | 12 ++ 5 files changed, 141 insertions(+), 37 deletions(-) create mode 100644 otter/src/web/templates/views/signup.html diff --git a/otter/src/server/web/auth.rs b/otter/src/server/web/auth.rs index 0aeeec6..78bf57f 100644 --- a/otter/src/server/web/auth.rs +++ b/otter/src/server/web/auth.rs @@ -30,6 +30,52 @@ pub fn router(ctx: Context) -> Router { // loop .route("/login", get(get_login).post(post_login)) .route("/logout", post(post_logout)) + .route("/signup", get(get_signup)) +} + +/// Middleware that authenticates the current user via the session token. If the credentials are +/// invalid, the user is redirected to the login page. +pub async fn auth_web_middleware( + State(ctx): State, + mut req: Request, + next: Next, +) -> Response { + // SAFETY: this extractor's error type is Infallible + let jar: CookieJar = req.extract_parts().await.unwrap(); + let redirect = Redirect::to("/login"); + + match extract_session(ctx, &jar).await { + Ok(Some(session)) => { + req.extensions_mut().insert(session); + + next.run(req).await + } + Ok(None) => redirect.into_response(), + Err(err) => err.into_response(), + } +} + +pub async fn extract_session(ctx: Context, jar: &CookieJar) -> AppResult> { + if let Some(session_id) = jar + .get(super::SESSION_ID_COOKIE) + .and_then(|c| c.value().parse::().ok()) + { + match tokio::task::spawn_blocking(move || { + let session = ctx.store.get_session(session_id)?; + ctx.store.refresh_session(&session)?; + + Ok(session) + }) + .await + .unwrap() + { + Ok(session) => Ok(Some(session)), + Err(gpodder::AuthErr::UnknownSession) => Ok(None), + Err(err) => Err(AppError::from(err)), + } + } else { + Ok(None) + } } async fn get_login(State(ctx): State, headers: HeaderMap, jar: CookieJar) -> Response { @@ -103,47 +149,64 @@ async fn post_logout(State(ctx): State, jar: CookieJar) -> AppResult AppResult> { - if let Some(session_id) = jar - .get(super::SESSION_ID_COOKIE) - .and_then(|c| c.value().parse::().ok()) - { - match tokio::task::spawn_blocking(move || { - let session = ctx.store.get_session(session_id)?; - ctx.store.refresh_session(&session)?; +#[derive(Deserialize)] +struct SignupForm { + username: String, + password: String, + password_confirm: String, +} - Ok(session) +struct SignupValidation { + username_available: bool, + passwords_match: bool, +} + +impl SignupForm { + fn validate(&self, ctx: &Context) -> AppResult { + let username_available = match ctx.store.get_user(&self.username) { + Ok(_) => false, + Err(AuthErr::UnknownUser) => true, + Err(err) => { + return Err(err.into()); + } + }; + let passwords_match = self.password == self.password_confirm; + + Ok(SignupValidation { + username_available, + passwords_match, }) + } +} + +impl SignupValidation { + pub fn valid(&self) -> bool { + self.username_available && self.passwords_match + } +} + +async fn get_signup(State(ctx): State, headers: HeaderMap, jar: CookieJar) -> Response { + if extract_session(ctx.clone(), &jar) .await - .unwrap() - { - Ok(session) => Ok(Some(session)), - Err(gpodder::AuthErr::UnknownSession) => Ok(None), - Err(err) => Err(AppError::from(err)), - } + .ok() + .flatten() + .is_some() + { + Redirect::to("/").into_response() } else { - Ok(None) - } -} - -/// Middleware that authenticates the current user via the session token. If the credentials are -/// invalid, the user is redirected to the login page. -pub async fn auth_web_middleware( - State(ctx): State, - mut req: Request, - next: Next, -) -> Response { - // SAFETY: this extractor's error type is Infallible - let jar: CookieJar = req.extract_parts().await.unwrap(); - let redirect = Redirect::to("/login"); - - match extract_session(ctx, &jar).await { - Ok(Some(session)) => { - req.extensions_mut().insert(session); - - next.run(req).await + View::Signup { + username_available: true, + passwords_match: true, } - Ok(None) => redirect.into_response(), - Err(err) => err.into_response(), + .page(&headers) + .response(&ctx.tera) + .into_response() } } + +async fn post_signup( + State(ctx): State, + Form(signup): Form, +) -> AppResult { + todo!() +} diff --git a/otter/src/web/mod.rs b/otter/src/web/mod.rs index d151186..f72e0a9 100644 --- a/otter/src/web/mod.rs +++ b/otter/src/web/mod.rs @@ -89,6 +89,14 @@ pub fn initialize_tera() -> tera::Result { View::Users(Vec::new(), 0, None).template(), include_str!("templates/views/users.html"), ), + ( + View::Signup { + username_available: true, + passwords_match: true, + } + .template(), + include_str!("templates/views/signup.html"), + ), ])?; Ok(tera) diff --git a/otter/src/web/templates/views/signup.html b/otter/src/web/templates/views/signup.html new file mode 100644 index 0000000..00dab19 --- /dev/null +++ b/otter/src/web/templates/views/signup.html @@ -0,0 +1,11 @@ +
+
+ + + + + + + +
+
diff --git a/otter/src/web/templates/views/users.html b/otter/src/web/templates/views/users.html index f66f3fe..fac9ce2 100644 --- a/otter/src/web/templates/views/users.html +++ b/otter/src/web/templates/views/users.html @@ -12,11 +12,21 @@ + {%- for user in users %} + {% endfor -%} diff --git a/otter/src/web/view.rs b/otter/src/web/view.rs index 689f3eb..049ceaa 100644 --- a/otter/src/web/view.rs +++ b/otter/src/web/view.rs @@ -8,6 +8,10 @@ pub enum View { Login, Sessions(Vec, i64, Option), Users(Vec, i64, Option), + Signup { + username_available: bool, + passwords_match: bool, + }, } #[derive(Serialize)] @@ -30,6 +34,7 @@ impl Template for View { Self::Login => "views/login.html", Self::Sessions(..) => "views/sessions.html", Self::Users(..) => "views/users.html", + Self::Signup { .. } => "views/signup.html", } } @@ -61,6 +66,13 @@ impl Template for View { ctx.insert("next_page_query", &query.encode()); } } + Self::Signup { + username_available, + passwords_match, + } => { + ctx.insert("username_available", &username_available); + ctx.insert("passwords_match", &passwords_match); + } _ => {} };
UsernameAction
{{ user.username }} + {%- if user.id != current_user_id -%} + Remove + {%- else -%} + Current user + {%- endif -%} +