1
0
mirror of https://github.com/matrix-org/matrix-authentication-service.git synced 2025-07-29 22:01:14 +03:00

data-model: simplify users and sessions

This commit is contained in:
Quentin Gliech
2022-12-06 17:50:55 +01:00
parent dff2f98167
commit feebbd0e97
34 changed files with 399 additions and 491 deletions

2
Cargo.lock generated
View File

@ -2760,6 +2760,7 @@ dependencies = [
"mas-jose", "mas-jose",
"oauth2-types", "oauth2-types",
"rand 0.8.5", "rand 0.8.5",
"rand_chacha 0.3.1",
"serde", "serde",
"thiserror", "thiserror",
"ulid", "ulid",
@ -3128,6 +3129,7 @@ dependencies = [
"mas-data-model", "mas-data-model",
"mas-router", "mas-router",
"oauth2-types", "oauth2-types",
"rand 0.8.5",
"serde", "serde",
"serde_json", "serde_json",
"serde_urlencoded", "serde_urlencoded",

View File

@ -14,10 +14,7 @@
use axum_extra::extract::cookie::{Cookie, PrivateCookieJar}; use axum_extra::extract::cookie::{Cookie, PrivateCookieJar};
use mas_data_model::BrowserSession; use mas_data_model::BrowserSession;
use mas_storage::{ use mas_storage::user::{lookup_active_session, ActiveSessionLookupError};
user::{lookup_active_session, ActiveSessionLookupError},
PostgresqlBackend,
};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use sqlx::{Executor, Postgres}; use sqlx::{Executor, Postgres};
use ulid::Ulid; use ulid::Ulid;
@ -33,9 +30,9 @@ pub struct SessionInfo {
impl SessionInfo { impl SessionInfo {
/// Forge the cookie from a [`BrowserSession`] /// Forge the cookie from a [`BrowserSession`]
#[must_use] #[must_use]
pub fn from_session(session: &BrowserSession<PostgresqlBackend>) -> Self { pub fn from_session(session: &BrowserSession) -> Self {
Self { Self {
current: Some(session.data), current: Some(session.id),
} }
} }
@ -50,7 +47,7 @@ impl SessionInfo {
pub async fn load_session( pub async fn load_session(
&self, &self,
executor: impl Executor<'_, Database = Postgres>, executor: impl Executor<'_, Database = Postgres>,
) -> Result<Option<BrowserSession<PostgresqlBackend>>, ActiveSessionLookupError> { ) -> Result<Option<BrowserSession>, ActiveSessionLookupError> {
let session_id = if let Some(id) = self.current { let session_id = if let Some(id) = self.current {
id id
} else { } else {
@ -70,7 +67,7 @@ pub trait SessionInfoExt {
fn update_session_info(self, info: &SessionInfo) -> Self; fn update_session_info(self, info: &SessionInfo) -> Self;
#[must_use] #[must_use]
fn set_session(self, session: &BrowserSession<PostgresqlBackend>) -> Self fn set_session(self, session: &BrowserSession) -> Self
where where
Self: Sized, Self: Sized,
{ {

View File

@ -204,7 +204,7 @@ impl Options {
let user = let user =
register_user(&mut txn, &mut rng, &clock, hasher, username, password).await?; register_user(&mut txn, &mut rng, &clock, hasher, username, password).await?;
txn.commit().await?; txn.commit().await?;
info!(user.id = %user.data, %user.username, "User registered"); info!(%user.id, %user.username, "User registered");
Ok(()) Ok(())
} }
@ -217,7 +217,7 @@ impl Options {
let user = lookup_user_by_username(&mut txn, username).await?; let user = lookup_user_by_username(&mut txn, username).await?;
set_password(&mut txn, &mut rng, &clock, hasher, &user, password).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?; txn.commit().await?;
Ok(()) Ok(())

View File

@ -16,6 +16,7 @@ use camino::Utf8PathBuf;
use clap::Parser; use clap::Parser;
use mas_storage::Clock; use mas_storage::Clock;
use mas_templates::Templates; use mas_templates::Templates;
use rand::SeedableRng;
#[derive(Parser, Debug)] #[derive(Parser, Debug)]
pub(super) struct Options { pub(super) struct Options {
@ -38,9 +39,11 @@ impl Options {
match &self.subcommand { match &self.subcommand {
SC::Check { path } => { SC::Check { path } => {
let clock = Clock::default(); 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 url_builder = mas_router::UrlBuilder::new("https://example.com/".parse()?);
let templates = Templates::load(path.clone(), url_builder).await?; let templates = Templates::load(path.clone(), url_builder).await?;
templates.check_render(clock.now()).await?; templates.check_render(clock.now(), &mut rng).await?;
Ok(()) Ok(())
} }

View File

@ -13,6 +13,7 @@ url = { version = "2.3.1", features = ["serde"] }
crc = "3.0.0" crc = "3.0.0"
rand = "0.8.5" rand = "0.8.5"
ulid = "1.0.0" ulid = "1.0.0"
rand_chacha = "0.3.1"
mas-iana = { path = "../iana" } mas-iana = { path = "../iana" }
mas-jose = { path = "../jose" } mas-jose = { path = "../jose" }

View File

@ -86,7 +86,7 @@ impl TryFrom<String> for Device {
pub struct CompatSession<T: StorageBackend> { pub struct CompatSession<T: StorageBackend> {
#[serde(skip_serializing)] #[serde(skip_serializing)]
pub data: T::CompatSessionData, pub data: T::CompatSessionData,
pub user: User<T>, pub user: User,
pub device: Device, pub device: Device,
pub created_at: DateTime<Utc>, pub created_at: DateTime<Utc>,
pub finished_at: Option<DateTime<Utc>>, pub finished_at: Option<DateTime<Utc>>,
@ -96,7 +96,7 @@ impl<S: StorageBackendMarker> From<CompatSession<S>> for CompatSession<()> {
fn from(t: CompatSession<S>) -> Self { fn from(t: CompatSession<S>) -> Self {
Self { Self {
data: (), data: (),
user: t.user.into(), user: t.user,
device: t.device, device: t.device,
created_at: t.created_at, created_at: t.created_at,
finished_at: t.finished_at, finished_at: t.finished_at,
@ -125,7 +125,7 @@ impl<S: StorageBackendMarker> From<CompatAccessToken<S>> for CompatAccessToken<(
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq)]
pub struct CompatRefreshToken<T: StorageBackend> { pub struct CompatRefreshToken<T: StorageBackend> {
pub data: T::RefreshTokenData, pub data: T::CompatRefreshTokenData,
pub token: String, pub token: String,
pub created_at: DateTime<Utc>, pub created_at: DateTime<Utc>,
} }

View File

@ -26,7 +26,7 @@ use crate::{
pub struct Session<T: StorageBackend> { pub struct Session<T: StorageBackend> {
#[serde(skip_serializing)] #[serde(skip_serializing)]
pub data: T::SessionData, pub data: T::SessionData,
pub browser_session: BrowserSession<T>, pub browser_session: BrowserSession,
pub client: Client<T>, pub client: Client<T>,
pub scope: Scope, pub scope: Scope,
} }
@ -35,7 +35,7 @@ impl<S: StorageBackendMarker> From<Session<S>> for Session<()> {
fn from(s: Session<S>) -> Self { fn from(s: Session<S>) -> Self {
Session { Session {
data: (), data: (),
browser_session: s.browser_session.into(), browser_session: s.browser_session,
client: s.client.into(), client: s.client.into(),
scope: s.scope, scope: s.scope,
} }

View File

@ -30,16 +30,9 @@ impl<T: Clone + Debug + PartialEq + Serialize + DeserializeOwned + Default + Syn
} }
pub trait StorageBackend { pub trait StorageBackend {
type UserData: Data;
type UserEmailData: Data;
type UserEmailVerificationData: Data;
type AuthenticationData: Data;
type BrowserSessionData: Data;
type ClientData: Data; type ClientData: Data;
type SessionData: Data; type SessionData: Data;
type AuthorizationGrantData: Data; type AuthorizationGrantData: Data;
type AccessTokenData: Data;
type RefreshTokenData: Data;
type CompatAccessTokenData: Data; type CompatAccessTokenData: Data;
type CompatRefreshTokenData: Data; type CompatRefreshTokenData: Data;
type CompatSessionData: Data; type CompatSessionData: Data;
@ -47,18 +40,11 @@ pub trait StorageBackend {
} }
impl StorageBackend for () { impl StorageBackend for () {
type AccessTokenData = ();
type AuthenticationData = ();
type AuthorizationGrantData = (); type AuthorizationGrantData = ();
type BrowserSessionData = ();
type ClientData = (); type ClientData = ();
type CompatAccessTokenData = (); type CompatAccessTokenData = ();
type CompatRefreshTokenData = (); type CompatRefreshTokenData = ();
type CompatSessionData = (); type CompatSessionData = ();
type CompatSsoLoginData = (); type CompatSsoLoginData = ();
type RefreshTokenData = ();
type SessionData = (); type SessionData = ();
type UserData = ();
type UserEmailData = ();
type UserEmailVerificationData = ();
} }

View File

@ -13,27 +13,23 @@
// limitations under the License. // limitations under the License.
use chrono::{DateTime, Duration, Utc}; use chrono::{DateTime, Duration, Utc};
use rand::{Rng, SeedableRng};
use serde::Serialize; use serde::Serialize;
use ulid::Ulid;
use crate::traits::{StorageBackend, StorageBackendMarker}; #[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct User {
#[derive(Debug, Clone, PartialEq, Serialize)] pub id: Ulid,
#[serde(bound = "T: StorageBackend")]
pub struct User<T: StorageBackend> {
pub data: T::UserData,
pub username: String, pub username: String,
pub sub: String, pub sub: String,
pub primary_email: Option<UserEmail<T>>, pub primary_email: Option<UserEmail>,
} }
impl<T: StorageBackend> User<T> impl User {
where
T::UserData: Default,
{
#[must_use] #[must_use]
pub fn samples(_now: chrono::DateTime<Utc>) -> Vec<Self> { pub fn samples(now: chrono::DateTime<Utc>, rng: &mut impl Rng) -> Vec<Self> {
vec![User { vec![User {
data: Default::default(), id: Ulid::from_datetime_with_source(now.into(), rng),
username: "john".to_owned(), username: "john".to_owned(),
sub: "123-456".to_owned(), sub: "123-456".to_owned(),
primary_email: None, primary_email: None,
@ -41,55 +37,22 @@ where
} }
} }
impl<S: StorageBackendMarker> From<User<S>> for User<()> { #[derive(Debug, Clone, PartialEq, Eq, Serialize)]
fn from(u: User<S>) -> Self { pub struct Authentication {
User { pub id: Ulid,
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<T: StorageBackend> {
#[serde(skip_serializing)]
pub data: T::AuthenticationData,
pub created_at: DateTime<Utc>, pub created_at: DateTime<Utc>,
} }
impl<S: StorageBackendMarker> From<Authentication<S>> for Authentication<()> { #[derive(Debug, Clone, PartialEq, Eq, Serialize)]
fn from(a: Authentication<S>) -> Self { pub struct BrowserSession {
Authentication { pub id: Ulid,
data: (), pub user: User,
created_at: a.created_at,
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize)]
#[serde(bound = "T: StorageBackend")]
pub struct BrowserSession<T: StorageBackend> {
pub data: T::BrowserSessionData,
pub user: User<T>,
pub created_at: DateTime<Utc>, pub created_at: DateTime<Utc>,
pub last_authentication: Option<Authentication<T>>, pub last_authentication: Option<Authentication>,
} }
impl<S: StorageBackendMarker> From<BrowserSession<S>> for BrowserSession<()> { impl BrowserSession {
fn from(s: BrowserSession<S>) -> Self { #[must_use]
BrowserSession {
data: (),
user: s.user.into(),
created_at: s.created_at,
last_authentication: s.last_authentication.map(Into::into),
}
}
}
impl<S: StorageBackend> BrowserSession<S> {
pub fn was_authenticated_after(&self, after: DateTime<Utc>) -> bool { pub fn was_authenticated_after(&self, after: DateTime<Utc>) -> bool {
if let Some(auth) = &self.last_authentication { if let Some(auth) = &self.last_authentication {
auth.created_at > after auth.created_at > after
@ -99,17 +62,13 @@ impl<S: StorageBackend> BrowserSession<S> {
} }
} }
impl<T: StorageBackend> BrowserSession<T> impl BrowserSession {
where
T::BrowserSessionData: Default,
T::UserData: Default,
{
#[must_use] #[must_use]
pub fn samples(now: chrono::DateTime<Utc>) -> Vec<Self> { pub fn samples(now: chrono::DateTime<Utc>, rng: &mut impl Rng) -> Vec<Self> {
User::<T>::samples(now) User::samples(now, rng)
.into_iter() .into_iter()
.map(|user| BrowserSession { .map(|user| BrowserSession {
data: Default::default(), id: Ulid::from_datetime_with_source(now.into(), rng),
user, user,
created_at: now, created_at: now,
last_authentication: None, last_authentication: None,
@ -118,41 +77,26 @@ where
} }
} }
#[derive(Debug, Clone, PartialEq, Serialize)] #[derive(Debug, Clone, PartialEq, Eq, Serialize)]
#[serde(bound = "T: StorageBackend")] pub struct UserEmail {
pub struct UserEmail<T: StorageBackend> { pub id: Ulid,
pub data: T::UserEmailData,
pub email: String, pub email: String,
pub created_at: DateTime<Utc>, pub created_at: DateTime<Utc>,
pub confirmed_at: Option<DateTime<Utc>>, pub confirmed_at: Option<DateTime<Utc>>,
} }
impl<S: StorageBackendMarker> From<UserEmail<S>> for UserEmail<()> { impl UserEmail {
fn from(e: UserEmail<S>) -> Self {
Self {
data: (),
email: e.email,
created_at: e.created_at,
confirmed_at: e.confirmed_at,
}
}
}
impl<T: StorageBackend> UserEmail<T>
where
T::UserEmailData: Default,
{
#[must_use] #[must_use]
pub fn samples(now: chrono::DateTime<Utc>) -> Vec<Self> { pub fn samples(now: chrono::DateTime<Utc>, rng: &mut impl Rng) -> Vec<Self> {
vec![ vec![
Self { Self {
data: T::UserEmailData::default(), id: Ulid::from_datetime_with_source(now.into(), rng),
email: "alice@example.com".to_owned(), email: "alice@example.com".to_owned(),
created_at: now, created_at: now,
confirmed_at: Some(now), confirmed_at: Some(now),
}, },
Self { Self {
data: T::UserEmailData::default(), id: Ulid::from_datetime_with_source(now.into(), rng),
email: "bob@example.com".to_owned(), email: "bob@example.com".to_owned(),
created_at: now, created_at: now,
confirmed_at: None, confirmed_at: None,
@ -168,34 +112,18 @@ pub enum UserEmailVerificationState {
Valid, Valid,
} }
#[derive(Debug, Clone, PartialEq, Serialize)] #[derive(Debug, Clone, PartialEq, Eq, Serialize)]
#[serde(bound = "T: StorageBackend")] pub struct UserEmailVerification {
pub struct UserEmailVerification<T: StorageBackend> { pub id: Ulid,
pub data: T::UserEmailVerificationData, pub email: UserEmail,
pub email: UserEmail<T>,
pub code: String, pub code: String,
pub created_at: DateTime<Utc>, pub created_at: DateTime<Utc>,
pub state: UserEmailVerificationState, pub state: UserEmailVerificationState,
} }
impl<S: StorageBackendMarker> From<UserEmailVerification<S>> for UserEmailVerification<()> { impl UserEmailVerification {
fn from(v: UserEmailVerification<S>) -> Self {
Self {
data: (),
email: v.email.into(),
code: v.code,
created_at: v.created_at,
state: v.state,
}
}
}
impl<T: StorageBackend> UserEmailVerification<T>
where
T::UserEmailData: Default + Clone,
{
#[must_use] #[must_use]
pub fn samples(now: chrono::DateTime<Utc>) -> Vec<Self> { pub fn samples(now: chrono::DateTime<Utc>, rng: &mut impl Rng) -> Vec<Self> {
let states = [ let states = [
UserEmailVerificationState::AlreadyUsed { UserEmailVerificationState::AlreadyUsed {
when: now - Duration::minutes(5), when: now - Duration::minutes(5),
@ -208,14 +136,18 @@ where
states states
.into_iter() .into_iter()
.flat_map(|state| { .flat_map(move |state| {
UserEmail::samples(now).into_iter().map(move |email| Self { let mut rng =
data: Default::default(), rand_chacha::ChaChaRng::from_rng(&mut *rng).expect("could not seed rng");
code: "123456".to_owned(), UserEmail::samples(now, &mut rng)
email, .into_iter()
created_at: now - Duration::minutes(10), .map(move |email| Self {
state: state.clone(), 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() .collect()
} }

View File

@ -114,7 +114,7 @@ impl RootQuery {
let Some(session) = session else { return Ok(None) }; let Some(session) = session else { return Ok(None) };
let current_user = session.user; let current_user = session.user;
if current_user.data == id { if current_user.id == id {
Ok(Some(User(current_user))) Ok(Some(User(current_user)))
} else { } else {
Ok(None) Ok(None)
@ -141,7 +141,7 @@ impl RootQuery {
.to_option()?; .to_option()?;
let ret = browser_session.and_then(|browser_session| { 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)) Some(BrowserSession(browser_session))
} else { } else {
None None
@ -193,7 +193,7 @@ impl RootQuery {
.to_option()?; .to_option()?;
// Ensure that the link belongs to the current user // 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)) Ok(link.map(UpstreamOAuth2Link::new))
} }

View File

@ -14,16 +14,15 @@
use async_graphql::{Description, Object, ID}; use async_graphql::{Description, Object, ID};
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use mas_storage::PostgresqlBackend;
use super::{NodeType, User}; use super::{NodeType, User};
/// A browser session represents a logged in user in a browser. /// A browser session represents a logged in user in a browser.
#[derive(Description)] #[derive(Description)]
pub struct BrowserSession(pub mas_data_model::BrowserSession<PostgresqlBackend>); pub struct BrowserSession(pub mas_data_model::BrowserSession);
impl From<mas_data_model::BrowserSession<PostgresqlBackend>> for BrowserSession { impl From<mas_data_model::BrowserSession> for BrowserSession {
fn from(v: mas_data_model::BrowserSession<PostgresqlBackend>) -> Self { fn from(v: mas_data_model::BrowserSession) -> Self {
Self(v) Self(v)
} }
} }
@ -32,7 +31,7 @@ impl From<mas_data_model::BrowserSession<PostgresqlBackend>> for BrowserSession
impl BrowserSession { impl BrowserSession {
/// ID of the object. /// ID of the object.
pub async fn id(&self) -> ID { pub async fn id(&self) -> ID {
NodeType::BrowserSession.id(self.0.data) NodeType::BrowserSession.id(self.0.id)
} }
/// The user logged in this session. /// The user logged in this session.
@ -54,13 +53,13 @@ impl BrowserSession {
/// An authentication records when a user enter their credential in a browser /// An authentication records when a user enter their credential in a browser
/// session. /// session.
#[derive(Description)] #[derive(Description)]
pub struct Authentication(pub mas_data_model::Authentication<PostgresqlBackend>); pub struct Authentication(pub mas_data_model::Authentication);
#[Object(use_type_description)] #[Object(use_type_description)]
impl Authentication { impl Authentication {
/// ID of the object. /// ID of the object.
pub async fn id(&self) -> ID { pub async fn id(&self) -> ID {
NodeType::Authentication.id(self.0.data) NodeType::Authentication.id(self.0.id)
} }
/// When the object was created. /// When the object was created.

View File

@ -14,7 +14,6 @@
use async_graphql::{Context, Object, ID}; use async_graphql::{Context, Object, ID};
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use mas_storage::PostgresqlBackend;
use sqlx::PgPool; use sqlx::PgPool;
use super::{NodeType, User}; use super::{NodeType, User};
@ -69,7 +68,7 @@ impl UpstreamOAuth2Link {
pub struct UpstreamOAuth2Link { pub struct UpstreamOAuth2Link {
link: mas_data_model::UpstreamOAuthLink, link: mas_data_model::UpstreamOAuthLink,
provider: Option<mas_data_model::UpstreamOAuthProvider>, provider: Option<mas_data_model::UpstreamOAuthProvider>,
user: Option<mas_data_model::User<PostgresqlBackend>>, user: Option<mas_data_model::User>,
} }
#[Object] #[Object]

View File

@ -17,7 +17,6 @@ use async_graphql::{
Context, Description, Object, ID, Context, Description, Object, ID,
}; };
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use mas_storage::PostgresqlBackend;
use sqlx::PgPool; use sqlx::PgPool;
use super::{ use super::{
@ -27,16 +26,16 @@ use super::{
#[derive(Description)] #[derive(Description)]
/// A user is an individual's account. /// A user is an individual's account.
pub struct User(pub mas_data_model::User<PostgresqlBackend>); pub struct User(pub mas_data_model::User);
impl From<mas_data_model::User<PostgresqlBackend>> for User { impl From<mas_data_model::User> for User {
fn from(v: mas_data_model::User<PostgresqlBackend>) -> Self { fn from(v: mas_data_model::User) -> Self {
Self(v) Self(v)
} }
} }
impl From<mas_data_model::BrowserSession<PostgresqlBackend>> for User { impl From<mas_data_model::BrowserSession> for User {
fn from(v: mas_data_model::BrowserSession<PostgresqlBackend>) -> Self { fn from(v: mas_data_model::BrowserSession) -> Self {
Self(v.user) Self(v.user)
} }
} }
@ -45,7 +44,7 @@ impl From<mas_data_model::BrowserSession<PostgresqlBackend>> for User {
impl User { impl User {
/// ID of the object. /// ID of the object.
pub async fn id(&self) -> ID { pub async fn id(&self) -> ID {
NodeType::User.id(self.0.data) NodeType::User.id(self.0.id)
} }
/// Username chosen by the user. /// Username chosen by the user.
@ -143,7 +142,7 @@ impl User {
let mut connection = Connection::new(has_previous_page, has_next_page); let mut connection = Connection::new(has_previous_page, has_next_page);
connection.edges.extend(edges.into_iter().map(|u| { connection.edges.extend(edges.into_iter().map(|u| {
Edge::new( Edge::new(
OpaqueCursor(NodeCursor(NodeType::BrowserSession, u.data)), OpaqueCursor(NodeCursor(NodeType::BrowserSession, u.id)),
BrowserSession(u), BrowserSession(u),
) )
})); }));
@ -195,7 +194,7 @@ impl User {
); );
connection.edges.extend(edges.into_iter().map(|u| { connection.edges.extend(edges.into_iter().map(|u| {
Edge::new( Edge::new(
OpaqueCursor(NodeCursor(NodeType::UserEmail, u.data)), OpaqueCursor(NodeCursor(NodeType::UserEmail, u.id)),
UserEmail(u), UserEmail(u),
) )
})); }));
@ -309,13 +308,13 @@ impl User {
/// A user email address /// A user email address
#[derive(Description)] #[derive(Description)]
pub struct UserEmail(pub mas_data_model::UserEmail<PostgresqlBackend>); pub struct UserEmail(pub mas_data_model::UserEmail);
#[Object(use_type_description)] #[Object(use_type_description)]
impl UserEmail { impl UserEmail {
/// ID of the object. /// ID of the object.
pub async fn id(&self) -> ID { pub async fn id(&self) -> ID {
NodeType::UserEmail.id(self.0.data) NodeType::UserEmail.id(self.0.id)
} }
/// Email address /// Email address
@ -335,7 +334,7 @@ impl UserEmail {
} }
} }
pub struct UserEmailsPagination(mas_data_model::User<PostgresqlBackend>); pub struct UserEmailsPagination(mas_data_model::User);
#[Object] #[Object]
impl UserEmailsPagination { impl UserEmailsPagination {

View File

@ -186,7 +186,7 @@ impl From<IntoCallbackDestinationError> for GrantCompletionError {
pub(crate) async fn complete( pub(crate) async fn complete(
grant: AuthorizationGrant<PostgresqlBackend>, grant: AuthorizationGrant<PostgresqlBackend>,
browser_session: BrowserSession<PostgresqlBackend>, browser_session: BrowserSession,
policy_factory: &PolicyFactory, policy_factory: &PolicyFactory,
mut txn: Transaction<'_, Postgres>, mut txn: Transaction<'_, Postgres>,
) -> Result<AuthorizationResponse<Option<AccessTokenResponse>>, GrantCompletionError> { ) -> Result<AuthorizationResponse<Option<AccessTokenResponse>>, GrantCompletionError> {

View File

@ -138,7 +138,7 @@ pub(crate) async fn get(
let maybe_user_session = user_session_info.load_session(&mut txn).await?; let maybe_user_session = user_session_info.load_session(&mut txn).await?;
let render = match (maybe_user_session, link.user_id) { 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 // Session already linked, and link matches the currently logged
// user. Mark the session as consumed and renew the authentication. // user. Mark the session as consumed and renew the authentication.
consume_session(&mut txn, &clock, upstream_session).await?; consume_session(&mut txn, &clock, upstream_session).await?;

View File

@ -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 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 { let next = if let Some(action) = query.post_auth_action {
next.and_then(action) next.and_then(action)
} else { } else {

View File

@ -32,7 +32,7 @@ use mas_storage::{
add_user_email, add_user_email_verification_code, get_user_email, get_user_emails, add_user_email, add_user_email_verification_code, get_user_email, get_user_emails,
remove_user_email, set_user_email_as_primary, remove_user_email, set_user_email_as_primary,
}, },
Clock, PostgresqlBackend, Clock,
}; };
use mas_templates::{AccountEmailsContext, EmailVerificationContext, TemplateContext, Templates}; use mas_templates::{AccountEmailsContext, EmailVerificationContext, TemplateContext, Templates};
use rand::{distributions::Uniform, Rng}; use rand::{distributions::Uniform, Rng};
@ -47,9 +47,9 @@ pub mod verify;
#[serde(tag = "action", rename_all = "snake_case")] #[serde(tag = "action", rename_all = "snake_case")]
pub enum ManagementForm { pub enum ManagementForm {
Add { email: String }, Add { email: String },
ResendConfirmation { data: String }, ResendConfirmation { id: String },
SetPrimary { data: String }, SetPrimary { id: String },
Remove { data: String }, Remove { id: String },
} }
pub(crate) async fn get( pub(crate) async fn get(
@ -77,7 +77,7 @@ async fn render(
rng: impl Rng + Send, rng: impl Rng + Send,
clock: &Clock, clock: &Clock,
templates: Templates, templates: Templates,
session: BrowserSession<PostgresqlBackend>, session: BrowserSession,
cookie_jar: PrivateCookieJar<Encrypter>, cookie_jar: PrivateCookieJar<Encrypter>,
executor: impl PgExecutor<'_>, executor: impl PgExecutor<'_>,
) -> Result<Response, FancyError> { ) -> Result<Response, FancyError> {
@ -99,8 +99,8 @@ async fn start_email_verification(
executor: impl PgExecutor<'_>, executor: impl PgExecutor<'_>,
mut rng: impl Rng + Send, mut rng: impl Rng + Send,
clock: &Clock, clock: &Clock,
user: &User<PostgresqlBackend>, user: &User,
user_email: UserEmail<PostgresqlBackend>, user_email: UserEmail,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
// First, generate a code // First, generate a code
let range = Uniform::<u32>::from(0..1_000_000); let range = Uniform::<u32>::from(0..1_000_000);
@ -126,7 +126,7 @@ async fn start_email_verification(
mailer.send_verification_email(mailbox, &context).await?; mailer.send_verification_email(mailbox, &context).await?;
info!( info!(
email.id = %verification.email.data, email.id = %verification.email.id,
"Verification email sent" "Verification email sent"
); );
Ok(()) Ok(())
@ -159,7 +159,7 @@ pub(crate) async fn post(
ManagementForm::Add { email } => { ManagementForm::Add { email } => {
let user_email = let user_email =
add_user_email(&mut txn, &mut rng, &clock, &session.user, email).await?; 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( start_email_verification(
&mailer, &mailer,
&mut txn, &mut txn,
@ -172,11 +172,11 @@ pub(crate) async fn post(
txn.commit().await?; txn.commit().await?;
return Ok((cookie_jar, next.go()).into_response()); return Ok((cookie_jar, next.go()).into_response());
} }
ManagementForm::ResendConfirmation { data } => { ManagementForm::ResendConfirmation { id } => {
let id = data.parse()?; let id = id.parse()?;
let user_email = get_user_email(&mut txn, &session.user, id).await?; 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( start_email_verification(
&mailer, &mailer,
&mut txn, &mut txn,
@ -189,14 +189,14 @@ pub(crate) async fn post(
txn.commit().await?; txn.commit().await?;
return Ok((cookie_jar, next.go()).into_response()); return Ok((cookie_jar, next.go()).into_response());
} }
ManagementForm::Remove { data } => { ManagementForm::Remove { id } => {
let id = data.parse()?; let id = id.parse()?;
let email = get_user_email(&mut txn, &session.user, id).await?; let email = get_user_email(&mut txn, &session.user, id).await?;
remove_user_email(&mut txn, email).await?; remove_user_email(&mut txn, email).await?;
} }
ManagementForm::SetPrimary { data } => { ManagementForm::SetPrimary { id } => {
let id = data.parse()?; let id = id.parse()?;
let email = get_user_email(&mut txn, &session.user, id).await?; let email = get_user_email(&mut txn, &session.user, id).await?;
set_user_email_as_primary(&mut txn, &email).await?; set_user_email_as_primary(&mut txn, &email).await?;
session.user.primary_email = Some(email); session.user.primary_email = Some(email);

View File

@ -27,7 +27,7 @@ use mas_keystore::Encrypter;
use mas_router::Route; use mas_router::Route;
use mas_storage::{ use mas_storage::{
user::{authenticate_session, set_password}, user::{authenticate_session, set_password},
Clock, PostgresqlBackend, Clock,
}; };
use mas_templates::{EmptyContext, TemplateContext, Templates}; use mas_templates::{EmptyContext, TemplateContext, Templates};
use rand::Rng; use rand::Rng;
@ -65,7 +65,7 @@ async fn render(
rng: impl Rng + Send, rng: impl Rng + Send,
clock: &Clock, clock: &Clock,
templates: Templates, templates: Templates,
session: BrowserSession<PostgresqlBackend>, session: BrowserSession,
cookie_jar: PrivateCookieJar<Encrypter>, cookie_jar: PrivateCookieJar<Encrypter>,
) -> Result<Response, FancyError> { ) -> Result<Response, FancyError> {
let (csrf_token, cookie_jar) = cookie_jar.csrf_token(clock.now(), rng); let (csrf_token, cookie_jar) = cookie_jar.csrf_token(clock.now(), rng);

View File

@ -219,7 +219,7 @@ pub(crate) async fn post(
mailer.send_verification_email(mailbox, &context).await?; 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); .and_maybe(query.post_auth_action);
let session = start_session(&mut txn, &mut rng, &clock, user).await?; let session = start_session(&mut txn, &mut rng, &clock, user).await?;

View File

@ -213,7 +213,7 @@ impl Policy {
pub async fn evaluate_authorization_grant<T: StorageBackend + std::fmt::Debug>( pub async fn evaluate_authorization_grant<T: StorageBackend + std::fmt::Debug>(
&mut self, &mut self,
authorization_grant: &AuthorizationGrant<T>, authorization_grant: &AuthorizationGrant<T>,
user: &User<T>, user: &User,
) -> Result<EvaluationResult, anyhow::Error> { ) -> Result<EvaluationResult, anyhow::Error> {
let authorization_grant = serde_json::to_value(authorization_grant)?; let authorization_grant = serde_json::to_value(authorization_grant)?;
let user = serde_json::to_value(user)?; let user = serde_json::to_value(user)?;

View File

@ -136,7 +136,7 @@ pub async fn lookup_active_compat_access_token(
res.user_email_confirmed_at, res.user_email_confirmed_at,
) { ) {
(Some(id), Some(email), Some(created_at), confirmed_at) => Some(UserEmail { (Some(id), Some(email), Some(created_at), confirmed_at) => Some(UserEmail {
data: id.into(), id: id.into(),
email, email,
created_at, created_at,
confirmed_at, confirmed_at,
@ -147,7 +147,7 @@ pub async fn lookup_active_compat_access_token(
let id = Ulid::from(res.user_id); let id = Ulid::from(res.user_id);
let user = User { let user = User {
data: id, id,
username: res.user_username, username: res.user_username,
sub: id.to_string(), sub: id.to_string(),
primary_email, primary_email,
@ -274,7 +274,7 @@ pub async fn lookup_active_compat_refresh_token(
res.user_email_confirmed_at, res.user_email_confirmed_at,
) { ) {
(Some(id), Some(email), Some(created_at), confirmed_at) => Some(UserEmail { (Some(id), Some(email), Some(created_at), confirmed_at) => Some(UserEmail {
data: id.into(), id: id.into(),
email, email,
created_at, created_at,
confirmed_at, confirmed_at,
@ -285,7 +285,7 @@ pub async fn lookup_active_compat_refresh_token(
let id = Ulid::from(res.user_id); let id = Ulid::from(res.user_id);
let user = User { let user = User {
data: id, id,
username: res.user_username, username: res.user_username,
sub: id.to_string(), sub: id.to_string(),
primary_email, primary_email,
@ -326,7 +326,7 @@ pub async fn compat_login(
// First, lookup the user // First, lookup the user
let user = lookup_user_by_username(&mut txn, username).await?; 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 // Now, fetch the hashed password from the user associated with that session
let hashed_password: String = sqlx::query_scalar!( let hashed_password: String = sqlx::query_scalar!(
@ -337,7 +337,7 @@ pub async fn compat_login(
ORDER BY up.created_at DESC ORDER BY up.created_at DESC
LIMIT 1 LIMIT 1
"#, "#,
Uuid::from(user.data), Uuid::from(user.id),
) )
.fetch_one(&mut txn) .fetch_one(&mut txn)
.instrument(tracing::info_span!("Lookup hashed password")) .instrument(tracing::info_span!("Lookup hashed password"))
@ -365,7 +365,7 @@ pub async fn compat_login(
VALUES ($1, $2, $3, $4) VALUES ($1, $2, $3, $4)
"#, "#,
Uuid::from(id), Uuid::from(id),
Uuid::from(user.data), Uuid::from(user.id),
device.as_str(), device.as_str(),
created_at, created_at,
) )
@ -392,7 +392,7 @@ pub async fn compat_login(
compat_session.id = %session.data, compat_session.id = %session.data,
compat_session.device.id = session.device.as_str(), compat_session.device.id = session.device.as_str(),
compat_access_token.id, compat_access_token.id,
user.id = %session.user.data, user.id = %session.user.id,
), ),
err(Display), err(Display),
)] )]
@ -477,7 +477,7 @@ pub async fn expire_compat_access_token(
compat_session.device.id = session.device.as_str(), compat_session.device.id = session.device.as_str(),
compat_access_token.id = %access_token.data, compat_access_token.id = %access_token.data,
compat_refresh_token.id, compat_refresh_token.id,
user.id = %session.user.data, user.id = %session.user.id,
), ),
err(Display), err(Display),
)] )]
@ -668,7 +668,7 @@ impl TryFrom<CompatSsoLoginLookup> for CompatSsoLogin<PostgresqlBackend> {
res.user_email_confirmed_at, res.user_email_confirmed_at,
) { ) {
(Some(id), Some(email), Some(created_at), confirmed_at) => Some(UserEmail { (Some(id), Some(email), Some(created_at), confirmed_at) => Some(UserEmail {
data: id.into(), id: id.into(),
email, email,
created_at, created_at,
confirmed_at, confirmed_at,
@ -681,7 +681,7 @@ impl TryFrom<CompatSsoLoginLookup> for CompatSsoLogin<PostgresqlBackend> {
(Some(id), Some(username), primary_email) => { (Some(id), Some(username), primary_email) => {
let id = Ulid::from(id); let id = Ulid::from(id);
Some(User { Some(User {
data: id, id,
username, username,
sub: id.to_string(), sub: id.to_string(),
primary_email, primary_email,
@ -808,14 +808,14 @@ pub async fn get_compat_sso_login_by_id(
#[tracing::instrument( #[tracing::instrument(
skip_all, skip_all,
fields( fields(
user.id = %user.data, %user.id,
user.username = user.username, %user.username,
), ),
err(Display), err(Display),
)] )]
pub async fn get_paginated_user_compat_sso_logins( pub async fn get_paginated_user_compat_sso_logins(
executor: impl PgExecutor<'_>, executor: impl PgExecutor<'_>,
user: &User<PostgresqlBackend>, user: &User,
before: Option<Ulid>, before: Option<Ulid>,
after: Option<Ulid>, after: Option<Ulid>,
first: Option<usize>, first: Option<usize>,
@ -854,7 +854,7 @@ pub async fn get_paginated_user_compat_sso_logins(
query query
.push(" WHERE cs.user_id = ") .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)?; .generate_pagination("cl.compat_sso_login_id", before, after, first, last)?;
let span = info_span!( let span = info_span!(
@ -919,7 +919,7 @@ pub async fn get_compat_sso_login_by_token(
#[tracing::instrument( #[tracing::instrument(
skip_all, skip_all,
fields( fields(
user.id = %user.data, %user.id,
compat_sso_login.id = %login.data, compat_sso_login.id = %login.data,
compat_sso_login.redirect_uri = %login.redirect_uri, compat_sso_login.redirect_uri = %login.redirect_uri,
compat_session.id, compat_session.id,
@ -931,7 +931,7 @@ pub async fn fullfill_compat_sso_login(
conn: impl Acquire<'_, Database = Postgres> + Send, conn: impl Acquire<'_, Database = Postgres> + Send,
mut rng: impl Rng + Send, mut rng: impl Rng + Send,
clock: &Clock, clock: &Clock,
user: User<PostgresqlBackend>, user: User,
mut login: CompatSsoLogin<PostgresqlBackend>, mut login: CompatSsoLogin<PostgresqlBackend>,
device: Device, device: Device,
) -> Result<CompatSsoLogin<PostgresqlBackend>, anyhow::Error> { ) -> Result<CompatSsoLogin<PostgresqlBackend>, anyhow::Error> {
@ -943,7 +943,7 @@ pub async fn fullfill_compat_sso_login(
let created_at = clock.now(); let created_at = clock.now();
let id = Ulid::from_datetime_with_source(created_at.into(), &mut rng); 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!( sqlx::query!(
r#" r#"
@ -951,7 +951,7 @@ pub async fn fullfill_compat_sso_login(
VALUES ($1, $2, $3, $4) VALUES ($1, $2, $3, $4)
"#, "#,
Uuid::from(id), Uuid::from(id),
Uuid::from(user.data), Uuid::from(user.id),
device.as_str(), device.as_str(),
created_at, created_at,
) )

View File

@ -105,20 +105,13 @@ pub struct DatabaseInconsistencyError;
pub struct PostgresqlBackend; pub struct PostgresqlBackend;
impl StorageBackend for PostgresqlBackend { impl StorageBackend for PostgresqlBackend {
type AccessTokenData = Ulid;
type AuthenticationData = Ulid;
type AuthorizationGrantData = Ulid; type AuthorizationGrantData = Ulid;
type BrowserSessionData = Ulid;
type ClientData = Ulid; type ClientData = Ulid;
type CompatAccessTokenData = Ulid; type CompatAccessTokenData = Ulid;
type CompatRefreshTokenData = Ulid; type CompatRefreshTokenData = Ulid;
type CompatSessionData = Ulid; type CompatSessionData = Ulid;
type CompatSsoLoginData = Ulid; type CompatSsoLoginData = Ulid;
type RefreshTokenData = Ulid;
type SessionData = Ulid; type SessionData = Ulid;
type UserData = Ulid;
type UserEmailData = Ulid;
type UserEmailVerificationData = Ulid;
} }
impl StorageBackendMarker for PostgresqlBackend {} impl StorageBackendMarker for PostgresqlBackend {}

View File

@ -29,7 +29,7 @@ use crate::{Clock, DatabaseInconsistencyError, LookupError, PostgresqlBackend};
fields( fields(
session.id = %session.data, session.id = %session.data,
client.id = %session.client.data, client.id = %session.client.data,
user.id = %session.browser_session.user.data, user.id = %session.browser_session.user.id,
access_token.id, access_token.id,
), ),
err(Debug), err(Debug),
@ -178,7 +178,7 @@ pub async fn lookup_active_access_token(
res.user_email_confirmed_at, res.user_email_confirmed_at,
) { ) {
(Some(id), Some(email), Some(created_at), confirmed_at) => Some(UserEmail { (Some(id), Some(email), Some(created_at), confirmed_at) => Some(UserEmail {
data: id.into(), id: id.into(),
email, email,
created_at, created_at,
confirmed_at, confirmed_at,
@ -189,7 +189,7 @@ pub async fn lookup_active_access_token(
let id = Ulid::from(res.user_id); let id = Ulid::from(res.user_id);
let user = User { let user = User {
data: id, id,
username: res.user_username, username: res.user_username,
sub: id.to_string(), sub: id.to_string(),
primary_email, primary_email,
@ -201,14 +201,14 @@ pub async fn lookup_active_access_token(
) { ) {
(None, None) => None, (None, None) => None,
(Some(id), Some(created_at)) => Some(Authentication { (Some(id), Some(created_at)) => Some(Authentication {
data: id.into(), id: id.into(),
created_at, created_at,
}), }),
_ => return Err(DatabaseInconsistencyError.into()), _ => return Err(DatabaseInconsistencyError.into()),
}; };
let browser_session = BrowserSession { let browser_session = BrowserSession {
data: res.user_session_id.into(), id: res.user_session_id.into(),
created_at: res.user_session_created_at, created_at: res.user_session_created_at,
user, user,
last_authentication, last_authentication,

View File

@ -187,7 +187,7 @@ impl GrantLookup {
self.user_session_last_authentication_created_at, self.user_session_last_authentication_created_at,
) { ) {
(Some(id), Some(created_at)) => Some(Authentication { (Some(id), Some(created_at)) => Some(Authentication {
data: id.into(), id: id.into(),
created_at, created_at,
}), }),
(None, None) => None, (None, None) => None,
@ -201,7 +201,7 @@ impl GrantLookup {
self.user_email_confirmed_at, self.user_email_confirmed_at,
) { ) {
(Some(id), Some(email), Some(created_at), confirmed_at) => Some(UserEmail { (Some(id), Some(email), Some(created_at), confirmed_at) => Some(UserEmail {
data: id.into(), id: id.into(),
email, email,
created_at, created_at,
confirmed_at, confirmed_at,
@ -230,14 +230,14 @@ impl GrantLookup {
) => { ) => {
let user_id = Ulid::from(user_id); let user_id = Ulid::from(user_id);
let user = User { let user = User {
data: user_id, id: user_id,
username: user_username, username: user_username,
sub: user_id.to_string(), sub: user_id.to_string(),
primary_email, primary_email,
}; };
let browser_session = BrowserSession { let browser_session = BrowserSession {
data: user_session_id.into(), id: user_session_id.into(),
user, user,
created_at: user_session_created_at, created_at: user_session_created_at,
last_authentication, last_authentication,
@ -500,8 +500,8 @@ pub async fn lookup_grant_by_code(
grant.id = %grant.data, grant.id = %grant.data,
client.id = %grant.client.data, client.id = %grant.client.data,
session.id, session.id,
user_session.id = %browser_session.data, user_session.id = %browser_session.id,
user.id = %browser_session.user.data, user.id = %browser_session.user.id,
), ),
err(Debug), err(Debug),
)] )]
@ -510,7 +510,7 @@ pub async fn derive_session(
mut rng: impl Rng + Send, mut rng: impl Rng + Send,
clock: &Clock, clock: &Clock,
grant: &AuthorizationGrant<PostgresqlBackend>, grant: &AuthorizationGrant<PostgresqlBackend>,
browser_session: BrowserSession<PostgresqlBackend>, browser_session: BrowserSession,
) -> Result<Session<PostgresqlBackend>, anyhow::Error> { ) -> Result<Session<PostgresqlBackend>, anyhow::Error> {
let created_at = clock.now(); let created_at = clock.now();
let id = Ulid::from_datetime_with_source(created_at.into(), &mut rng); 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 og.oauth2_authorization_grant_id = $4
"#, "#,
Uuid::from(id), Uuid::from(id),
Uuid::from(browser_session.data), Uuid::from(browser_session.id),
created_at, created_at,
Uuid::from(grant.data), Uuid::from(grant.data),
) )
@ -554,8 +554,8 @@ pub async fn derive_session(
grant.id = %grant.data, grant.id = %grant.data,
client.id = %grant.client.data, client.id = %grant.client.data,
session.id = %session.data, session.id = %session.data,
user_session.id = %session.browser_session.data, user_session.id = %session.browser_session.id,
user.id = %session.browser_session.user.data, user.id = %session.browser_session.user.id,
), ),
err(Debug), err(Debug),
)] )]

View File

@ -26,14 +26,14 @@ use crate::{Clock, PostgresqlBackend};
#[tracing::instrument( #[tracing::instrument(
skip_all, skip_all,
fields( fields(
user.id = %user.data, %user.id,
client.id = %client.data, client.id = %client.data,
), ),
err(Debug), err(Debug),
)] )]
pub async fn fetch_client_consent( pub async fn fetch_client_consent(
executor: impl PgExecutor<'_>, executor: impl PgExecutor<'_>,
user: &User<PostgresqlBackend>, user: &User,
client: &Client<PostgresqlBackend>, client: &Client<PostgresqlBackend>,
) -> Result<Scope, anyhow::Error> { ) -> Result<Scope, anyhow::Error> {
let scope_tokens: Vec<String> = sqlx::query_scalar!( let scope_tokens: Vec<String> = sqlx::query_scalar!(
@ -42,7 +42,7 @@ pub async fn fetch_client_consent(
FROM oauth2_consents FROM oauth2_consents
WHERE user_id = $1 AND oauth2_client_id = $2 WHERE user_id = $1 AND oauth2_client_id = $2
"#, "#,
Uuid::from(user.data), Uuid::from(user.id),
Uuid::from(client.data), Uuid::from(client.data),
) )
.fetch_all(executor) .fetch_all(executor)
@ -59,7 +59,7 @@ pub async fn fetch_client_consent(
#[tracing::instrument( #[tracing::instrument(
skip_all, skip_all,
fields( fields(
user.id = %user.data, %user.id,
client.id = %client.data, client.id = %client.data,
scope = scope.to_string(), scope = scope.to_string(),
), ),
@ -69,7 +69,7 @@ pub async fn insert_client_consent(
executor: impl PgExecutor<'_>, executor: impl PgExecutor<'_>,
mut rng: impl Rng + Send, mut rng: impl Rng + Send,
clock: &Clock, clock: &Clock,
user: &User<PostgresqlBackend>, user: &User,
client: &Client<PostgresqlBackend>, client: &Client<PostgresqlBackend>,
scope: &Scope, scope: &Scope,
) -> Result<(), anyhow::Error> { ) -> 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 ON CONFLICT (user_id, oauth2_client_id, scope_token) DO UPDATE SET refreshed_at = $5
"#, "#,
&ids, &ids,
Uuid::from(user.data), Uuid::from(user.id),
Uuid::from(client.data), Uuid::from(client.data),
&tokens, &tokens,
now, now,

View File

@ -38,8 +38,8 @@ pub mod refresh_token;
skip_all, skip_all,
fields( fields(
session.id = %session.data, session.id = %session.data,
user.id = %session.browser_session.user.data, user.id = %session.browser_session.user.id,
user_session.id = %session.browser_session.data, user_session.id = %session.browser_session.id,
client.id = %session.client.data, client.id = %session.client.data,
), ),
err(Debug), err(Debug),
@ -78,14 +78,14 @@ struct OAuthSessionLookup {
#[tracing::instrument( #[tracing::instrument(
skip_all, skip_all,
fields( fields(
user.id = %user.data, %user.id,
user.username = user.username, user.username = user.username,
), ),
err(Display), err(Display),
)] )]
pub async fn get_paginated_user_oauth_sessions( pub async fn get_paginated_user_oauth_sessions(
conn: &mut PgConnection, conn: &mut PgConnection,
user: &User<PostgresqlBackend>, user: &User,
before: Option<Ulid>, before: Option<Ulid>,
after: Option<Ulid>, after: Option<Ulid>,
first: Option<usize>, first: Option<usize>,
@ -108,7 +108,7 @@ pub async fn get_paginated_user_oauth_sessions(
query query
.push(" WHERE us.user_id = ") .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)?; .generate_pagination("oauth2_session_id", before, after, first, last)?;
let span = info_span!( 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 // TODO: this can generate N queries instead of batching. This is less than
// ideal // ideal
let mut browser_sessions: HashMap<Ulid, BrowserSession<PostgresqlBackend>> = HashMap::new(); let mut browser_sessions: HashMap<Ulid, BrowserSession> = HashMap::new();
for id in browser_session_ids { for id in browser_session_ids {
let v = lookup_active_session(&mut *conn, id).await?; let v = lookup_active_session(&mut *conn, id).await?;
browser_sessions.insert(id, v); browser_sessions.insert(id, v);

View File

@ -30,8 +30,8 @@ use crate::{Clock, DatabaseInconsistencyError, LookupError, PostgresqlBackend};
skip_all, skip_all,
fields( fields(
session.id = %session.data, session.id = %session.data,
user.id = %session.browser_session.user.data, user.id = %session.browser_session.user.id,
user_session.id = %session.browser_session.data, user_session.id = %session.browser_session.id,
client.id = %session.client.data, client.id = %session.client.data,
refresh_token.id, refresh_token.id,
), ),
@ -206,7 +206,7 @@ pub async fn lookup_active_refresh_token(
res.user_email_confirmed_at, res.user_email_confirmed_at,
) { ) {
(Some(id), Some(email), Some(created_at), confirmed_at) => Some(UserEmail { (Some(id), Some(email), Some(created_at), confirmed_at) => Some(UserEmail {
data: id.into(), id: id.into(),
email, email,
created_at, created_at,
confirmed_at, confirmed_at,
@ -217,7 +217,7 @@ pub async fn lookup_active_refresh_token(
let id = Ulid::from(res.user_id); let id = Ulid::from(res.user_id);
let user = User { let user = User {
data: id, id,
username: res.user_username, username: res.user_username,
sub: id.to_string(), sub: id.to_string(),
primary_email, primary_email,
@ -229,14 +229,14 @@ pub async fn lookup_active_refresh_token(
) { ) {
(None, None) => None, (None, None) => None,
(Some(id), Some(created_at)) => Some(Authentication { (Some(id), Some(created_at)) => Some(Authentication {
data: id.into(), id: id.into(),
created_at, created_at,
}), }),
_ => return Err(DatabaseInconsistencyError.into()), _ => return Err(DatabaseInconsistencyError.into()),
}; };
let browser_session = BrowserSession { let browser_session = BrowserSession {
data: res.user_session_id.into(), id: res.user_session_id.into(),
created_at: res.user_session_created_at, created_at: res.user_session_created_at,
user, user,
last_authentication, last_authentication,

View File

@ -22,7 +22,7 @@ use uuid::Uuid;
use crate::{ use crate::{
pagination::{process_page, QueryBuilderExt}, pagination::{process_page, QueryBuilderExt},
Clock, GenericLookupError, PostgresqlBackend, Clock, GenericLookupError,
}; };
#[derive(sqlx::FromRow)] #[derive(sqlx::FromRow)]
@ -168,7 +168,7 @@ pub async fn add_link(
fields( fields(
%upstream_oauth_link.id, %upstream_oauth_link.id,
%upstream_oauth_link.subject, %upstream_oauth_link.subject,
user.id = %user.data, %user.id,
%user.username, %user.username,
), ),
err, err,
@ -176,7 +176,7 @@ pub async fn add_link(
pub async fn associate_link_to_user( pub async fn associate_link_to_user(
executor: impl PgExecutor<'_>, executor: impl PgExecutor<'_>,
upstream_oauth_link: &UpstreamOAuthLink, upstream_oauth_link: &UpstreamOAuthLink,
user: &User<PostgresqlBackend>, user: &User,
) -> Result<(), sqlx::Error> { ) -> Result<(), sqlx::Error> {
sqlx::query!( sqlx::query!(
r#" r#"
@ -184,7 +184,7 @@ pub async fn associate_link_to_user(
SET user_id = $1 SET user_id = $1
WHERE upstream_oauth_link_id = $2 WHERE upstream_oauth_link_id = $2
"#, "#,
Uuid::from(user.data), Uuid::from(user.id),
Uuid::from(upstream_oauth_link.id), Uuid::from(upstream_oauth_link.id),
) )
.execute(executor) .execute(executor)
@ -193,10 +193,14 @@ pub async fn associate_link_to_user(
Ok(()) 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( pub async fn get_paginated_user_links(
executor: impl PgExecutor<'_>, executor: impl PgExecutor<'_>,
user: &User<PostgresqlBackend>, user: &User,
before: Option<Ulid>, before: Option<Ulid>,
after: Option<Ulid>, after: Option<Ulid>,
first: Option<usize>, first: Option<usize>,
@ -216,7 +220,7 @@ pub async fn get_paginated_user_links(
query query
.push(" WHERE user_id = ") .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)?; .generate_pagination("upstream_oauth_link_id", before, after, first, last)?;
let span = info_span!( let span = info_span!(

View File

@ -30,7 +30,7 @@ use tracing::{info_span, Instrument};
use ulid::Ulid; use ulid::Ulid;
use uuid::Uuid; use uuid::Uuid;
use super::{DatabaseInconsistencyError, PostgresqlBackend}; use super::DatabaseInconsistencyError;
use crate::{ use crate::{
pagination::{process_page, QueryBuilderExt}, pagination::{process_page, QueryBuilderExt},
Clock, GenericLookupError, LookupError, Clock, GenericLookupError, LookupError,
@ -77,7 +77,7 @@ pub async fn login(
clock: &Clock, clock: &Clock,
username: &str, username: &str,
password: &str, password: &str,
) -> Result<BrowserSession<PostgresqlBackend>, LoginError> { ) -> Result<BrowserSession, LoginError> {
let mut txn = conn.begin().await.context("could not start transaction")?; let mut txn = conn.begin().await.context("could not start transaction")?;
let user = lookup_user_by_username(&mut txn, username) let user = lookup_user_by_username(&mut txn, username)
.await .await
@ -137,10 +137,10 @@ struct SessionLookup {
user_email_confirmed_at: Option<DateTime<Utc>>, user_email_confirmed_at: Option<DateTime<Utc>>,
} }
impl TryInto<BrowserSession<PostgresqlBackend>> for SessionLookup { impl TryInto<BrowserSession> for SessionLookup {
type Error = DatabaseInconsistencyError; type Error = DatabaseInconsistencyError;
fn try_into(self) -> Result<BrowserSession<PostgresqlBackend>, Self::Error> { fn try_into(self) -> Result<BrowserSession, Self::Error> {
let primary_email = match ( let primary_email = match (
self.user_email_id, self.user_email_id,
self.user_email, self.user_email,
@ -148,7 +148,7 @@ impl TryInto<BrowserSession<PostgresqlBackend>> for SessionLookup {
self.user_email_confirmed_at, self.user_email_confirmed_at,
) { ) {
(Some(id), Some(email), Some(created_at), confirmed_at) => Some(UserEmail { (Some(id), Some(email), Some(created_at), confirmed_at) => Some(UserEmail {
data: id.into(), id: id.into(),
email, email,
created_at, created_at,
confirmed_at, confirmed_at,
@ -159,7 +159,7 @@ impl TryInto<BrowserSession<PostgresqlBackend>> for SessionLookup {
let id = Ulid::from(self.user_id); let id = Ulid::from(self.user_id);
let user = User { let user = User {
data: id, id,
username: self.username, username: self.username,
sub: id.to_string(), sub: id.to_string(),
primary_email, primary_email,
@ -167,7 +167,7 @@ impl TryInto<BrowserSession<PostgresqlBackend>> for SessionLookup {
let last_authentication = match (self.last_authentication_id, self.last_authd_at) { let last_authentication = match (self.last_authentication_id, self.last_authd_at) {
(Some(id), Some(created_at)) => Some(Authentication { (Some(id), Some(created_at)) => Some(Authentication {
data: id.into(), id: id.into(),
created_at, created_at,
}), }),
(None, None) => None, (None, None) => None,
@ -175,7 +175,7 @@ impl TryInto<BrowserSession<PostgresqlBackend>> for SessionLookup {
}; };
Ok(BrowserSession { Ok(BrowserSession {
data: self.user_session_id.into(), id: self.user_session_id.into(),
user, user,
created_at: self.created_at, created_at: self.created_at,
last_authentication, last_authentication,
@ -191,7 +191,7 @@ impl TryInto<BrowserSession<PostgresqlBackend>> for SessionLookup {
pub async fn lookup_active_session( pub async fn lookup_active_session(
executor: impl PgExecutor<'_>, executor: impl PgExecutor<'_>,
id: Ulid, id: Ulid,
) -> Result<BrowserSession<PostgresqlBackend>, ActiveSessionLookupError> { ) -> Result<BrowserSession, ActiveSessionLookupError> {
let res = sqlx::query_as!( let res = sqlx::query_as!(
SessionLookup, SessionLookup,
r#" r#"
@ -229,19 +229,19 @@ pub async fn lookup_active_session(
#[tracing::instrument( #[tracing::instrument(
skip_all, skip_all,
fields( fields(
user.id = %user.data, %user.id,
user.username = user.username, %user.username,
), ),
err(Display), err(Display),
)] )]
pub async fn get_paginated_user_sessions( pub async fn get_paginated_user_sessions(
executor: impl PgExecutor<'_>, executor: impl PgExecutor<'_>,
user: &User<PostgresqlBackend>, user: &User,
before: Option<Ulid>, before: Option<Ulid>,
after: Option<Ulid>, after: Option<Ulid>,
first: Option<usize>, first: Option<usize>,
last: Option<usize>, last: Option<usize>,
) -> Result<(bool, bool, Vec<BrowserSession<PostgresqlBackend>>), anyhow::Error> { ) -> Result<(bool, bool, Vec<BrowserSession>), anyhow::Error> {
let mut query = QueryBuilder::new( let mut query = QueryBuilder::new(
r#" r#"
SELECT SELECT
@ -267,7 +267,7 @@ pub async fn get_paginated_user_sessions(
query query
.push(" WHERE s.finished_at IS NULL AND s.user_id = ") .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)?; .generate_pagination("s.user_session_id", before, after, first, last)?;
let span = info_span!("Fetch paginated user emails", db.statement = query.sql()); 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( #[tracing::instrument(
skip_all, skip_all,
fields( fields(
user.id = %user.data, %user.id,
user_session.id, user_session.id,
), ),
err(Display), err(Display),
@ -295,8 +295,8 @@ pub async fn start_session(
executor: impl PgExecutor<'_>, executor: impl PgExecutor<'_>,
mut rng: impl Rng + Send, mut rng: impl Rng + Send,
clock: &Clock, clock: &Clock,
user: User<PostgresqlBackend>, user: User,
) -> Result<BrowserSession<PostgresqlBackend>, anyhow::Error> { ) -> Result<BrowserSession, anyhow::Error> {
let created_at = clock.now(); let created_at = clock.now();
let id = Ulid::from_datetime_with_source(created_at.into(), &mut rng); let id = Ulid::from_datetime_with_source(created_at.into(), &mut rng);
tracing::Span::current().record("user_session.id", tracing::field::display(id)); tracing::Span::current().record("user_session.id", tracing::field::display(id));
@ -307,7 +307,7 @@ pub async fn start_session(
VALUES ($1, $2, $3) VALUES ($1, $2, $3)
"#, "#,
Uuid::from(id), Uuid::from(id),
Uuid::from(user.data), Uuid::from(user.id),
created_at, created_at,
) )
.execute(executor) .execute(executor)
@ -315,7 +315,7 @@ pub async fn start_session(
.context("could not create session")?; .context("could not create session")?;
let session = BrowserSession { let session = BrowserSession {
data: id, id,
user, user,
created_at, created_at,
last_authentication: None, last_authentication: None,
@ -326,12 +326,12 @@ pub async fn start_session(
#[tracing::instrument( #[tracing::instrument(
skip_all, skip_all,
fields(user.id = %user.data), fields(%user.id),
err(Display), err(Display),
)] )]
pub async fn count_active_sessions( pub async fn count_active_sessions(
executor: impl PgExecutor<'_>, executor: impl PgExecutor<'_>,
user: &User<PostgresqlBackend>, user: &User,
) -> Result<usize, anyhow::Error> { ) -> Result<usize, anyhow::Error> {
let res = sqlx::query_scalar!( let res = sqlx::query_scalar!(
r#" r#"
@ -339,7 +339,7 @@ pub async fn count_active_sessions(
FROM user_sessions s FROM user_sessions s
WHERE s.user_id = $1 AND s.finished_at IS NULL WHERE s.user_id = $1 AND s.finished_at IS NULL
"#, "#,
Uuid::from(user.data), Uuid::from(user.id),
) )
.fetch_one(executor) .fetch_one(executor)
.await? .await?
@ -366,8 +366,8 @@ pub enum AuthenticationError {
#[tracing::instrument( #[tracing::instrument(
skip_all, skip_all,
fields( fields(
user.id = %session.user.data, user.id = %user_session.user.id,
user_session.id = %session.data, %user_session.id,
user_session_authentication.id, user_session_authentication.id,
), ),
err, err,
@ -376,7 +376,7 @@ pub async fn authenticate_session(
txn: &mut Transaction<'_, Postgres>, txn: &mut Transaction<'_, Postgres>,
mut rng: impl Rng + Send, mut rng: impl Rng + Send,
clock: &Clock, clock: &Clock,
session: &mut BrowserSession<PostgresqlBackend>, user_session: &mut BrowserSession,
password: &str, password: &str,
) -> Result<(), AuthenticationError> { ) -> Result<(), AuthenticationError> {
// First, fetch the hashed password from the user associated with that session // 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 ORDER BY up.created_at DESC
LIMIT 1 LIMIT 1
"#, "#,
Uuid::from(session.user.data), Uuid::from(user_session.user.id),
) )
.fetch_one(txn.borrow_mut()) .fetch_one(txn.borrow_mut())
.instrument(tracing::info_span!("Lookup hashed password")) .instrument(tracing::info_span!("Lookup hashed password"))
@ -423,7 +423,7 @@ pub async fn authenticate_session(
VALUES ($1, $2, $3) VALUES ($1, $2, $3)
"#, "#,
Uuid::from(id), Uuid::from(id),
Uuid::from(session.data), Uuid::from(user_session.id),
created_at, created_at,
) )
.execute(txn.borrow_mut()) .execute(txn.borrow_mut())
@ -431,10 +431,7 @@ pub async fn authenticate_session(
.await .await
.map_err(AuthenticationError::Save)?; .map_err(AuthenticationError::Save)?;
session.last_authentication = Some(Authentication { user_session.last_authentication = Some(Authentication { id, created_at });
data: id,
created_at,
});
Ok(()) Ok(())
} }
@ -442,9 +439,9 @@ pub async fn authenticate_session(
#[tracing::instrument( #[tracing::instrument(
skip_all, skip_all,
fields( fields(
user.id = %session.user.data, user.id = %user_session.user.id,
%upstream_oauth_link.id, %upstream_oauth_link.id,
user_session.id = %session.data, %user_session.id,
user_session_authentication.id, user_session_authentication.id,
), ),
err, err,
@ -453,7 +450,7 @@ pub async fn authenticate_session_with_upstream(
executor: impl PgExecutor<'_>, executor: impl PgExecutor<'_>,
mut rng: impl Rng + Send, mut rng: impl Rng + Send,
clock: &Clock, clock: &Clock,
session: &mut BrowserSession<PostgresqlBackend>, user_session: &mut BrowserSession,
upstream_oauth_link: &UpstreamOAuthLink, upstream_oauth_link: &UpstreamOAuthLink,
) -> Result<(), sqlx::Error> { ) -> Result<(), sqlx::Error> {
let created_at = clock.now(); let created_at = clock.now();
@ -470,17 +467,14 @@ pub async fn authenticate_session_with_upstream(
VALUES ($1, $2, $3) VALUES ($1, $2, $3)
"#, "#,
Uuid::from(id), Uuid::from(id),
Uuid::from(session.data), Uuid::from(user_session.id),
created_at, created_at,
) )
.execute(executor) .execute(executor)
.instrument(tracing::info_span!("Save authentication")) .instrument(tracing::info_span!("Save authentication"))
.await?; .await?;
session.last_authentication = Some(Authentication { user_session.last_authentication = Some(Authentication { id, created_at });
data: id,
created_at,
});
Ok(()) Ok(())
} }
@ -500,7 +494,7 @@ pub async fn register_user(
phf: impl PasswordHasher + Send, phf: impl PasswordHasher + Send,
username: &str, username: &str,
password: &str, password: &str,
) -> Result<User<PostgresqlBackend>, anyhow::Error> { ) -> Result<User, anyhow::Error> {
let created_at = clock.now(); let created_at = clock.now();
let id = Ulid::from_datetime_with_source(created_at.into(), &mut rng); let id = Ulid::from_datetime_with_source(created_at.into(), &mut rng);
tracing::Span::current().record("user.id", tracing::field::display(id)); tracing::Span::current().record("user.id", tracing::field::display(id));
@ -520,7 +514,7 @@ pub async fn register_user(
.context("could not insert user")?; .context("could not insert user")?;
let user = User { let user = User {
data: id, id,
username: username.to_owned(), username: username.to_owned(),
sub: id.to_string(), sub: id.to_string(),
primary_email: None, primary_email: None,
@ -544,7 +538,7 @@ pub async fn register_passwordless_user(
mut rng: impl Rng + Send, mut rng: impl Rng + Send,
clock: &Clock, clock: &Clock,
username: &str, username: &str,
) -> Result<User<PostgresqlBackend>, sqlx::Error> { ) -> Result<User, sqlx::Error> {
let created_at = clock.now(); let created_at = clock.now();
let id = Ulid::from_datetime_with_source(created_at.into(), &mut rng); let id = Ulid::from_datetime_with_source(created_at.into(), &mut rng);
tracing::Span::current().record("user.id", tracing::field::display(id)); tracing::Span::current().record("user.id", tracing::field::display(id));
@ -562,7 +556,7 @@ pub async fn register_passwordless_user(
.await?; .await?;
Ok(User { Ok(User {
data: id, id,
username: username.to_owned(), username: username.to_owned(),
sub: id.to_string(), sub: id.to_string(),
primary_email: None, primary_email: None,
@ -572,7 +566,7 @@ pub async fn register_passwordless_user(
#[tracing::instrument( #[tracing::instrument(
skip_all, skip_all,
fields( fields(
user.id = %user.data, %user.id,
user_password.id, user_password.id,
), ),
err(Display), err(Display),
@ -582,7 +576,7 @@ pub async fn set_password(
mut rng: impl CryptoRng + Rng + Send, mut rng: impl CryptoRng + Rng + Send,
clock: &Clock, clock: &Clock,
phf: impl PasswordHasher + Send, phf: impl PasswordHasher + Send,
user: &User<PostgresqlBackend>, user: &User,
password: &str, password: &str,
) -> Result<(), anyhow::Error> { ) -> Result<(), anyhow::Error> {
let created_at = clock.now(); let created_at = clock.now();
@ -598,7 +592,7 @@ pub async fn set_password(
VALUES ($1, $2, $3, $4) VALUES ($1, $2, $3, $4)
"#, "#,
Uuid::from(id), Uuid::from(id),
Uuid::from(user.data), Uuid::from(user.id),
hashed_password.to_string(), hashed_password.to_string(),
created_at, created_at,
) )
@ -612,13 +606,13 @@ pub async fn set_password(
#[tracing::instrument( #[tracing::instrument(
skip_all, skip_all,
fields(user_session.id = %session.data), fields(%user_session.id),
err(Display), err(Display),
)] )]
pub async fn end_session( pub async fn end_session(
executor: impl PgExecutor<'_>, executor: impl PgExecutor<'_>,
clock: &Clock, clock: &Clock,
session: &BrowserSession<PostgresqlBackend>, user_session: &BrowserSession,
) -> Result<(), anyhow::Error> { ) -> Result<(), anyhow::Error> {
let now = clock.now(); let now = clock.now();
let res = sqlx::query!( let res = sqlx::query!(
@ -628,7 +622,7 @@ pub async fn end_session(
WHERE user_session_id = $2 WHERE user_session_id = $2
"#, "#,
now, now,
Uuid::from(session.data), Uuid::from(user_session.id),
) )
.execute(executor) .execute(executor)
.instrument(info_span!("End session")) .instrument(info_span!("End session"))
@ -663,7 +657,7 @@ impl LookupError for UserLookupError {
pub async fn lookup_user_by_username( pub async fn lookup_user_by_username(
executor: impl PgExecutor<'_>, executor: impl PgExecutor<'_>,
username: &str, username: &str,
) -> Result<User<PostgresqlBackend>, UserLookupError> { ) -> Result<User, UserLookupError> {
let res = sqlx::query_as!( let res = sqlx::query_as!(
UserLookup, UserLookup,
r#" r#"
@ -694,7 +688,7 @@ pub async fn lookup_user_by_username(
res.user_email_confirmed_at, res.user_email_confirmed_at,
) { ) {
(Some(id), Some(email), Some(created_at), confirmed_at) => Some(UserEmail { (Some(id), Some(email), Some(created_at), confirmed_at) => Some(UserEmail {
data: id.into(), id: id.into(),
email, email,
created_at, created_at,
confirmed_at, confirmed_at,
@ -705,7 +699,7 @@ pub async fn lookup_user_by_username(
let id = Ulid::from(res.user_id); let id = Ulid::from(res.user_id);
Ok(User { Ok(User {
data: id, id,
username: res.user_username, username: res.user_username,
sub: id.to_string(), sub: id.to_string(),
primary_email, primary_email,
@ -717,10 +711,7 @@ pub async fn lookup_user_by_username(
fields(user.id = %id), fields(user.id = %id),
err, err,
)] )]
pub async fn lookup_user( pub async fn lookup_user(executor: impl PgExecutor<'_>, id: Ulid) -> Result<User, UserLookupError> {
executor: impl PgExecutor<'_>,
id: Ulid,
) -> Result<User<PostgresqlBackend>, UserLookupError> {
let res = sqlx::query_as!( let res = sqlx::query_as!(
UserLookup, UserLookup,
r#" r#"
@ -751,7 +742,7 @@ pub async fn lookup_user(
res.user_email_confirmed_at, res.user_email_confirmed_at,
) { ) {
(Some(id), Some(email), Some(created_at), confirmed_at) => Some(UserEmail { (Some(id), Some(email), Some(created_at), confirmed_at) => Some(UserEmail {
data: id.into(), id: id.into(),
email, email,
created_at, created_at,
confirmed_at, confirmed_at,
@ -762,7 +753,7 @@ pub async fn lookup_user(
let id = Ulid::from(res.user_id); let id = Ulid::from(res.user_id);
Ok(User { Ok(User {
data: id, id,
username: res.user_username, username: res.user_username,
sub: id.to_string(), sub: id.to_string(),
primary_email, primary_email,
@ -798,10 +789,10 @@ struct UserEmailLookup {
user_email_confirmed_at: Option<DateTime<Utc>>, user_email_confirmed_at: Option<DateTime<Utc>>,
} }
impl From<UserEmailLookup> for UserEmail<PostgresqlBackend> { impl From<UserEmailLookup> for UserEmail {
fn from(e: UserEmailLookup) -> UserEmail<PostgresqlBackend> { fn from(e: UserEmailLookup) -> UserEmail {
UserEmail { UserEmail {
data: e.user_email_id.into(), id: e.user_email_id.into(),
email: e.user_email, email: e.user_email,
created_at: e.user_email_created_at, created_at: e.user_email_created_at,
confirmed_at: e.user_email_confirmed_at, confirmed_at: e.user_email_confirmed_at,
@ -811,13 +802,13 @@ impl From<UserEmailLookup> for UserEmail<PostgresqlBackend> {
#[tracing::instrument( #[tracing::instrument(
skip_all, skip_all,
fields(user.id = %user.data, user.username = user.username), fields(%user.id, %user.username),
err(Display), err(Display),
)] )]
pub async fn get_user_emails( pub async fn get_user_emails(
executor: impl PgExecutor<'_>, executor: impl PgExecutor<'_>,
user: &User<PostgresqlBackend>, user: &User,
) -> Result<Vec<UserEmail<PostgresqlBackend>>, anyhow::Error> { ) -> Result<Vec<UserEmail>, anyhow::Error> {
let res = sqlx::query_as!( let res = sqlx::query_as!(
UserEmailLookup, UserEmailLookup,
r#" r#"
@ -832,7 +823,7 @@ pub async fn get_user_emails(
ORDER BY ue.email ASC ORDER BY ue.email ASC
"#, "#,
Uuid::from(user.data), Uuid::from(user.id),
) )
.fetch_all(executor) .fetch_all(executor)
.instrument(info_span!("Fetch user emails")) .instrument(info_span!("Fetch user emails"))
@ -843,12 +834,12 @@ pub async fn get_user_emails(
#[tracing::instrument( #[tracing::instrument(
skip_all, skip_all,
fields(user.id = %user.data, user.username = user.username), fields(%user.id, %user.username),
err(Display), err(Display),
)] )]
pub async fn count_user_emails( pub async fn count_user_emails(
executor: impl PgExecutor<'_>, executor: impl PgExecutor<'_>,
user: &User<PostgresqlBackend>, user: &User,
) -> Result<i64, anyhow::Error> { ) -> Result<i64, anyhow::Error> {
let res = sqlx::query_scalar!( let res = sqlx::query_scalar!(
r#" r#"
@ -856,7 +847,7 @@ pub async fn count_user_emails(
FROM user_emails ue FROM user_emails ue
WHERE ue.user_id = $1 WHERE ue.user_id = $1
"#, "#,
Uuid::from(user.data), Uuid::from(user.id),
) )
.fetch_one(executor) .fetch_one(executor)
.instrument(info_span!("Count user emails")) .instrument(info_span!("Count user emails"))
@ -867,20 +858,17 @@ pub async fn count_user_emails(
#[tracing::instrument( #[tracing::instrument(
skip_all, skip_all,
fields( fields(%user.id, %user.username),
user.id = %user.data,
user.username = user.username,
),
err(Display), err(Display),
)] )]
pub async fn get_paginated_user_emails( pub async fn get_paginated_user_emails(
executor: impl PgExecutor<'_>, executor: impl PgExecutor<'_>,
user: &User<PostgresqlBackend>, user: &User,
before: Option<Ulid>, before: Option<Ulid>,
after: Option<Ulid>, after: Option<Ulid>,
first: Option<usize>, first: Option<usize>,
last: Option<usize>, last: Option<usize>,
) -> Result<(bool, bool, Vec<UserEmail<PostgresqlBackend>>), anyhow::Error> { ) -> Result<(bool, bool, Vec<UserEmail>), anyhow::Error> {
let mut query = QueryBuilder::new( let mut query = QueryBuilder::new(
r#" r#"
SELECT SELECT
@ -894,7 +882,7 @@ pub async fn get_paginated_user_emails(
query query
.push(" WHERE ue.user_id = ") .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)?; .generate_pagination("ue.user_email_id", before, after, first, last)?;
let span = info_span!("Fetch paginated user sessions", db.statement = query.sql()); 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( #[tracing::instrument(
skip_all, skip_all,
fields( fields(
user.id = %user.data, %user.id,
user.username = user.username, %user.username,
user_email.id = %id, user_email.id = %id,
), ),
err(Display), err(Display),
)] )]
pub async fn get_user_email( pub async fn get_user_email(
executor: impl PgExecutor<'_>, executor: impl PgExecutor<'_>,
user: &User<PostgresqlBackend>, user: &User,
id: Ulid, id: Ulid,
) -> Result<UserEmail<PostgresqlBackend>, anyhow::Error> { ) -> Result<UserEmail, anyhow::Error> {
let res = sqlx::query_as!( let res = sqlx::query_as!(
UserEmailLookup, UserEmailLookup,
r#" r#"
@ -940,7 +928,7 @@ pub async fn get_user_email(
WHERE ue.user_id = $1 WHERE ue.user_id = $1
AND ue.user_email_id = $2 AND ue.user_email_id = $2
"#, "#,
Uuid::from(user.data), Uuid::from(user.id),
Uuid::from(id), Uuid::from(id),
) )
.fetch_one(executor) .fetch_one(executor)
@ -953,8 +941,8 @@ pub async fn get_user_email(
#[tracing::instrument( #[tracing::instrument(
skip_all, skip_all,
fields( fields(
user.id = %user.data, %user.id,
user.username = user.username, %user.username,
user_email.id, user_email.id,
user_email.email = %email, user_email.email = %email,
), ),
@ -964,9 +952,9 @@ pub async fn add_user_email(
executor: impl PgExecutor<'_>, executor: impl PgExecutor<'_>,
mut rng: impl Rng + Send, mut rng: impl Rng + Send,
clock: &Clock, clock: &Clock,
user: &User<PostgresqlBackend>, user: &User,
email: String, email: String,
) -> Result<UserEmail<PostgresqlBackend>, anyhow::Error> { ) -> Result<UserEmail, anyhow::Error> {
let created_at = clock.now(); let created_at = clock.now();
let id = Ulid::from_datetime_with_source(created_at.into(), &mut rng); let id = Ulid::from_datetime_with_source(created_at.into(), &mut rng);
tracing::Span::current().record("user_email.id", tracing::field::display(id)); 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) VALUES ($1, $2, $3, $4)
"#, "#,
Uuid::from(id), Uuid::from(id),
Uuid::from(user.data), Uuid::from(user.id),
&email, &email,
created_at, created_at,
) )
@ -987,7 +975,7 @@ pub async fn add_user_email(
.context("could not insert user email")?; .context("could not insert user email")?;
Ok(UserEmail { Ok(UserEmail {
data: id, id,
email, email,
created_at, created_at,
confirmed_at: None, confirmed_at: None,
@ -997,14 +985,14 @@ pub async fn add_user_email(
#[tracing::instrument( #[tracing::instrument(
skip_all, skip_all,
fields( fields(
user_email.id = %email.data, %user_email.id,
user_email.email = %email.email, %user_email.email,
), ),
err(Display), err(Display),
)] )]
pub async fn set_user_email_as_primary( pub async fn set_user_email_as_primary(
executor: impl PgExecutor<'_>, executor: impl PgExecutor<'_>,
email: &UserEmail<PostgresqlBackend>, user_email: &UserEmail,
) -> Result<(), anyhow::Error> { ) -> Result<(), anyhow::Error> {
sqlx::query!( sqlx::query!(
r#" r#"
@ -1014,7 +1002,7 @@ pub async fn set_user_email_as_primary(
WHERE user_emails.user_email_id = $1 WHERE user_emails.user_email_id = $1
AND users.user_id = user_emails.user_id AND users.user_id = user_emails.user_id
"#, "#,
Uuid::from(email.data), Uuid::from(user_email.id),
) )
.execute(executor) .execute(executor)
.instrument(info_span!("Add user email")) .instrument(info_span!("Add user email"))
@ -1027,21 +1015,21 @@ pub async fn set_user_email_as_primary(
#[tracing::instrument( #[tracing::instrument(
skip_all, skip_all,
fields( fields(
user_email.id = %email.data, %user_email.id,
user_email.email = %email.email, %user_email.email,
), ),
err(Display), err(Display),
)] )]
pub async fn remove_user_email( pub async fn remove_user_email(
executor: impl PgExecutor<'_>, executor: impl PgExecutor<'_>,
email: UserEmail<PostgresqlBackend>, user_email: UserEmail,
) -> Result<(), anyhow::Error> { ) -> Result<(), anyhow::Error> {
sqlx::query!( sqlx::query!(
r#" r#"
DELETE FROM user_emails DELETE FROM user_emails
WHERE user_emails.user_email_id = $1 WHERE user_emails.user_email_id = $1
"#, "#,
Uuid::from(email.data), Uuid::from(user_email.id),
) )
.execute(executor) .execute(executor)
.instrument(info_span!("Remove user email")) .instrument(info_span!("Remove user email"))
@ -1054,16 +1042,16 @@ pub async fn remove_user_email(
#[tracing::instrument( #[tracing::instrument(
skip_all, skip_all,
fields( fields(
user.id = %user.data, %user.id,
user_email.email = email, user_email.email = email,
), ),
err(Display), err(Display),
)] )]
pub async fn lookup_user_email( pub async fn lookup_user_email(
executor: impl PgExecutor<'_>, executor: impl PgExecutor<'_>,
user: &User<PostgresqlBackend>, user: &User,
email: &str, email: &str,
) -> Result<UserEmail<PostgresqlBackend>, anyhow::Error> { ) -> Result<UserEmail, anyhow::Error> {
let res = sqlx::query_as!( let res = sqlx::query_as!(
UserEmailLookup, UserEmailLookup,
r#" r#"
@ -1077,7 +1065,7 @@ pub async fn lookup_user_email(
WHERE ue.user_id = $1 WHERE ue.user_id = $1
AND ue.email = $2 AND ue.email = $2
"#, "#,
Uuid::from(user.data), Uuid::from(user.id),
email, email,
) )
.fetch_one(executor) .fetch_one(executor)
@ -1091,16 +1079,16 @@ pub async fn lookup_user_email(
#[tracing::instrument( #[tracing::instrument(
skip_all, skip_all,
fields( fields(
user.id = %user.data, %user.id,
user_email.id = %id, user_email.id = %id,
), ),
err, err,
)] )]
pub async fn lookup_user_email_by_id( pub async fn lookup_user_email_by_id(
executor: impl PgExecutor<'_>, executor: impl PgExecutor<'_>,
user: &User<PostgresqlBackend>, user: &User,
id: Ulid, id: Ulid,
) -> Result<UserEmail<PostgresqlBackend>, GenericLookupError> { ) -> Result<UserEmail, GenericLookupError> {
let res = sqlx::query_as!( let res = sqlx::query_as!(
UserEmailLookup, UserEmailLookup,
r#" r#"
@ -1114,7 +1102,7 @@ pub async fn lookup_user_email_by_id(
WHERE ue.user_id = $1 WHERE ue.user_id = $1
AND ue.user_email_id = $2 AND ue.user_email_id = $2
"#, "#,
Uuid::from(user.data), Uuid::from(user.id),
Uuid::from(id), Uuid::from(id),
) )
.fetch_one(executor) .fetch_one(executor)
@ -1127,16 +1115,14 @@ pub async fn lookup_user_email_by_id(
#[tracing::instrument( #[tracing::instrument(
skip_all, skip_all,
fields( fields(%user_email.id),
user_email.id = %email.data,
),
err(Display), err(Display),
)] )]
pub async fn mark_user_email_as_verified( pub async fn mark_user_email_as_verified(
executor: impl PgExecutor<'_>, executor: impl PgExecutor<'_>,
clock: &Clock, clock: &Clock,
mut email: UserEmail<PostgresqlBackend>, mut user_email: UserEmail,
) -> Result<UserEmail<PostgresqlBackend>, anyhow::Error> { ) -> Result<UserEmail, anyhow::Error> {
let confirmed_at = clock.now(); let confirmed_at = clock.now();
sqlx::query!( sqlx::query!(
r#" r#"
@ -1144,7 +1130,7 @@ pub async fn mark_user_email_as_verified(
SET confirmed_at = $2 SET confirmed_at = $2
WHERE user_email_id = $1 WHERE user_email_id = $1
"#, "#,
Uuid::from(email.data), Uuid::from(user_email.id),
confirmed_at, confirmed_at,
) )
.execute(executor) .execute(executor)
@ -1152,9 +1138,9 @@ pub async fn mark_user_email_as_verified(
.await .await
.context("could not update user email")?; .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 { struct UserEmailConfirmationCodeLookup {
@ -1167,17 +1153,15 @@ struct UserEmailConfirmationCodeLookup {
#[tracing::instrument( #[tracing::instrument(
skip_all, skip_all,
fields( fields(%user_email.id),
user_email.id = %email.data,
),
err(Display), err(Display),
)] )]
pub async fn lookup_user_email_verification_code( pub async fn lookup_user_email_verification_code(
executor: impl PgExecutor<'_>, executor: impl PgExecutor<'_>,
clock: &Clock, clock: &Clock,
email: UserEmail<PostgresqlBackend>, user_email: UserEmail,
code: &str, code: &str,
) -> Result<UserEmailVerification<PostgresqlBackend>, anyhow::Error> { ) -> Result<UserEmailVerification, anyhow::Error> {
let now = clock.now(); let now = clock.now();
let res = sqlx::query_as!( let res = sqlx::query_as!(
@ -1194,7 +1178,7 @@ pub async fn lookup_user_email_verification_code(
AND ec.user_email_id = $2 AND ec.user_email_id = $2
"#, "#,
code, code,
Uuid::from(email.data), Uuid::from(user_email.id),
) )
.fetch_one(executor) .fetch_one(executor)
.instrument(info_span!("Lookup user email verification")) .instrument(info_span!("Lookup user email verification"))
@ -1212,9 +1196,9 @@ pub async fn lookup_user_email_verification_code(
}; };
Ok(UserEmailVerification { Ok(UserEmailVerification {
data: res.user_email_confirmation_code_id.into(), id: res.user_email_confirmation_code_id.into(),
code: res.code, code: res.code,
email, email: user_email,
state, state,
created_at: res.created_at, created_at: res.created_at,
}) })
@ -1223,16 +1207,19 @@ pub async fn lookup_user_email_verification_code(
#[tracing::instrument( #[tracing::instrument(
skip_all, skip_all,
fields( fields(
user_email_verification.id = %verification.data, %user_email_verification.id,
), ),
err(Display), err(Display),
)] )]
pub async fn consume_email_verification( pub async fn consume_email_verification(
executor: impl PgExecutor<'_>, executor: impl PgExecutor<'_>,
clock: &Clock, clock: &Clock,
mut verification: UserEmailVerification<PostgresqlBackend>, mut user_email_verification: UserEmailVerification,
) -> Result<UserEmailVerification<PostgresqlBackend>, anyhow::Error> { ) -> Result<UserEmailVerification, anyhow::Error> {
if !matches!(verification.state, UserEmailVerificationState::Valid) { if !matches!(
user_email_verification.state,
UserEmailVerificationState::Valid
) {
bail!("user email verification in wrong state"); bail!("user email verification in wrong state");
} }
@ -1244,7 +1231,7 @@ pub async fn consume_email_verification(
SET consumed_at = $2 SET consumed_at = $2
WHERE user_email_confirmation_code_id = $1 WHERE user_email_confirmation_code_id = $1
"#, "#,
Uuid::from(verification.data), Uuid::from(user_email_verification.id),
consumed_at consumed_at
) )
.execute(executor) .execute(executor)
@ -1252,16 +1239,16 @@ pub async fn consume_email_verification(
.await .await
.context("could not update user email verification")?; .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( #[tracing::instrument(
skip_all, skip_all,
fields( fields(
user_email.id = %email.data, %user_email.id,
user_email.email = %email.email, %user_email.email,
user_email_confirmation.id, user_email_confirmation.id,
user_email_confirmation.code = code, user_email_confirmation.code = code,
), ),
@ -1271,10 +1258,10 @@ pub async fn add_user_email_verification_code(
executor: impl PgExecutor<'_>, executor: impl PgExecutor<'_>,
mut rng: impl Rng + Send, mut rng: impl Rng + Send,
clock: &Clock, clock: &Clock,
email: UserEmail<PostgresqlBackend>, user_email: UserEmail,
max_age: chrono::Duration, max_age: chrono::Duration,
code: String, code: String,
) -> Result<UserEmailVerification<PostgresqlBackend>, anyhow::Error> { ) -> Result<UserEmailVerification, anyhow::Error> {
let created_at = clock.now(); let created_at = clock.now();
let id = Ulid::from_datetime_with_source(created_at.into(), &mut rng); let id = Ulid::from_datetime_with_source(created_at.into(), &mut rng);
tracing::Span::current().record("user_email_confirmation.id", tracing::field::display(id)); 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) VALUES ($1, $2, $3, $4, $5)
"#, "#,
Uuid::from(id), Uuid::from(id),
Uuid::from(email.data), Uuid::from(user_email.id),
code, code,
created_at, created_at,
expires_at, expires_at,
@ -1298,8 +1285,8 @@ pub async fn add_user_email_verification_code(
.context("could not insert user email verification code")?; .context("could not insert user email verification code")?;
let verification = UserEmailVerification { let verification = UserEmailVerification {
data: id, id,
email, email: user_email,
code, code,
created_at, created_at,
state: UserEmailVerificationState::Valid, state: UserEmailVerificationState::Valid,
@ -1331,10 +1318,10 @@ mod tests {
assert!(exists); assert!(exists);
let session = login(&mut txn, &mut rng, &clock, "john", "hunter2").await?; 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?; 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?; txn.commit().await?;

View File

@ -22,6 +22,7 @@ chrono = "0.4.23"
url = "2.3.1" url = "2.3.1"
http = "0.2.8" http = "0.2.8"
ulid = { version = "1.0.0", features = ["serde"] } ulid = { version = "1.0.0", features = ["serde"] }
rand = "0.8.5"
oauth2-types = { path = "../oauth2-types" } oauth2-types = { path = "../oauth2-types" }
mas-data-model = { path = "../data-model" } mas-data-model = { path = "../data-model" }

View File

@ -18,10 +18,11 @@
use chrono::Utc; use chrono::Utc;
use mas_data_model::{ use mas_data_model::{
AuthorizationGrant, BrowserSession, CompatSsoLogin, CompatSsoLoginState, StorageBackend, AuthorizationGrant, BrowserSession, CompatSsoLogin, CompatSsoLoginState, UpstreamOAuthLink,
UpstreamOAuthLink, UpstreamOAuthProvider, User, UserEmail, UserEmailVerification, UpstreamOAuthProvider, User, UserEmail, UserEmailVerification,
}; };
use mas_router::{PostAuthAction, Route}; use mas_router::{PostAuthAction, Route};
use rand::Rng;
use serde::{ser::SerializeStruct, Deserialize, Serialize}; use serde::{ser::SerializeStruct, Deserialize, Serialize};
use ulid::Ulid; use ulid::Ulid;
use url::Url; use url::Url;
@ -31,31 +32,26 @@ use crate::{FormField, FormState};
/// Helper trait to construct context wrappers /// Helper trait to construct context wrappers
pub trait TemplateContext: Serialize { pub trait TemplateContext: Serialize {
/// Attach a user session to the template context /// Attach a user session to the template context
fn with_session<S: StorageBackend>( fn with_session(self, current_session: BrowserSession) -> WithSession<Self>
self,
current_session: BrowserSession<S>,
) -> WithSession<Self>
where where
Self: Sized, Self: Sized,
BrowserSession<S>: Into<BrowserSession<()>>,
{ {
WithSession { WithSession {
current_session: current_session.into(), current_session,
inner: self, inner: self,
} }
} }
/// Attach an optional user session to the template context /// Attach an optional user session to the template context
fn maybe_with_session<S: StorageBackend>( fn maybe_with_session(
self, self,
current_session: Option<BrowserSession<S>>, current_session: Option<BrowserSession>,
) -> WithOptionalSession<Self> ) -> WithOptionalSession<Self>
where where
Self: Sized, Self: Sized,
BrowserSession<S>: Into<BrowserSession<()>>,
{ {
WithOptionalSession { WithOptionalSession {
current_session: current_session.map(Into::into), current_session,
inner: self, inner: self,
} }
} }
@ -77,13 +73,13 @@ pub trait TemplateContext: Serialize {
/// ///
/// This is then used to check for template validity in unit tests and in /// This is then used to check for template validity in unit tests and in
/// the CLI (`cargo run -- templates check`) /// the CLI (`cargo run -- templates check`)
fn sample(now: chrono::DateTime<Utc>) -> Vec<Self> fn sample(now: chrono::DateTime<Utc>, rng: &mut impl Rng) -> Vec<Self>
where where
Self: Sized; Self: Sized;
} }
impl TemplateContext for () { impl TemplateContext for () {
fn sample(_now: chrono::DateTime<Utc>) -> Vec<Self> fn sample(_now: chrono::DateTime<Utc>, _rng: &mut impl Rng) -> Vec<Self>
where where
Self: Sized, Self: Sized,
{ {
@ -101,11 +97,11 @@ pub struct WithCsrf<T> {
} }
impl<T: TemplateContext> TemplateContext for WithCsrf<T> { impl<T: TemplateContext> TemplateContext for WithCsrf<T> {
fn sample(now: chrono::DateTime<Utc>) -> Vec<Self> fn sample(now: chrono::DateTime<Utc>, rng: &mut impl Rng) -> Vec<Self>
where where
Self: Sized, Self: Sized,
{ {
T::sample(now) T::sample(now, rng)
.into_iter() .into_iter()
.map(|inner| WithCsrf { .map(|inner| WithCsrf {
csrf_token: "fake_csrf_token".into(), csrf_token: "fake_csrf_token".into(),
@ -118,24 +114,26 @@ impl<T: TemplateContext> TemplateContext for WithCsrf<T> {
/// Context with a user session in it /// Context with a user session in it
#[derive(Serialize)] #[derive(Serialize)]
pub struct WithSession<T> { pub struct WithSession<T> {
current_session: BrowserSession<()>, current_session: BrowserSession,
#[serde(flatten)] #[serde(flatten)]
inner: T, inner: T,
} }
impl<T: TemplateContext> TemplateContext for WithSession<T> { impl<T: TemplateContext> TemplateContext for WithSession<T> {
fn sample(now: chrono::DateTime<Utc>) -> Vec<Self> fn sample(now: chrono::DateTime<Utc>, rng: &mut impl Rng) -> Vec<Self>
where where
Self: Sized, Self: Sized,
{ {
BrowserSession::samples(now) BrowserSession::samples(now, rng)
.into_iter() .into_iter()
.flat_map(|session| { .flat_map(|session| {
T::sample(now).into_iter().map(move |inner| WithSession { T::sample(now, rng)
current_session: session.clone(), .into_iter()
inner, .map(move |inner| WithSession {
}) current_session: session.clone(),
inner,
})
}) })
.collect() .collect()
} }
@ -144,23 +142,23 @@ impl<T: TemplateContext> TemplateContext for WithSession<T> {
/// Context with an optional user session in it /// Context with an optional user session in it
#[derive(Serialize)] #[derive(Serialize)]
pub struct WithOptionalSession<T> { pub struct WithOptionalSession<T> {
current_session: Option<BrowserSession<()>>, current_session: Option<BrowserSession>,
#[serde(flatten)] #[serde(flatten)]
inner: T, inner: T,
} }
impl<T: TemplateContext> TemplateContext for WithOptionalSession<T> { impl<T: TemplateContext> TemplateContext for WithOptionalSession<T> {
fn sample(now: chrono::DateTime<Utc>) -> Vec<Self> fn sample(now: chrono::DateTime<Utc>, rng: &mut impl Rng) -> Vec<Self>
where where
Self: Sized, Self: Sized,
{ {
BrowserSession::samples(now) BrowserSession::samples(now, rng)
.into_iter() .into_iter()
.map(Some) // Wrap all samples in an Option .map(Some) // Wrap all samples in an Option
.chain(std::iter::once(None)) // Add the "None" option .chain(std::iter::once(None)) // Add the "None" option
.flat_map(|session| { .flat_map(|session| {
T::sample(now) T::sample(now, rng)
.into_iter() .into_iter()
.map(move |inner| WithOptionalSession { .map(move |inner| WithOptionalSession {
current_session: session.clone(), current_session: session.clone(),
@ -188,7 +186,7 @@ impl Serialize for EmptyContext {
} }
impl TemplateContext for EmptyContext { impl TemplateContext for EmptyContext {
fn sample(_now: chrono::DateTime<Utc>) -> Vec<Self> fn sample(_now: chrono::DateTime<Utc>, _rng: &mut impl Rng) -> Vec<Self>
where where
Self: Sized, Self: Sized,
{ {
@ -212,7 +210,7 @@ impl IndexContext {
} }
impl TemplateContext for IndexContext { impl TemplateContext for IndexContext {
fn sample(_now: chrono::DateTime<Utc>) -> Vec<Self> fn sample(_now: chrono::DateTime<Utc>, _rng: &mut impl Rng) -> Vec<Self>
where where
Self: Sized, Self: Sized,
{ {
@ -294,7 +292,7 @@ pub struct LoginContext {
} }
impl TemplateContext for LoginContext { impl TemplateContext for LoginContext {
fn sample(_now: chrono::DateTime<Utc>) -> Vec<Self> fn sample(_now: chrono::DateTime<Utc>, _rng: &mut impl Rng) -> Vec<Self>
where where
Self: Sized, Self: Sized,
{ {
@ -364,7 +362,7 @@ pub struct RegisterContext {
} }
impl TemplateContext for RegisterContext { impl TemplateContext for RegisterContext {
fn sample(_now: chrono::DateTime<Utc>) -> Vec<Self> fn sample(_now: chrono::DateTime<Utc>, _rng: &mut impl Rng) -> Vec<Self>
where where
Self: Sized, Self: Sized,
{ {
@ -401,7 +399,7 @@ pub struct ConsentContext {
} }
impl TemplateContext for ConsentContext { impl TemplateContext for ConsentContext {
fn sample(_now: chrono::DateTime<Utc>) -> Vec<Self> fn sample(_now: chrono::DateTime<Utc>, _rng: &mut impl Rng) -> Vec<Self>
where where
Self: Sized, Self: Sized,
{ {
@ -432,7 +430,7 @@ pub struct PolicyViolationContext {
} }
impl TemplateContext for PolicyViolationContext { impl TemplateContext for PolicyViolationContext {
fn sample(_now: chrono::DateTime<Utc>) -> Vec<Self> fn sample(_now: chrono::DateTime<Utc>, _rng: &mut impl Rng) -> Vec<Self>
where where
Self: Sized, Self: Sized,
{ {
@ -480,7 +478,7 @@ pub struct ReauthContext {
} }
impl TemplateContext for ReauthContext { impl TemplateContext for ReauthContext {
fn sample(_now: chrono::DateTime<Utc>) -> Vec<Self> fn sample(_now: chrono::DateTime<Utc>, _rng: &mut impl Rng) -> Vec<Self>
where where
Self: Sized, Self: Sized,
{ {
@ -519,7 +517,7 @@ pub struct CompatSsoContext {
} }
impl TemplateContext for CompatSsoContext { impl TemplateContext for CompatSsoContext {
fn sample(now: chrono::DateTime<Utc>) -> Vec<Self> fn sample(now: chrono::DateTime<Utc>, rng: &mut impl Rng) -> Vec<Self>
where where
Self: Sized, Self: Sized,
{ {
@ -531,7 +529,9 @@ impl TemplateContext for CompatSsoContext {
created_at: now, created_at: now,
state: CompatSsoLoginState::Pending, 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)] #[derive(Serialize)]
pub struct AccountContext { pub struct AccountContext {
active_sessions: usize, active_sessions: usize,
emails: Vec<UserEmail<()>>, emails: Vec<UserEmail>,
} }
impl AccountContext { impl AccountContext {
@ -562,7 +562,7 @@ impl AccountContext {
#[must_use] #[must_use]
pub fn new<T>(active_sessions: usize, emails: Vec<T>) -> Self pub fn new<T>(active_sessions: usize, emails: Vec<T>) -> Self
where where
T: Into<UserEmail<()>>, T: Into<UserEmail>,
{ {
Self { Self {
active_sessions, active_sessions,
@ -572,36 +572,35 @@ impl AccountContext {
} }
impl TemplateContext for AccountContext { impl TemplateContext for AccountContext {
fn sample(now: chrono::DateTime<Utc>) -> Vec<Self> fn sample(now: chrono::DateTime<Utc>, rng: &mut impl Rng) -> Vec<Self>
where where
Self: Sized, Self: Sized,
{ {
let emails: Vec<UserEmail<()>> = UserEmail::samples(now); let emails: Vec<UserEmail> = UserEmail::samples(now, rng);
vec![Self::new(5, emails)] vec![Self::new(5, emails)]
} }
} }
/// Context used by the `account/emails.html` template /// Context used by the `account/emails.html` template
#[derive(Serialize)] #[derive(Serialize)]
#[serde(bound(serialize = "T: StorageBackend"))] pub struct AccountEmailsContext {
pub struct AccountEmailsContext<T: StorageBackend> { emails: Vec<UserEmail>,
emails: Vec<UserEmail<T>>,
} }
impl<T: StorageBackend> AccountEmailsContext<T> { impl AccountEmailsContext {
/// Constructs a context for the email management page /// Constructs a context for the email management page
#[must_use] #[must_use]
pub fn new(emails: Vec<UserEmail<T>>) -> Self { pub fn new(emails: Vec<UserEmail>) -> Self {
Self { emails } Self { emails }
} }
} }
impl<T: StorageBackend> TemplateContext for AccountEmailsContext<T> { impl TemplateContext for AccountEmailsContext {
fn sample(now: chrono::DateTime<Utc>) -> Vec<Self> fn sample(now: chrono::DateTime<Utc>, rng: &mut impl Rng) -> Vec<Self>
where where
Self: Sized, Self: Sized,
{ {
let emails: Vec<UserEmail<T>> = UserEmail::samples(now); let emails: Vec<UserEmail> = UserEmail::samples(now, rng);
vec![Self::new(emails)] vec![Self::new(emails)]
} }
} }
@ -609,35 +608,35 @@ impl<T: StorageBackend> TemplateContext for AccountEmailsContext<T> {
/// Context used by the `emails/verification.{txt,html,subject}` templates /// Context used by the `emails/verification.{txt,html,subject}` templates
#[derive(Serialize)] #[derive(Serialize)]
pub struct EmailVerificationContext { pub struct EmailVerificationContext {
user: User<()>, user: User,
verification: UserEmailVerification<()>, verification: UserEmailVerification,
} }
impl EmailVerificationContext { impl EmailVerificationContext {
/// Constructs a context for the verification email /// Constructs a context for the verification email
#[must_use] #[must_use]
pub fn new(user: User<()>, verification: UserEmailVerification<()>) -> Self { pub fn new(user: User, verification: UserEmailVerification) -> Self {
Self { user, verification } Self { user, verification }
} }
} }
impl TemplateContext for EmailVerificationContext { impl TemplateContext for EmailVerificationContext {
fn sample(now: chrono::DateTime<Utc>) -> Vec<Self> fn sample(now: chrono::DateTime<Utc>, rng: &mut impl Rng) -> Vec<Self>
where where
Self: Sized, Self: Sized,
{ {
User::samples(now) User::samples(now, rng)
.into_iter() .into_iter()
.map(|user| { .map(|user| {
let email = UserEmail { let email = UserEmail {
data: (), id: Ulid::from_datetime_with_source(now.into(), rng),
email: "foobar@example.com".to_owned(), email: "foobar@example.com".to_owned(),
created_at: now, created_at: now,
confirmed_at: None, confirmed_at: None,
}; };
let verification = UserEmailVerification { let verification = UserEmailVerification {
data: (), id: Ulid::from_datetime_with_source(now.into(), rng),
code: "123456".to_owned(), code: "123456".to_owned(),
email, email,
created_at: now, created_at: now,
@ -670,7 +669,7 @@ impl FormField for EmailVerificationFormField {
#[derive(Serialize)] #[derive(Serialize)]
pub struct EmailVerificationPageContext { pub struct EmailVerificationPageContext {
form: FormState<EmailVerificationFormField>, form: FormState<EmailVerificationFormField>,
email: UserEmail<()>, email: UserEmail,
} }
impl EmailVerificationPageContext { impl EmailVerificationPageContext {
@ -678,7 +677,7 @@ impl EmailVerificationPageContext {
#[must_use] #[must_use]
pub fn new<T>(email: T) -> Self pub fn new<T>(email: T) -> Self
where where
T: Into<UserEmail<()>>, T: Into<UserEmail>,
{ {
Self { Self {
form: FormState::default(), form: FormState::default(),
@ -694,12 +693,12 @@ impl EmailVerificationPageContext {
} }
impl TemplateContext for EmailVerificationPageContext { impl TemplateContext for EmailVerificationPageContext {
fn sample(now: chrono::DateTime<Utc>) -> Vec<Self> fn sample(now: chrono::DateTime<Utc>, rng: &mut impl Rng) -> Vec<Self>
where where
Self: Sized, Self: Sized,
{ {
let email = UserEmail { let email = UserEmail {
data: (), id: Ulid::from_datetime_with_source(now.into(), rng),
email: "foobar@example.com".to_owned(), email: "foobar@example.com".to_owned(),
created_at: now, created_at: now,
confirmed_at: None, confirmed_at: None,
@ -749,7 +748,7 @@ impl EmailAddContext {
} }
impl TemplateContext for EmailAddContext { impl TemplateContext for EmailAddContext {
fn sample(_now: chrono::DateTime<Utc>) -> Vec<Self> fn sample(_now: chrono::DateTime<Utc>, _rng: &mut impl Rng) -> Vec<Self>
where where
Self: Sized, Self: Sized,
{ {
@ -761,14 +760,14 @@ impl TemplateContext for EmailAddContext {
/// templates /// templates
#[derive(Serialize)] #[derive(Serialize)]
pub struct UpstreamExistingLinkContext { pub struct UpstreamExistingLinkContext {
linked_user: User<()>, linked_user: User,
} }
impl UpstreamExistingLinkContext { impl UpstreamExistingLinkContext {
/// Constructs a new context with an existing linked user /// Constructs a new context with an existing linked user
pub fn new<T>(linked_user: T) -> Self pub fn new<T>(linked_user: T) -> Self
where where
T: Into<User<()>>, T: Into<User>,
{ {
Self { Self {
linked_user: linked_user.into(), linked_user: linked_user.into(),
@ -777,11 +776,11 @@ impl UpstreamExistingLinkContext {
} }
impl TemplateContext for UpstreamExistingLinkContext { impl TemplateContext for UpstreamExistingLinkContext {
fn sample(now: chrono::DateTime<Utc>) -> Vec<Self> fn sample(now: chrono::DateTime<Utc>, rng: &mut impl Rng) -> Vec<Self>
where where
Self: Sized, Self: Sized,
{ {
User::samples(now) User::samples(now, rng)
.into_iter() .into_iter()
.map(|linked_user| Self { linked_user }) .map(|linked_user| Self { linked_user })
.collect() .collect()
@ -805,7 +804,7 @@ impl UpstreamSuggestLink {
} }
impl TemplateContext for UpstreamSuggestLink { impl TemplateContext for UpstreamSuggestLink {
fn sample(_now: chrono::DateTime<Utc>) -> Vec<Self> fn sample(_now: chrono::DateTime<Utc>, _rng: &mut impl Rng) -> Vec<Self>
where where
Self: Sized, Self: Sized,
{ {
@ -831,7 +830,7 @@ impl UpstreamRegister {
} }
impl TemplateContext for UpstreamRegister { impl TemplateContext for UpstreamRegister {
fn sample(_now: chrono::DateTime<Utc>) -> Vec<Self> fn sample(_now: chrono::DateTime<Utc>, _rng: &mut impl Rng) -> Vec<Self>
where where
Self: Sized, Self: Sized,
{ {
@ -847,11 +846,11 @@ pub struct FormPostContext<T> {
} }
impl<T: TemplateContext> TemplateContext for FormPostContext<T> { impl<T: TemplateContext> TemplateContext for FormPostContext<T> {
fn sample(now: chrono::DateTime<Utc>) -> Vec<Self> fn sample(now: chrono::DateTime<Utc>, rng: &mut impl Rng) -> Vec<Self>
where where
Self: Sized, Self: Sized,
{ {
let sample_params = T::sample(now); let sample_params = T::sample(now, rng);
sample_params sample_params
.into_iter() .into_iter()
.map(|params| FormPostContext { .map(|params| FormPostContext {
@ -881,7 +880,7 @@ pub struct ErrorContext {
} }
impl TemplateContext for ErrorContext { impl TemplateContext for ErrorContext {
fn sample(_now: chrono::DateTime<Utc>) -> Vec<Self> fn sample(_now: chrono::DateTime<Utc>, _rng: &mut impl Rng) -> Vec<Self>
where where
Self: Sized, Self: Sized,
{ {

View File

@ -28,8 +28,8 @@ use std::{collections::HashSet, string::ToString, sync::Arc};
use anyhow::Context as _; use anyhow::Context as _;
use camino::{Utf8Path, Utf8PathBuf}; use camino::{Utf8Path, Utf8PathBuf};
use mas_data_model::StorageBackend;
use mas_router::UrlBuilder; use mas_router::UrlBuilder;
use rand::Rng;
use serde::Serialize; use serde::Serialize;
use tera::{Context, Error as TeraError, Tera}; use tera::{Context, Error as TeraError, Tera};
use thiserror::Error; use thiserror::Error;
@ -201,7 +201,7 @@ register_templates! {
pub fn render_account_password(WithCsrf<WithSession<EmptyContext>>) { "pages/account/password.html" } pub fn render_account_password(WithCsrf<WithSession<EmptyContext>>) { "pages/account/password.html" }
/// Render the emails management /// Render the emails management
pub fn render_account_emails<T: StorageBackend>(WithCsrf<WithSession<AccountEmailsContext<T>>>) { "pages/account/emails/index.html" } pub fn render_account_emails(WithCsrf<WithSession<AccountEmailsContext>>) { "pages/account/emails/index.html" }
/// Render the email verification page /// Render the email verification page
pub fn render_account_verify_email(WithCsrf<WithSession<EmailVerificationPageContext>>) { "pages/account/emails/verify.html" } pub fn render_account_verify_email(WithCsrf<WithSession<EmailVerificationPageContext>>) { "pages/account/emails/verify.html" }
@ -246,29 +246,33 @@ register_templates! {
impl Templates { impl Templates {
/// Render all templates with the generated samples to check if they render /// Render all templates with the generated samples to check if they render
/// properly /// properly
pub async fn check_render(&self, now: chrono::DateTime<chrono::Utc>) -> anyhow::Result<()> { pub async fn check_render(
check::render_login(self, now).await?; &self,
check::render_register(self, now).await?; now: chrono::DateTime<chrono::Utc>,
check::render_consent(self, now).await?; rng: &mut impl Rng,
check::render_policy_violation(self, now).await?; ) -> anyhow::Result<()> {
check::render_sso_login(self, now).await?; check::render_login(self, now, rng).await?;
check::render_index(self, now).await?; check::render_register(self, now, rng).await?;
check::render_account_index(self, now).await?; check::render_consent(self, now, rng).await?;
check::render_account_password(self, now).await?; check::render_policy_violation(self, now, rng).await?;
check::render_account_emails::<()>(self, now).await?; check::render_sso_login(self, now, rng).await?;
check::render_account_add_email(self, now).await?; check::render_index(self, now, rng).await?;
check::render_account_verify_email(self, now).await?; check::render_account_index(self, now, rng).await?;
check::render_reauth(self, now).await?; check::render_account_password(self, now, rng).await?;
check::render_form_post::<EmptyContext>(self, now).await?; check::render_account_emails(self, now, rng).await?;
check::render_error(self, now).await?; check::render_account_add_email(self, now, rng).await?;
check::render_email_verification_txt(self, now).await?; check::render_account_verify_email(self, now, rng).await?;
check::render_email_verification_html(self, now).await?; check::render_reauth(self, now, rng).await?;
check::render_email_verification_subject(self, now).await?; check::render_form_post::<EmptyContext>(self, now, rng).await?;
check::render_upstream_oauth2_already_linked(self, now).await?; check::render_error(self, now, rng).await?;
check::render_upstream_oauth2_link_mismatch(self, now).await?; check::render_email_verification_txt(self, now, rng).await?;
check::render_upstream_oauth2_suggest_link(self, now).await?; check::render_email_verification_html(self, now, rng).await?;
check::render_upstream_oauth2_do_login(self, now).await?; check::render_email_verification_subject(self, now, rng).await?;
check::render_upstream_oauth2_do_register(self, now).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(()) Ok(())
} }
} }
@ -281,10 +285,12 @@ mod tests {
async fn check_builtin_templates() { async fn check_builtin_templates() {
#[allow(clippy::disallowed_methods)] #[allow(clippy::disallowed_methods)]
let now = chrono::Utc::now(); 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 path = Utf8Path::new(env!("CARGO_MANIFEST_DIR")).join("../../templates/");
let url_builder = UrlBuilder::new("https://example.com/".parse().unwrap()); let url_builder = UrlBuilder::new("https://example.com/".parse().unwrap());
let templates = Templates::load(path, url_builder).await.unwrap(); let templates = Templates::load(path, url_builder).await.unwrap();
templates.check_render(now).await.unwrap(); templates.check_render(now, &mut rng).await.unwrap();
} }
} }

View File

@ -75,9 +75,9 @@ macro_rules! register_templates {
#[doc = concat!("Render the `", $template, "` template with sample contexts")] #[doc = concat!("Render the `", $template, "` template with sample contexts")]
pub async fn $name pub async fn $name
$(< $( $lt $( : $clt $(+ $dlt )* + TemplateContext )? ),+ >)? $(< $( $lt $( : $clt $(+ $dlt )* + TemplateContext )? ),+ >)?
(templates: &Templates, now: chrono::DateTime<chrono::Utc>) (templates: &Templates, now: chrono::DateTime<chrono::Utc>, rng: &mut impl rand::Rng)
-> anyhow::Result<()> { -> anyhow::Result<()> {
let samples: Vec< $param > = TemplateContext::sample(now); let samples: Vec< $param > = TemplateContext::sample(now, rng);
let name = $template; let name = $template;
for sample in samples { for sample in samples {

View File

@ -37,7 +37,7 @@ limitations under the License.
{% for item in emails %} {% for item in emails %}
<form class="flex my-2 items-center justify-items-center" method="POST"> <form class="flex my-2 items-center justify-items-center" method="POST">
<input type="hidden" name="csrf" value="{{ csrf_token }}" /> <input type="hidden" name="csrf" value="{{ csrf_token }}" />
<input type="hidden" name="data" value="{{ item.data }}" /> <input type="hidden" name="id" value="{{ item.id }}" />
<div class="font-bold flex-1">{{ item.email }}</div> <div class="font-bold flex-1">{{ item.email }}</div>
{% if item.confirmed_at %} {% if item.confirmed_at %}
<div class="mr-4">Verified</div> <div class="mr-4">Verified</div>