fix(server): --session-cookie-secure flag; scope+char-count password; invalid-email test
This commit is contained in:
@@ -21,6 +21,10 @@ pub struct Config {
|
|||||||
|
|
||||||
/// Send the session cookie with the `Secure` attribute (HTTPS-only). Disable
|
/// Send the session cookie with the `Secure` attribute (HTTPS-only). Disable
|
||||||
/// only for plain-HTTP self-hosting behind no TLS at all.
|
/// only for plain-HTTP self-hosting behind no TLS at all.
|
||||||
#[arg(long, env = "SESSION_COOKIE_SECURE", default_value_t = true)]
|
#[arg(
|
||||||
|
long = "session-cookie-secure",
|
||||||
|
env = "SESSION_COOKIE_SECURE",
|
||||||
|
default_value_t = true
|
||||||
|
)]
|
||||||
pub cookie_secure: bool,
|
pub cookie_secure: bool,
|
||||||
}
|
}
|
||||||
|
|||||||
+16
-13
@@ -48,24 +48,27 @@ pub async fn serve(listener: TcpListener, state: AppState) -> anyhow::Result<()>
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create a user from the CLI (admin bootstrap). Reads the password from the
|
/// Create a user from the CLI (admin bootstrap). Opens its own connection (CLI
|
||||||
/// `BOOTSTRAP_PASSWORD` env var if set, otherwise prompts (hidden input).
|
/// one-shot); reads the password from the `BOOTSTRAP_PASSWORD` env var if set,
|
||||||
|
/// otherwise prompts (hidden input). The plaintext is not zeroized, but it is
|
||||||
|
/// confined to the scope below and dropped before any network I/O.
|
||||||
pub async fn create_user(database_url: &str, email: &str, role: Role) -> anyhow::Result<()> {
|
pub async fn create_user(database_url: &str, email: &str, role: Role) -> anyhow::Result<()> {
|
||||||
let email = Email::parse(email).map_err(|err| anyhow::anyhow!("{err}"))?;
|
let email = Email::parse(email).map_err(|err| anyhow::anyhow!("{err}"))?;
|
||||||
|
|
||||||
let password = match std::env::var("BOOTSTRAP_PASSWORD") {
|
// Read, validate, and hash the password in a scope so the plaintext `String` is
|
||||||
Ok(p) => p,
|
// dropped before we open a connection / run any awaits.
|
||||||
Err(_) => rpassword::prompt_password("Password: ").context("reading password")?,
|
let password_hash = {
|
||||||
|
let password = match std::env::var("BOOTSTRAP_PASSWORD") {
|
||||||
|
Ok(p) => p,
|
||||||
|
Err(_) => rpassword::prompt_password("Password: ").context("reading password")?,
|
||||||
|
};
|
||||||
|
anyhow::ensure!(
|
||||||
|
password.chars().count() >= 8,
|
||||||
|
"password must be at least 8 characters"
|
||||||
|
);
|
||||||
|
auth::hash_password(&password).map_err(|err| anyhow::anyhow!("hashing password: {err}"))?
|
||||||
};
|
};
|
||||||
|
|
||||||
anyhow::ensure!(
|
|
||||||
password.len() >= 8,
|
|
||||||
"password must be at least 8 characters"
|
|
||||||
);
|
|
||||||
|
|
||||||
let password_hash =
|
|
||||||
auth::hash_password(&password).map_err(|err| anyhow::anyhow!("hashing password: {err}"))?;
|
|
||||||
|
|
||||||
let db = Db::connect(database_url)
|
let db = Db::connect(database_url)
|
||||||
.await
|
.await
|
||||||
.context("connecting to the database")?;
|
.context("connecting to the database")?;
|
||||||
|
|||||||
@@ -38,3 +38,13 @@ async fn create_user_persists_and_password_verifies(pool: PgPool) {
|
|||||||
assert_eq!(user.role, Role::Admin);
|
assert_eq!(user.role, Role::Admin);
|
||||||
assert!(auth::verify_password("bootstrap-pw-123", &stored_hash));
|
assert!(auth::verify_password("bootstrap-pw-123", &stored_hash));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn create_user_rejects_invalid_email() {
|
||||||
|
// The email is parsed before the password is read or the DB is touched, so an
|
||||||
|
// invalid email errors out without reaching the (unreachable) database URL.
|
||||||
|
let err = server::create_user("postgres://unused", "not-an-email", Role::Admin)
|
||||||
|
.await
|
||||||
|
.unwrap_err();
|
||||||
|
assert!(err.to_string().contains("email"), "got: {err}");
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user