Compare commits
	
		
			No commits in common. "ee9db5ae36c2f0da3f8e3a71ba569d61cf5fe4a0" and "69e84b42660ce8e8778a6fc0724481d40b00412b" have entirely different histories. 
		
	
	
		
			ee9db5ae36
			...
			69e84b4266
		
	
		| 
						 | 
				
			
			@ -7,13 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 | 
			
		|||
 | 
			
		||||
## [Unreleased](https://git.rustybever.be/Chewing_Bever/otter)
 | 
			
		||||
 | 
			
		||||
## [0.3.0](https://git.rustybever.be/Chewing_Bever/otter/src/tag/0.3.0)
 | 
			
		||||
 | 
			
		||||
* Public sign-up page (disabled by default)
 | 
			
		||||
* Private sign-up links
 | 
			
		||||
* New CLI commands
 | 
			
		||||
    * Add users
 | 
			
		||||
    * Generate signup links
 | 
			
		||||
* CLI command to add new users
 | 
			
		||||
* Added public sign-up page (disabled by default)
 | 
			
		||||
 | 
			
		||||
## [0.2.1](https://git.rustybever.be/Chewing_Bever/otter/src/tag/0.2.1)
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -821,7 +821,7 @@ dependencies = [
 | 
			
		|||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "gpodder"
 | 
			
		||||
version = "0.3.0"
 | 
			
		||||
version = "0.2.1"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "argon2",
 | 
			
		||||
 "chrono",
 | 
			
		||||
| 
						 | 
				
			
			@ -830,7 +830,7 @@ dependencies = [
 | 
			
		|||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "gpodder_sqlite"
 | 
			
		||||
version = "0.3.0"
 | 
			
		||||
version = "0.2.1"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "chrono",
 | 
			
		||||
 "criterion",
 | 
			
		||||
| 
						 | 
				
			
			@ -1246,7 +1246,7 @@ checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e"
 | 
			
		|||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "otter"
 | 
			
		||||
version = "0.3.0"
 | 
			
		||||
version = "0.2.1"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "axum",
 | 
			
		||||
 "axum-extra",
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -7,7 +7,7 @@ members = [
 | 
			
		|||
]
 | 
			
		||||
 | 
			
		||||
[workspace.package]
 | 
			
		||||
version = "0.3.0"
 | 
			
		||||
version = "0.2.1"
 | 
			
		||||
edition = "2024"
 | 
			
		||||
 | 
			
		||||
[workspace.dependencies]
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -20,10 +20,6 @@ the TOML file.
 | 
			
		|||
* `log_level` (`OTTER_LOG_LEVEL`): how verbose the logging should be; one of
 | 
			
		||||
  `debug`, `info`, `warn` or `error`
 | 
			
		||||
    * Default: `warn`
 | 
			
		||||
* `allow_public_signups` (`OTTER_ALLOW_PUBLIC_SIGNUPS`): whether public signups
 | 
			
		||||
  are allowed. This enables the `/signup` route and adds a notice to sign up at
 | 
			
		||||
  the bottom of the login page.
 | 
			
		||||
    * Default: `false`
 | 
			
		||||
 | 
			
		||||
## Network (`net`)
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,3 +1,6 @@
 | 
			
		|||
use chrono::Utc;
 | 
			
		||||
use rand::Rng;
 | 
			
		||||
 | 
			
		||||
use crate::{AuthErr, Page, models};
 | 
			
		||||
 | 
			
		||||
/// Admin view of the repository, providing methods only allowed by admins
 | 
			
		||||
| 
						 | 
				
			
			@ -14,4 +17,21 @@ impl<'a> AdminRepository<'a> {
 | 
			
		|||
    ) -> Result<Vec<models::User>, AuthErr> {
 | 
			
		||||
        self.store.paginated_users(page, filter)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Generate a new unique signup link ID
 | 
			
		||||
    pub fn generate_signup_link(&self) -> Result<models::SignupLink, AuthErr> {
 | 
			
		||||
        let link = models::SignupLink {
 | 
			
		||||
            id: rand::thread_rng().r#gen(),
 | 
			
		||||
            time_created: Utc::now(),
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        self.store.insert_signup_link(&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)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -5,10 +5,10 @@ use std::sync::Arc;
 | 
			
		|||
 | 
			
		||||
use argon2::{Argon2, PasswordHash, PasswordHasher, PasswordVerifier, password_hash::SaltString};
 | 
			
		||||
use chrono::{TimeDelta, Utc};
 | 
			
		||||
use rand::{Rng, rngs::OsRng};
 | 
			
		||||
use rand::rngs::OsRng;
 | 
			
		||||
 | 
			
		||||
use crate::{
 | 
			
		||||
    SignupLink, models,
 | 
			
		||||
    models,
 | 
			
		||||
    store::{AuthErr, GpodderStore},
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -117,25 +117,4 @@ impl GpodderRepository {
 | 
			
		|||
 | 
			
		||||
        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)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Generate a new unique signup link ID
 | 
			
		||||
    pub fn generate_signup_link(&self) -> Result<models::SignupLink, AuthErr> {
 | 
			
		||||
        let link = models::SignupLink {
 | 
			
		||||
            id: rand::thread_rng().r#gen(),
 | 
			
		||||
            time_created: Utc::now(),
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        self.store.insert_signup_link(&link)?;
 | 
			
		||||
 | 
			
		||||
        Ok(link)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -18,10 +18,7 @@ pub enum Command {
 | 
			
		|||
 | 
			
		||||
#[derive(Subcommand)]
 | 
			
		||||
pub enum UserCommand {
 | 
			
		||||
    /// Add a new user
 | 
			
		||||
    Add { username: String, password: String },
 | 
			
		||||
    /// Generate a signup link ID
 | 
			
		||||
    GenerateSignupLink,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl Command {
 | 
			
		||||
| 
						 | 
				
			
			@ -62,11 +59,6 @@ impl UserCommand {
 | 
			
		|||
            Self::Add { username, password } => {
 | 
			
		||||
                store.create_user(username, password)?;
 | 
			
		||||
            }
 | 
			
		||||
            Self::GenerateSignupLink => {
 | 
			
		||||
                let link = store.generate_signup_link()?;
 | 
			
		||||
 | 
			
		||||
                println!("/signup/{}", link.id);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        Ok(())
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,7 +1,7 @@
 | 
			
		|||
use axum::{
 | 
			
		||||
    Form, RequestExt, Router,
 | 
			
		||||
    extract::{Path, Request, State},
 | 
			
		||||
    http::{HeaderMap, StatusCode},
 | 
			
		||||
    extract::{Request, State},
 | 
			
		||||
    http::HeaderMap,
 | 
			
		||||
    middleware::Next,
 | 
			
		||||
    response::{IntoResponse, Redirect, Response},
 | 
			
		||||
    routing::{get, post},
 | 
			
		||||
| 
						 | 
				
			
			@ -35,8 +35,6 @@ pub fn router(ctx: Context) -> Router<Context> {
 | 
			
		|||
    // security mistakes
 | 
			
		||||
    if ctx.config.allow_public_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
 | 
			
		||||
| 
						 | 
				
			
			@ -263,63 +261,3 @@ async fn post_signup(
 | 
			
		|||
        .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