summaryrefslogtreecommitdiff
path: root/src/login
diff options
context:
space:
mode:
Diffstat (limited to 'src/login')
-rw-r--r--src/login/app.rs56
-rw-r--r--src/login/create.rs95
-rw-r--r--src/login/event.rs36
-rw-r--r--src/login/history.rs52
-rw-r--r--src/login/id.rs24
-rw-r--r--src/login/mod.rs15
-rw-r--r--src/login/password.rs65
-rw-r--r--src/login/repo.rs153
-rw-r--r--src/login/routes/login/mod.rs4
-rw-r--r--src/login/routes/login/post.rs52
-rw-r--r--src/login/routes/login/test.rs128
-rw-r--r--src/login/routes/logout/mod.rs4
-rw-r--r--src/login/routes/logout/post.rs47
-rw-r--r--src/login/routes/logout/test.rs79
-rw-r--r--src/login/routes/mod.rs14
-rw-r--r--src/login/routes/password/mod.rs4
-rw-r--r--src/login/routes/password/post.rs54
-rw-r--r--src/login/routes/password/test.rs68
-rw-r--r--src/login/snapshot.rs51
-rw-r--r--src/login/validate.rs23
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)
-}