summaryrefslogtreecommitdiff
path: root/src/token/repo/token.rs
diff options
context:
space:
mode:
authorOwen Jacobson <owen@grimoire.ca>2024-10-21 00:36:44 -0400
committerOwen Jacobson <owen@grimoire.ca>2024-10-22 10:58:11 -0400
commit3f9648eed48cd8b6cd35d0ae2ee5bbe25fa735ac (patch)
tree8ecdd86cd9e09d8a3bd55ec1f72888a81498cc38 /src/token/repo/token.rs
parent379e97c2cb145bc3a495aa14746273d83b508214 (diff)
Canonicalize login and channel names.
Canonicalization does two things: * It prevents duplicate names that differ only by case or only by normalization/encoding sequence; and * It makes certain name-based comparisons "case-insensitive" (generalizing via Unicode's case-folding rules). This change is complicated, as it means that every name now needs to be stored in two forms. Unfortunately, this is _very likely_ a breaking schema change. The migrations in this commit perform a best-effort attempt to canonicalize existing channel or login names, but it's likely any existing channels or logins with non-ASCII characters will not be canonicalize correctly. Since clients look at all channel names and all login names on boot, and since the code in this commit verifies canonicalization when reading from the database, this will effectively make the server un-usuable until any incorrectly-canonicalized values are either manually canonicalized, or removed It might be possible to do better with [the `icu` sqlite3 extension][icu], but (a) I'm not convinced of that and (b) this commit is already huge; adding database extension support would make it far larger. [icu]: https://sqlite.org/src/dir/ext/icu For some references on why it's worth storing usernames this way, see <https://www.b-list.org/weblog/2018/nov/26/case/> and the refernced talk, as well as <https://www.b-list.org/weblog/2018/feb/11/usernames/>. Bennett's treatment of this issue is, to my eye, much more readable than the referenced Unicode technical reports, and I'm inclined to trust his opinion given that he maintains a widely-used, internet-facing user registration library for Django.
Diffstat (limited to 'src/token/repo/token.rs')
-rw-r--r--src/token/repo/token.rs68
1 files changed, 48 insertions, 20 deletions
diff --git a/src/token/repo/token.rs b/src/token/repo/token.rs
index 960bb72..35ea385 100644
--- a/src/token/repo/token.rs
+++ b/src/token/repo/token.rs
@@ -3,7 +3,10 @@ use uuid::Uuid;
use crate::{
clock::DateTime,
- login::{self, History, Login, Name},
+ db::NotFound,
+ event::{Instant, Sequence},
+ login::{self, History, Login},
+ name::{self, Name},
token::{Id, Secret},
};
@@ -100,53 +103,78 @@ impl<'c> Tokens<'c> {
}
// Validate a token by its secret, retrieving the associated Login record.
- // Will return [None] if the token is not valid. The token's last-used
- // timestamp will be set to `used_at`.
+ // Will return an error if the token is not valid. If successful, the
+ // retrieved token's last-used timestamp will be set to `used_at`.
pub async fn validate(
&mut self,
secret: &Secret,
used_at: &DateTime,
- ) -> Result<(Id, Login), sqlx::Error> {
+ ) -> Result<(Id, History), LoadError> {
// I would use `update … returning` to do this in one query, but
// sqlite3, as of this writing, does not allow an update's `returning`
// clause to reference columns from tables joined into the update. Two
// queries is fine, but it feels untidy.
- sqlx::query!(
+ let (token, login) = sqlx::query!(
r#"
update token
set last_used_at = $1
where secret = $2
+ returning
+ id as "token: Id",
+ login as "login: login::Id"
"#,
used_at,
secret,
)
- .execute(&mut *self.0)
+ .map(|row| (row.token, row.login))
+ .fetch_one(&mut *self.0)
.await?;
let login = sqlx::query!(
r#"
select
- token.id as "token_id: Id",
- login.id as "login_id: login::Id",
- login.name as "login_name: Name"
+ id as "id: login::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 login
- join token on login.id = token.login
- where token.secret = $1
+ where id = $1
"#,
- secret,
+ login,
)
.map(|row| {
- (
- row.token_id,
- Login {
- id: row.login_id,
- name: row.login_name,
+ 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_one(&mut *self.0)
- .await?;
+ .await??;
+
+ Ok((token, login))
+ }
+}
+
+#[derive(Debug, thiserror::Error)]
+#[error(transparent)]
+pub enum LoadError {
+ Database(#[from] sqlx::Error),
+ Name(#[from] name::Error),
+}
+
+impl<T> NotFound for Result<T, LoadError> {
+ type Ok = T;
+ type Error = LoadError;
- Ok(login)
+ fn optional(self) -> Result<Option<T>, LoadError> {
+ match self {
+ Ok(value) => Ok(Some(value)),
+ Err(LoadError::Database(sqlx::Error::RowNotFound)) => Ok(None),
+ Err(other) => Err(other),
+ }
}
}