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",
"oauth2-types",
"rand 0.8.5",
"rand_chacha 0.3.1",
"serde",
"thiserror",
"ulid",
@ -3128,6 +3129,7 @@ dependencies = [
"mas-data-model",
"mas-router",
"oauth2-types",
"rand 0.8.5",
"serde",
"serde_json",
"serde_urlencoded",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -186,7 +186,7 @@ impl From<IntoCallbackDestinationError> for GrantCompletionError {
pub(crate) async fn complete(
grant: AuthorizationGrant<PostgresqlBackend>,
browser_session: BrowserSession<PostgresqlBackend>,
browser_session: BrowserSession,
policy_factory: &PolicyFactory,
mut txn: Transaction<'_, Postgres>,
) -> 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 render = match (maybe_user_session, link.user_id) {
(Some(mut session), Some(user_id)) if session.user.data == user_id => {
(Some(mut session), Some(user_id)) if session.user.id == user_id => {
// Session already linked, and link matches the currently logged
// user. Mark the session as consumed and renew the authentication.
consume_session(&mut txn, &clock, upstream_session).await?;

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 next = mas_router::AccountVerifyEmail::new(user_email.data);
let next = mas_router::AccountVerifyEmail::new(user_email.id);
let next = if let Some(action) = query.post_auth_action {
next.and_then(action)
} else {

View File

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

View File

@ -27,7 +27,7 @@ use mas_keystore::Encrypter;
use mas_router::Route;
use mas_storage::{
user::{authenticate_session, set_password},
Clock, PostgresqlBackend,
Clock,
};
use mas_templates::{EmptyContext, TemplateContext, Templates};
use rand::Rng;
@ -65,7 +65,7 @@ async fn render(
rng: impl Rng + Send,
clock: &Clock,
templates: Templates,
session: BrowserSession<PostgresqlBackend>,
session: BrowserSession,
cookie_jar: PrivateCookieJar<Encrypter>,
) -> Result<Response, FancyError> {
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?;
let next = mas_router::AccountVerifyEmail::new(verification.email.data)
let next = mas_router::AccountVerifyEmail::new(verification.email.id)
.and_maybe(query.post_auth_action);
let session = start_session(&mut txn, &mut rng, &clock, user).await?;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -37,7 +37,7 @@ limitations under the License.
{% for item in emails %}
<form class="flex my-2 items-center justify-items-center" method="POST">
<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>
{% if item.confirmed_at %}
<div class="mr-4">Verified</div>