From feebbd0e97309f6e76b65feff124175d910482c2 Mon Sep 17 00:00:00 2001 From: Quentin Gliech Date: Tue, 6 Dec 2022 17:50:55 +0100 Subject: [PATCH] data-model: simplify users and sessions --- Cargo.lock | 2 + crates/axum-utils/src/session.rs | 13 +- crates/cli/src/commands/manage.rs | 4 +- crates/cli/src/commands/templates.rs | 5 +- crates/data-model/Cargo.toml | 1 + crates/data-model/src/compat.rs | 6 +- crates/data-model/src/oauth2/session.rs | 4 +- crates/data-model/src/traits.rs | 14 - crates/data-model/src/users.rs | 164 ++++------- crates/graphql/src/lib.rs | 6 +- crates/graphql/src/model/browser_sessions.rs | 13 +- crates/graphql/src/model/upstream_oauth.rs | 3 +- crates/graphql/src/model/users.rs | 23 +- .../src/oauth2/authorization/complete.rs | 2 +- crates/handlers/src/upstream_oauth2/link.rs | 2 +- .../handlers/src/views/account/emails/add.rs | 2 +- .../handlers/src/views/account/emails/mod.rs | 32 +-- crates/handlers/src/views/account/password.rs | 4 +- crates/handlers/src/views/register.rs | 2 +- crates/policy/src/lib.rs | 2 +- crates/storage/src/compat.rs | 38 +-- crates/storage/src/lib.rs | 7 - crates/storage/src/oauth2/access_token.rs | 10 +- .../storage/src/oauth2/authorization_grant.rs | 20 +- crates/storage/src/oauth2/consent.rs | 12 +- crates/storage/src/oauth2/mod.rs | 12 +- crates/storage/src/oauth2/refresh_token.rs | 12 +- crates/storage/src/upstream_oauth2/link.rs | 18 +- crates/storage/src/user.rs | 255 +++++++++--------- crates/templates/Cargo.toml | 1 + crates/templates/src/context.rs | 137 +++++----- crates/templates/src/lib.rs | 58 ++-- crates/templates/src/macros.rs | 4 +- templates/pages/account/emails/index.html | 2 +- 34 files changed, 399 insertions(+), 491 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b05e4215..ffaafd89 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2760,6 +2760,7 @@ dependencies = [ "mas-jose", "oauth2-types", "rand 0.8.5", + "rand_chacha 0.3.1", "serde", "thiserror", "ulid", @@ -3128,6 +3129,7 @@ dependencies = [ "mas-data-model", "mas-router", "oauth2-types", + "rand 0.8.5", "serde", "serde_json", "serde_urlencoded", diff --git a/crates/axum-utils/src/session.rs b/crates/axum-utils/src/session.rs index 3e9d4fb8..afd28ddb 100644 --- a/crates/axum-utils/src/session.rs +++ b/crates/axum-utils/src/session.rs @@ -14,10 +14,7 @@ use axum_extra::extract::cookie::{Cookie, PrivateCookieJar}; use mas_data_model::BrowserSession; -use mas_storage::{ - user::{lookup_active_session, ActiveSessionLookupError}, - PostgresqlBackend, -}; +use mas_storage::user::{lookup_active_session, ActiveSessionLookupError}; use serde::{Deserialize, Serialize}; use sqlx::{Executor, Postgres}; use ulid::Ulid; @@ -33,9 +30,9 @@ pub struct SessionInfo { impl SessionInfo { /// Forge the cookie from a [`BrowserSession`] #[must_use] - pub fn from_session(session: &BrowserSession) -> Self { + pub fn from_session(session: &BrowserSession) -> Self { Self { - current: Some(session.data), + current: Some(session.id), } } @@ -50,7 +47,7 @@ impl SessionInfo { pub async fn load_session( &self, executor: impl Executor<'_, Database = Postgres>, - ) -> Result>, ActiveSessionLookupError> { + ) -> Result, ActiveSessionLookupError> { let session_id = if let Some(id) = self.current { id } else { @@ -70,7 +67,7 @@ pub trait SessionInfoExt { fn update_session_info(self, info: &SessionInfo) -> Self; #[must_use] - fn set_session(self, session: &BrowserSession) -> Self + fn set_session(self, session: &BrowserSession) -> Self where Self: Sized, { diff --git a/crates/cli/src/commands/manage.rs b/crates/cli/src/commands/manage.rs index c651b7d4..6d43a112 100644 --- a/crates/cli/src/commands/manage.rs +++ b/crates/cli/src/commands/manage.rs @@ -204,7 +204,7 @@ impl Options { let user = register_user(&mut txn, &mut rng, &clock, hasher, username, password).await?; txn.commit().await?; - info!(user.id = %user.data, %user.username, "User registered"); + info!(%user.id, %user.username, "User registered"); Ok(()) } @@ -217,7 +217,7 @@ impl Options { let user = lookup_user_by_username(&mut txn, username).await?; set_password(&mut txn, &mut rng, &clock, hasher, &user, password).await?; - info!(user.id = %user.data, %user.username, "Password changed"); + info!(%user.id, %user.username, "Password changed"); txn.commit().await?; Ok(()) diff --git a/crates/cli/src/commands/templates.rs b/crates/cli/src/commands/templates.rs index 5effcd45..186a097e 100644 --- a/crates/cli/src/commands/templates.rs +++ b/crates/cli/src/commands/templates.rs @@ -16,6 +16,7 @@ use camino::Utf8PathBuf; use clap::Parser; use mas_storage::Clock; use mas_templates::Templates; +use rand::SeedableRng; #[derive(Parser, Debug)] pub(super) struct Options { @@ -38,9 +39,11 @@ impl Options { match &self.subcommand { SC::Check { path } => { let clock = Clock::default(); + // XXX: we should disallow SeedableRng::from_entropy + let mut rng = rand_chacha::ChaChaRng::from_entropy(); let url_builder = mas_router::UrlBuilder::new("https://example.com/".parse()?); let templates = Templates::load(path.clone(), url_builder).await?; - templates.check_render(clock.now()).await?; + templates.check_render(clock.now(), &mut rng).await?; Ok(()) } diff --git a/crates/data-model/Cargo.toml b/crates/data-model/Cargo.toml index ce10a701..92e10e93 100644 --- a/crates/data-model/Cargo.toml +++ b/crates/data-model/Cargo.toml @@ -13,6 +13,7 @@ url = { version = "2.3.1", features = ["serde"] } crc = "3.0.0" rand = "0.8.5" ulid = "1.0.0" +rand_chacha = "0.3.1" mas-iana = { path = "../iana" } mas-jose = { path = "../jose" } diff --git a/crates/data-model/src/compat.rs b/crates/data-model/src/compat.rs index 42416d68..05136edd 100644 --- a/crates/data-model/src/compat.rs +++ b/crates/data-model/src/compat.rs @@ -86,7 +86,7 @@ impl TryFrom for Device { pub struct CompatSession { #[serde(skip_serializing)] pub data: T::CompatSessionData, - pub user: User, + pub user: User, pub device: Device, pub created_at: DateTime, pub finished_at: Option>, @@ -96,7 +96,7 @@ impl From> for CompatSession<()> { fn from(t: CompatSession) -> Self { Self { data: (), - user: t.user.into(), + user: t.user, device: t.device, created_at: t.created_at, finished_at: t.finished_at, @@ -125,7 +125,7 @@ impl From> for CompatAccessToken<( #[derive(Debug, Clone, PartialEq)] pub struct CompatRefreshToken { - pub data: T::RefreshTokenData, + pub data: T::CompatRefreshTokenData, pub token: String, pub created_at: DateTime, } diff --git a/crates/data-model/src/oauth2/session.rs b/crates/data-model/src/oauth2/session.rs index 5558d87c..988b49af 100644 --- a/crates/data-model/src/oauth2/session.rs +++ b/crates/data-model/src/oauth2/session.rs @@ -26,7 +26,7 @@ use crate::{ pub struct Session { #[serde(skip_serializing)] pub data: T::SessionData, - pub browser_session: BrowserSession, + pub browser_session: BrowserSession, pub client: Client, pub scope: Scope, } @@ -35,7 +35,7 @@ impl From> for Session<()> { fn from(s: Session) -> Self { Session { data: (), - browser_session: s.browser_session.into(), + browser_session: s.browser_session, client: s.client.into(), scope: s.scope, } diff --git a/crates/data-model/src/traits.rs b/crates/data-model/src/traits.rs index 1486f18f..49d1d1fe 100644 --- a/crates/data-model/src/traits.rs +++ b/crates/data-model/src/traits.rs @@ -30,16 +30,9 @@ impl { - pub data: T::UserData, +#[derive(Debug, Clone, PartialEq, Eq, Serialize)] +pub struct User { + pub id: Ulid, pub username: String, pub sub: String, - pub primary_email: Option>, + pub primary_email: Option, } -impl User -where - T::UserData: Default, -{ +impl User { #[must_use] - pub fn samples(_now: chrono::DateTime) -> Vec { + pub fn samples(now: chrono::DateTime, rng: &mut impl Rng) -> Vec { vec![User { - data: Default::default(), + id: Ulid::from_datetime_with_source(now.into(), rng), username: "john".to_owned(), sub: "123-456".to_owned(), primary_email: None, @@ -41,55 +37,22 @@ where } } -impl From> for User<()> { - fn from(u: User) -> Self { - User { - data: (), - username: u.username, - sub: u.sub, - primary_email: u.primary_email.map(Into::into), - } - } -} - -#[derive(Debug, Clone, PartialEq, Serialize)] -#[serde(bound = "T: StorageBackend")] -pub struct Authentication { - #[serde(skip_serializing)] - pub data: T::AuthenticationData, +#[derive(Debug, Clone, PartialEq, Eq, Serialize)] +pub struct Authentication { + pub id: Ulid, pub created_at: DateTime, } -impl From> for Authentication<()> { - fn from(a: Authentication) -> Self { - Authentication { - data: (), - created_at: a.created_at, - } - } -} - -#[derive(Debug, Clone, PartialEq, Serialize)] -#[serde(bound = "T: StorageBackend")] -pub struct BrowserSession { - pub data: T::BrowserSessionData, - pub user: User, +#[derive(Debug, Clone, PartialEq, Eq, Serialize)] +pub struct BrowserSession { + pub id: Ulid, + pub user: User, pub created_at: DateTime, - pub last_authentication: Option>, + pub last_authentication: Option, } -impl From> for BrowserSession<()> { - fn from(s: BrowserSession) -> Self { - BrowserSession { - data: (), - user: s.user.into(), - created_at: s.created_at, - last_authentication: s.last_authentication.map(Into::into), - } - } -} - -impl BrowserSession { +impl BrowserSession { + #[must_use] pub fn was_authenticated_after(&self, after: DateTime) -> bool { if let Some(auth) = &self.last_authentication { auth.created_at > after @@ -99,17 +62,13 @@ impl BrowserSession { } } -impl BrowserSession -where - T::BrowserSessionData: Default, - T::UserData: Default, -{ +impl BrowserSession { #[must_use] - pub fn samples(now: chrono::DateTime) -> Vec { - User::::samples(now) + pub fn samples(now: chrono::DateTime, rng: &mut impl Rng) -> Vec { + User::samples(now, rng) .into_iter() .map(|user| BrowserSession { - data: Default::default(), + id: Ulid::from_datetime_with_source(now.into(), rng), user, created_at: now, last_authentication: None, @@ -118,41 +77,26 @@ where } } -#[derive(Debug, Clone, PartialEq, Serialize)] -#[serde(bound = "T: StorageBackend")] -pub struct UserEmail { - pub data: T::UserEmailData, +#[derive(Debug, Clone, PartialEq, Eq, Serialize)] +pub struct UserEmail { + pub id: Ulid, pub email: String, pub created_at: DateTime, pub confirmed_at: Option>, } -impl From> for UserEmail<()> { - fn from(e: UserEmail) -> Self { - Self { - data: (), - email: e.email, - created_at: e.created_at, - confirmed_at: e.confirmed_at, - } - } -} - -impl UserEmail -where - T::UserEmailData: Default, -{ +impl UserEmail { #[must_use] - pub fn samples(now: chrono::DateTime) -> Vec { + pub fn samples(now: chrono::DateTime, rng: &mut impl Rng) -> Vec { vec![ Self { - data: T::UserEmailData::default(), + id: Ulid::from_datetime_with_source(now.into(), rng), email: "alice@example.com".to_owned(), created_at: now, confirmed_at: Some(now), }, Self { - data: T::UserEmailData::default(), + id: Ulid::from_datetime_with_source(now.into(), rng), email: "bob@example.com".to_owned(), created_at: now, confirmed_at: None, @@ -168,34 +112,18 @@ pub enum UserEmailVerificationState { Valid, } -#[derive(Debug, Clone, PartialEq, Serialize)] -#[serde(bound = "T: StorageBackend")] -pub struct UserEmailVerification { - pub data: T::UserEmailVerificationData, - pub email: UserEmail, +#[derive(Debug, Clone, PartialEq, Eq, Serialize)] +pub struct UserEmailVerification { + pub id: Ulid, + pub email: UserEmail, pub code: String, pub created_at: DateTime, pub state: UserEmailVerificationState, } -impl From> for UserEmailVerification<()> { - fn from(v: UserEmailVerification) -> Self { - Self { - data: (), - email: v.email.into(), - code: v.code, - created_at: v.created_at, - state: v.state, - } - } -} - -impl UserEmailVerification -where - T::UserEmailData: Default + Clone, -{ +impl UserEmailVerification { #[must_use] - pub fn samples(now: chrono::DateTime) -> Vec { + pub fn samples(now: chrono::DateTime, rng: &mut impl Rng) -> Vec { let states = [ UserEmailVerificationState::AlreadyUsed { when: now - Duration::minutes(5), @@ -208,14 +136,18 @@ where states .into_iter() - .flat_map(|state| { - UserEmail::samples(now).into_iter().map(move |email| Self { - data: Default::default(), - code: "123456".to_owned(), - email, - created_at: now - Duration::minutes(10), - state: state.clone(), - }) + .flat_map(move |state| { + let mut rng = + rand_chacha::ChaChaRng::from_rng(&mut *rng).expect("could not seed rng"); + UserEmail::samples(now, &mut rng) + .into_iter() + .map(move |email| Self { + id: Ulid::from_datetime_with_source(now.into(), &mut rng), + code: "123456".to_owned(), + email, + created_at: now - Duration::minutes(10), + state: state.clone(), + }) }) .collect() } diff --git a/crates/graphql/src/lib.rs b/crates/graphql/src/lib.rs index 4f1c77a0..0d5af1ba 100644 --- a/crates/graphql/src/lib.rs +++ b/crates/graphql/src/lib.rs @@ -114,7 +114,7 @@ impl RootQuery { let Some(session) = session else { return Ok(None) }; let current_user = session.user; - if current_user.data == id { + if current_user.id == id { Ok(Some(User(current_user))) } else { Ok(None) @@ -141,7 +141,7 @@ impl RootQuery { .to_option()?; let ret = browser_session.and_then(|browser_session| { - if browser_session.user.data == current_user.data { + if browser_session.user.id == current_user.id { Some(BrowserSession(browser_session)) } else { None @@ -193,7 +193,7 @@ impl RootQuery { .to_option()?; // Ensure that the link belongs to the current user - let link = link.filter(|link| link.user_id == Some(current_user.data)); + let link = link.filter(|link| link.user_id == Some(current_user.id)); Ok(link.map(UpstreamOAuth2Link::new)) } diff --git a/crates/graphql/src/model/browser_sessions.rs b/crates/graphql/src/model/browser_sessions.rs index a9d97ce1..2602433a 100644 --- a/crates/graphql/src/model/browser_sessions.rs +++ b/crates/graphql/src/model/browser_sessions.rs @@ -14,16 +14,15 @@ use async_graphql::{Description, Object, ID}; use chrono::{DateTime, Utc}; -use mas_storage::PostgresqlBackend; use super::{NodeType, User}; /// A browser session represents a logged in user in a browser. #[derive(Description)] -pub struct BrowserSession(pub mas_data_model::BrowserSession); +pub struct BrowserSession(pub mas_data_model::BrowserSession); -impl From> for BrowserSession { - fn from(v: mas_data_model::BrowserSession) -> Self { +impl From for BrowserSession { + fn from(v: mas_data_model::BrowserSession) -> Self { Self(v) } } @@ -32,7 +31,7 @@ impl From> for BrowserSession impl BrowserSession { /// ID of the object. pub async fn id(&self) -> ID { - NodeType::BrowserSession.id(self.0.data) + NodeType::BrowserSession.id(self.0.id) } /// The user logged in this session. @@ -54,13 +53,13 @@ impl BrowserSession { /// An authentication records when a user enter their credential in a browser /// session. #[derive(Description)] -pub struct Authentication(pub mas_data_model::Authentication); +pub struct Authentication(pub mas_data_model::Authentication); #[Object(use_type_description)] impl Authentication { /// ID of the object. pub async fn id(&self) -> ID { - NodeType::Authentication.id(self.0.data) + NodeType::Authentication.id(self.0.id) } /// When the object was created. diff --git a/crates/graphql/src/model/upstream_oauth.rs b/crates/graphql/src/model/upstream_oauth.rs index 3060d010..41790161 100644 --- a/crates/graphql/src/model/upstream_oauth.rs +++ b/crates/graphql/src/model/upstream_oauth.rs @@ -14,7 +14,6 @@ use async_graphql::{Context, Object, ID}; use chrono::{DateTime, Utc}; -use mas_storage::PostgresqlBackend; use sqlx::PgPool; use super::{NodeType, User}; @@ -69,7 +68,7 @@ impl UpstreamOAuth2Link { pub struct UpstreamOAuth2Link { link: mas_data_model::UpstreamOAuthLink, provider: Option, - user: Option>, + user: Option, } #[Object] diff --git a/crates/graphql/src/model/users.rs b/crates/graphql/src/model/users.rs index 1631f217..6a15b0da 100644 --- a/crates/graphql/src/model/users.rs +++ b/crates/graphql/src/model/users.rs @@ -17,7 +17,6 @@ use async_graphql::{ Context, Description, Object, ID, }; use chrono::{DateTime, Utc}; -use mas_storage::PostgresqlBackend; use sqlx::PgPool; use super::{ @@ -27,16 +26,16 @@ use super::{ #[derive(Description)] /// A user is an individual's account. -pub struct User(pub mas_data_model::User); +pub struct User(pub mas_data_model::User); -impl From> for User { - fn from(v: mas_data_model::User) -> Self { +impl From for User { + fn from(v: mas_data_model::User) -> Self { Self(v) } } -impl From> for User { - fn from(v: mas_data_model::BrowserSession) -> Self { +impl From for User { + fn from(v: mas_data_model::BrowserSession) -> Self { Self(v.user) } } @@ -45,7 +44,7 @@ impl From> for User { impl User { /// ID of the object. pub async fn id(&self) -> ID { - NodeType::User.id(self.0.data) + NodeType::User.id(self.0.id) } /// Username chosen by the user. @@ -143,7 +142,7 @@ impl User { let mut connection = Connection::new(has_previous_page, has_next_page); connection.edges.extend(edges.into_iter().map(|u| { Edge::new( - OpaqueCursor(NodeCursor(NodeType::BrowserSession, u.data)), + OpaqueCursor(NodeCursor(NodeType::BrowserSession, u.id)), BrowserSession(u), ) })); @@ -195,7 +194,7 @@ impl User { ); connection.edges.extend(edges.into_iter().map(|u| { Edge::new( - OpaqueCursor(NodeCursor(NodeType::UserEmail, u.data)), + OpaqueCursor(NodeCursor(NodeType::UserEmail, u.id)), UserEmail(u), ) })); @@ -309,13 +308,13 @@ impl User { /// A user email address #[derive(Description)] -pub struct UserEmail(pub mas_data_model::UserEmail); +pub struct UserEmail(pub mas_data_model::UserEmail); #[Object(use_type_description)] impl UserEmail { /// ID of the object. pub async fn id(&self) -> ID { - NodeType::UserEmail.id(self.0.data) + NodeType::UserEmail.id(self.0.id) } /// Email address @@ -335,7 +334,7 @@ impl UserEmail { } } -pub struct UserEmailsPagination(mas_data_model::User); +pub struct UserEmailsPagination(mas_data_model::User); #[Object] impl UserEmailsPagination { diff --git a/crates/handlers/src/oauth2/authorization/complete.rs b/crates/handlers/src/oauth2/authorization/complete.rs index a592dc40..b1410c0b 100644 --- a/crates/handlers/src/oauth2/authorization/complete.rs +++ b/crates/handlers/src/oauth2/authorization/complete.rs @@ -186,7 +186,7 @@ impl From for GrantCompletionError { pub(crate) async fn complete( grant: AuthorizationGrant, - browser_session: BrowserSession, + browser_session: BrowserSession, policy_factory: &PolicyFactory, mut txn: Transaction<'_, Postgres>, ) -> Result>, GrantCompletionError> { diff --git a/crates/handlers/src/upstream_oauth2/link.rs b/crates/handlers/src/upstream_oauth2/link.rs index 717b6340..329ea6e2 100644 --- a/crates/handlers/src/upstream_oauth2/link.rs +++ b/crates/handlers/src/upstream_oauth2/link.rs @@ -138,7 +138,7 @@ pub(crate) async fn get( let maybe_user_session = user_session_info.load_session(&mut txn).await?; let render = match (maybe_user_session, link.user_id) { - (Some(mut session), Some(user_id)) if session.user.data == user_id => { + (Some(mut session), Some(user_id)) if session.user.id == user_id => { // Session already linked, and link matches the currently logged // user. Mark the session as consumed and renew the authentication. consume_session(&mut txn, &clock, upstream_session).await?; diff --git a/crates/handlers/src/views/account/emails/add.rs b/crates/handlers/src/views/account/emails/add.rs index a3b46846..7dc4aae1 100644 --- a/crates/handlers/src/views/account/emails/add.rs +++ b/crates/handlers/src/views/account/emails/add.rs @@ -89,7 +89,7 @@ pub(crate) async fn post( }; let user_email = add_user_email(&mut txn, &mut rng, &clock, &session.user, form.email).await?; - let next = mas_router::AccountVerifyEmail::new(user_email.data); + let next = mas_router::AccountVerifyEmail::new(user_email.id); let next = if let Some(action) = query.post_auth_action { next.and_then(action) } else { diff --git a/crates/handlers/src/views/account/emails/mod.rs b/crates/handlers/src/views/account/emails/mod.rs index 57741100..880da99b 100644 --- a/crates/handlers/src/views/account/emails/mod.rs +++ b/crates/handlers/src/views/account/emails/mod.rs @@ -32,7 +32,7 @@ use mas_storage::{ add_user_email, add_user_email_verification_code, get_user_email, get_user_emails, remove_user_email, set_user_email_as_primary, }, - Clock, PostgresqlBackend, + Clock, }; use mas_templates::{AccountEmailsContext, EmailVerificationContext, TemplateContext, Templates}; use rand::{distributions::Uniform, Rng}; @@ -47,9 +47,9 @@ pub mod verify; #[serde(tag = "action", rename_all = "snake_case")] pub enum ManagementForm { Add { email: String }, - ResendConfirmation { data: String }, - SetPrimary { data: String }, - Remove { data: String }, + ResendConfirmation { id: String }, + SetPrimary { id: String }, + Remove { id: String }, } pub(crate) async fn get( @@ -77,7 +77,7 @@ async fn render( rng: impl Rng + Send, clock: &Clock, templates: Templates, - session: BrowserSession, + session: BrowserSession, cookie_jar: PrivateCookieJar, executor: impl PgExecutor<'_>, ) -> Result { @@ -99,8 +99,8 @@ async fn start_email_verification( executor: impl PgExecutor<'_>, mut rng: impl Rng + Send, clock: &Clock, - user: &User, - user_email: UserEmail, + user: &User, + user_email: UserEmail, ) -> anyhow::Result<()> { // First, generate a code let range = Uniform::::from(0..1_000_000); @@ -126,7 +126,7 @@ async fn start_email_verification( mailer.send_verification_email(mailbox, &context).await?; info!( - email.id = %verification.email.data, + email.id = %verification.email.id, "Verification email sent" ); Ok(()) @@ -159,7 +159,7 @@ pub(crate) async fn post( ManagementForm::Add { email } => { let user_email = add_user_email(&mut txn, &mut rng, &clock, &session.user, email).await?; - let next = mas_router::AccountVerifyEmail::new(user_email.data); + let next = mas_router::AccountVerifyEmail::new(user_email.id); start_email_verification( &mailer, &mut txn, @@ -172,11 +172,11 @@ pub(crate) async fn post( txn.commit().await?; return Ok((cookie_jar, next.go()).into_response()); } - ManagementForm::ResendConfirmation { data } => { - let id = data.parse()?; + ManagementForm::ResendConfirmation { id } => { + let id = id.parse()?; let user_email = get_user_email(&mut txn, &session.user, id).await?; - let next = mas_router::AccountVerifyEmail::new(user_email.data); + let next = mas_router::AccountVerifyEmail::new(user_email.id); start_email_verification( &mailer, &mut txn, @@ -189,14 +189,14 @@ pub(crate) async fn post( txn.commit().await?; return Ok((cookie_jar, next.go()).into_response()); } - ManagementForm::Remove { data } => { - let id = data.parse()?; + ManagementForm::Remove { id } => { + let id = id.parse()?; let email = get_user_email(&mut txn, &session.user, id).await?; remove_user_email(&mut txn, email).await?; } - ManagementForm::SetPrimary { data } => { - let id = data.parse()?; + ManagementForm::SetPrimary { id } => { + let id = id.parse()?; let email = get_user_email(&mut txn, &session.user, id).await?; set_user_email_as_primary(&mut txn, &email).await?; session.user.primary_email = Some(email); diff --git a/crates/handlers/src/views/account/password.rs b/crates/handlers/src/views/account/password.rs index 661c3bf6..7f650660 100644 --- a/crates/handlers/src/views/account/password.rs +++ b/crates/handlers/src/views/account/password.rs @@ -27,7 +27,7 @@ use mas_keystore::Encrypter; use mas_router::Route; use mas_storage::{ user::{authenticate_session, set_password}, - Clock, PostgresqlBackend, + Clock, }; use mas_templates::{EmptyContext, TemplateContext, Templates}; use rand::Rng; @@ -65,7 +65,7 @@ async fn render( rng: impl Rng + Send, clock: &Clock, templates: Templates, - session: BrowserSession, + session: BrowserSession, cookie_jar: PrivateCookieJar, ) -> Result { let (csrf_token, cookie_jar) = cookie_jar.csrf_token(clock.now(), rng); diff --git a/crates/handlers/src/views/register.rs b/crates/handlers/src/views/register.rs index 28e1306e..458c2c87 100644 --- a/crates/handlers/src/views/register.rs +++ b/crates/handlers/src/views/register.rs @@ -219,7 +219,7 @@ pub(crate) async fn post( mailer.send_verification_email(mailbox, &context).await?; - let next = mas_router::AccountVerifyEmail::new(verification.email.data) + let next = mas_router::AccountVerifyEmail::new(verification.email.id) .and_maybe(query.post_auth_action); let session = start_session(&mut txn, &mut rng, &clock, user).await?; diff --git a/crates/policy/src/lib.rs b/crates/policy/src/lib.rs index 2569ad83..033b33bb 100644 --- a/crates/policy/src/lib.rs +++ b/crates/policy/src/lib.rs @@ -213,7 +213,7 @@ impl Policy { pub async fn evaluate_authorization_grant( &mut self, authorization_grant: &AuthorizationGrant, - user: &User, + user: &User, ) -> Result { let authorization_grant = serde_json::to_value(authorization_grant)?; let user = serde_json::to_value(user)?; diff --git a/crates/storage/src/compat.rs b/crates/storage/src/compat.rs index d2cf2a1d..0aa13262 100644 --- a/crates/storage/src/compat.rs +++ b/crates/storage/src/compat.rs @@ -136,7 +136,7 @@ pub async fn lookup_active_compat_access_token( res.user_email_confirmed_at, ) { (Some(id), Some(email), Some(created_at), confirmed_at) => Some(UserEmail { - data: id.into(), + id: id.into(), email, created_at, confirmed_at, @@ -147,7 +147,7 @@ pub async fn lookup_active_compat_access_token( let id = Ulid::from(res.user_id); let user = User { - data: id, + id, username: res.user_username, sub: id.to_string(), primary_email, @@ -274,7 +274,7 @@ pub async fn lookup_active_compat_refresh_token( res.user_email_confirmed_at, ) { (Some(id), Some(email), Some(created_at), confirmed_at) => Some(UserEmail { - data: id.into(), + id: id.into(), email, created_at, confirmed_at, @@ -285,7 +285,7 @@ pub async fn lookup_active_compat_refresh_token( let id = Ulid::from(res.user_id); let user = User { - data: id, + id, username: res.user_username, sub: id.to_string(), primary_email, @@ -326,7 +326,7 @@ pub async fn compat_login( // First, lookup the user let user = lookup_user_by_username(&mut txn, username).await?; - tracing::Span::current().record("user.id", tracing::field::display(user.data)); + tracing::Span::current().record("user.id", tracing::field::display(user.id)); // Now, fetch the hashed password from the user associated with that session let hashed_password: String = sqlx::query_scalar!( @@ -337,7 +337,7 @@ pub async fn compat_login( ORDER BY up.created_at DESC LIMIT 1 "#, - Uuid::from(user.data), + Uuid::from(user.id), ) .fetch_one(&mut txn) .instrument(tracing::info_span!("Lookup hashed password")) @@ -365,7 +365,7 @@ pub async fn compat_login( VALUES ($1, $2, $3, $4) "#, Uuid::from(id), - Uuid::from(user.data), + Uuid::from(user.id), device.as_str(), created_at, ) @@ -392,7 +392,7 @@ pub async fn compat_login( compat_session.id = %session.data, compat_session.device.id = session.device.as_str(), compat_access_token.id, - user.id = %session.user.data, + user.id = %session.user.id, ), err(Display), )] @@ -477,7 +477,7 @@ pub async fn expire_compat_access_token( compat_session.device.id = session.device.as_str(), compat_access_token.id = %access_token.data, compat_refresh_token.id, - user.id = %session.user.data, + user.id = %session.user.id, ), err(Display), )] @@ -668,7 +668,7 @@ impl TryFrom for CompatSsoLogin { res.user_email_confirmed_at, ) { (Some(id), Some(email), Some(created_at), confirmed_at) => Some(UserEmail { - data: id.into(), + id: id.into(), email, created_at, confirmed_at, @@ -681,7 +681,7 @@ impl TryFrom for CompatSsoLogin { (Some(id), Some(username), primary_email) => { let id = Ulid::from(id); Some(User { - data: id, + id, username, sub: id.to_string(), primary_email, @@ -808,14 +808,14 @@ pub async fn get_compat_sso_login_by_id( #[tracing::instrument( skip_all, fields( - user.id = %user.data, - user.username = user.username, + %user.id, + %user.username, ), err(Display), )] pub async fn get_paginated_user_compat_sso_logins( executor: impl PgExecutor<'_>, - user: &User, + user: &User, before: Option, after: Option, first: Option, @@ -854,7 +854,7 @@ pub async fn get_paginated_user_compat_sso_logins( query .push(" WHERE cs.user_id = ") - .push_bind(Uuid::from(user.data)) + .push_bind(Uuid::from(user.id)) .generate_pagination("cl.compat_sso_login_id", before, after, first, last)?; let span = info_span!( @@ -919,7 +919,7 @@ pub async fn get_compat_sso_login_by_token( #[tracing::instrument( skip_all, fields( - user.id = %user.data, + %user.id, compat_sso_login.id = %login.data, compat_sso_login.redirect_uri = %login.redirect_uri, compat_session.id, @@ -931,7 +931,7 @@ pub async fn fullfill_compat_sso_login( conn: impl Acquire<'_, Database = Postgres> + Send, mut rng: impl Rng + Send, clock: &Clock, - user: User, + user: User, mut login: CompatSsoLogin, device: Device, ) -> Result, anyhow::Error> { @@ -943,7 +943,7 @@ pub async fn fullfill_compat_sso_login( let created_at = clock.now(); let id = Ulid::from_datetime_with_source(created_at.into(), &mut rng); - tracing::Span::current().record("user.id", tracing::field::display(user.data)); + tracing::Span::current().record("compat_session.id", tracing::field::display(id)); sqlx::query!( r#" @@ -951,7 +951,7 @@ pub async fn fullfill_compat_sso_login( VALUES ($1, $2, $3, $4) "#, Uuid::from(id), - Uuid::from(user.data), + Uuid::from(user.id), device.as_str(), created_at, ) diff --git a/crates/storage/src/lib.rs b/crates/storage/src/lib.rs index b47ea744..9cc4057a 100644 --- a/crates/storage/src/lib.rs +++ b/crates/storage/src/lib.rs @@ -105,20 +105,13 @@ pub struct DatabaseInconsistencyError; pub struct PostgresqlBackend; impl StorageBackend for PostgresqlBackend { - type AccessTokenData = Ulid; - type AuthenticationData = Ulid; type AuthorizationGrantData = Ulid; - type BrowserSessionData = Ulid; type ClientData = Ulid; type CompatAccessTokenData = Ulid; type CompatRefreshTokenData = Ulid; type CompatSessionData = Ulid; type CompatSsoLoginData = Ulid; - type RefreshTokenData = Ulid; type SessionData = Ulid; - type UserData = Ulid; - type UserEmailData = Ulid; - type UserEmailVerificationData = Ulid; } impl StorageBackendMarker for PostgresqlBackend {} diff --git a/crates/storage/src/oauth2/access_token.rs b/crates/storage/src/oauth2/access_token.rs index 3cdb0732..21a0f61f 100644 --- a/crates/storage/src/oauth2/access_token.rs +++ b/crates/storage/src/oauth2/access_token.rs @@ -29,7 +29,7 @@ use crate::{Clock, DatabaseInconsistencyError, LookupError, PostgresqlBackend}; fields( session.id = %session.data, client.id = %session.client.data, - user.id = %session.browser_session.user.data, + user.id = %session.browser_session.user.id, access_token.id, ), err(Debug), @@ -178,7 +178,7 @@ pub async fn lookup_active_access_token( res.user_email_confirmed_at, ) { (Some(id), Some(email), Some(created_at), confirmed_at) => Some(UserEmail { - data: id.into(), + id: id.into(), email, created_at, confirmed_at, @@ -189,7 +189,7 @@ pub async fn lookup_active_access_token( let id = Ulid::from(res.user_id); let user = User { - data: id, + id, username: res.user_username, sub: id.to_string(), primary_email, @@ -201,14 +201,14 @@ pub async fn lookup_active_access_token( ) { (None, None) => None, (Some(id), Some(created_at)) => Some(Authentication { - data: id.into(), + id: id.into(), created_at, }), _ => return Err(DatabaseInconsistencyError.into()), }; let browser_session = BrowserSession { - data: res.user_session_id.into(), + id: res.user_session_id.into(), created_at: res.user_session_created_at, user, last_authentication, diff --git a/crates/storage/src/oauth2/authorization_grant.rs b/crates/storage/src/oauth2/authorization_grant.rs index f8a7041b..f378a8cc 100644 --- a/crates/storage/src/oauth2/authorization_grant.rs +++ b/crates/storage/src/oauth2/authorization_grant.rs @@ -187,7 +187,7 @@ impl GrantLookup { self.user_session_last_authentication_created_at, ) { (Some(id), Some(created_at)) => Some(Authentication { - data: id.into(), + id: id.into(), created_at, }), (None, None) => None, @@ -201,7 +201,7 @@ impl GrantLookup { self.user_email_confirmed_at, ) { (Some(id), Some(email), Some(created_at), confirmed_at) => Some(UserEmail { - data: id.into(), + id: id.into(), email, created_at, confirmed_at, @@ -230,14 +230,14 @@ impl GrantLookup { ) => { let user_id = Ulid::from(user_id); let user = User { - data: user_id, + id: user_id, username: user_username, sub: user_id.to_string(), primary_email, }; let browser_session = BrowserSession { - data: user_session_id.into(), + id: user_session_id.into(), user, created_at: user_session_created_at, last_authentication, @@ -500,8 +500,8 @@ pub async fn lookup_grant_by_code( grant.id = %grant.data, client.id = %grant.client.data, session.id, - user_session.id = %browser_session.data, - user.id = %browser_session.user.data, + user_session.id = %browser_session.id, + user.id = %browser_session.user.id, ), err(Debug), )] @@ -510,7 +510,7 @@ pub async fn derive_session( mut rng: impl Rng + Send, clock: &Clock, grant: &AuthorizationGrant, - browser_session: BrowserSession, + browser_session: BrowserSession, ) -> Result, anyhow::Error> { let created_at = clock.now(); let id = Ulid::from_datetime_with_source(created_at.into(), &mut rng); @@ -532,7 +532,7 @@ pub async fn derive_session( og.oauth2_authorization_grant_id = $4 "#, Uuid::from(id), - Uuid::from(browser_session.data), + Uuid::from(browser_session.id), created_at, Uuid::from(grant.data), ) @@ -554,8 +554,8 @@ pub async fn derive_session( grant.id = %grant.data, client.id = %grant.client.data, session.id = %session.data, - user_session.id = %session.browser_session.data, - user.id = %session.browser_session.user.data, + user_session.id = %session.browser_session.id, + user.id = %session.browser_session.user.id, ), err(Debug), )] diff --git a/crates/storage/src/oauth2/consent.rs b/crates/storage/src/oauth2/consent.rs index b19100db..c3883537 100644 --- a/crates/storage/src/oauth2/consent.rs +++ b/crates/storage/src/oauth2/consent.rs @@ -26,14 +26,14 @@ use crate::{Clock, PostgresqlBackend}; #[tracing::instrument( skip_all, fields( - user.id = %user.data, + %user.id, client.id = %client.data, ), err(Debug), )] pub async fn fetch_client_consent( executor: impl PgExecutor<'_>, - user: &User, + user: &User, client: &Client, ) -> Result { let scope_tokens: Vec = sqlx::query_scalar!( @@ -42,7 +42,7 @@ pub async fn fetch_client_consent( FROM oauth2_consents WHERE user_id = $1 AND oauth2_client_id = $2 "#, - Uuid::from(user.data), + Uuid::from(user.id), Uuid::from(client.data), ) .fetch_all(executor) @@ -59,7 +59,7 @@ pub async fn fetch_client_consent( #[tracing::instrument( skip_all, fields( - user.id = %user.data, + %user.id, client.id = %client.data, scope = scope.to_string(), ), @@ -69,7 +69,7 @@ pub async fn insert_client_consent( executor: impl PgExecutor<'_>, mut rng: impl Rng + Send, clock: &Clock, - user: &User, + user: &User, client: &Client, scope: &Scope, ) -> Result<(), anyhow::Error> { @@ -92,7 +92,7 @@ pub async fn insert_client_consent( ON CONFLICT (user_id, oauth2_client_id, scope_token) DO UPDATE SET refreshed_at = $5 "#, &ids, - Uuid::from(user.data), + Uuid::from(user.id), Uuid::from(client.data), &tokens, now, diff --git a/crates/storage/src/oauth2/mod.rs b/crates/storage/src/oauth2/mod.rs index b465893c..0f8e1485 100644 --- a/crates/storage/src/oauth2/mod.rs +++ b/crates/storage/src/oauth2/mod.rs @@ -38,8 +38,8 @@ pub mod refresh_token; skip_all, fields( session.id = %session.data, - user.id = %session.browser_session.user.data, - user_session.id = %session.browser_session.data, + user.id = %session.browser_session.user.id, + user_session.id = %session.browser_session.id, client.id = %session.client.data, ), err(Debug), @@ -78,14 +78,14 @@ struct OAuthSessionLookup { #[tracing::instrument( skip_all, fields( - user.id = %user.data, + %user.id, user.username = user.username, ), err(Display), )] pub async fn get_paginated_user_oauth_sessions( conn: &mut PgConnection, - user: &User, + user: &User, before: Option, after: Option, first: Option, @@ -108,7 +108,7 @@ pub async fn get_paginated_user_oauth_sessions( query .push(" WHERE us.user_id = ") - .push_bind(Uuid::from(user.data)) + .push_bind(Uuid::from(user.id)) .generate_pagination("oauth2_session_id", before, after, first, last)?; let span = info_span!( @@ -135,7 +135,7 @@ pub async fn get_paginated_user_oauth_sessions( // TODO: this can generate N queries instead of batching. This is less than // ideal - let mut browser_sessions: HashMap> = HashMap::new(); + let mut browser_sessions: HashMap = HashMap::new(); for id in browser_session_ids { let v = lookup_active_session(&mut *conn, id).await?; browser_sessions.insert(id, v); diff --git a/crates/storage/src/oauth2/refresh_token.rs b/crates/storage/src/oauth2/refresh_token.rs index a91285ca..65b7f168 100644 --- a/crates/storage/src/oauth2/refresh_token.rs +++ b/crates/storage/src/oauth2/refresh_token.rs @@ -30,8 +30,8 @@ use crate::{Clock, DatabaseInconsistencyError, LookupError, PostgresqlBackend}; skip_all, fields( session.id = %session.data, - user.id = %session.browser_session.user.data, - user_session.id = %session.browser_session.data, + user.id = %session.browser_session.user.id, + user_session.id = %session.browser_session.id, client.id = %session.client.data, refresh_token.id, ), @@ -206,7 +206,7 @@ pub async fn lookup_active_refresh_token( res.user_email_confirmed_at, ) { (Some(id), Some(email), Some(created_at), confirmed_at) => Some(UserEmail { - data: id.into(), + id: id.into(), email, created_at, confirmed_at, @@ -217,7 +217,7 @@ pub async fn lookup_active_refresh_token( let id = Ulid::from(res.user_id); let user = User { - data: id, + id, username: res.user_username, sub: id.to_string(), primary_email, @@ -229,14 +229,14 @@ pub async fn lookup_active_refresh_token( ) { (None, None) => None, (Some(id), Some(created_at)) => Some(Authentication { - data: id.into(), + id: id.into(), created_at, }), _ => return Err(DatabaseInconsistencyError.into()), }; let browser_session = BrowserSession { - data: res.user_session_id.into(), + id: res.user_session_id.into(), created_at: res.user_session_created_at, user, last_authentication, diff --git a/crates/storage/src/upstream_oauth2/link.rs b/crates/storage/src/upstream_oauth2/link.rs index 49bc4fd1..69c0daff 100644 --- a/crates/storage/src/upstream_oauth2/link.rs +++ b/crates/storage/src/upstream_oauth2/link.rs @@ -22,7 +22,7 @@ use uuid::Uuid; use crate::{ pagination::{process_page, QueryBuilderExt}, - Clock, GenericLookupError, PostgresqlBackend, + Clock, GenericLookupError, }; #[derive(sqlx::FromRow)] @@ -168,7 +168,7 @@ pub async fn add_link( fields( %upstream_oauth_link.id, %upstream_oauth_link.subject, - user.id = %user.data, + %user.id, %user.username, ), err, @@ -176,7 +176,7 @@ pub async fn add_link( pub async fn associate_link_to_user( executor: impl PgExecutor<'_>, upstream_oauth_link: &UpstreamOAuthLink, - user: &User, + user: &User, ) -> Result<(), sqlx::Error> { sqlx::query!( r#" @@ -184,7 +184,7 @@ pub async fn associate_link_to_user( SET user_id = $1 WHERE upstream_oauth_link_id = $2 "#, - Uuid::from(user.data), + Uuid::from(user.id), Uuid::from(upstream_oauth_link.id), ) .execute(executor) @@ -193,10 +193,14 @@ pub async fn associate_link_to_user( Ok(()) } -#[tracing::instrument(skip_all, err(Display))] +#[tracing::instrument( + skip_all, + fields(%user.id, %user.username), + err(Display) +)] pub async fn get_paginated_user_links( executor: impl PgExecutor<'_>, - user: &User, + user: &User, before: Option, after: Option, first: Option, @@ -216,7 +220,7 @@ pub async fn get_paginated_user_links( query .push(" WHERE user_id = ") - .push_bind(Uuid::from(user.data)) + .push_bind(Uuid::from(user.id)) .generate_pagination("upstream_oauth_link_id", before, after, first, last)?; let span = info_span!( diff --git a/crates/storage/src/user.rs b/crates/storage/src/user.rs index d2514203..26af7cb5 100644 --- a/crates/storage/src/user.rs +++ b/crates/storage/src/user.rs @@ -30,7 +30,7 @@ use tracing::{info_span, Instrument}; use ulid::Ulid; use uuid::Uuid; -use super::{DatabaseInconsistencyError, PostgresqlBackend}; +use super::DatabaseInconsistencyError; use crate::{ pagination::{process_page, QueryBuilderExt}, Clock, GenericLookupError, LookupError, @@ -77,7 +77,7 @@ pub async fn login( clock: &Clock, username: &str, password: &str, -) -> Result, LoginError> { +) -> Result { let mut txn = conn.begin().await.context("could not start transaction")?; let user = lookup_user_by_username(&mut txn, username) .await @@ -137,10 +137,10 @@ struct SessionLookup { user_email_confirmed_at: Option>, } -impl TryInto> for SessionLookup { +impl TryInto for SessionLookup { type Error = DatabaseInconsistencyError; - fn try_into(self) -> Result, Self::Error> { + fn try_into(self) -> Result { let primary_email = match ( self.user_email_id, self.user_email, @@ -148,7 +148,7 @@ impl TryInto> for SessionLookup { self.user_email_confirmed_at, ) { (Some(id), Some(email), Some(created_at), confirmed_at) => Some(UserEmail { - data: id.into(), + id: id.into(), email, created_at, confirmed_at, @@ -159,7 +159,7 @@ impl TryInto> for SessionLookup { let id = Ulid::from(self.user_id); let user = User { - data: id, + id, username: self.username, sub: id.to_string(), primary_email, @@ -167,7 +167,7 @@ impl TryInto> for SessionLookup { let last_authentication = match (self.last_authentication_id, self.last_authd_at) { (Some(id), Some(created_at)) => Some(Authentication { - data: id.into(), + id: id.into(), created_at, }), (None, None) => None, @@ -175,7 +175,7 @@ impl TryInto> for SessionLookup { }; Ok(BrowserSession { - data: self.user_session_id.into(), + id: self.user_session_id.into(), user, created_at: self.created_at, last_authentication, @@ -191,7 +191,7 @@ impl TryInto> for SessionLookup { pub async fn lookup_active_session( executor: impl PgExecutor<'_>, id: Ulid, -) -> Result, ActiveSessionLookupError> { +) -> Result { let res = sqlx::query_as!( SessionLookup, r#" @@ -229,19 +229,19 @@ pub async fn lookup_active_session( #[tracing::instrument( skip_all, fields( - user.id = %user.data, - user.username = user.username, + %user.id, + %user.username, ), err(Display), )] pub async fn get_paginated_user_sessions( executor: impl PgExecutor<'_>, - user: &User, + user: &User, before: Option, after: Option, first: Option, last: Option, -) -> Result<(bool, bool, Vec>), anyhow::Error> { +) -> Result<(bool, bool, Vec), anyhow::Error> { let mut query = QueryBuilder::new( r#" SELECT @@ -267,7 +267,7 @@ pub async fn get_paginated_user_sessions( query .push(" WHERE s.finished_at IS NULL AND s.user_id = ") - .push_bind(Uuid::from(user.data)) + .push_bind(Uuid::from(user.id)) .generate_pagination("s.user_session_id", before, after, first, last)?; let span = info_span!("Fetch paginated user emails", db.statement = query.sql()); @@ -286,7 +286,7 @@ pub async fn get_paginated_user_sessions( #[tracing::instrument( skip_all, fields( - user.id = %user.data, + %user.id, user_session.id, ), err(Display), @@ -295,8 +295,8 @@ pub async fn start_session( executor: impl PgExecutor<'_>, mut rng: impl Rng + Send, clock: &Clock, - user: User, -) -> Result, anyhow::Error> { + user: User, +) -> Result { let created_at = clock.now(); let id = Ulid::from_datetime_with_source(created_at.into(), &mut rng); tracing::Span::current().record("user_session.id", tracing::field::display(id)); @@ -307,7 +307,7 @@ pub async fn start_session( VALUES ($1, $2, $3) "#, Uuid::from(id), - Uuid::from(user.data), + Uuid::from(user.id), created_at, ) .execute(executor) @@ -315,7 +315,7 @@ pub async fn start_session( .context("could not create session")?; let session = BrowserSession { - data: id, + id, user, created_at, last_authentication: None, @@ -326,12 +326,12 @@ pub async fn start_session( #[tracing::instrument( skip_all, - fields(user.id = %user.data), + fields(%user.id), err(Display), )] pub async fn count_active_sessions( executor: impl PgExecutor<'_>, - user: &User, + user: &User, ) -> Result { let res = sqlx::query_scalar!( r#" @@ -339,7 +339,7 @@ pub async fn count_active_sessions( FROM user_sessions s WHERE s.user_id = $1 AND s.finished_at IS NULL "#, - Uuid::from(user.data), + Uuid::from(user.id), ) .fetch_one(executor) .await? @@ -366,8 +366,8 @@ pub enum AuthenticationError { #[tracing::instrument( skip_all, fields( - user.id = %session.user.data, - user_session.id = %session.data, + user.id = %user_session.user.id, + %user_session.id, user_session_authentication.id, ), err, @@ -376,7 +376,7 @@ pub async fn authenticate_session( txn: &mut Transaction<'_, Postgres>, mut rng: impl Rng + Send, clock: &Clock, - session: &mut BrowserSession, + user_session: &mut BrowserSession, password: &str, ) -> Result<(), AuthenticationError> { // First, fetch the hashed password from the user associated with that session @@ -388,7 +388,7 @@ pub async fn authenticate_session( ORDER BY up.created_at DESC LIMIT 1 "#, - Uuid::from(session.user.data), + Uuid::from(user_session.user.id), ) .fetch_one(txn.borrow_mut()) .instrument(tracing::info_span!("Lookup hashed password")) @@ -423,7 +423,7 @@ pub async fn authenticate_session( VALUES ($1, $2, $3) "#, Uuid::from(id), - Uuid::from(session.data), + Uuid::from(user_session.id), created_at, ) .execute(txn.borrow_mut()) @@ -431,10 +431,7 @@ pub async fn authenticate_session( .await .map_err(AuthenticationError::Save)?; - session.last_authentication = Some(Authentication { - data: id, - created_at, - }); + user_session.last_authentication = Some(Authentication { id, created_at }); Ok(()) } @@ -442,9 +439,9 @@ pub async fn authenticate_session( #[tracing::instrument( skip_all, fields( - user.id = %session.user.data, + user.id = %user_session.user.id, %upstream_oauth_link.id, - user_session.id = %session.data, + %user_session.id, user_session_authentication.id, ), err, @@ -453,7 +450,7 @@ pub async fn authenticate_session_with_upstream( executor: impl PgExecutor<'_>, mut rng: impl Rng + Send, clock: &Clock, - session: &mut BrowserSession, + user_session: &mut BrowserSession, upstream_oauth_link: &UpstreamOAuthLink, ) -> Result<(), sqlx::Error> { let created_at = clock.now(); @@ -470,17 +467,14 @@ pub async fn authenticate_session_with_upstream( VALUES ($1, $2, $3) "#, Uuid::from(id), - Uuid::from(session.data), + Uuid::from(user_session.id), created_at, ) .execute(executor) .instrument(tracing::info_span!("Save authentication")) .await?; - session.last_authentication = Some(Authentication { - data: id, - created_at, - }); + user_session.last_authentication = Some(Authentication { id, created_at }); Ok(()) } @@ -500,7 +494,7 @@ pub async fn register_user( phf: impl PasswordHasher + Send, username: &str, password: &str, -) -> Result, anyhow::Error> { +) -> Result { let created_at = clock.now(); let id = Ulid::from_datetime_with_source(created_at.into(), &mut rng); tracing::Span::current().record("user.id", tracing::field::display(id)); @@ -520,7 +514,7 @@ pub async fn register_user( .context("could not insert user")?; let user = User { - data: id, + id, username: username.to_owned(), sub: id.to_string(), primary_email: None, @@ -544,7 +538,7 @@ pub async fn register_passwordless_user( mut rng: impl Rng + Send, clock: &Clock, username: &str, -) -> Result, sqlx::Error> { +) -> Result { let created_at = clock.now(); let id = Ulid::from_datetime_with_source(created_at.into(), &mut rng); tracing::Span::current().record("user.id", tracing::field::display(id)); @@ -562,7 +556,7 @@ pub async fn register_passwordless_user( .await?; Ok(User { - data: id, + id, username: username.to_owned(), sub: id.to_string(), primary_email: None, @@ -572,7 +566,7 @@ pub async fn register_passwordless_user( #[tracing::instrument( skip_all, fields( - user.id = %user.data, + %user.id, user_password.id, ), err(Display), @@ -582,7 +576,7 @@ pub async fn set_password( mut rng: impl CryptoRng + Rng + Send, clock: &Clock, phf: impl PasswordHasher + Send, - user: &User, + user: &User, password: &str, ) -> Result<(), anyhow::Error> { let created_at = clock.now(); @@ -598,7 +592,7 @@ pub async fn set_password( VALUES ($1, $2, $3, $4) "#, Uuid::from(id), - Uuid::from(user.data), + Uuid::from(user.id), hashed_password.to_string(), created_at, ) @@ -612,13 +606,13 @@ pub async fn set_password( #[tracing::instrument( skip_all, - fields(user_session.id = %session.data), + fields(%user_session.id), err(Display), )] pub async fn end_session( executor: impl PgExecutor<'_>, clock: &Clock, - session: &BrowserSession, + user_session: &BrowserSession, ) -> Result<(), anyhow::Error> { let now = clock.now(); let res = sqlx::query!( @@ -628,7 +622,7 @@ pub async fn end_session( WHERE user_session_id = $2 "#, now, - Uuid::from(session.data), + Uuid::from(user_session.id), ) .execute(executor) .instrument(info_span!("End session")) @@ -663,7 +657,7 @@ impl LookupError for UserLookupError { pub async fn lookup_user_by_username( executor: impl PgExecutor<'_>, username: &str, -) -> Result, UserLookupError> { +) -> Result { let res = sqlx::query_as!( UserLookup, r#" @@ -694,7 +688,7 @@ pub async fn lookup_user_by_username( res.user_email_confirmed_at, ) { (Some(id), Some(email), Some(created_at), confirmed_at) => Some(UserEmail { - data: id.into(), + id: id.into(), email, created_at, confirmed_at, @@ -705,7 +699,7 @@ pub async fn lookup_user_by_username( let id = Ulid::from(res.user_id); Ok(User { - data: id, + id, username: res.user_username, sub: id.to_string(), primary_email, @@ -717,10 +711,7 @@ pub async fn lookup_user_by_username( fields(user.id = %id), err, )] -pub async fn lookup_user( - executor: impl PgExecutor<'_>, - id: Ulid, -) -> Result, UserLookupError> { +pub async fn lookup_user(executor: impl PgExecutor<'_>, id: Ulid) -> Result { let res = sqlx::query_as!( UserLookup, r#" @@ -751,7 +742,7 @@ pub async fn lookup_user( res.user_email_confirmed_at, ) { (Some(id), Some(email), Some(created_at), confirmed_at) => Some(UserEmail { - data: id.into(), + id: id.into(), email, created_at, confirmed_at, @@ -762,7 +753,7 @@ pub async fn lookup_user( let id = Ulid::from(res.user_id); Ok(User { - data: id, + id, username: res.user_username, sub: id.to_string(), primary_email, @@ -798,10 +789,10 @@ struct UserEmailLookup { user_email_confirmed_at: Option>, } -impl From for UserEmail { - fn from(e: UserEmailLookup) -> UserEmail { +impl From for UserEmail { + fn from(e: UserEmailLookup) -> UserEmail { UserEmail { - data: e.user_email_id.into(), + id: e.user_email_id.into(), email: e.user_email, created_at: e.user_email_created_at, confirmed_at: e.user_email_confirmed_at, @@ -811,13 +802,13 @@ impl From for UserEmail { #[tracing::instrument( skip_all, - fields(user.id = %user.data, user.username = user.username), + fields(%user.id, %user.username), err(Display), )] pub async fn get_user_emails( executor: impl PgExecutor<'_>, - user: &User, -) -> Result>, anyhow::Error> { + user: &User, +) -> Result, anyhow::Error> { let res = sqlx::query_as!( UserEmailLookup, r#" @@ -832,7 +823,7 @@ pub async fn get_user_emails( ORDER BY ue.email ASC "#, - Uuid::from(user.data), + Uuid::from(user.id), ) .fetch_all(executor) .instrument(info_span!("Fetch user emails")) @@ -843,12 +834,12 @@ pub async fn get_user_emails( #[tracing::instrument( skip_all, - fields(user.id = %user.data, user.username = user.username), + fields(%user.id, %user.username), err(Display), )] pub async fn count_user_emails( executor: impl PgExecutor<'_>, - user: &User, + user: &User, ) -> Result { let res = sqlx::query_scalar!( r#" @@ -856,7 +847,7 @@ pub async fn count_user_emails( FROM user_emails ue WHERE ue.user_id = $1 "#, - Uuid::from(user.data), + Uuid::from(user.id), ) .fetch_one(executor) .instrument(info_span!("Count user emails")) @@ -867,20 +858,17 @@ pub async fn count_user_emails( #[tracing::instrument( skip_all, - fields( - user.id = %user.data, - user.username = user.username, - ), + fields(%user.id, %user.username), err(Display), )] pub async fn get_paginated_user_emails( executor: impl PgExecutor<'_>, - user: &User, + user: &User, before: Option, after: Option, first: Option, last: Option, -) -> Result<(bool, bool, Vec>), anyhow::Error> { +) -> Result<(bool, bool, Vec), anyhow::Error> { let mut query = QueryBuilder::new( r#" SELECT @@ -894,7 +882,7 @@ pub async fn get_paginated_user_emails( query .push(" WHERE ue.user_id = ") - .push_bind(Uuid::from(user.data)) + .push_bind(Uuid::from(user.id)) .generate_pagination("ue.user_email_id", before, after, first, last)?; let span = info_span!("Fetch paginated user sessions", db.statement = query.sql()); @@ -916,17 +904,17 @@ pub async fn get_paginated_user_emails( #[tracing::instrument( skip_all, fields( - user.id = %user.data, - user.username = user.username, + %user.id, + %user.username, user_email.id = %id, ), err(Display), )] pub async fn get_user_email( executor: impl PgExecutor<'_>, - user: &User, + user: &User, id: Ulid, -) -> Result, anyhow::Error> { +) -> Result { let res = sqlx::query_as!( UserEmailLookup, r#" @@ -940,7 +928,7 @@ pub async fn get_user_email( WHERE ue.user_id = $1 AND ue.user_email_id = $2 "#, - Uuid::from(user.data), + Uuid::from(user.id), Uuid::from(id), ) .fetch_one(executor) @@ -953,8 +941,8 @@ pub async fn get_user_email( #[tracing::instrument( skip_all, fields( - user.id = %user.data, - user.username = user.username, + %user.id, + %user.username, user_email.id, user_email.email = %email, ), @@ -964,9 +952,9 @@ pub async fn add_user_email( executor: impl PgExecutor<'_>, mut rng: impl Rng + Send, clock: &Clock, - user: &User, + user: &User, email: String, -) -> Result, anyhow::Error> { +) -> Result { let created_at = clock.now(); let id = Ulid::from_datetime_with_source(created_at.into(), &mut rng); tracing::Span::current().record("user_email.id", tracing::field::display(id)); @@ -977,7 +965,7 @@ pub async fn add_user_email( VALUES ($1, $2, $3, $4) "#, Uuid::from(id), - Uuid::from(user.data), + Uuid::from(user.id), &email, created_at, ) @@ -987,7 +975,7 @@ pub async fn add_user_email( .context("could not insert user email")?; Ok(UserEmail { - data: id, + id, email, created_at, confirmed_at: None, @@ -997,14 +985,14 @@ pub async fn add_user_email( #[tracing::instrument( skip_all, fields( - user_email.id = %email.data, - user_email.email = %email.email, + %user_email.id, + %user_email.email, ), err(Display), )] pub async fn set_user_email_as_primary( executor: impl PgExecutor<'_>, - email: &UserEmail, + user_email: &UserEmail, ) -> Result<(), anyhow::Error> { sqlx::query!( r#" @@ -1014,7 +1002,7 @@ pub async fn set_user_email_as_primary( WHERE user_emails.user_email_id = $1 AND users.user_id = user_emails.user_id "#, - Uuid::from(email.data), + Uuid::from(user_email.id), ) .execute(executor) .instrument(info_span!("Add user email")) @@ -1027,21 +1015,21 @@ pub async fn set_user_email_as_primary( #[tracing::instrument( skip_all, fields( - user_email.id = %email.data, - user_email.email = %email.email, + %user_email.id, + %user_email.email, ), err(Display), )] pub async fn remove_user_email( executor: impl PgExecutor<'_>, - email: UserEmail, + user_email: UserEmail, ) -> Result<(), anyhow::Error> { sqlx::query!( r#" DELETE FROM user_emails WHERE user_emails.user_email_id = $1 "#, - Uuid::from(email.data), + Uuid::from(user_email.id), ) .execute(executor) .instrument(info_span!("Remove user email")) @@ -1054,16 +1042,16 @@ pub async fn remove_user_email( #[tracing::instrument( skip_all, fields( - user.id = %user.data, + %user.id, user_email.email = email, ), err(Display), )] pub async fn lookup_user_email( executor: impl PgExecutor<'_>, - user: &User, + user: &User, email: &str, -) -> Result, anyhow::Error> { +) -> Result { let res = sqlx::query_as!( UserEmailLookup, r#" @@ -1077,7 +1065,7 @@ pub async fn lookup_user_email( WHERE ue.user_id = $1 AND ue.email = $2 "#, - Uuid::from(user.data), + Uuid::from(user.id), email, ) .fetch_one(executor) @@ -1091,16 +1079,16 @@ pub async fn lookup_user_email( #[tracing::instrument( skip_all, fields( - user.id = %user.data, + %user.id, user_email.id = %id, ), err, )] pub async fn lookup_user_email_by_id( executor: impl PgExecutor<'_>, - user: &User, + user: &User, id: Ulid, -) -> Result, GenericLookupError> { +) -> Result { let res = sqlx::query_as!( UserEmailLookup, r#" @@ -1114,7 +1102,7 @@ pub async fn lookup_user_email_by_id( WHERE ue.user_id = $1 AND ue.user_email_id = $2 "#, - Uuid::from(user.data), + Uuid::from(user.id), Uuid::from(id), ) .fetch_one(executor) @@ -1127,16 +1115,14 @@ pub async fn lookup_user_email_by_id( #[tracing::instrument( skip_all, - fields( - user_email.id = %email.data, - ), + fields(%user_email.id), err(Display), )] pub async fn mark_user_email_as_verified( executor: impl PgExecutor<'_>, clock: &Clock, - mut email: UserEmail, -) -> Result, anyhow::Error> { + mut user_email: UserEmail, +) -> Result { let confirmed_at = clock.now(); sqlx::query!( r#" @@ -1144,7 +1130,7 @@ pub async fn mark_user_email_as_verified( SET confirmed_at = $2 WHERE user_email_id = $1 "#, - Uuid::from(email.data), + Uuid::from(user_email.id), confirmed_at, ) .execute(executor) @@ -1152,9 +1138,9 @@ pub async fn mark_user_email_as_verified( .await .context("could not update user email")?; - email.confirmed_at = Some(confirmed_at); + user_email.confirmed_at = Some(confirmed_at); - Ok(email) + Ok(user_email) } struct UserEmailConfirmationCodeLookup { @@ -1167,17 +1153,15 @@ struct UserEmailConfirmationCodeLookup { #[tracing::instrument( skip_all, - fields( - user_email.id = %email.data, - ), + fields(%user_email.id), err(Display), )] pub async fn lookup_user_email_verification_code( executor: impl PgExecutor<'_>, clock: &Clock, - email: UserEmail, + user_email: UserEmail, code: &str, -) -> Result, anyhow::Error> { +) -> Result { let now = clock.now(); let res = sqlx::query_as!( @@ -1194,7 +1178,7 @@ pub async fn lookup_user_email_verification_code( AND ec.user_email_id = $2 "#, code, - Uuid::from(email.data), + Uuid::from(user_email.id), ) .fetch_one(executor) .instrument(info_span!("Lookup user email verification")) @@ -1212,9 +1196,9 @@ pub async fn lookup_user_email_verification_code( }; Ok(UserEmailVerification { - data: res.user_email_confirmation_code_id.into(), + id: res.user_email_confirmation_code_id.into(), code: res.code, - email, + email: user_email, state, created_at: res.created_at, }) @@ -1223,16 +1207,19 @@ pub async fn lookup_user_email_verification_code( #[tracing::instrument( skip_all, fields( - user_email_verification.id = %verification.data, + %user_email_verification.id, ), err(Display), )] pub async fn consume_email_verification( executor: impl PgExecutor<'_>, clock: &Clock, - mut verification: UserEmailVerification, -) -> Result, anyhow::Error> { - if !matches!(verification.state, UserEmailVerificationState::Valid) { + mut user_email_verification: UserEmailVerification, +) -> Result { + if !matches!( + user_email_verification.state, + UserEmailVerificationState::Valid + ) { bail!("user email verification in wrong state"); } @@ -1244,7 +1231,7 @@ pub async fn consume_email_verification( SET consumed_at = $2 WHERE user_email_confirmation_code_id = $1 "#, - Uuid::from(verification.data), + Uuid::from(user_email_verification.id), consumed_at ) .execute(executor) @@ -1252,16 +1239,16 @@ pub async fn consume_email_verification( .await .context("could not update user email verification")?; - verification.state = UserEmailVerificationState::AlreadyUsed { when: consumed_at }; + user_email_verification.state = UserEmailVerificationState::AlreadyUsed { when: consumed_at }; - Ok(verification) + Ok(user_email_verification) } #[tracing::instrument( skip_all, fields( - user_email.id = %email.data, - user_email.email = %email.email, + %user_email.id, + %user_email.email, user_email_confirmation.id, user_email_confirmation.code = code, ), @@ -1271,10 +1258,10 @@ pub async fn add_user_email_verification_code( executor: impl PgExecutor<'_>, mut rng: impl Rng + Send, clock: &Clock, - email: UserEmail, + user_email: UserEmail, max_age: chrono::Duration, code: String, -) -> Result, anyhow::Error> { +) -> Result { let created_at = clock.now(); let id = Ulid::from_datetime_with_source(created_at.into(), &mut rng); tracing::Span::current().record("user_email_confirmation.id", tracing::field::display(id)); @@ -1287,7 +1274,7 @@ pub async fn add_user_email_verification_code( VALUES ($1, $2, $3, $4, $5) "#, Uuid::from(id), - Uuid::from(email.data), + Uuid::from(user_email.id), code, created_at, expires_at, @@ -1298,8 +1285,8 @@ pub async fn add_user_email_verification_code( .context("could not insert user email verification code")?; let verification = UserEmailVerification { - data: id, - email, + id, + email: user_email, code, created_at, state: UserEmailVerificationState::Valid, @@ -1331,10 +1318,10 @@ mod tests { assert!(exists); let session = login(&mut txn, &mut rng, &clock, "john", "hunter2").await?; - assert_eq!(session.user.data, user.data); + assert_eq!(session.user.id, user.id); let user2 = lookup_user_by_username(&mut txn, "john").await?; - assert_eq!(user.data, user2.data); + assert_eq!(user.id, user2.id); txn.commit().await?; diff --git a/crates/templates/Cargo.toml b/crates/templates/Cargo.toml index d3884011..2174ebdc 100644 --- a/crates/templates/Cargo.toml +++ b/crates/templates/Cargo.toml @@ -22,6 +22,7 @@ chrono = "0.4.23" url = "2.3.1" http = "0.2.8" ulid = { version = "1.0.0", features = ["serde"] } +rand = "0.8.5" oauth2-types = { path = "../oauth2-types" } mas-data-model = { path = "../data-model" } diff --git a/crates/templates/src/context.rs b/crates/templates/src/context.rs index 56eeb03c..61d070f5 100644 --- a/crates/templates/src/context.rs +++ b/crates/templates/src/context.rs @@ -18,10 +18,11 @@ use chrono::Utc; use mas_data_model::{ - AuthorizationGrant, BrowserSession, CompatSsoLogin, CompatSsoLoginState, StorageBackend, - UpstreamOAuthLink, UpstreamOAuthProvider, User, UserEmail, UserEmailVerification, + AuthorizationGrant, BrowserSession, CompatSsoLogin, CompatSsoLoginState, UpstreamOAuthLink, + UpstreamOAuthProvider, User, UserEmail, UserEmailVerification, }; use mas_router::{PostAuthAction, Route}; +use rand::Rng; use serde::{ser::SerializeStruct, Deserialize, Serialize}; use ulid::Ulid; use url::Url; @@ -31,31 +32,26 @@ use crate::{FormField, FormState}; /// Helper trait to construct context wrappers pub trait TemplateContext: Serialize { /// Attach a user session to the template context - fn with_session( - self, - current_session: BrowserSession, - ) -> WithSession + fn with_session(self, current_session: BrowserSession) -> WithSession where Self: Sized, - BrowserSession: Into>, { WithSession { - current_session: current_session.into(), + current_session, inner: self, } } /// Attach an optional user session to the template context - fn maybe_with_session( + fn maybe_with_session( self, - current_session: Option>, + current_session: Option, ) -> WithOptionalSession where Self: Sized, - BrowserSession: Into>, { WithOptionalSession { - current_session: current_session.map(Into::into), + current_session, inner: self, } } @@ -77,13 +73,13 @@ pub trait TemplateContext: Serialize { /// /// This is then used to check for template validity in unit tests and in /// the CLI (`cargo run -- templates check`) - fn sample(now: chrono::DateTime) -> Vec + fn sample(now: chrono::DateTime, rng: &mut impl Rng) -> Vec where Self: Sized; } impl TemplateContext for () { - fn sample(_now: chrono::DateTime) -> Vec + fn sample(_now: chrono::DateTime, _rng: &mut impl Rng) -> Vec where Self: Sized, { @@ -101,11 +97,11 @@ pub struct WithCsrf { } impl TemplateContext for WithCsrf { - fn sample(now: chrono::DateTime) -> Vec + fn sample(now: chrono::DateTime, rng: &mut impl Rng) -> Vec where Self: Sized, { - T::sample(now) + T::sample(now, rng) .into_iter() .map(|inner| WithCsrf { csrf_token: "fake_csrf_token".into(), @@ -118,24 +114,26 @@ impl TemplateContext for WithCsrf { /// Context with a user session in it #[derive(Serialize)] pub struct WithSession { - current_session: BrowserSession<()>, + current_session: BrowserSession, #[serde(flatten)] inner: T, } impl TemplateContext for WithSession { - fn sample(now: chrono::DateTime) -> Vec + fn sample(now: chrono::DateTime, rng: &mut impl Rng) -> Vec where Self: Sized, { - BrowserSession::samples(now) + BrowserSession::samples(now, rng) .into_iter() .flat_map(|session| { - T::sample(now).into_iter().map(move |inner| WithSession { - current_session: session.clone(), - inner, - }) + T::sample(now, rng) + .into_iter() + .map(move |inner| WithSession { + current_session: session.clone(), + inner, + }) }) .collect() } @@ -144,23 +142,23 @@ impl TemplateContext for WithSession { /// Context with an optional user session in it #[derive(Serialize)] pub struct WithOptionalSession { - current_session: Option>, + current_session: Option, #[serde(flatten)] inner: T, } impl TemplateContext for WithOptionalSession { - fn sample(now: chrono::DateTime) -> Vec + fn sample(now: chrono::DateTime, rng: &mut impl Rng) -> Vec where Self: Sized, { - BrowserSession::samples(now) + BrowserSession::samples(now, rng) .into_iter() .map(Some) // Wrap all samples in an Option .chain(std::iter::once(None)) // Add the "None" option .flat_map(|session| { - T::sample(now) + T::sample(now, rng) .into_iter() .map(move |inner| WithOptionalSession { current_session: session.clone(), @@ -188,7 +186,7 @@ impl Serialize for EmptyContext { } impl TemplateContext for EmptyContext { - fn sample(_now: chrono::DateTime) -> Vec + fn sample(_now: chrono::DateTime, _rng: &mut impl Rng) -> Vec where Self: Sized, { @@ -212,7 +210,7 @@ impl IndexContext { } impl TemplateContext for IndexContext { - fn sample(_now: chrono::DateTime) -> Vec + fn sample(_now: chrono::DateTime, _rng: &mut impl Rng) -> Vec where Self: Sized, { @@ -294,7 +292,7 @@ pub struct LoginContext { } impl TemplateContext for LoginContext { - fn sample(_now: chrono::DateTime) -> Vec + fn sample(_now: chrono::DateTime, _rng: &mut impl Rng) -> Vec where Self: Sized, { @@ -364,7 +362,7 @@ pub struct RegisterContext { } impl TemplateContext for RegisterContext { - fn sample(_now: chrono::DateTime) -> Vec + fn sample(_now: chrono::DateTime, _rng: &mut impl Rng) -> Vec where Self: Sized, { @@ -401,7 +399,7 @@ pub struct ConsentContext { } impl TemplateContext for ConsentContext { - fn sample(_now: chrono::DateTime) -> Vec + fn sample(_now: chrono::DateTime, _rng: &mut impl Rng) -> Vec where Self: Sized, { @@ -432,7 +430,7 @@ pub struct PolicyViolationContext { } impl TemplateContext for PolicyViolationContext { - fn sample(_now: chrono::DateTime) -> Vec + fn sample(_now: chrono::DateTime, _rng: &mut impl Rng) -> Vec where Self: Sized, { @@ -480,7 +478,7 @@ pub struct ReauthContext { } impl TemplateContext for ReauthContext { - fn sample(_now: chrono::DateTime) -> Vec + fn sample(_now: chrono::DateTime, _rng: &mut impl Rng) -> Vec where Self: Sized, { @@ -519,7 +517,7 @@ pub struct CompatSsoContext { } impl TemplateContext for CompatSsoContext { - fn sample(now: chrono::DateTime) -> Vec + fn sample(now: chrono::DateTime, rng: &mut impl Rng) -> Vec where Self: Sized, { @@ -531,7 +529,9 @@ impl TemplateContext for CompatSsoContext { created_at: now, state: CompatSsoLoginState::Pending, }, - action: PostAuthAction::ContinueCompatSsoLogin { data: Ulid::nil() }, + action: PostAuthAction::ContinueCompatSsoLogin { + data: Ulid::from_datetime_with_source(now.into(), rng), + }, }] } } @@ -554,7 +554,7 @@ impl CompatSsoContext { #[derive(Serialize)] pub struct AccountContext { active_sessions: usize, - emails: Vec>, + emails: Vec, } impl AccountContext { @@ -562,7 +562,7 @@ impl AccountContext { #[must_use] pub fn new(active_sessions: usize, emails: Vec) -> Self where - T: Into>, + T: Into, { Self { active_sessions, @@ -572,36 +572,35 @@ impl AccountContext { } impl TemplateContext for AccountContext { - fn sample(now: chrono::DateTime) -> Vec + fn sample(now: chrono::DateTime, rng: &mut impl Rng) -> Vec where Self: Sized, { - let emails: Vec> = UserEmail::samples(now); + let emails: Vec = UserEmail::samples(now, rng); vec![Self::new(5, emails)] } } /// Context used by the `account/emails.html` template #[derive(Serialize)] -#[serde(bound(serialize = "T: StorageBackend"))] -pub struct AccountEmailsContext { - emails: Vec>, +pub struct AccountEmailsContext { + emails: Vec, } -impl AccountEmailsContext { +impl AccountEmailsContext { /// Constructs a context for the email management page #[must_use] - pub fn new(emails: Vec>) -> Self { + pub fn new(emails: Vec) -> Self { Self { emails } } } -impl TemplateContext for AccountEmailsContext { - fn sample(now: chrono::DateTime) -> Vec +impl TemplateContext for AccountEmailsContext { + fn sample(now: chrono::DateTime, rng: &mut impl Rng) -> Vec where Self: Sized, { - let emails: Vec> = UserEmail::samples(now); + let emails: Vec = UserEmail::samples(now, rng); vec![Self::new(emails)] } } @@ -609,35 +608,35 @@ impl TemplateContext for AccountEmailsContext { /// Context used by the `emails/verification.{txt,html,subject}` templates #[derive(Serialize)] pub struct EmailVerificationContext { - user: User<()>, - verification: UserEmailVerification<()>, + user: User, + verification: UserEmailVerification, } impl EmailVerificationContext { /// Constructs a context for the verification email #[must_use] - pub fn new(user: User<()>, verification: UserEmailVerification<()>) -> Self { + pub fn new(user: User, verification: UserEmailVerification) -> Self { Self { user, verification } } } impl TemplateContext for EmailVerificationContext { - fn sample(now: chrono::DateTime) -> Vec + fn sample(now: chrono::DateTime, rng: &mut impl Rng) -> Vec where Self: Sized, { - User::samples(now) + User::samples(now, rng) .into_iter() .map(|user| { let email = UserEmail { - data: (), + id: Ulid::from_datetime_with_source(now.into(), rng), email: "foobar@example.com".to_owned(), created_at: now, confirmed_at: None, }; let verification = UserEmailVerification { - data: (), + id: Ulid::from_datetime_with_source(now.into(), rng), code: "123456".to_owned(), email, created_at: now, @@ -670,7 +669,7 @@ impl FormField for EmailVerificationFormField { #[derive(Serialize)] pub struct EmailVerificationPageContext { form: FormState, - email: UserEmail<()>, + email: UserEmail, } impl EmailVerificationPageContext { @@ -678,7 +677,7 @@ impl EmailVerificationPageContext { #[must_use] pub fn new(email: T) -> Self where - T: Into>, + T: Into, { Self { form: FormState::default(), @@ -694,12 +693,12 @@ impl EmailVerificationPageContext { } impl TemplateContext for EmailVerificationPageContext { - fn sample(now: chrono::DateTime) -> Vec + fn sample(now: chrono::DateTime, rng: &mut impl Rng) -> Vec where Self: Sized, { let email = UserEmail { - data: (), + id: Ulid::from_datetime_with_source(now.into(), rng), email: "foobar@example.com".to_owned(), created_at: now, confirmed_at: None, @@ -749,7 +748,7 @@ impl EmailAddContext { } impl TemplateContext for EmailAddContext { - fn sample(_now: chrono::DateTime) -> Vec + fn sample(_now: chrono::DateTime, _rng: &mut impl Rng) -> Vec where Self: Sized, { @@ -761,14 +760,14 @@ impl TemplateContext for EmailAddContext { /// templates #[derive(Serialize)] pub struct UpstreamExistingLinkContext { - linked_user: User<()>, + linked_user: User, } impl UpstreamExistingLinkContext { /// Constructs a new context with an existing linked user pub fn new(linked_user: T) -> Self where - T: Into>, + T: Into, { Self { linked_user: linked_user.into(), @@ -777,11 +776,11 @@ impl UpstreamExistingLinkContext { } impl TemplateContext for UpstreamExistingLinkContext { - fn sample(now: chrono::DateTime) -> Vec + fn sample(now: chrono::DateTime, rng: &mut impl Rng) -> Vec where Self: Sized, { - User::samples(now) + User::samples(now, rng) .into_iter() .map(|linked_user| Self { linked_user }) .collect() @@ -805,7 +804,7 @@ impl UpstreamSuggestLink { } impl TemplateContext for UpstreamSuggestLink { - fn sample(_now: chrono::DateTime) -> Vec + fn sample(_now: chrono::DateTime, _rng: &mut impl Rng) -> Vec where Self: Sized, { @@ -831,7 +830,7 @@ impl UpstreamRegister { } impl TemplateContext for UpstreamRegister { - fn sample(_now: chrono::DateTime) -> Vec + fn sample(_now: chrono::DateTime, _rng: &mut impl Rng) -> Vec where Self: Sized, { @@ -847,11 +846,11 @@ pub struct FormPostContext { } impl TemplateContext for FormPostContext { - fn sample(now: chrono::DateTime) -> Vec + fn sample(now: chrono::DateTime, rng: &mut impl Rng) -> Vec where Self: Sized, { - let sample_params = T::sample(now); + let sample_params = T::sample(now, rng); sample_params .into_iter() .map(|params| FormPostContext { @@ -881,7 +880,7 @@ pub struct ErrorContext { } impl TemplateContext for ErrorContext { - fn sample(_now: chrono::DateTime) -> Vec + fn sample(_now: chrono::DateTime, _rng: &mut impl Rng) -> Vec where Self: Sized, { diff --git a/crates/templates/src/lib.rs b/crates/templates/src/lib.rs index 30633912..99e0cb7b 100644 --- a/crates/templates/src/lib.rs +++ b/crates/templates/src/lib.rs @@ -28,8 +28,8 @@ use std::{collections::HashSet, string::ToString, sync::Arc}; use anyhow::Context as _; use camino::{Utf8Path, Utf8PathBuf}; -use mas_data_model::StorageBackend; use mas_router::UrlBuilder; +use rand::Rng; use serde::Serialize; use tera::{Context, Error as TeraError, Tera}; use thiserror::Error; @@ -201,7 +201,7 @@ register_templates! { pub fn render_account_password(WithCsrf>) { "pages/account/password.html" } /// Render the emails management - pub fn render_account_emails(WithCsrf>>) { "pages/account/emails/index.html" } + pub fn render_account_emails(WithCsrf>) { "pages/account/emails/index.html" } /// Render the email verification page pub fn render_account_verify_email(WithCsrf>) { "pages/account/emails/verify.html" } @@ -246,29 +246,33 @@ register_templates! { impl Templates { /// Render all templates with the generated samples to check if they render /// properly - pub async fn check_render(&self, now: chrono::DateTime) -> anyhow::Result<()> { - check::render_login(self, now).await?; - check::render_register(self, now).await?; - check::render_consent(self, now).await?; - check::render_policy_violation(self, now).await?; - check::render_sso_login(self, now).await?; - check::render_index(self, now).await?; - check::render_account_index(self, now).await?; - check::render_account_password(self, now).await?; - check::render_account_emails::<()>(self, now).await?; - check::render_account_add_email(self, now).await?; - check::render_account_verify_email(self, now).await?; - check::render_reauth(self, now).await?; - check::render_form_post::(self, now).await?; - check::render_error(self, now).await?; - check::render_email_verification_txt(self, now).await?; - check::render_email_verification_html(self, now).await?; - check::render_email_verification_subject(self, now).await?; - check::render_upstream_oauth2_already_linked(self, now).await?; - check::render_upstream_oauth2_link_mismatch(self, now).await?; - check::render_upstream_oauth2_suggest_link(self, now).await?; - check::render_upstream_oauth2_do_login(self, now).await?; - check::render_upstream_oauth2_do_register(self, now).await?; + pub async fn check_render( + &self, + now: chrono::DateTime, + rng: &mut impl Rng, + ) -> anyhow::Result<()> { + check::render_login(self, now, rng).await?; + check::render_register(self, now, rng).await?; + check::render_consent(self, now, rng).await?; + check::render_policy_violation(self, now, rng).await?; + check::render_sso_login(self, now, rng).await?; + check::render_index(self, now, rng).await?; + check::render_account_index(self, now, rng).await?; + check::render_account_password(self, now, rng).await?; + check::render_account_emails(self, now, rng).await?; + check::render_account_add_email(self, now, rng).await?; + check::render_account_verify_email(self, now, rng).await?; + check::render_reauth(self, now, rng).await?; + check::render_form_post::(self, now, rng).await?; + check::render_error(self, now, rng).await?; + check::render_email_verification_txt(self, now, rng).await?; + check::render_email_verification_html(self, now, rng).await?; + check::render_email_verification_subject(self, now, rng).await?; + check::render_upstream_oauth2_already_linked(self, now, rng).await?; + check::render_upstream_oauth2_link_mismatch(self, now, rng).await?; + check::render_upstream_oauth2_suggest_link(self, now, rng).await?; + check::render_upstream_oauth2_do_login(self, now, rng).await?; + check::render_upstream_oauth2_do_register(self, now, rng).await?; Ok(()) } } @@ -281,10 +285,12 @@ mod tests { async fn check_builtin_templates() { #[allow(clippy::disallowed_methods)] let now = chrono::Utc::now(); + #[allow(clippy::disallowed_methods)] + let mut rng = rand::thread_rng(); let path = Utf8Path::new(env!("CARGO_MANIFEST_DIR")).join("../../templates/"); let url_builder = UrlBuilder::new("https://example.com/".parse().unwrap()); let templates = Templates::load(path, url_builder).await.unwrap(); - templates.check_render(now).await.unwrap(); + templates.check_render(now, &mut rng).await.unwrap(); } } diff --git a/crates/templates/src/macros.rs b/crates/templates/src/macros.rs index 2bce0d8e..dd9a2d4e 100644 --- a/crates/templates/src/macros.rs +++ b/crates/templates/src/macros.rs @@ -75,9 +75,9 @@ macro_rules! register_templates { #[doc = concat!("Render the `", $template, "` template with sample contexts")] pub async fn $name $(< $( $lt $( : $clt $(+ $dlt )* + TemplateContext )? ),+ >)? - (templates: &Templates, now: chrono::DateTime) + (templates: &Templates, now: chrono::DateTime, rng: &mut impl rand::Rng) -> anyhow::Result<()> { - let samples: Vec< $param > = TemplateContext::sample(now); + let samples: Vec< $param > = TemplateContext::sample(now, rng); let name = $template; for sample in samples { diff --git a/templates/pages/account/emails/index.html b/templates/pages/account/emails/index.html index 0bf86cd3..4e0b512d 100644 --- a/templates/pages/account/emails/index.html +++ b/templates/pages/account/emails/index.html @@ -37,7 +37,7 @@ limitations under the License. {% for item in emails %}
- +
{{ item.email }}
{% if item.confirmed_at %}
Verified