feat(server): add routes for private sign-up links
							parent
							
								
									69e84b4266
								
							
						
					
					
						commit
						722317603d
					
				| 
						 | 
					@ -8,7 +8,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 | 
				
			||||||
## [Unreleased](https://git.rustybever.be/Chewing_Bever/otter)
 | 
					## [Unreleased](https://git.rustybever.be/Chewing_Bever/otter)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
* CLI command to add new users
 | 
					* CLI command to add new users
 | 
				
			||||||
* Added public sign-up page (disabled by default)
 | 
					* Public sign-up page (disabled by default)
 | 
				
			||||||
 | 
					* Private sign-up links
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## [0.2.1](https://git.rustybever.be/Chewing_Bever/otter/src/tag/0.2.1)
 | 
					## [0.2.1](https://git.rustybever.be/Chewing_Bever/otter/src/tag/0.2.1)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -29,9 +29,4 @@ impl<'a> AdminRepository<'a> {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        Ok(link)
 | 
					        Ok(link)
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					 | 
				
			||||||
    /// Remove the signup link with the given ID, if it exists
 | 
					 | 
				
			||||||
    pub fn remove_signup_link(&self, id: i64) -> Result<bool, AuthErr> {
 | 
					 | 
				
			||||||
        self.store.remove_signup_link(id)
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -8,7 +8,7 @@ use chrono::{TimeDelta, Utc};
 | 
				
			||||||
use rand::rngs::OsRng;
 | 
					use rand::rngs::OsRng;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use crate::{
 | 
					use crate::{
 | 
				
			||||||
    models,
 | 
					    SignupLink, models,
 | 
				
			||||||
    store::{AuthErr, GpodderStore},
 | 
					    store::{AuthErr, GpodderStore},
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -117,4 +117,13 @@ impl GpodderRepository {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        self.store.remove_old_sessions(min_last_seen)
 | 
					        self.store.remove_old_sessions(min_last_seen)
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub fn get_signup_link(&self, id: i64) -> Result<Option<SignupLink>, AuthErr> {
 | 
				
			||||||
 | 
					        self.store.get_signup_link(id)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// Remove the signup link with the given ID, if it exists
 | 
				
			||||||
 | 
					    pub fn remove_signup_link(&self, id: i64) -> Result<bool, AuthErr> {
 | 
				
			||||||
 | 
					        self.store.remove_signup_link(id)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,7 +1,7 @@
 | 
				
			||||||
use axum::{
 | 
					use axum::{
 | 
				
			||||||
    Form, RequestExt, Router,
 | 
					    Form, RequestExt, Router,
 | 
				
			||||||
    extract::{Request, State},
 | 
					    extract::{Path, Request, State},
 | 
				
			||||||
    http::HeaderMap,
 | 
					    http::{HeaderMap, StatusCode},
 | 
				
			||||||
    middleware::Next,
 | 
					    middleware::Next,
 | 
				
			||||||
    response::{IntoResponse, Redirect, Response},
 | 
					    response::{IntoResponse, Redirect, Response},
 | 
				
			||||||
    routing::{get, post},
 | 
					    routing::{get, post},
 | 
				
			||||||
| 
						 | 
					@ -35,6 +35,8 @@ pub fn router(ctx: Context) -> Router<Context> {
 | 
				
			||||||
    // security mistakes
 | 
					    // security mistakes
 | 
				
			||||||
    if ctx.config.allow_public_signup {
 | 
					    if ctx.config.allow_public_signup {
 | 
				
			||||||
        router = router.route("/signup", get(get_signup).post(post_signup))
 | 
					        router = router.route("/signup", get(get_signup).post(post_signup))
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					        router = router.route("/signup/{id}", get(get_signup_link).post(post_signup_link));
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    router
 | 
					    router
 | 
				
			||||||
| 
						 | 
					@ -261,3 +263,63 @@ async fn post_signup(
 | 
				
			||||||
        .into_response())
 | 
					        .into_response())
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async fn get_signup_link(
 | 
				
			||||||
 | 
					    State(ctx): State<Context>,
 | 
				
			||||||
 | 
					    Path(id): Path<i64>,
 | 
				
			||||||
 | 
					    headers: HeaderMap,
 | 
				
			||||||
 | 
					    jar: CookieJar,
 | 
				
			||||||
 | 
					) -> AppResult<Response> {
 | 
				
			||||||
 | 
					    let ctx_clone = ctx.clone();
 | 
				
			||||||
 | 
					    let signup_link = tokio::task::spawn_blocking(move || ctx_clone.store.get_signup_link(id))
 | 
				
			||||||
 | 
					        .await
 | 
				
			||||||
 | 
					        .unwrap()?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Just redirect to / if it's an invalid sign-up link
 | 
				
			||||||
 | 
					    if signup_link.is_none()
 | 
				
			||||||
 | 
					        || extract_session(ctx.clone(), &jar)
 | 
				
			||||||
 | 
					            .await
 | 
				
			||||||
 | 
					            .ok()
 | 
				
			||||||
 | 
					            .flatten()
 | 
				
			||||||
 | 
					            .is_some()
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        Ok(Redirect::to("/").into_response())
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					        Ok(View::Signup {
 | 
				
			||||||
 | 
					            username: None,
 | 
				
			||||||
 | 
					            username_available: true,
 | 
				
			||||||
 | 
					            passwords_match: true,
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        .page(&headers)
 | 
				
			||||||
 | 
					        .response(&ctx.tera)
 | 
				
			||||||
 | 
					        .into_response())
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async fn post_signup_link(
 | 
				
			||||||
 | 
					    State(ctx): State<Context>,
 | 
				
			||||||
 | 
					    Path(id): Path<i64>,
 | 
				
			||||||
 | 
					    jar: CookieJar,
 | 
				
			||||||
 | 
					    headers: HeaderMap,
 | 
				
			||||||
 | 
					    user_agent: Option<TypedHeader<UserAgent>>,
 | 
				
			||||||
 | 
					    signup: Form<SignupForm>,
 | 
				
			||||||
 | 
					) -> AppResult<Response> {
 | 
				
			||||||
 | 
					    let ctx_clone = ctx.clone();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if tokio::task::spawn_blocking(move || ctx_clone.store.get_signup_link(id))
 | 
				
			||||||
 | 
					        .await
 | 
				
			||||||
 | 
					        .unwrap()?
 | 
				
			||||||
 | 
					        .is_some()
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        let response = post_signup(State(ctx.clone()), jar, headers, user_agent, signup).await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Signup flow was successful, so remove the signup link
 | 
				
			||||||
 | 
					        tokio::task::spawn_blocking(move || ctx.store.remove_signup_link(id))
 | 
				
			||||||
 | 
					            .await
 | 
				
			||||||
 | 
					            .unwrap()?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Ok(response)
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					        Ok(StatusCode::NOT_FOUND.into_response())
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
		Reference in New Issue