From d0d5fa20200a7ad70173ba87ae47c33b60f44a3b Mon Sep 17 00:00:00 2001 From: Owen Jacobson Date: Tue, 26 Aug 2025 00:44:29 -0400 Subject: Split `user` into a chat-facing entity and an authentication-facing entity. The taxonomy is now as follows: * A _login_ is someone's identity for the purposes of authenticating to the service. Logins are not synchronized, and in fact are not published anywhere in the current API. They have a login ID, a name and a password. * A _user_ is someone's identity for the purpose of participating in conversations. Users _are_ synchronized, as before. They have a user ID, a name, and a creation instant for the purposes of synchronization. In practice, a user exists for every login - in fact, users' names are stored in the login table and are joined in, rather than being stored redundantly in the user table. A login ID and its corresponding user ID are always equal, and the user and login ID types support conversion and comparison to facilitate their use in this context. Tokens are now associated with logins, not users. The currently-acting identity is passed down into app types as a login, not a user, and then resolved to a user where appropriate within the app methods. As a side effect, the `GET /api/boot` method now returns a `login` key instead of a `user` key. The structure of the nested value is unchanged. --- src/user/handlers/logout/mod.rs | 53 ----------------------------- src/user/handlers/logout/test.rs | 72 ---------------------------------------- 2 files changed, 125 deletions(-) delete mode 100644 src/user/handlers/logout/mod.rs delete mode 100644 src/user/handlers/logout/test.rs (limited to 'src/user/handlers/logout') diff --git a/src/user/handlers/logout/mod.rs b/src/user/handlers/logout/mod.rs deleted file mode 100644 index f759451..0000000 --- a/src/user/handlers/logout/mod.rs +++ /dev/null @@ -1,53 +0,0 @@ -use axum::{ - extract::{Json, State}, - response::{IntoResponse, Response}, -}; - -use crate::{ - app::App, - clock::RequestedAt, - empty::Empty, - error::{Internal, Unauthorized}, - token::{app, extract::IdentityCookie}, -}; - -#[cfg(test)] -mod test; - -pub async fn handler( - State(app): State, - RequestedAt(now): RequestedAt, - identity: IdentityCookie, - Json(_): Json, -) -> Result<(IdentityCookie, Empty), Error> { - if let Some(secret) = identity.secret() { - let validated_ident = app.tokens().validate(&secret, &now).await?; - app.tokens().logout(&validated_ident.token).await?; - } - - let identity = identity.clear(); - Ok((identity, Empty)) -} - -// 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; - match error { - app::ValidateError::InvalidToken | app::ValidateError::LoginDeleted => { - Unauthorized.into_response() - } - app::ValidateError::Name(_) | app::ValidateError::Database(_) => { - Internal::from(error).into_response() - } - } - } -} diff --git a/src/user/handlers/logout/test.rs b/src/user/handlers/logout/test.rs deleted file mode 100644 index 8ad4853..0000000 --- a/src/user/handlers/logout/test.rs +++ /dev/null @@ -1,72 +0,0 @@ -use axum::extract::{Json, State}; - -use crate::{ - empty::Empty, - test::{fixtures, verify}, - token::app, -}; - -#[tokio::test] -async fn successful() { - // Set up the environment - - let app = fixtures::scratch_app().await; - let now = fixtures::now(); - let creds = fixtures::user::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, Empty) = super::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()); - - // Verify the semantics - verify::token::invalid(&app, &secret).await; -} - -#[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, Empty) = super::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()); -} - -#[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 super::Error(error) = - super::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)); -} -- cgit v1.2.3