summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorojacobson <ojacobson@noreply.codeberg.org>2025-08-26 04:32:42 +0200
committerojacobson <ojacobson@noreply.codeberg.org>2025-08-26 04:32:42 +0200
commit25914826e0f256789d943cd25375b2444130ce01 (patch)
tree44ce77c5af10f2b90308ab31e9b383975ebfd280
parent53944ef14af4d37c08464cb1bb9f3a8f09277194 (diff)
parentf6a79204c2ce9a15d7909c1c389417e0b7351cad (diff)
Remove unused response bodies from a number of API endpoints.
This removes the response body from the following methods: * `POST /api/setup` * `POST /api/auth/login` * `POST /api/invite/:id` * `POST /api/password` The bodies returned from these methods were something of a rough guess as to what might be useful. Actual client development has shown that we don't use _any_ of the data from any of these API responses, so let's not tie ourselves to future compatibility by continuing to send them. We can add a body to a bodyless method a _lot_ more easily than we can change the body of a method that already returns one, after all. These changes are not backwards compatible for clients which care about the existing bodies. To my knowledge, there are no such clients; the included client definitely doesn't care. ## Internals Not only does this change stop returning bodies at the API surface, but it also stops retrieving and returning values used internally to construct those responses, simplifying the code a bit in the process. One side effect of this is that tests that need to log in a user now need to manually verify the returned token secret, to convert it back into a user, whereas the previous versions returned both a token secret and a user during password login. I don't love the increase in the size of the tests, but I think it's the right tradeoff (and this change is code net-negative anyways). Merges no-content into main.
-rw-r--r--docs/api/authentication.md53
-rw-r--r--docs/api/initial-setup.md27
-rw-r--r--docs/api/invitations.md39
-rw-r--r--src/empty.rs11
-rw-r--r--src/event/handlers/stream/test/invite.rs14
-rw-r--r--src/event/handlers/stream/test/setup.rs7
-rw-r--r--src/invite/app.rs6
-rw-r--r--src/invite/handlers/accept/mod.rs8
-rw-r--r--src/invite/handlers/accept/test.rs16
-rw-r--r--src/lib.rs1
-rw-r--r--src/setup/app.rs11
-rw-r--r--src/setup/handlers/setup/mod.rs10
-rw-r--r--src/setup/handlers/setup/test.rs19
-rw-r--r--src/test/fixtures/cookie.rs4
-rw-r--r--src/token/app.rs22
-rw-r--r--src/user/app.rs3
-rw-r--r--src/user/create.rs5
-rw-r--r--src/user/handlers/login/mod.rs8
-rw-r--r--src/user/handlers/login/test.rs7
-rw-r--r--src/user/handlers/logout/mod.rs6
-rw-r--r--src/user/handlers/logout/test.rs13
-rw-r--r--src/user/handlers/password/mod.rs8
-rw-r--r--src/user/handlers/password/test.rs20
-rw-r--r--src/user/history.rs1
24 files changed, 138 insertions, 181 deletions
diff --git a/docs/api/authentication.md b/docs/api/authentication.md
index fbd5959..189103e 100644
--- a/docs/api/authentication.md
+++ b/docs/api/authentication.md
@@ -71,32 +71,15 @@ The request must have the following fields:
<!-- This prose is duplicated by 03-initial-setup.md and in 04-invitations.md, with small changes for context. If you edit it here, edit it there, too. -->
-This endpoint will respond with a status of
-`200 Okay` when successful. The body of the response will be a JSON object describing the authenticated user:
-
-```json
-{
- "id": "Uabcd1234",
- "name": "Andrea"
-}
-```
-
-The response will include the following fields:
-
-| Field | Type | Description |
-| :----- | :----- | :----------------------------- |
-| `id` | string | The authenticated user's ID. |
-| `name` | string | The authenticated user's name. |
+This endpoint will respond with a status of `204 No Content` when successful.
-The response will include a `Set-Cookie` header for the
-`identity` cookie, providing the client with a newly-minted identity token associated with the user identified in the request. This token's value must be kept confidential.
+The response will include a `Set-Cookie` header for the `identity` cookie, providing the client with a newly-minted identity token associated with the user identified in the request. This token's value must be kept confidential.
The cookie will expire if it is not used regularly.
### Authentication failure
-This endpoint will respond with a status of
-`401 Unauthorized` if the login name and password do not correspond to an existing user.
+This endpoint will respond with a status of `401 Unauthorized` if the login name and password do not correspond to an existing user.
## `POST /api/auth/logout`
@@ -114,13 +97,16 @@ The request must be an empty JSON object.
This endpoint will respond with a status of `204 No Content` when successful.
-The response will include a `Set-Cookie` header that clears the
-`identity` cookie. Regardless of whether the client clears the cookie, the service also invalidates the token.
+The response will include a `Set-Cookie` header that clears the `identity` cookie. Regardless of whether the client clears the cookie, the service also invalidates the token.
## `POST /api/password`
Changes the current user's password, and invalidates all outstanding identity tokens.
+### Authentication failure
+
+This endpoint will respond with a status of `401 Unauthorized` if the provided identity token is not valid.
+
### Request
```json
@@ -139,29 +125,12 @@ The request must have the following fields:
### Success
-This endpoint will respond with a status of
-`200 Okay` when successful. The body of the response will be a JSON object describing the authenticated user:
-
-```json
-{
- "id": "Uabcd1234",
- "name": "Andrea"
-}
-```
-
-The response will include the following fields:
-
-| Field | Type | Description |
-| :----- | :----- | :----------------------------- |
-| `id` | string | The authenticated user's ID. |
-| `name` | string | The authenticated user's name. |
+This endpoint will respond with a status of `204 No Content` when successful.
-The response will include a `Set-Cookie` header for the
-`identity` cookie, providing the client with a newly-minted identity token associated with the login identified in the request. This token's value must be kept confidential. All previously-created identity cookies will cease to be valid.
+The response will include a `Set-Cookie` header for the `identity` cookie, providing the client with a newly-minted identity token associated with the login identified in the request. This token's value must be kept confidential. All previously-created identity cookies will cease to be valid.
The cookie will expire if it is not used regularly.
### Authentication failure
-This endpoint will respond with a status of `400 Bad Request` if the
-`password` does not match the login's current password.
+This endpoint will respond with a status of `400 Bad Request` if the `password` does not match the login's current password.
diff --git a/docs/api/initial-setup.md b/docs/api/initial-setup.md
index a3f6506..b52771f 100644
--- a/docs/api/initial-setup.md
+++ b/docs/api/initial-setup.md
@@ -17,8 +17,7 @@ New instances of this service require an initial setup step before they can full
## Requests before setup completed
-Before the service is set up, all API endpoints, other than those specifically documented as exceptions, will return a status of
-`503 Service Unavailable` to all requests.
+Before the service is set up, all API endpoints, other than those specifically documented as exceptions, will return a status of `503 Service Unavailable` to all requests.
Initial setup can be completed only once.
@@ -64,29 +63,9 @@ The proposed `name` must be valid. The precise definition of valid is still up i
<!-- This prose is duplicated from authentication.md, with small changes for context. If you edit it here, edit it there, too. -->
-This endpoint will respond with a status of
-`200 Okay` when successful. The body of the response will be a JSON object describing the newly-created user:
+This endpoint will respond with a status of `204 No Content` when successful.
-```json
-{
- "id": "Uabcd1234",
- "name": "Andrea"
-}
-```
-
-The response will include the following fields:
-
-| Field | Type | Description |
-| :----- | :----- | :------------------------------------------------------------------------------------------------------------------------------------------------- |
-| `id` | string | A unique identifier for the newly-created user. This can be used to associate the user with other events, or to make API calls targeting the user. |
-| `name` | string | The user's name. |
-
-The returned name may not be identical to the name requested, as the name will be converted to [normalization form C](http://www.unicode.org/reports/tr15/) automatically. The returned name will include this normalization; the service will use the normalized name elsewhere, and does not store the originally requested name.
-
-The provided password will also be converted to normalization form C. However, the normalized password is not returned to the client.
-
-The response will include a `Set-Cookie` header for the
-`identity` cookie, providing the client with a newly-minted identity token associated with the initial user created for this request. See the [authentication](./authentication) section for details on how this cookie may be used.
+The response will include a `Set-Cookie` header for the `identity` cookie, providing the client with a newly-minted identity token associated with the initial user created for this request. See the [authentication](./authentication) section for details on how this cookie may be used.
The cookie will expire if it is not used regularly.
diff --git a/docs/api/invitations.md b/docs/api/invitations.md
index 04e92c8..9b86b9e 100644
--- a/docs/api/invitations.md
+++ b/docs/api/invitations.md
@@ -39,8 +39,7 @@ The request must be an empty JSON object.
### Success
-This endpoint will respond with a status of
-`200 Okay` when successful. The body of the response will be a JSON object describing the new invitation:
+This endpoint will respond with a status of `200 Okay` when successful. The body of the response will be a JSON object describing the new invitation:
```json
{
@@ -58,8 +57,7 @@ The response will include the following fields:
| `issuer` | string | The user ID of the invitation's issuer. |
| `issued_at` | string | The timestamp from which the invitation will expire. |
-Clients and their operators are responsible for delivering the invitation to the invitee. Clients are strongly recommended to construct a URL for the invitation so that the invitee can take action on it easily. The included client supports URLs of the format
-`https://example.net/invite/:id` (with the `:id` placeholder substituted with the invitation's ID).
+Clients and their operators are responsible for delivering the invitation to the invitee. Clients are strongly recommended to construct a URL for the invitation so that the invitee can take action on it easily. The included client supports URLs of the format `https://example.net/invite/:id` (with the `:id` placeholder substituted with the invitation's ID).
## `GET /api/invite/:id`
@@ -75,8 +73,7 @@ This endpoint requires the following path parameter:
### On success
-This endpoint will respond with a status of
-`200 Okay` when successful. The body of the response will be a JSON object describing the invitation:
+This endpoint will respond with a status of `200 Okay` when successful. The body of the response will be a JSON object describing the invitation:
```json
{
@@ -96,13 +93,11 @@ The response will include the following fields:
| `issuer` | string | The name of the invitation's issuer. |
| `issued_at` | string | The timestamp from which the invitation will expire. |
-Clients should present the
-`issuer` to the user when presenting an invitation, so as to personalize the invitation and help them understand their connection with the service.
+Clients should present the `issuer` to the user when presenting an invitation, so as to personalize the invitation and help them understand their connection with the service.
### Invitation not found
-This endpoint will respond with a status of
-`404 Not Found` when the invitation ID either does not exist, or has already been accepted.
+This endpoint will respond with a status of `404 Not Found` when the invitation ID either does not exist, or has already been accepted.
## `POST /api/invite/:id`
@@ -146,29 +141,9 @@ The proposed `name` must be valid. The precise definition of valid is still up i
<!-- This prose is duplicated from authentication.md, with small changes for context. If you edit it here, edit it there, too. -->
-This endpoint will respond with a status of
-`200 Okay` when successful. The body of the response will be a JSON object describing the newly-created user:
-
-```json
-{
- "id": "Uabcd1234",
- "name": "Andrea"
-}
-```
-
-The response will include the following fields:
-
-| Field | Type | Description |
-| :----- | :----- | :------------------------------------------------------------------------------------------------------------------------------------------------- |
-| `id` | string | A unique identifier for the newly-created user. This can be used to associate the user with other events, or to make API calls targeting the user. |
-| `name` | string | The user's name. |
-
-The returned name may not be identical to the name requested, as the name will be converted to [normalization form C](http://www.unicode.org/reports/tr15/) automatically. The returned name will include this normalization; the service will use the normalized name elsewhere, and does not store the originally requested name.
-
-The provided password will also be converted to normalization form C. However, the normalized password is not returned to the client.
+This endpoint will respond with a status of `204 No Content` when successful.
-The response will include a `Set-Cookie` header for the
-`identity` cookie, providing the client with a newly-minted identity token associated with the login created for this request. See the [authentication](./authentication.md) section for details on how this cookie may be used.
+The response will include a `Set-Cookie` header for the `identity` cookie, providing the client with a newly-minted identity token associated with the login created for this request. See the [authentication](./authentication.md) section for details on how this cookie may be used.
The cookie will expire if it is not used regularly.
diff --git a/src/empty.rs b/src/empty.rs
new file mode 100644
index 0000000..2c66051
--- /dev/null
+++ b/src/empty.rs
@@ -0,0 +1,11 @@
+use axum::http::StatusCode;
+use axum::response::{IntoResponse, Response};
+
+#[derive(Debug)]
+pub struct Empty;
+
+impl IntoResponse for Empty {
+ fn into_response(self) -> Response {
+ StatusCode::NO_CONTENT.into_response()
+ }
+}
diff --git a/src/event/handlers/stream/test/invite.rs b/src/event/handlers/stream/test/invite.rs
index 01372ce..f2d6950 100644
--- a/src/event/handlers/stream/test/invite.rs
+++ b/src/event/handlers/stream/test/invite.rs
@@ -28,11 +28,16 @@ async fn accepting_invite() {
// Accept the invite
let (name, password) = fixtures::user::propose();
- let (joiner, _) = app
+ let joiner = app
.invites()
.accept(&invite.id, &name, &password, &fixtures::now())
.await
.expect("accepting an invite succeeds");
+ let (_, joiner) = app
+ .tokens()
+ .validate(&joiner, &fixtures::now())
+ .await
+ .expect("a newly-issued secret is valid");
// Expect a login created event
@@ -57,11 +62,16 @@ async fn previously_accepted_invite() {
// Accept the invite
let (name, password) = fixtures::user::propose();
- let (joiner, _) = app
+ let joiner = app
.invites()
.accept(&invite.id, &name, &password, &fixtures::now())
.await
.expect("accepting an invite succeeds");
+ let (_, joiner) = app
+ .tokens()
+ .validate(&joiner, &fixtures::now())
+ .await
+ .expect("a newly-issued secret is valid");
// Subscribe
diff --git a/src/event/handlers/stream/test/setup.rs b/src/event/handlers/stream/test/setup.rs
index 992b962..297162e 100644
--- a/src/event/handlers/stream/test/setup.rs
+++ b/src/event/handlers/stream/test/setup.rs
@@ -17,11 +17,16 @@ async fn previously_completed() {
// Complete initial setup
let (name, password) = fixtures::user::propose();
- let (owner, _) = app
+ let secret = app
.setup()
.initial(&name, &password, &fixtures::now())
.await
.expect("initial setup in an empty app succeeds");
+ let (_, owner) = app
+ .tokens()
+ .validate(&secret, &fixtures::now())
+ .await
+ .expect("secret returned by initial setup should be valid");
// Subscribe to events
diff --git a/src/invite/app.rs b/src/invite/app.rs
index a286a8f..1c85562 100644
--- a/src/invite/app.rs
+++ b/src/invite/app.rs
@@ -47,7 +47,7 @@ impl<'a> Invites<'a> {
name: &Name,
password: &Password,
accepted_at: &DateTime,
- ) -> Result<(User, Secret), AcceptError> {
+ ) -> Result<Secret, AcceptError> {
let create = Create::begin(name, password, accepted_at);
let mut tx = self.db.begin().await?;
@@ -74,9 +74,9 @@ impl<'a> Invites<'a> {
let secret = tx.tokens().issue(stored.user(), accepted_at).await?;
tx.commit().await?;
- let login = stored.publish(self.events);
+ stored.publish(self.events);
- Ok((login.as_created(), secret))
+ Ok(secret)
}
pub async fn expire(&self, relative_to: &DateTime) -> Result<(), sqlx::Error> {
diff --git a/src/invite/handlers/accept/mod.rs b/src/invite/handlers/accept/mod.rs
index 9fa4d6a..cdf385f 100644
--- a/src/invite/handlers/accept/mod.rs
+++ b/src/invite/handlers/accept/mod.rs
@@ -7,12 +7,12 @@ use axum::{
use crate::{
app::App,
clock::RequestedAt,
+ empty::Empty,
error::{Internal, NotFound},
invite::{app, handlers::PathInfo},
name::Name,
password::Password,
token::extract::IdentityCookie,
- user::User,
};
#[cfg(test)]
@@ -24,14 +24,14 @@ pub async fn handler(
identity: IdentityCookie,
Path(invite): Path<PathInfo>,
Json(request): Json<Request>,
-) -> Result<(IdentityCookie, Json<User>), Error> {
- let (login, secret) = app
+) -> Result<(IdentityCookie, Empty), Error> {
+ let secret = app
.invites()
.accept(&invite, &request.name, &request.password, &accepted_at)
.await
.map_err(Error)?;
let identity = identity.set(secret);
- Ok((identity, Json(login)))
+ Ok((identity, Empty))
}
#[derive(serde::Deserialize)]
diff --git a/src/invite/handlers/accept/test.rs b/src/invite/handlers/accept/test.rs
index cb13900..adc7aa4 100644
--- a/src/invite/handlers/accept/test.rs
+++ b/src/invite/handlers/accept/test.rs
@@ -1,6 +1,6 @@
use axum::extract::{Json, Path, State};
-use crate::{invite::app::AcceptError, name::Name, test::fixtures};
+use crate::{empty::Empty, invite::app::AcceptError, name::Name, test::fixtures};
#[tokio::test]
async fn valid_invite() {
@@ -18,7 +18,7 @@ async fn valid_invite() {
name: name.clone(),
password: password.clone(),
};
- let (identity, Json(response)) = super::handler(
+ let (identity, Empty) = super::handler(
State(app.clone()),
fixtures::now(),
identity,
@@ -31,7 +31,6 @@ async fn valid_invite() {
// Verify the response
assert!(identity.secret().is_some());
- assert_eq!(name, response.name);
// Verify that the issued token is valid
@@ -43,16 +42,21 @@ async fn valid_invite() {
.validate(&secret, &fixtures::now())
.await
.expect("newly-issued identity cookie is valid");
- assert_eq!(response, login);
+ assert_eq!(name, login.name);
// Verify that the given credentials can log in
- let (login, _) = app
+ let secret = app
.tokens()
.login(&name, &password, &fixtures::now())
.await
.expect("credentials given on signup are valid");
- assert_eq!(response, login);
+ let (_, login) = app
+ .tokens()
+ .validate(&secret, &fixtures::now())
+ .await
+ .expect("validating a newly-issued token secret succeeds");
+ assert_eq!(name, login.name);
}
#[tokio::test]
diff --git a/src/lib.rs b/src/lib.rs
index 2d385cc..b3299d7 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -9,6 +9,7 @@ pub mod cli;
mod clock;
mod conversation;
mod db;
+mod empty;
mod error;
mod event;
mod expire;
diff --git a/src/setup/app.rs b/src/setup/app.rs
index 1210175..1856519 100644
--- a/src/setup/app.rs
+++ b/src/setup/app.rs
@@ -7,10 +7,7 @@ use crate::{
name::Name,
password::Password,
token::{Secret, repo::Provider as _},
- user::{
- User,
- create::{self, Create},
- },
+ user::create::{self, Create},
};
pub struct Setup<'a> {
@@ -28,7 +25,7 @@ impl<'a> Setup<'a> {
name: &Name,
password: &Password,
created_at: &DateTime,
- ) -> Result<(User, Secret), Error> {
+ ) -> Result<Secret, Error> {
let create = Create::begin(name, password, created_at);
let validated = create.validate()?;
@@ -42,9 +39,9 @@ impl<'a> Setup<'a> {
let secret = tx.tokens().issue(stored.user(), created_at).await?;
tx.commit().await?;
- let user = stored.publish(self.events);
+ stored.publish(self.events);
- Ok((user.as_created(), secret))
+ Ok(secret)
}
pub async fn completed(&self) -> Result<bool, sqlx::Error> {
diff --git a/src/setup/handlers/setup/mod.rs b/src/setup/handlers/setup/mod.rs
index 9e31282..fe24798 100644
--- a/src/setup/handlers/setup/mod.rs
+++ b/src/setup/handlers/setup/mod.rs
@@ -5,8 +5,8 @@ use axum::{
};
use crate::{
- app::App, clock::RequestedAt, error::Internal, name::Name, password::Password, setup::app,
- token::extract::IdentityCookie, user::User,
+ app::App, clock::RequestedAt, empty::Empty, error::Internal, name::Name, password::Password,
+ setup::app, token::extract::IdentityCookie,
};
#[cfg(test)]
@@ -17,14 +17,14 @@ pub async fn handler(
RequestedAt(setup_at): RequestedAt,
identity: IdentityCookie,
Json(request): Json<Request>,
-) -> Result<(IdentityCookie, Json<User>), Error> {
- let (user, secret) = app
+) -> Result<(IdentityCookie, Empty), Error> {
+ let secret = app
.setup()
.initial(&request.name, &request.password, &setup_at)
.await
.map_err(Error)?;
let identity = identity.set(secret);
- Ok((identity, Json(user)))
+ Ok((identity, Empty))
}
#[derive(serde::Deserialize)]
diff --git a/src/setup/handlers/setup/test.rs b/src/setup/handlers/setup/test.rs
index 8243ac3..4a37690 100644
--- a/src/setup/handlers/setup/test.rs
+++ b/src/setup/handlers/setup/test.rs
@@ -1,6 +1,6 @@
use axum::extract::{Json, State};
-use crate::{setup::app, test::fixtures};
+use crate::{empty::Empty, setup::app, test::fixtures};
#[tokio::test]
async fn fresh_instance() {
@@ -15,15 +15,11 @@ async fn fresh_instance() {
name: name.clone(),
password: password.clone(),
};
- let (identity, Json(response)) =
+ let (identity, Empty) =
super::handler(State(app.clone()), fixtures::now(), identity, Json(request))
.await
.expect("setup in a fresh app succeeds");
- // Verify the response
-
- assert_eq!(name, response.name);
-
// Verify that the issued token is valid
let secret = identity
@@ -34,16 +30,21 @@ async fn fresh_instance() {
.validate(&secret, &fixtures::now())
.await
.expect("newly-issued identity cookie is valid");
- assert_eq!(response, login);
+ assert_eq!(name, login.name);
// Verify that the given credentials can log in
- let (login, _) = app
+ let secret = app
.tokens()
.login(&name, &password, &fixtures::now())
.await
.expect("credentials given on signup are valid");
- assert_eq!(response, login);
+ let (_, login) = app
+ .tokens()
+ .validate(&secret, &fixtures::now())
+ .await
+ .expect("validating a newly-issued token secret succeeds");
+ assert_eq!(name, login.name);
}
#[tokio::test]
diff --git a/src/test/fixtures/cookie.rs b/src/test/fixtures/cookie.rs
index 41779db..f5a32a6 100644
--- a/src/test/fixtures/cookie.rs
+++ b/src/test/fixtures/cookie.rs
@@ -18,13 +18,13 @@ pub async fn logged_in(
now: &RequestedAt,
) -> IdentityCookie {
let (name, password) = credentials;
- let (_, token) = app
+ let secret = app
.tokens()
.login(name, password, now)
.await
.expect("should succeed given known-valid credentials");
- IdentityCookie::new().set(token)
+ IdentityCookie::new().set(secret)
}
pub fn secret(identity: &IdentityCookie) -> Secret {
diff --git a/src/token/app.rs b/src/token/app.rs
index 49f9a45..56c0e21 100644
--- a/src/token/app.rs
+++ b/src/token/app.rs
@@ -32,7 +32,7 @@ impl<'a> Tokens<'a> {
name: &Name,
password: &Password,
login_at: &DateTime,
- ) -> Result<(User, Secret), LoginError> {
+ ) -> Result<Secret, LoginError> {
let mut tx = self.db.begin().await?;
let (user, stored_hash) = tx
.auth()
@@ -47,18 +47,16 @@ impl<'a> Tokens<'a> {
// if the account is deleted during that time.
tx.commit().await?;
- let snapshot = user.as_snapshot().ok_or(LoginError::Rejected)?;
+ user.as_snapshot().ok_or(LoginError::Rejected)?;
- let token = if stored_hash.verify(password)? {
+ if stored_hash.verify(password)? {
let mut tx = self.db.begin().await?;
- let token = tx.tokens().issue(&user, login_at).await?;
+ let secret = tx.tokens().issue(&user, login_at).await?;
tx.commit().await?;
- token
+ Ok(secret)
} else {
- Err(LoginError::Rejected)?
- };
-
- Ok((snapshot, token))
+ Err(LoginError::Rejected)
+ }
}
pub async fn change_password(
@@ -67,7 +65,7 @@ impl<'a> Tokens<'a> {
password: &Password,
to: &Password,
changed_at: &DateTime,
- ) -> Result<(User, Secret), LoginError> {
+ ) -> Result<Secret, LoginError> {
let mut tx = self.db.begin().await?;
let (user, stored_hash) = tx
.auth()
@@ -86,7 +84,7 @@ impl<'a> Tokens<'a> {
return Err(LoginError::Rejected);
}
- let snapshot = user.as_snapshot().ok_or(LoginError::Rejected)?;
+ user.as_snapshot().ok_or(LoginError::Rejected)?;
let to_hash = to.hash()?;
let mut tx = self.db.begin().await?;
@@ -99,7 +97,7 @@ impl<'a> Tokens<'a> {
self.token_events.broadcast(event);
}
- Ok((snapshot, secret))
+ Ok(secret)
}
pub async fn validate(
diff --git a/src/user/app.rs b/src/user/app.rs
index 5f58981..301c39c 100644
--- a/src/user/app.rs
+++ b/src/user/app.rs
@@ -29,7 +29,8 @@ impl<'a> Users<'a> {
let stored = validated.store(&mut tx).await?;
tx.commit().await?;
- let user = stored.publish(self.events);
+ let user = stored.user().to_owned();
+ stored.publish(self.events);
Ok(user.as_created())
}
diff --git a/src/user/create.rs b/src/user/create.rs
index 0e7a118..5d7bf65 100644
--- a/src/user/create.rs
+++ b/src/user/create.rs
@@ -73,13 +73,10 @@ pub struct Stored {
}
impl Stored {
- #[must_use = "dropping a user creation attempt is likely a mistake"]
- pub fn publish(self, events: &Broadcaster) -> History {
+ pub fn publish(self, events: &Broadcaster) {
let Self { user } = self;
events.broadcast(user.events().map(Event::from).collect::<Vec<_>>());
-
- user
}
pub fn user(&self) -> &History {
diff --git a/src/user/handlers/login/mod.rs b/src/user/handlers/login/mod.rs
index da88885..d3e0e8c 100644
--- a/src/user/handlers/login/mod.rs
+++ b/src/user/handlers/login/mod.rs
@@ -7,11 +7,11 @@ use axum::{
use crate::{
app::App,
clock::RequestedAt,
+ empty::Empty,
error::Internal,
name::Name,
password::Password,
token::{app, extract::IdentityCookie},
- user::User,
};
#[cfg(test)]
@@ -22,14 +22,14 @@ pub async fn handler(
RequestedAt(now): RequestedAt,
identity: IdentityCookie,
Json(request): Json<Request>,
-) -> Result<(IdentityCookie, Json<User>), Error> {
- let (user, secret) = app
+) -> Result<(IdentityCookie, Empty), Error> {
+ let secret = app
.tokens()
.login(&request.name, &request.password, &now)
.await
.map_err(Error)?;
let identity = identity.set(secret);
- Ok((identity, Json(user)))
+ Ok((identity, Empty))
}
#[derive(serde::Deserialize)]
diff --git a/src/user/handlers/login/test.rs b/src/user/handlers/login/test.rs
index b8f24f6..bdd1957 100644
--- a/src/user/handlers/login/test.rs
+++ b/src/user/handlers/login/test.rs
@@ -1,6 +1,6 @@
use axum::extract::{Json, State};
-use crate::{test::fixtures, token::app};
+use crate::{empty::Empty, test::fixtures, token::app};
#[tokio::test]
async fn correct_credentials() {
@@ -17,14 +17,13 @@ async fn correct_credentials() {
name: name.clone(),
password,
};
- let (identity, Json(response)) =
+ let (identity, Empty) =
super::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");
@@ -38,7 +37,7 @@ async fn correct_credentials() {
.await
.expect("identity secret is valid");
- assert_eq!(response, validated_login);
+ assert_eq!(name, validated_login.name);
}
#[tokio::test]
diff --git a/src/user/handlers/logout/mod.rs b/src/user/handlers/logout/mod.rs
index 45a376a..4450e4c 100644
--- a/src/user/handlers/logout/mod.rs
+++ b/src/user/handlers/logout/mod.rs
@@ -1,12 +1,12 @@
use axum::{
extract::{Json, State},
- http::StatusCode,
response::{IntoResponse, Response},
};
use crate::{
app::App,
clock::RequestedAt,
+ empty::Empty,
error::{Internal, Unauthorized},
token::{app, extract::IdentityCookie},
};
@@ -19,14 +19,14 @@ pub async fn handler(
RequestedAt(now): RequestedAt,
identity: IdentityCookie,
Json(_): Json<Request>,
-) -> Result<(IdentityCookie, StatusCode), Error> {
+) -> Result<(IdentityCookie, Empty), 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))
+ Ok((identity, Empty))
}
// This forces the only valid request to be `{}`, and not the infinite
diff --git a/src/user/handlers/logout/test.rs b/src/user/handlers/logout/test.rs
index 8dc4636..7151ddf 100644
--- a/src/user/handlers/logout/test.rs
+++ b/src/user/handlers/logout/test.rs
@@ -1,9 +1,6 @@
-use axum::{
- extract::{Json, State},
- http::StatusCode,
-};
+use axum::extract::{Json, State};
-use crate::{test::fixtures, token::app};
+use crate::{empty::Empty, test::fixtures, token::app};
#[tokio::test]
async fn successful() {
@@ -17,7 +14,7 @@ async fn successful() {
// Call the endpoint
- let (response_identity, response_status) = super::handler(
+ let (response_identity, Empty) = super::handler(
State(app.clone()),
fixtures::now(),
identity.clone(),
@@ -29,7 +26,6 @@ async fn successful() {
// 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
@@ -49,14 +45,13 @@ async fn no_identity() {
// Call the endpoint
let identity = fixtures::cookie::not_logged_in();
- let (identity, status) = super::handler(State(app), fixtures::now(), identity, Json::default())
+ 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());
- assert_eq!(StatusCode::NO_CONTENT, status);
}
#[tokio::test]
diff --git a/src/user/handlers/password/mod.rs b/src/user/handlers/password/mod.rs
index c327e87..5e69c1c 100644
--- a/src/user/handlers/password/mod.rs
+++ b/src/user/handlers/password/mod.rs
@@ -7,13 +7,13 @@ use axum::{
use crate::{
app::App,
clock::RequestedAt,
+ empty::Empty,
error::Internal,
password::Password,
token::{
app,
extract::{Identity, IdentityCookie},
},
- user::User,
};
#[cfg(test)]
@@ -25,14 +25,14 @@ pub async fn handler(
identity: Identity,
cookie: IdentityCookie,
Json(request): Json<Request>,
-) -> Result<(IdentityCookie, Json<User>), Error> {
- let (login, secret) = app
+) -> Result<(IdentityCookie, Empty), Error> {
+ let secret = app
.tokens()
.change_password(&identity.user, &request.password, &request.to, &now)
.await
.map_err(Error)?;
let cookie = cookie.set(secret);
- Ok((cookie, Json(login)))
+ Ok((cookie, Empty))
}
#[derive(serde::Deserialize)]
diff --git a/src/user/handlers/password/test.rs b/src/user/handlers/password/test.rs
index 42e41d8..ffa12f3 100644
--- a/src/user/handlers/password/test.rs
+++ b/src/user/handlers/password/test.rs
@@ -1,6 +1,7 @@
use axum::extract::{Json, State};
use crate::{
+ empty::Empty,
test::fixtures,
token::app::{LoginError, ValidateError},
};
@@ -20,7 +21,7 @@ async fn password_change() {
password: password.clone(),
to: to.clone(),
};
- let (new_cookie, Json(response)) = super::handler(
+ let (new_cookie, Empty) = super::handler(
State(app.clone()),
fixtures::now(),
identity.clone(),
@@ -34,7 +35,15 @@ async fn password_change() {
assert_ne!(cookie.secret(), new_cookie.secret());
// Verify that we're still ourselves
- assert_eq!(identity.user, response);
+ let new_secret = new_cookie
+ .secret()
+ .expect("we should have a secret after changing our password");
+ let (_, login) = app
+ .tokens()
+ .validate(&new_secret, &fixtures::now())
+ .await
+ .expect("the newly-issued secret should be valid");
+ assert_eq!(identity.user, login);
// Verify that our original token is no longer valid
let validate_err = app
@@ -58,10 +67,15 @@ async fn password_change() {
assert!(matches!(login_err, LoginError::Rejected));
// Verify that our new password is valid
- let (login, _) = app
+ let secret = app
.tokens()
.login(&name, &to, &fixtures::now())
.await
.expect("logging in with the new password should succeed");
+ let (_, login) = app
+ .tokens()
+ .validate(&secret, &fixtures::now())
+ .await
+ .expect("validating a newly-issued token secret succeeds");
assert_eq!(identity.user, login);
}
diff --git a/src/user/history.rs b/src/user/history.rs
index 72e0aee..4f99130 100644
--- a/src/user/history.rs
+++ b/src/user/history.rs
@@ -20,6 +20,7 @@ impl History {
// if this returns a redacted or modified version of the user. If we implement
// renames by redacting the original name, then this should return the edited
// user, not the original, even if that's not how it was "as created.")
+ #[cfg(test)]
pub fn as_created(&self) -> User {
self.user.clone()
}