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)
|
## [Unreleased](https://git.rustybever.be/Chewing_Bever/otter)
|
||||||
|
|
||||||
## [0.3.0](https://git.rustybever.be/Chewing_Bever/otter/src/tag/0.3.0)
|
* 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
|
|
||||||
* New CLI commands
|
|
||||||
* Add users
|
|
||||||
* Generate signup 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)
|
||||||
|
|
||||||
|
|
|
@ -821,7 +821,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "gpodder"
|
name = "gpodder"
|
||||||
version = "0.3.0"
|
version = "0.2.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"argon2",
|
"argon2",
|
||||||
"chrono",
|
"chrono",
|
||||||
|
@ -830,7 +830,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "gpodder_sqlite"
|
name = "gpodder_sqlite"
|
||||||
version = "0.3.0"
|
version = "0.2.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"chrono",
|
"chrono",
|
||||||
"criterion",
|
"criterion",
|
||||||
|
@ -1246,7 +1246,7 @@ checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "otter"
|
name = "otter"
|
||||||
version = "0.3.0"
|
version = "0.2.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"axum",
|
"axum",
|
||||||
"axum-extra",
|
"axum-extra",
|
||||||
|
|
|
@ -7,7 +7,7 @@ members = [
|
||||||
]
|
]
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
version = "0.3.0"
|
version = "0.2.1"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
|
|
|
@ -20,10 +20,6 @@ the TOML file.
|
||||||
* `log_level` (`OTTER_LOG_LEVEL`): how verbose the logging should be; one of
|
* `log_level` (`OTTER_LOG_LEVEL`): how verbose the logging should be; one of
|
||||||
`debug`, `info`, `warn` or `error`
|
`debug`, `info`, `warn` or `error`
|
||||||
* Default: `warn`
|
* 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`)
|
## Network (`net`)
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,6 @@
|
||||||
|
use chrono::Utc;
|
||||||
|
use rand::Rng;
|
||||||
|
|
||||||
use crate::{AuthErr, Page, models};
|
use crate::{AuthErr, Page, models};
|
||||||
|
|
||||||
/// Admin view of the repository, providing methods only allowed by admins
|
/// Admin view of the repository, providing methods only allowed by admins
|
||||||
|
@ -14,4 +17,21 @@ impl<'a> AdminRepository<'a> {
|
||||||
) -> Result<Vec<models::User>, AuthErr> {
|
) -> Result<Vec<models::User>, AuthErr> {
|
||||||
self.store.paginated_users(page, filter)
|
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 argon2::{Argon2, PasswordHash, PasswordHasher, PasswordVerifier, password_hash::SaltString};
|
||||||
use chrono::{TimeDelta, Utc};
|
use chrono::{TimeDelta, Utc};
|
||||||
use rand::{Rng, rngs::OsRng};
|
use rand::rngs::OsRng;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
SignupLink, models,
|
models,
|
||||||
store::{AuthErr, GpodderStore},
|
store::{AuthErr, GpodderStore},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -117,25 +117,4 @@ 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)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 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)]
|
#[derive(Subcommand)]
|
||||||
pub enum UserCommand {
|
pub enum UserCommand {
|
||||||
/// Add a new user
|
|
||||||
Add { username: String, password: String },
|
Add { username: String, password: String },
|
||||||
/// Generate a signup link ID
|
|
||||||
GenerateSignupLink,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Command {
|
impl Command {
|
||||||
|
@ -62,11 +59,6 @@ impl UserCommand {
|
||||||
Self::Add { username, password } => {
|
Self::Add { username, password } => {
|
||||||
store.create_user(username, password)?;
|
store.create_user(username, password)?;
|
||||||
}
|
}
|
||||||
Self::GenerateSignupLink => {
|
|
||||||
let link = store.generate_signup_link()?;
|
|
||||||
|
|
||||||
println!("/signup/{}", link.id);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
use axum::{
|
use axum::{
|
||||||
Form, RequestExt, Router,
|
Form, RequestExt, Router,
|
||||||
extract::{Path, Request, State},
|
extract::{Request, State},
|
||||||
http::{HeaderMap, StatusCode},
|
http::HeaderMap,
|
||||||
middleware::Next,
|
middleware::Next,
|
||||||
response::{IntoResponse, Redirect, Response},
|
response::{IntoResponse, Redirect, Response},
|
||||||
routing::{get, post},
|
routing::{get, post},
|
||||||
|
@ -35,8 +35,6 @@ 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
|
||||||
|
@ -263,63 +261,3 @@ 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