diff options
| author | Owen Jacobson <owen@grimoire.ca> | 2025-03-23 15:58:33 -0400 |
|---|---|---|
| committer | Owen Jacobson <owen@grimoire.ca> | 2025-03-23 16:25:22 -0400 |
| commit | 2420f1e75d54a5f209b0267715f078a369d81eb1 (patch) | |
| tree | 20edd531a3f2f765a23fef8e7a508c91bc7dc294 /src/login | |
| parent | 7e15690d54ff849596401b43d163df9353062850 (diff) | |
Rename the `login` module to `user`.
Diffstat (limited to 'src/login')
| -rw-r--r-- | src/login/app.rs | 56 | ||||
| -rw-r--r-- | src/login/create.rs | 95 | ||||
| -rw-r--r-- | src/login/event.rs | 36 | ||||
| -rw-r--r-- | src/login/history.rs | 52 | ||||
| -rw-r--r-- | src/login/id.rs | 24 | ||||
| -rw-r--r-- | src/login/mod.rs | 15 | ||||
| -rw-r--r-- | src/login/password.rs | 65 | ||||
| -rw-r--r-- | src/login/repo.rs | 153 | ||||
| -rw-r--r-- | src/login/routes/login/mod.rs | 4 | ||||
| -rw-r--r-- | src/login/routes/login/post.rs | 52 | ||||
| -rw-r--r-- | src/login/routes/login/test.rs | 128 | ||||
| -rw-r--r-- | src/login/routes/logout/mod.rs | 4 | ||||
| -rw-r--r-- | src/login/routes/logout/post.rs | 47 | ||||
| -rw-r--r-- | src/login/routes/logout/test.rs | 79 | ||||
| -rw-r--r-- | src/login/routes/mod.rs | 14 | ||||
| -rw-r--r-- | src/login/routes/password/mod.rs | 4 | ||||
| -rw-r--r-- | src/login/routes/password/post.rs | 54 | ||||
| -rw-r--r-- | src/login/routes/password/test.rs | 68 | ||||
| -rw-r--r-- | src/login/snapshot.rs | 51 | ||||
| -rw-r--r-- | src/login/validate.rs | 23 |
20 files changed, 0 insertions, 1024 deletions
diff --git a/src/login/app.rs b/src/login/app.rs deleted file mode 100644 index 2da4d6a..0000000 --- a/src/login/app.rs +++ /dev/null @@ -1,56 +0,0 @@ -use sqlx::sqlite::SqlitePool; - -use super::{ - Login, Password, - create::{self, Create}, -}; -use crate::{clock::DateTime, event::Broadcaster, name::Name}; - -pub struct Logins<'a> { - db: &'a SqlitePool, - events: &'a Broadcaster, -} - -impl<'a> Logins<'a> { - pub const fn new(db: &'a SqlitePool, events: &'a Broadcaster) -> Self { - Self { db, events } - } - - pub async fn create( - &self, - name: &Name, - password: &Password, - created_at: &DateTime, - ) -> Result<Login, CreateError> { - let create = Create::begin(name, password, created_at); - let validated = create.validate()?; - - let mut tx = self.db.begin().await?; - let stored = validated.store(&mut tx).await?; - tx.commit().await?; - - let login = stored.publish(self.events); - - Ok(login.as_created()) - } -} - -#[derive(Debug, thiserror::Error)] -pub enum CreateError { - #[error("invalid login name: {0}")] - InvalidName(Name), - #[error(transparent)] - PasswordHash(#[from] password_hash::Error), - #[error(transparent)] - Database(#[from] sqlx::Error), -} - -#[cfg(test)] -impl From<create::Error> for CreateError { - fn from(error: create::Error) -> Self { - match error { - create::Error::InvalidName(name) => Self::InvalidName(name), - create::Error::PasswordHash(error) => Self::PasswordHash(error), - } - } -} diff --git a/src/login/create.rs b/src/login/create.rs deleted file mode 100644 index c5cea08..0000000 --- a/src/login/create.rs +++ /dev/null @@ -1,95 +0,0 @@ -use sqlx::{sqlite::Sqlite, Transaction}; - -use super::{password::StoredHash, repo::Provider as _, validate, History, Password}; -use crate::{ - clock::DateTime, - event::{repo::Provider as _, Broadcaster, Event}, - name::Name, -}; - -#[must_use = "dropping a login creation attempt is likely a mistake"] -pub struct Create<'a> { - name: &'a Name, - password: &'a Password, - created_at: &'a DateTime, -} - -impl<'a> Create<'a> { - pub fn begin(name: &'a Name, password: &'a Password, created_at: &'a DateTime) -> Self { - Self { - name, - password, - created_at, - } - } - - pub fn validate(self) -> Result<Validated<'a>, Error> { - let Self { - name, - password, - created_at, - } = self; - - if !validate::name(name) { - return Err(Error::InvalidName(name.clone())); - } - - let password_hash = password.hash()?; - - Ok(Validated { - name, - password_hash, - created_at, - }) - } -} - -#[must_use = "dropping a login creation attempt is likely a mistake"] -pub struct Validated<'a> { - name: &'a Name, - password_hash: StoredHash, - created_at: &'a DateTime, -} - -impl Validated<'_> { - pub async fn store(self, tx: &mut Transaction<'_, Sqlite>) -> Result<Stored, sqlx::Error> { - let Self { - name, - password_hash, - created_at, - } = self; - - let created = tx.sequence().next(created_at).await?; - let login = tx.logins().create(name, &password_hash, &created).await?; - - Ok(Stored { login }) - } -} - -#[must_use = "dropping a login creation attempt is likely a mistake"] -pub struct Stored { - login: History, -} - -impl Stored { - #[must_use = "dropping a login creation attempt is likely a mistake"] - pub fn publish(self, events: &Broadcaster) -> History { - let Self { login } = self; - - events.broadcast(login.events().map(Event::from).collect::<Vec<_>>()); - - login - } - - pub fn login(&self) -> &History { - &self.login - } -} - -#[derive(Debug, thiserror::Error)] -pub enum Error { - #[error("invalid login name: {0}")] - InvalidName(Name), - #[error(transparent)] - PasswordHash(#[from] password_hash::Error), -} diff --git a/src/login/event.rs b/src/login/event.rs deleted file mode 100644 index b03451a..0000000 --- a/src/login/event.rs +++ /dev/null @@ -1,36 +0,0 @@ -use super::snapshot::Login; -use crate::event::{Instant, Sequenced}; - -#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize)] -#[serde(tag = "event", rename_all = "snake_case")] -pub enum Event { - Created(Created), -} - -impl Sequenced for Event { - fn instant(&self) -> Instant { - match self { - Self::Created(created) => created.instant(), - } - } -} - -#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize)] -pub struct Created { - #[serde(flatten)] - pub instant: Instant, - #[serde(flatten)] - pub login: Login, -} - -impl Sequenced for Created { - fn instant(&self) -> Instant { - self.instant - } -} - -impl From<Created> for Event { - fn from(event: Created) -> Self { - Self::Created(event) - } -} diff --git a/src/login/history.rs b/src/login/history.rs deleted file mode 100644 index d67bcce..0000000 --- a/src/login/history.rs +++ /dev/null @@ -1,52 +0,0 @@ -use super::{ - Id, Login, - event::{Created, Event}, -}; -use crate::event::{Instant, Sequence}; - -#[derive(Clone, Debug, Eq, PartialEq)] -pub struct History { - pub login: Login, - pub created: Instant, -} - -// State interface -impl History { - pub fn id(&self) -> &Id { - &self.login.id - } - - // Snapshot of this login as it was when created. (Note to the future: it's okay - // if this returns a redacted or modified version of the login. If we implement - // renames by redacting the original name, then this should return the edited - // login, not the original, even if that's not how it was "as created.") - pub fn as_created(&self) -> Login { - self.login.clone() - } - - pub fn as_of(&self, resume_point: Sequence) -> Option<Login> { - self.events() - .filter(Sequence::up_to(resume_point)) - .collect() - } - - // Snapshot of this login, as of all events recorded in this history. - pub fn as_snapshot(&self) -> Option<Login> { - self.events().collect() - } -} - -// Events interface -impl History { - fn created(&self) -> Event { - Created { - instant: self.created, - login: self.login.clone(), - } - .into() - } - - pub fn events(&self) -> impl Iterator<Item = Event> + use<> { - [self.created()].into_iter() - } -} diff --git a/src/login/id.rs b/src/login/id.rs deleted file mode 100644 index c46d697..0000000 --- a/src/login/id.rs +++ /dev/null @@ -1,24 +0,0 @@ -use crate::id::Id as BaseId; - -// Stable identifier for a [Login]. Prefixed with `L`. -#[derive(Clone, Debug, Eq, PartialEq, sqlx::Type, serde::Serialize)] -#[sqlx(transparent)] -pub struct Id(BaseId); - -impl From<BaseId> for Id { - fn from(id: BaseId) -> Self { - Self(id) - } -} - -impl Id { - pub fn generate() -> Self { - BaseId::generate("L") - } -} - -impl std::fmt::Display for Id { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - self.0.fmt(f) - } -} diff --git a/src/login/mod.rs b/src/login/mod.rs deleted file mode 100644 index 006fa0c..0000000 --- a/src/login/mod.rs +++ /dev/null @@ -1,15 +0,0 @@ -#[cfg(test)] -pub mod app; -pub mod create; -pub mod event; -mod history; -mod id; -pub mod password; -pub mod repo; -mod routes; -mod snapshot; -mod validate; - -pub use self::{ - event::Event, history::History, id::Id, password::Password, routes::router, snapshot::Login, -}; diff --git a/src/login/password.rs b/src/login/password.rs deleted file mode 100644 index e1d164e..0000000 --- a/src/login/password.rs +++ /dev/null @@ -1,65 +0,0 @@ -use std::fmt; - -use argon2::Argon2; -use password_hash::{PasswordHash, PasswordHasher, PasswordVerifier, SaltString}; -use rand_core::OsRng; - -use crate::normalize::nfc; - -#[derive(sqlx::Type)] -#[sqlx(transparent)] -pub struct StoredHash(String); - -impl StoredHash { - pub fn verify(&self, password: &Password) -> Result<bool, password_hash::Error> { - let hash = PasswordHash::new(&self.0)?; - - match Argon2::default().verify_password(password.as_bytes(), &hash) { - // Successful authentication, not an error - Ok(()) => Ok(true), - // Unsuccessful authentication, also not an error - Err(password_hash::errors::Error::Password) => Ok(false), - // Password validation failed for some other reason, treat as an error - Err(err) => Err(err), - } - } -} - -impl fmt::Debug for StoredHash { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_tuple("StoredHash").field(&"********").finish() - } -} - -#[derive(Clone, serde::Deserialize)] -#[serde(transparent)] -pub struct Password(nfc::String); - -impl Password { - pub fn hash(&self) -> Result<StoredHash, password_hash::Error> { - let Self(password) = self; - let salt = SaltString::generate(&mut OsRng); - let argon2 = Argon2::default(); - let hash = argon2 - .hash_password(password.as_bytes(), &salt)? - .to_string(); - Ok(StoredHash(hash)) - } - - fn as_bytes(&self) -> &[u8] { - let Self(value) = self; - value.as_bytes() - } -} - -impl fmt::Debug for Password { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_tuple("Password").field(&"********").finish() - } -} - -impl From<String> for Password { - fn from(password: String) -> Self { - Password(password.into()) - } -} diff --git a/src/login/repo.rs b/src/login/repo.rs deleted file mode 100644 index 128d6b8..0000000 --- a/src/login/repo.rs +++ /dev/null @@ -1,153 +0,0 @@ -use futures::stream::{StreamExt as _, TryStreamExt as _}; -use sqlx::{SqliteConnection, Transaction, sqlite::Sqlite}; - -use crate::{ - clock::DateTime, - event::{Instant, Sequence}, - login::{History, Id, Login, password::StoredHash}, - name::{self, Name}, -}; - -pub trait Provider { - fn logins(&mut self) -> Logins; -} - -impl Provider for Transaction<'_, Sqlite> { - fn logins(&mut self) -> Logins { - Logins(self) - } -} - -pub struct Logins<'t>(&'t mut SqliteConnection); - -impl Logins<'_> { - pub async fn create( - &mut self, - name: &Name, - password_hash: &StoredHash, - created: &Instant, - ) -> Result<History, sqlx::Error> { - let id = Id::generate(); - let display_name = name.display(); - let canonical_name = name.canonical(); - - sqlx::query!( - r#" - insert - into user (id, display_name, canonical_name, password_hash, created_sequence, created_at) - values ($1, $2, $3, $4, $5, $6) - "#, - id, - display_name, - canonical_name, - password_hash, - created.sequence, - created.at, - ) - .execute(&mut *self.0) - .await?; - - let login = History { - created: *created, - login: Login { - id, - name: name.clone(), - }, - }; - - Ok(login) - } - - pub async fn set_password( - &mut self, - login: &History, - to: &StoredHash, - ) -> Result<(), sqlx::Error> { - let login = login.id(); - - sqlx::query_scalar!( - r#" - update user - set password_hash = $1 - where id = $2 - returning id as "id: Id" - "#, - to, - login, - ) - .fetch_one(&mut *self.0) - .await?; - - Ok(()) - } - - pub async fn all(&mut self, resume_at: Sequence) -> Result<Vec<History>, LoadError> { - let logins = sqlx::query!( - r#" - select - id as "id: Id", - display_name as "display_name: String", - canonical_name as "canonical_name: String", - created_sequence as "created_sequence: Sequence", - created_at as "created_at: DateTime" - from user - where created_sequence <= $1 - order by canonical_name - "#, - resume_at, - ) - .map(|row| { - Ok::<_, LoadError>(History { - login: Login { - id: row.id, - name: Name::new(row.display_name, row.canonical_name)?, - }, - created: Instant::new(row.created_at, row.created_sequence), - }) - }) - .fetch(&mut *self.0) - .map(|res| res?) - .try_collect() - .await?; - - Ok(logins) - } - - pub async fn replay(&mut self, resume_at: Sequence) -> Result<Vec<History>, LoadError> { - let logins = sqlx::query!( - r#" - select - id as "id: Id", - display_name as "display_name: String", - canonical_name as "canonical_name: String", - created_sequence as "created_sequence: Sequence", - created_at as "created_at: DateTime" - from user - where created_sequence > $1 - "#, - resume_at, - ) - .map(|row| { - Ok::<_, name::Error>(History { - login: Login { - id: row.id, - name: Name::new(row.display_name, row.canonical_name)?, - }, - created: Instant::new(row.created_at, row.created_sequence), - }) - }) - .fetch(&mut *self.0) - .map(|res| Ok::<_, LoadError>(res??)) - .try_collect() - .await?; - - Ok(logins) - } -} - -#[derive(Debug, thiserror::Error)] -#[error(transparent)] -pub enum LoadError { - Database(#[from] sqlx::Error), - Name(#[from] name::Error), -} diff --git a/src/login/routes/login/mod.rs b/src/login/routes/login/mod.rs deleted file mode 100644 index 36b384e..0000000 --- a/src/login/routes/login/mod.rs +++ /dev/null @@ -1,4 +0,0 @@ -pub mod post; - -#[cfg(test)] -mod test; diff --git a/src/login/routes/login/post.rs b/src/login/routes/login/post.rs deleted file mode 100644 index 96da5c5..0000000 --- a/src/login/routes/login/post.rs +++ /dev/null @@ -1,52 +0,0 @@ -use axum::{ - extract::{Json, State}, - http::StatusCode, - response::{IntoResponse, Response}, -}; - -use crate::{ - app::App, - clock::RequestedAt, - error::Internal, - login::{Login, Password}, - name::Name, - token::{app, extract::IdentityCookie}, -}; - -pub async fn handler( - State(app): State<App>, - RequestedAt(now): RequestedAt, - identity: IdentityCookie, - Json(request): Json<Request>, -) -> Result<(IdentityCookie, Json<Login>), Error> { - let (login, secret) = app - .tokens() - .login(&request.name, &request.password, &now) - .await - .map_err(Error)?; - let identity = identity.set(secret); - Ok((identity, Json(login))) -} - -#[derive(serde::Deserialize)] -pub struct Request { - pub name: Name, - pub password: Password, -} - -#[derive(Debug, thiserror::Error)] -#[error(transparent)] -pub struct Error(#[from] pub app::LoginError); - -impl IntoResponse for Error { - fn into_response(self) -> Response { - let Self(error) = self; - match error { - app::LoginError::Rejected => { - // not error::Unauthorized due to differing messaging - (StatusCode::UNAUTHORIZED, "invalid name or password").into_response() - } - other => Internal::from(other).into_response(), - } - } -} diff --git a/src/login/routes/login/test.rs b/src/login/routes/login/test.rs deleted file mode 100644 index 7399796..0000000 --- a/src/login/routes/login/test.rs +++ /dev/null @@ -1,128 +0,0 @@ -use axum::extract::{Json, State}; - -use super::post; -use crate::{test::fixtures, token::app}; - -#[tokio::test] -async fn correct_credentials() { - // Set up the environment - - let app = fixtures::scratch_app().await; - let (name, password) = fixtures::login::create_with_password(&app, &fixtures::now()).await; - - // Call the endpoint - - let identity = fixtures::cookie::not_logged_in(); - let logged_in_at = fixtures::now(); - let request = post::Request { - name: name.clone(), - password, - }; - let (identity, Json(response)) = - post::handler(State(app.clone()), logged_in_at, identity, Json(request)) - .await - .expect("logged in with valid credentials"); - - // Verify the return value's basic structure - - assert_eq!(name, response.name); - let secret = identity - .secret() - .expect("logged in with valid credentials issues an identity cookie"); - - // Verify the semantics - - let validated_at = fixtures::now(); - let (_, validated_login) = app - .tokens() - .validate(&secret, &validated_at) - .await - .expect("identity secret is valid"); - - assert_eq!(response, validated_login); -} - -#[tokio::test] -async fn invalid_name() { - // Set up the environment - - let app = fixtures::scratch_app().await; - - // Call the endpoint - - let identity = fixtures::cookie::not_logged_in(); - let logged_in_at = fixtures::now(); - let (name, password) = fixtures::login::propose(); - let request = post::Request { - name: name.clone(), - password, - }; - let post::Error(error) = - post::handler(State(app.clone()), logged_in_at, identity, Json(request)) - .await - .expect_err("logged in with an incorrect password fails"); - - // Verify the return value's basic structure - - assert!(matches!(error, app::LoginError::Rejected)); -} - -#[tokio::test] -async fn incorrect_password() { - // Set up the environment - - let app = fixtures::scratch_app().await; - let login = fixtures::login::create(&app, &fixtures::now()).await; - - // Call the endpoint - - let logged_in_at = fixtures::now(); - let identity = fixtures::cookie::not_logged_in(); - let request = post::Request { - name: login.name, - password: fixtures::login::propose_password(), - }; - let post::Error(error) = - post::handler(State(app.clone()), logged_in_at, identity, Json(request)) - .await - .expect_err("logged in with an incorrect password"); - - // Verify the return value's basic structure - - assert!(matches!(error, app::LoginError::Rejected)); -} - -#[tokio::test] -async fn token_expires() { - // Set up the environment - - let app = fixtures::scratch_app().await; - let (name, password) = fixtures::login::create_with_password(&app, &fixtures::now()).await; - - // Call the endpoint - - let logged_in_at = fixtures::ancient(); - let identity = fixtures::cookie::not_logged_in(); - let request = post::Request { name, password }; - let (identity, _) = post::handler(State(app.clone()), logged_in_at, identity, Json(request)) - .await - .expect("logged in with valid credentials"); - let secret = identity.secret().expect("logged in with valid credentials"); - - // Verify the semantics - - let expired_at = fixtures::now(); - app.tokens() - .expire(&expired_at) - .await - .expect("expiring tokens never fails"); - - let verified_at = fixtures::now(); - let error = app - .tokens() - .validate(&secret, &verified_at) - .await - .expect_err("validating an expired token"); - - assert!(matches!(error, app::ValidateError::InvalidToken)); -} diff --git a/src/login/routes/logout/mod.rs b/src/login/routes/logout/mod.rs deleted file mode 100644 index 36b384e..0000000 --- a/src/login/routes/logout/mod.rs +++ /dev/null @@ -1,4 +0,0 @@ -pub mod post; - -#[cfg(test)] -mod test; diff --git a/src/login/routes/logout/post.rs b/src/login/routes/logout/post.rs deleted file mode 100644 index bb09b9f..0000000 --- a/src/login/routes/logout/post.rs +++ /dev/null @@ -1,47 +0,0 @@ -use axum::{ - extract::{Json, State}, - http::StatusCode, - response::{IntoResponse, Response}, -}; - -use crate::{ - app::App, - clock::RequestedAt, - error::{Internal, Unauthorized}, - token::{app, extract::IdentityCookie}, -}; - -pub async fn handler( - State(app): State<App>, - RequestedAt(now): RequestedAt, - identity: IdentityCookie, - Json(_): Json<Request>, -) -> Result<(IdentityCookie, StatusCode), Error> { - if let Some(secret) = identity.secret() { - let (token, _) = app.tokens().validate(&secret, &now).await?; - app.tokens().logout(&token).await?; - } - - let identity = identity.clear(); - Ok((identity, StatusCode::NO_CONTENT)) -} - -// This forces the only valid request to be `{}`, and not the infinite -// variation allowed when there's no body extractor. -#[derive(Default, serde::Deserialize)] -pub struct Request {} - -#[derive(Debug, thiserror::Error)] -#[error(transparent)] -pub struct Error(#[from] pub app::ValidateError); - -impl IntoResponse for Error { - fn into_response(self) -> Response { - let Self(error) = self; - #[allow(clippy::match_wildcard_for_single_variants)] - match error { - app::ValidateError::InvalidToken => Unauthorized.into_response(), - other => Internal::from(other).into_response(), - } - } -} diff --git a/src/login/routes/logout/test.rs b/src/login/routes/logout/test.rs deleted file mode 100644 index 775fa9f..0000000 --- a/src/login/routes/logout/test.rs +++ /dev/null @@ -1,79 +0,0 @@ -use axum::{ - extract::{Json, State}, - http::StatusCode, -}; - -use super::post; -use crate::{test::fixtures, token::app}; - -#[tokio::test] -async fn successful() { - // Set up the environment - - let app = fixtures::scratch_app().await; - let now = fixtures::now(); - let creds = fixtures::login::create_with_password(&app, &fixtures::now()).await; - let identity = fixtures::cookie::logged_in(&app, &creds, &now).await; - let secret = fixtures::cookie::secret(&identity); - - // Call the endpoint - - let (response_identity, response_status) = post::handler( - State(app.clone()), - fixtures::now(), - identity.clone(), - Json::default(), - ) - .await - .expect("logged out with a valid token"); - - // Verify the return value's basic structure - - assert!(response_identity.secret().is_none()); - assert_eq!(StatusCode::NO_CONTENT, response_status); - - // Verify the semantics - let error = app - .tokens() - .validate(&secret, &now) - .await - .expect_err("secret is invalid"); - assert!(matches!(error, app::ValidateError::InvalidToken)); -} - -#[tokio::test] -async fn no_identity() { - // Set up the environment - - let app = fixtures::scratch_app().await; - - // Call the endpoint - - let identity = fixtures::cookie::not_logged_in(); - let (identity, status) = post::handler(State(app), fixtures::now(), identity, Json::default()) - .await - .expect("logged out with no token succeeds"); - - // Verify the return value's basic structure - - assert!(identity.secret().is_none()); - assert_eq!(StatusCode::NO_CONTENT, status); -} - -#[tokio::test] -async fn invalid_token() { - // Set up the environment - - let app = fixtures::scratch_app().await; - - // Call the endpoint - - let identity = fixtures::cookie::fictitious(); - let post::Error(error) = post::handler(State(app), fixtures::now(), identity, Json::default()) - .await - .expect_err("logged out with an invalid token fails"); - - // Verify the return value's basic structure - - assert!(matches!(error, app::ValidateError::InvalidToken)); -} diff --git a/src/login/routes/mod.rs b/src/login/routes/mod.rs deleted file mode 100644 index ade96cb..0000000 --- a/src/login/routes/mod.rs +++ /dev/null @@ -1,14 +0,0 @@ -use axum::{Router, routing::post}; - -use crate::app::App; - -mod login; -mod logout; -mod password; - -pub fn router() -> Router<App> { - Router::new() - .route("/api/password", post(password::post::handler)) - .route("/api/auth/login", post(login::post::handler)) - .route("/api/auth/logout", post(logout::post::handler)) -} diff --git a/src/login/routes/password/mod.rs b/src/login/routes/password/mod.rs deleted file mode 100644 index 36b384e..0000000 --- a/src/login/routes/password/mod.rs +++ /dev/null @@ -1,4 +0,0 @@ -pub mod post; - -#[cfg(test)] -mod test; diff --git a/src/login/routes/password/post.rs b/src/login/routes/password/post.rs deleted file mode 100644 index 4723754..0000000 --- a/src/login/routes/password/post.rs +++ /dev/null @@ -1,54 +0,0 @@ -use axum::{ - extract::{Json, State}, - http::StatusCode, - response::{IntoResponse, Response}, -}; - -use crate::{ - app::App, - clock::RequestedAt, - error::Internal, - login::{Login, Password}, - token::{ - app, - extract::{Identity, IdentityCookie}, - }, -}; - -pub async fn handler( - State(app): State<App>, - RequestedAt(now): RequestedAt, - identity: Identity, - cookie: IdentityCookie, - Json(request): Json<Request>, -) -> Result<(IdentityCookie, Json<Login>), Error> { - let (login, secret) = app - .tokens() - .change_password(&identity.login, &request.password, &request.to, &now) - .await - .map_err(Error)?; - let cookie = cookie.set(secret); - Ok((cookie, Json(login))) -} - -#[derive(serde::Deserialize)] -pub struct Request { - pub password: Password, - pub to: Password, -} - -#[derive(Debug, thiserror::Error)] -#[error(transparent)] -pub struct Error(#[from] pub app::LoginError); - -impl IntoResponse for Error { - fn into_response(self) -> Response { - let Self(error) = self; - match error { - app::LoginError::Rejected => { - (StatusCode::BAD_REQUEST, "invalid name or password").into_response() - } - other => Internal::from(other).into_response(), - } - } -} diff --git a/src/login/routes/password/test.rs b/src/login/routes/password/test.rs deleted file mode 100644 index c1974bf..0000000 --- a/src/login/routes/password/test.rs +++ /dev/null @@ -1,68 +0,0 @@ -use axum::extract::{Json, State}; - -use super::post; -use crate::{ - test::fixtures, - token::app::{LoginError, ValidateError}, -}; - -#[tokio::test] -async fn password_change() { - // Set up the environment - let app = fixtures::scratch_app().await; - let creds = fixtures::login::create_with_password(&app, &fixtures::now()).await; - let cookie = fixtures::cookie::logged_in(&app, &creds, &fixtures::now()).await; - let identity = fixtures::identity::from_cookie(&app, &cookie, &fixtures::now()).await; - - // Call the endpoint - let (name, password) = creds; - let to = fixtures::login::propose_password(); - let request = post::Request { - password: password.clone(), - to: to.clone(), - }; - let (new_cookie, Json(response)) = post::handler( - State(app.clone()), - fixtures::now(), - identity.clone(), - cookie.clone(), - Json(request), - ) - .await - .expect("changing passwords succeeds"); - - // Verify that we have a new session - assert_ne!(cookie.secret(), new_cookie.secret()); - - // Verify that we're still ourselves - assert_eq!(identity.login, response); - - // Verify that our original token is no longer valid - let validate_err = app - .tokens() - .validate( - &cookie - .secret() - .expect("original identity cookie has a secret"), - &fixtures::now(), - ) - .await - .expect_err("validating the original identity secret should fail"); - assert!(matches!(validate_err, ValidateError::InvalidToken)); - - // Verify that our original password is no longer valid - let login_err = app - .tokens() - .login(&name, &password, &fixtures::now()) - .await - .expect_err("logging in with the original password should fail"); - assert!(matches!(login_err, LoginError::Rejected)); - - // Verify that our new password is valid - let (login, _) = app - .tokens() - .login(&name, &to, &fixtures::now()) - .await - .expect("logging in with the new password should succeed"); - assert_eq!(identity.login, login); -} diff --git a/src/login/snapshot.rs b/src/login/snapshot.rs deleted file mode 100644 index 5c5dce0..0000000 --- a/src/login/snapshot.rs +++ /dev/null @@ -1,51 +0,0 @@ -use super::{ - Id, - event::{Created, Event}, -}; -use crate::name::Name; - -// This also implements FromRequestParts (see `./extract.rs`). As a result, it -// can be used as an extractor for endpoints that want to require login, or for -// endpoints that need to behave differently depending on whether the client is -// or is not logged in. -#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize)] -pub struct Login { - pub id: Id, - pub name: Name, - // The omission of the hashed password is deliberate, to minimize the - // chance that it ends up tangled up in debug output or in some other chunk - // of logic elsewhere. -} - -impl Login { - // Two reasons for this allow: - // - // 1. This is used to collect streams using a fold, below, which requires a type - // consistent with the fold, and - // 2. It's also consistent with the other history state machine types. - #[allow(clippy::unnecessary_wraps)] - fn apply(state: Option<Self>, event: Event) -> Option<Self> { - match (state, event) { - (None, Event::Created(event)) => Some(event.into()), - (state, event) => panic!("invalid message event {event:#?} for state {state:#?}"), - } - } -} - -impl FromIterator<Event> for Option<Login> { - fn from_iter<I: IntoIterator<Item = Event>>(events: I) -> Self { - events.into_iter().fold(None, Login::apply) - } -} - -impl From<&Created> for Login { - fn from(event: &Created) -> Self { - event.login.clone() - } -} - -impl From<Created> for Login { - fn from(event: Created) -> Self { - event.login - } -} diff --git a/src/login/validate.rs b/src/login/validate.rs deleted file mode 100644 index 0c97293..0000000 --- a/src/login/validate.rs +++ /dev/null @@ -1,23 +0,0 @@ -use unicode_segmentation::UnicodeSegmentation as _; - -use crate::name::Name; - -// Picked out of a hat. The power of two is not meaningful. -const NAME_TOO_LONG: usize = 64; - -pub fn name(name: &Name) -> bool { - let display = name.display(); - - [ - display.graphemes(true).count() < NAME_TOO_LONG, - display.chars().all(|ch| !ch.is_control()), - display.chars().next().is_some_and(|c| !c.is_whitespace()), - display.chars().last().is_some_and(|c| !c.is_whitespace()), - display - .chars() - .zip(display.chars().skip(1)) - .all(|(a, b)| !(a.is_whitespace() && b.is_whitespace())), - ] - .into_iter() - .all(|value| value) -} |
