feat: add signup GET route
							parent
							
								
									97b30b1840
								
							
						
					
					
						commit
						89f8b08b5e
					
				|  | @ -30,6 +30,52 @@ pub fn router(ctx: Context) -> Router<Context> { | ||||||
|         // loop
 |         // loop
 | ||||||
|         .route("/login", get(get_login).post(post_login)) |         .route("/login", get(get_login).post(post_login)) | ||||||
|         .route("/logout", post(post_logout)) |         .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<Context>, | ||||||
|  |     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<Option<Session>> { | ||||||
|  |     if let Some(session_id) = jar | ||||||
|  |         .get(super::SESSION_ID_COOKIE) | ||||||
|  |         .and_then(|c| c.value().parse::<i64>().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<Context>, headers: HeaderMap, jar: CookieJar) -> Response { | async fn get_login(State(ctx): State<Context>, headers: HeaderMap, jar: CookieJar) -> Response { | ||||||
|  | @ -103,47 +149,64 @@ async fn post_logout(State(ctx): State<Context>, jar: CookieJar) -> AppResult<im | ||||||
|     Ok(([("HX-Redirect", "/")], jar.remove(super::SESSION_ID_COOKIE))) |     Ok(([("HX-Redirect", "/")], jar.remove(super::SESSION_ID_COOKIE))) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| pub async fn extract_session(ctx: Context, jar: &CookieJar) -> AppResult<Option<Session>> { | #[derive(Deserialize)] | ||||||
|     if let Some(session_id) = jar | struct SignupForm { | ||||||
|         .get(super::SESSION_ID_COOKIE) |     username: String, | ||||||
|         .and_then(|c| c.value().parse::<i64>().ok()) |     password: String, | ||||||
|     { |     password_confirm: String, | ||||||
|         match tokio::task::spawn_blocking(move || { | } | ||||||
|             let session = ctx.store.get_session(session_id)?; |  | ||||||
|             ctx.store.refresh_session(&session)?; |  | ||||||
| 
 | 
 | ||||||
|             Ok(session) | struct SignupValidation { | ||||||
|  |     username_available: bool, | ||||||
|  |     passwords_match: bool, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | impl SignupForm { | ||||||
|  |     fn validate(&self, ctx: &Context) -> AppResult<SignupValidation> { | ||||||
|  |         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<Context>, headers: HeaderMap, jar: CookieJar) -> Response { | ||||||
|  |     if extract_session(ctx.clone(), &jar) | ||||||
|         .await |         .await | ||||||
|         .unwrap() |         .ok() | ||||||
|         { |         .flatten() | ||||||
|             Ok(session) => Ok(Some(session)), |         .is_some() | ||||||
|             Err(gpodder::AuthErr::UnknownSession) => Ok(None), |     { | ||||||
|             Err(err) => Err(AppError::from(err)), |         Redirect::to("/").into_response() | ||||||
|         } |  | ||||||
|     } else { |     } else { | ||||||
|         Ok(None) |         View::Signup { | ||||||
|     } |             username_available: true, | ||||||
| } |             passwords_match: true, | ||||||
| 
 |  | ||||||
| /// 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<Context>, |  | ||||||
|     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(), |         .page(&headers) | ||||||
|         Err(err) => err.into_response(), |         .response(&ctx.tera) | ||||||
|  |         .into_response() | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | async fn post_signup( | ||||||
|  |     State(ctx): State<Context>, | ||||||
|  |     Form(signup): Form<SignupForm>, | ||||||
|  | ) -> AppResult<Response> { | ||||||
|  |     todo!() | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @ -89,6 +89,14 @@ pub fn initialize_tera() -> tera::Result<tera::Tera> { | ||||||
|             View::Users(Vec::new(), 0, None).template(), |             View::Users(Vec::new(), 0, None).template(), | ||||||
|             include_str!("templates/views/users.html"), |             include_str!("templates/views/users.html"), | ||||||
|         ), |         ), | ||||||
|  |         ( | ||||||
|  |             View::Signup { | ||||||
|  |                 username_available: true, | ||||||
|  |                 passwords_match: true, | ||||||
|  |             } | ||||||
|  |             .template(), | ||||||
|  |             include_str!("templates/views/signup.html"), | ||||||
|  |         ), | ||||||
|     ])?; |     ])?; | ||||||
| 
 | 
 | ||||||
|     Ok(tera) |     Ok(tera) | ||||||
|  |  | ||||||
|  | @ -0,0 +1,11 @@ | ||||||
|  | <article> | ||||||
|  |     <form hx-post hx-target="#inner"> | ||||||
|  |         <label for="username">Username:</label> | ||||||
|  |         <input type="text" id="username" name="username"> | ||||||
|  |         <label for="password">Password:</label> | ||||||
|  |         <input type="password" id="password" name="password"> | ||||||
|  |         <label for="password_confirm">Confirm password:</label> | ||||||
|  |         <input type="password" id="password_confirm" name="password_confirm"> | ||||||
|  |         <input type="submit" value="Sign Up"> | ||||||
|  |     </form> | ||||||
|  | </article> | ||||||
|  | @ -12,11 +12,21 @@ | ||||||
| <table id="users"> | <table id="users"> | ||||||
|     <thead> |     <thead> | ||||||
|         <th>Username</th> |         <th>Username</th> | ||||||
|  |         <th>Action</th> | ||||||
|     </thead> |     </thead> | ||||||
|     <tbody> |     <tbody> | ||||||
|         {%- for user in users %} |         {%- for user in users %} | ||||||
|         <tr> |         <tr> | ||||||
|             <th>{{ user.username }}</th> |             <th>{{ user.username }}</th> | ||||||
|  |             <th> | ||||||
|  |                 {%- if user.id != current_user_id -%} | ||||||
|  |                 <a hx-delete="/users/{{ user.id }}" | ||||||
|  |                    hx-target="closest tr" | ||||||
|  |                 >Remove</a> | ||||||
|  |                 {%- else -%} | ||||||
|  |                 Current user | ||||||
|  |                 {%- endif -%} | ||||||
|  |             </th> | ||||||
|         </tr> |         </tr> | ||||||
|         {% endfor -%} |         {% endfor -%} | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -8,6 +8,10 @@ pub enum View { | ||||||
|     Login, |     Login, | ||||||
|     Sessions(Vec<gpodder::Session>, i64, Option<Query>), |     Sessions(Vec<gpodder::Session>, i64, Option<Query>), | ||||||
|     Users(Vec<gpodder::User>, i64, Option<Query>), |     Users(Vec<gpodder::User>, i64, Option<Query>), | ||||||
|  |     Signup { | ||||||
|  |         username_available: bool, | ||||||
|  |         passwords_match: bool, | ||||||
|  |     }, | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| #[derive(Serialize)] | #[derive(Serialize)] | ||||||
|  | @ -30,6 +34,7 @@ impl Template for View { | ||||||
|             Self::Login => "views/login.html", |             Self::Login => "views/login.html", | ||||||
|             Self::Sessions(..) => "views/sessions.html", |             Self::Sessions(..) => "views/sessions.html", | ||||||
|             Self::Users(..) => "views/users.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()); |                     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); | ||||||
|  |             } | ||||||
|             _ => {} |             _ => {} | ||||||
|         }; |         }; | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue