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

Remove the last authentication from the browser session model

This commit is contained in:
Quentin Gliech
2023-07-19 15:31:17 +02:00
parent 7e82ae845c
commit 802cf142fd
24 changed files with 325 additions and 204 deletions

View File

@ -60,7 +60,6 @@ pub struct BrowserSession {
pub user: User,
pub created_at: DateTime<Utc>,
pub finished_at: Option<DateTime<Utc>>,
pub last_authentication: Option<Authentication>,
}
impl BrowserSession {
@ -68,15 +67,6 @@ impl BrowserSession {
pub fn active(&self) -> bool {
self.finished_at.is_none()
}
#[must_use]
pub fn was_authenticated_after(&self, after: DateTime<Utc>) -> bool {
if let Some(auth) = &self.last_authentication {
auth.created_at > after
} else {
false
}
}
}
impl BrowserSession {
@ -89,7 +79,6 @@ impl BrowserSession {
user,
created_at: now,
finished_at: None,
last_authentication: None,
})
.collect()
}

View File

@ -12,10 +12,12 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use async_graphql::{Description, Enum, Object, ID};
use async_graphql::{Context, Description, Enum, Object, ID};
use chrono::{DateTime, Utc};
use mas_storage::{user::BrowserSessionRepository, RepositoryAccess};
use super::{NodeType, User};
use crate::state::ContextExt;
/// A browser session represents a logged in user in a browser.
#[derive(Description)]
@ -50,8 +52,21 @@ impl BrowserSession {
}
/// The most recent authentication of this session.
async fn last_authentication(&self) -> Option<Authentication> {
self.0.last_authentication.clone().map(Authentication)
async fn last_authentication(
&self,
ctx: &Context<'_>,
) -> Result<Option<Authentication>, async_graphql::Error> {
let state = ctx.state();
let mut repo = state.repository().await?;
let last_authentication = repo
.browser_session()
.get_last_authentication(&self.0)
.await?;
repo.cancel().await?;
Ok(last_authentication.map(Authentication))
}
/// When the object was created.

View File

@ -198,7 +198,7 @@ impl User {
before: Option<String>,
#[graphql(desc = "Returns the first *n* elements from the list.")] first: Option<i32>,
#[graphql(desc = "Returns the last *n* elements from the list.")] last: Option<i32>,
) -> Result<Connection<Cursor, BrowserSession>, async_graphql::Error> {
) -> Result<Connection<Cursor, BrowserSession, PreloadedTotalCount>, async_graphql::Error> {
let state = ctx.state();
let mut repo = state.repository().await?;
@ -225,9 +225,20 @@ impl User {
let page = repo.browser_session().list(filter, pagination).await?;
// Preload the total count if requested
let count = if ctx.look_ahead().field("totalCount").exists() {
Some(repo.browser_session().count(filter).await?)
} else {
None
};
repo.cancel().await?;
let mut connection = Connection::new(page.has_previous_page, page.has_next_page);
let mut connection = Connection::with_additional_fields(
page.has_previous_page,
page.has_next_page,
PreloadedTotalCount(count),
);
connection.edges.extend(page.edges.into_iter().map(|u| {
Edge::new(
OpaqueCursor(NodeCursor(NodeType::BrowserSession, u.id)),
@ -400,6 +411,17 @@ impl User {
}
}
pub struct PreloadedTotalCount(Option<usize>);
#[Object]
impl PreloadedTotalCount {
/// Identifies the total count of items in the connection.
async fn total_count(&self) -> Result<usize, async_graphql::Error> {
self.0
.ok_or_else(|| async_graphql::Error::new("total count not preloaded"))
}
}
/// A user email address
#[derive(Description)]
pub struct UserEmail(pub mas_data_model::UserEmail);

View File

@ -27,7 +27,8 @@ use mas_policy::PolicyFactory;
use mas_router::{PostAuthAction, Route, UrlBuilder};
use mas_storage::{
oauth2::{OAuth2AuthorizationGrantRepository, OAuth2ClientRepository, OAuth2SessionRepository},
BoxClock, BoxRepository, BoxRng,
user::BrowserSessionRepository,
BoxClock, BoxRepository, BoxRng, RepositoryAccess,
};
use mas_templates::Templates;
use oauth2_types::requests::AuthorizationResponse;
@ -194,10 +195,16 @@ pub(crate) async fn complete(
}
// Check if the authentication is fresh enough
if !browser_session.was_authenticated_after(grant.max_auth_time()) {
let authentication = repo
.browser_session()
.get_last_authentication(&browser_session)
.await?;
let authentication = authentication.filter(|auth| auth.created_at > grant.max_auth_time());
let Some(valid_authentication) = authentication else {
repo.save().await?;
return Err(GrantCompletionError::RequiresReauth);
}
};
// Run through the policy
let mut policy = policy_factory.instantiate().await?;
@ -257,6 +264,7 @@ pub(crate) async fn complete(
&grant,
&browser_session,
None,
Some(&valid_authentication),
)?);
}

View File

@ -16,7 +16,8 @@ use std::collections::HashMap;
use chrono::Duration;
use mas_data_model::{
AccessToken, AuthorizationGrant, BrowserSession, Client, RefreshToken, Session, TokenType,
AccessToken, Authentication, AuthorizationGrant, BrowserSession, Client, RefreshToken, Session,
TokenType,
};
use mas_iana::jose::JsonWebSignatureAlg;
use mas_jose::{
@ -60,6 +61,7 @@ pub(crate) fn generate_id_token(
grant: &AuthorizationGrant,
browser_session: &BrowserSession,
access_token: Option<&AccessToken>,
last_authentication: Option<&Authentication>,
) -> Result<String, IdTokenSignatureError> {
let mut claims = HashMap::new();
let now = clock.now();
@ -73,7 +75,7 @@ pub(crate) fn generate_id_token(
claims::NONCE.insert(&mut claims, nonce.clone())?;
}
if let Some(ref last_authentication) = browser_session.last_authentication {
if let Some(last_authentication) = last_authentication {
claims::AUTH_TIME.insert(&mut claims, last_authentication.created_at)?;
}
@ -113,7 +115,7 @@ pub(crate) async fn generate_token_pair<R: RepositoryAccess>(
let access_token = repo
.oauth2_access_token()
.add(rng, clock, session, access_token_str.clone(), ttl)
.add(rng, clock, session, access_token_str, ttl)
.await?;
let refresh_token = repo

View File

@ -302,6 +302,11 @@ async fn authorization_code_grant(
.await?
.ok_or(RouteError::NoSuchBrowserSession)?;
let last_authentication = repo
.browser_session()
.get_last_authentication(&browser_session)
.await?;
let ttl = Duration::minutes(5);
let (access_token, refresh_token) =
generate_token_pair(&mut rng, clock, &mut repo, &session, ttl).await?;
@ -316,6 +321,7 @@ async fn authorization_code_grant(
&authz_grant,
&browser_session,
Some(&access_token),
last_authentication.as_ref(),
)?)
} else {
None

View File

@ -214,9 +214,8 @@ pub(crate) async fn get(
.consume(&clock, upstream_session)
.await?;
let session = repo
.browser_session()
.authenticate_with_upstream(&mut rng, &clock, session, &link)
repo.browser_session()
.authenticate_with_upstream(&mut rng, &clock, &session, &link)
.await?;
cookie_jar = cookie_jar.set_session(&session);
@ -509,9 +508,8 @@ pub(crate) async fn post(
.consume(&clock, upstream_session)
.await?;
let session = repo
.browser_session()
.authenticate_with_upstream(&mut rng, &clock, session, &link)
repo.browser_session()
.authenticate_with_upstream(&mut rng, &clock, &session, &link)
.await?;
let cookie_jar = sessions_cookie

View File

@ -150,9 +150,8 @@ pub(crate) async fn post(
)
.await?;
let session = repo
.browser_session()
.authenticate_with_password(&mut rng, &clock, session, &user_password)
repo.browser_session()
.authenticate_with_password(&mut rng, &clock, &session, &user_password)
.await?;
let reply = render(&mut rng, &clock, templates.clone(), session, cookie_jar).await?;

View File

@ -250,9 +250,8 @@ async fn login(
.map_err(|_| FormError::Internal)?;
// And mark it as authenticated by the password
let user_session = repo
.browser_session()
.authenticate_with_password(&mut rng, clock, user_session, &user_password)
repo.browser_session()
.authenticate_with_password(&mut rng, clock, &user_session, &user_password)
.await
.map_err(|_| FormError::Internal)?;

View File

@ -147,9 +147,8 @@ pub(crate) async fn post(
};
// Mark the session as authenticated by the password
let session = repo
.browser_session()
.authenticate_with_password(&mut rng, &clock, session, &user_password)
repo.browser_session()
.authenticate_with_password(&mut rng, &clock, &session, &user_password)
.await?;
let cookie_jar = cookie_jar.set_session(&session);

View File

@ -209,9 +209,8 @@ pub(crate) async fn post(
let session = repo.browser_session().add(&mut rng, &clock, &user).await?;
let session = repo
.browser_session()
.authenticate_with_password(&mut rng, &clock, session, &user_password)
repo.browser_session()
.authenticate_with_password(&mut rng, &clock, &session, &user_password)
.await?;
repo.job()

View File

@ -0,0 +1,52 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT s.user_session_id\n , s.created_at AS \"user_session_created_at\"\n , s.finished_at AS \"user_session_finished_at\"\n , u.user_id\n , u.username AS \"user_username\"\n , u.primary_user_email_id AS \"user_primary_user_email_id\"\n FROM user_sessions s\n INNER JOIN users u\n USING (user_id)\n WHERE s.user_session_id = $1\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "user_session_id",
"type_info": "Uuid"
},
{
"ordinal": 1,
"name": "user_session_created_at",
"type_info": "Timestamptz"
},
{
"ordinal": 2,
"name": "user_session_finished_at",
"type_info": "Timestamptz"
},
{
"ordinal": 3,
"name": "user_id",
"type_info": "Uuid"
},
{
"ordinal": 4,
"name": "user_username",
"type_info": "Text"
},
{
"ordinal": 5,
"name": "user_primary_user_email_id",
"type_info": "Uuid"
}
],
"parameters": {
"Left": [
"Uuid"
]
},
"nullable": [
false,
false,
true,
false,
false,
true
]
},
"hash": "25d61a373560556deafe056c8cd2982ac472f5ec2fab08b0b5275c4b78c11a7e"
}

View File

@ -1,22 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT COUNT(*) as \"count!\"\n FROM user_sessions s\n WHERE s.user_id = $1 AND s.finished_at IS NULL\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "count!",
"type_info": "Int8"
}
],
"parameters": {
"Left": [
"Uuid"
]
},
"nullable": [
null
]
},
"hash": "751d549073d77ded84aea1aaba36d3b130ec71bc592d722eb75b959b80f0b4ff"
}

View File

@ -1,64 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT s.user_session_id\n , s.created_at AS \"user_session_created_at\"\n , s.finished_at AS \"user_session_finished_at\"\n , u.user_id\n , u.username AS \"user_username\"\n , u.primary_user_email_id AS \"user_primary_user_email_id\"\n , a.user_session_authentication_id AS \"last_authentication_id?\"\n , a.created_at AS \"last_authd_at?\"\n FROM user_sessions s\n INNER JOIN users u\n USING (user_id)\n LEFT JOIN user_session_authentications a\n USING (user_session_id)\n WHERE s.user_session_id = $1\n ORDER BY a.created_at DESC\n LIMIT 1\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "user_session_id",
"type_info": "Uuid"
},
{
"ordinal": 1,
"name": "user_session_created_at",
"type_info": "Timestamptz"
},
{
"ordinal": 2,
"name": "user_session_finished_at",
"type_info": "Timestamptz"
},
{
"ordinal": 3,
"name": "user_id",
"type_info": "Uuid"
},
{
"ordinal": 4,
"name": "user_username",
"type_info": "Text"
},
{
"ordinal": 5,
"name": "user_primary_user_email_id",
"type_info": "Uuid"
},
{
"ordinal": 6,
"name": "last_authentication_id?",
"type_info": "Uuid"
},
{
"ordinal": 7,
"name": "last_authd_at?",
"type_info": "Timestamptz"
}
],
"parameters": {
"Left": [
"Uuid"
]
},
"nullable": [
false,
false,
true,
false,
false,
true,
false,
false
]
},
"hash": "79295f3d3a75f831e9469aabfa720d381a254d00dbe39fef1e9652029d51b89b"
}

View File

@ -0,0 +1,28 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT user_session_authentication_id AS id\n , created_at\n FROM user_session_authentications\n WHERE user_session_id = $1\n ORDER BY created_at DESC\n LIMIT 1\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Uuid"
},
{
"ordinal": 1,
"name": "created_at",
"type_info": "Timestamptz"
}
],
"parameters": {
"Left": [
"Uuid"
]
},
"nullable": [
false,
false
]
},
"hash": "ce0dbf84b23f4d5cfbd068811149d88898d4c5df8ab557846e2f9184636f2dcf"
}

View File

@ -25,9 +25,9 @@ pub(crate) fn map_values(values: sea_query::Values) -> sqlx::postgres::PgArgumen
Value::SmallInt(i) => arguments.add(i),
Value::Int(i) => arguments.add(i),
Value::BigInt(i) => arguments.add(i),
Value::TinyUnsigned(u) => arguments.add(u.map(|u| u as i16)),
Value::SmallUnsigned(u) => arguments.add(u.map(|u| u as i32)),
Value::Unsigned(u) => arguments.add(u.map(|u| u as i64)),
Value::TinyUnsigned(u) => arguments.add(u.map(i16::from)),
Value::SmallUnsigned(u) => arguments.add(u.map(i32::from)),
Value::Unsigned(u) => arguments.add(u.map(i64::from)),
Value::BigUnsigned(u) => arguments.add(u.map(|u| i64::try_from(u).unwrap_or(i64::MAX))),
Value::Float(f) => arguments.add(f),
Value::Double(d) => arguments.add(d),
@ -41,6 +41,9 @@ pub(crate) fn map_values(values: sea_query::Values) -> sqlx::postgres::PgArgumen
Value::ChronoDateTimeLocal(dt) => arguments.add(dt.as_deref()),
Value::ChronoDateTimeWithTimeZone(dt) => arguments.add(dt.as_deref()),
Value::Uuid(u) => arguments.add(u.as_deref()),
// This depends on the features enabled for sea-query, so let's keep the wildcard
#[allow(unreachable_patterns)]
_ => unimplemented!(),
}
}

View File

@ -18,7 +18,7 @@ use mas_data_model::{Authentication, BrowserSession, Password, UpstreamOAuthLink
use mas_storage::{user::BrowserSessionRepository, Clock, Page, Pagination};
use rand::RngCore;
use sea_query::{Expr, IntoColumnRef, PostgresQueryBuilder};
use sqlx::{PgConnection, QueryBuilder};
use sqlx::PgConnection;
use ulid::Ulid;
use uuid::Uuid;
@ -50,8 +50,6 @@ struct SessionLookup {
user_id: Uuid,
user_username: String,
user_primary_user_email_id: Option<Uuid>,
last_authentication_id: Option<Uuid>,
last_authd_at: Option<DateTime<Utc>>,
}
#[derive(sea_query::Iden)]
@ -71,14 +69,6 @@ enum Users {
PrimaryUserEmailId,
}
#[derive(sea_query::Iden)]
enum SessionAuthentication {
Table,
UserSessionAuthenticationId,
UserSessionId,
CreatedAt,
}
impl TryFrom<SessionLookup> for BrowserSession {
type Error = DatabaseInconsistencyError;
@ -91,25 +81,11 @@ impl TryFrom<SessionLookup> for BrowserSession {
primary_user_email_id: value.user_primary_user_email_id.map(Into::into),
};
let last_authentication = match (value.last_authentication_id, value.last_authd_at) {
(Some(id), Some(created_at)) => Some(Authentication {
id: id.into(),
created_at,
}),
(None, None) => None,
_ => {
return Err(DatabaseInconsistencyError::on(
"user_session_authentications",
))
}
};
Ok(BrowserSession {
id: value.user_session_id.into(),
user,
created_at: value.user_session_created_at,
finished_at: value.user_session_finished_at,
last_authentication,
})
}
}
@ -137,16 +113,10 @@ impl<'c> BrowserSessionRepository for PgBrowserSessionRepository<'c> {
, u.user_id
, u.username AS "user_username"
, u.primary_user_email_id AS "user_primary_user_email_id"
, a.user_session_authentication_id AS "last_authentication_id?"
, a.created_at AS "last_authd_at?"
FROM user_sessions s
INNER JOIN users u
USING (user_id)
LEFT JOIN user_session_authentications a
USING (user_session_id)
WHERE s.user_session_id = $1
ORDER BY a.created_at DESC
LIMIT 1
"#,
Uuid::from(id),
)
@ -199,7 +169,6 @@ impl<'c> BrowserSessionRepository for PgBrowserSessionRepository<'c> {
user: user.clone(),
created_at,
finished_at: None,
last_authentication: None,
};
Ok(session)
@ -278,14 +247,6 @@ impl<'c> BrowserSessionRepository for PgBrowserSessionRepository<'c> {
Expr::col((Users::Table, Users::PrimaryUserEmailId)),
SessionLookupIden::UserPrimaryUserEmailId,
)
.expr_as(
Expr::value(None::<Uuid>),
SessionLookupIden::LastAuthenticationId,
)
.expr_as(
Expr::value(None::<DateTime<Utc>>),
SessionLookupIden::LastAuthdAt,
)
.from(UserSessions::Table)
.inner_join(
Users::Table,
@ -324,6 +285,45 @@ impl<'c> BrowserSessionRepository for PgBrowserSessionRepository<'c> {
Ok(page)
}
#[tracing::instrument(
name = "db.browser_session.count",
skip_all,
fields(
db.statement,
),
err,
)]
async fn count(
&mut self,
filter: mas_storage::user::BrowserSessionFilter<'_>,
) -> Result<usize, Self::Error> {
let (sql, values) = sea_query::Query::select()
.expr(Expr::col((UserSessions::Table, UserSessions::UserSessionId)).count())
.from(UserSessions::Table)
.and_where_option(filter.user().map(|user| {
Expr::col((UserSessions::Table, UserSessions::UserId)).eq(Uuid::from(user.id))
}))
.and_where_option(filter.state().map(|state| {
if state.is_active() {
Expr::col((UserSessions::Table, UserSessions::FinishedAt)).is_null()
} else {
Expr::col((UserSessions::Table, UserSessions::FinishedAt)).is_not_null()
}
}))
.build(PostgresQueryBuilder);
let arguments = map_values(values);
let count: i64 = sqlx::query_scalar_with(&sql, arguments)
.traced()
.fetch_one(&mut *self.conn)
.await?;
count
.try_into()
.map_err(DatabaseError::to_invalid_operation)
}
#[tracing::instrument(
name = "db.browser_session.authenticate_with_password",
skip_all,
@ -339,9 +339,9 @@ impl<'c> BrowserSessionRepository for PgBrowserSessionRepository<'c> {
&mut self,
rng: &mut (dyn RngCore + Send),
clock: &dyn Clock,
mut user_session: BrowserSession,
user_session: &BrowserSession,
user_password: &Password,
) -> Result<BrowserSession, Self::Error> {
) -> Result<Authentication, Self::Error> {
let _user_password = user_password;
let created_at = clock.now();
let id = Ulid::from_datetime_with_source(created_at.into(), rng);
@ -364,9 +364,7 @@ impl<'c> BrowserSessionRepository for PgBrowserSessionRepository<'c> {
.execute(&mut *self.conn)
.await?;
user_session.last_authentication = Some(Authentication { id, created_at });
Ok(user_session)
Ok(Authentication { id, created_at })
}
#[tracing::instrument(
@ -384,9 +382,9 @@ impl<'c> BrowserSessionRepository for PgBrowserSessionRepository<'c> {
&mut self,
rng: &mut (dyn RngCore + Send),
clock: &dyn Clock,
mut user_session: BrowserSession,
user_session: &BrowserSession,
upstream_oauth_link: &UpstreamOAuthLink,
) -> Result<BrowserSession, Self::Error> {
) -> Result<Authentication, Self::Error> {
let _upstream_oauth_link = upstream_oauth_link;
let created_at = clock.now();
let id = Ulid::from_datetime_with_source(created_at.into(), rng);
@ -409,8 +407,38 @@ impl<'c> BrowserSessionRepository for PgBrowserSessionRepository<'c> {
.execute(&mut *self.conn)
.await?;
user_session.last_authentication = Some(Authentication { id, created_at });
Ok(Authentication { id, created_at })
}
Ok(user_session)
#[tracing::instrument(
name = "db.browser_session.get_last_authentication",
skip_all,
fields(
db.statement,
%user_session.id,
),
err,
)]
async fn get_last_authentication(
&mut self,
user_session: &BrowserSession,
) -> Result<Option<Authentication>, Self::Error> {
let authentication = sqlx::query_as!(
Authentication,
r#"
SELECT user_session_authentication_id AS id
, created_at
FROM user_session_authentications
WHERE user_session_id = $1
ORDER BY created_at DESC
LIMIT 1
"#,
Uuid::from(user_session.id),
)
.traced()
.fetch_optional(&mut *self.conn)
.await?;
Ok(authentication)
}
}

View File

@ -15,7 +15,10 @@
use chrono::Duration;
use mas_storage::{
clock::MockClock,
user::{BrowserSessionRepository, UserEmailRepository, UserPasswordRepository, UserRepository},
user::{
BrowserSessionFilter, BrowserSessionRepository, UserEmailRepository,
UserPasswordRepository, UserRepository,
},
Pagination, Repository, RepositoryAccess,
};
use rand::SeedableRng;
@ -360,7 +363,11 @@ async fn test_user_session(pool: PgPool) {
.await
.unwrap();
assert_eq!(repo.browser_session().count_active(&user).await.unwrap(), 0);
let filter = BrowserSessionFilter::default()
.for_user(&user)
.active_only();
assert_eq!(repo.browser_session().count(filter).await.unwrap(), 0);
let session = repo
.browser_session()
@ -370,12 +377,12 @@ async fn test_user_session(pool: PgPool) {
assert_eq!(session.user.id, user.id);
assert!(session.finished_at.is_none());
assert_eq!(repo.browser_session().count_active(&user).await.unwrap(), 1);
assert_eq!(repo.browser_session().count(filter).await.unwrap(), 1);
// The session should be in the list of active sessions
let session_list = repo
.browser_session()
.list_active_paginated(&user, Pagination::first(10))
.list(filter, Pagination::first(10))
.await
.unwrap();
assert!(!session_list.has_next_page);
@ -400,12 +407,12 @@ async fn test_user_session(pool: PgPool) {
.unwrap();
// The active session counter is back to 0
assert_eq!(repo.browser_session().count_active(&user).await.unwrap(), 0);
assert_eq!(repo.browser_session().count(filter).await.unwrap(), 0);
// The session should not be in the list of active sessions anymore
let session_list = repo
.browser_session()
.list_active_paginated(&user, Pagination::first(10))
.list(filter, Pagination::first(10))
.await
.unwrap();
assert!(!session_list.has_next_page);

View File

@ -13,7 +13,7 @@
// limitations under the License.
use async_trait::async_trait;
use mas_data_model::{BrowserSession, Password, UpstreamOAuthLink, User};
use mas_data_model::{Authentication, BrowserSession, Password, UpstreamOAuthLink, User};
use rand_core::RngCore;
use ulid::Ulid;
@ -157,9 +157,18 @@ pub trait BrowserSessionRepository: Send + Sync {
pagination: Pagination,
) -> Result<Page<BrowserSession>, Self::Error>;
/// Authenticate a [`BrowserSession`] with the given [`Password`]
/// Count the number of [`BrowserSession`] with the given filter
///
/// Returns the updated [`BrowserSession`]
/// # Parameters
///
/// * `filter`: The filter to apply
///
/// # Errors
///
/// Returns [`Self::Error`] if the underlying repository fails
async fn count(&mut self, filter: BrowserSessionFilter<'_>) -> Result<usize, Self::Error>;
/// Authenticate a [`BrowserSession`] with the given [`Password`]
///
/// # Parameters
///
@ -175,14 +184,12 @@ pub trait BrowserSessionRepository: Send + Sync {
&mut self,
rng: &mut (dyn RngCore + Send),
clock: &dyn Clock,
user_session: BrowserSession,
user_session: &BrowserSession,
user_password: &Password,
) -> Result<BrowserSession, Self::Error>;
) -> Result<Authentication, Self::Error>;
/// Authenticate a [`BrowserSession`] with the given [`UpstreamOAuthLink`]
///
/// Returns the updated [`BrowserSession`]
///
/// # Parameters
///
/// * `rng`: The random number generator to use
@ -198,9 +205,23 @@ pub trait BrowserSessionRepository: Send + Sync {
&mut self,
rng: &mut (dyn RngCore + Send),
clock: &dyn Clock,
user_session: BrowserSession,
user_session: &BrowserSession,
upstream_oauth_link: &UpstreamOAuthLink,
) -> Result<BrowserSession, Self::Error>;
) -> Result<Authentication, Self::Error>;
/// Get the last successful authentication for a [`BrowserSession`]
///
/// # Params
///
/// * `user_session`: The session for which to get the last authentication
///
/// # Errors
///
/// Returns [`Self::Error`] if the underlying repository fails
async fn get_last_authentication(
&mut self,
user_session: &BrowserSession,
) -> Result<Option<Authentication>, Self::Error>;
}
repository_impl!(BrowserSessionRepository:
@ -223,19 +244,26 @@ repository_impl!(BrowserSessionRepository:
pagination: Pagination,
) -> Result<Page<BrowserSession>, Self::Error>;
async fn count(&mut self, filter: BrowserSessionFilter<'_>) -> Result<usize, Self::Error>;
async fn authenticate_with_password(
&mut self,
rng: &mut (dyn RngCore + Send),
clock: &dyn Clock,
user_session: BrowserSession,
user_session: &BrowserSession,
user_password: &Password,
) -> Result<BrowserSession, Self::Error>;
) -> Result<Authentication, Self::Error>;
async fn authenticate_with_upstream(
&mut self,
rng: &mut (dyn RngCore + Send),
clock: &dyn Clock,
user_session: BrowserSession,
user_session: &BrowserSession,
upstream_oauth_link: &UpstreamOAuthLink,
) -> Result<BrowserSession, Self::Error>;
) -> Result<Authentication, Self::Error>;
async fn get_last_authentication(
&mut self,
user_session: &BrowserSession,
) -> Result<Option<Authentication>, Self::Error>;
);

View File

@ -102,6 +102,10 @@ type BrowserSessionConnection {
A list of nodes.
"""
nodes: [BrowserSession!]!
"""
Identifies the total count of items in the connection.
"""
totalCount: Int!
}
"""

View File

@ -50,6 +50,8 @@ const QUERY = graphql(/* GraphQL */ `
before: $before
state: ACTIVE
) {
totalCount
edges {
cursor
node {
@ -129,6 +131,7 @@ const BrowserSessionList: React.FC<{ userId: string }> = ({ userId }) => {
<PaginationControls
onPrev={prevPage ? (): void => paginate(prevPage) : null}
onNext={nextPage ? (): void => paginate(nextPage) : null}
count={browserSessions.totalCount}
disabled={pending}
/>
{browserSessions.edges.map((n) => (

View File

@ -23,7 +23,7 @@ const documents = {
types.BrowserSession_SessionFragmentDoc,
"\n mutation EndBrowserSession($id: ID!) {\n endBrowserSession(input: { browserSessionId: $id }) {\n status\n browserSession {\n id\n ...BrowserSession_session\n }\n }\n }\n":
types.EndBrowserSessionDocument,
"\n query BrowserSessionList(\n $userId: ID!\n $first: Int\n $after: String\n $last: Int\n $before: String\n ) {\n user(id: $userId) {\n id\n browserSessions(\n first: $first\n after: $after\n last: $last\n before: $before\n state: ACTIVE\n ) {\n edges {\n cursor\n node {\n id\n ...BrowserSession_session\n }\n }\n\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n }\n }\n":
"\n query BrowserSessionList(\n $userId: ID!\n $first: Int\n $after: String\n $last: Int\n $before: String\n ) {\n user(id: $userId) {\n id\n browserSessions(\n first: $first\n after: $after\n last: $last\n before: $before\n state: ACTIVE\n ) {\n totalCount\n\n edges {\n cursor\n node {\n id\n ...BrowserSession_session\n }\n }\n\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n }\n }\n":
types.BrowserSessionListDocument,
"\n fragment CompatSession_sso_login on CompatSsoLogin {\n id\n redirectUri\n }\n":
types.CompatSession_Sso_LoginFragmentDoc,
@ -109,8 +109,8 @@ export function graphql(
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(
source: "\n query BrowserSessionList(\n $userId: ID!\n $first: Int\n $after: String\n $last: Int\n $before: String\n ) {\n user(id: $userId) {\n id\n browserSessions(\n first: $first\n after: $after\n last: $last\n before: $before\n state: ACTIVE\n ) {\n edges {\n cursor\n node {\n id\n ...BrowserSession_session\n }\n }\n\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n }\n }\n"
): typeof documents["\n query BrowserSessionList(\n $userId: ID!\n $first: Int\n $after: String\n $last: Int\n $before: String\n ) {\n user(id: $userId) {\n id\n browserSessions(\n first: $first\n after: $after\n last: $last\n before: $before\n state: ACTIVE\n ) {\n edges {\n cursor\n node {\n id\n ...BrowserSession_session\n }\n }\n\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n }\n }\n"];
source: "\n query BrowserSessionList(\n $userId: ID!\n $first: Int\n $after: String\n $last: Int\n $before: String\n ) {\n user(id: $userId) {\n id\n browserSessions(\n first: $first\n after: $after\n last: $last\n before: $before\n state: ACTIVE\n ) {\n totalCount\n\n edges {\n cursor\n node {\n id\n ...BrowserSession_session\n }\n }\n\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n }\n }\n"
): typeof documents["\n query BrowserSessionList(\n $userId: ID!\n $first: Int\n $after: String\n $last: Int\n $before: String\n ) {\n user(id: $userId) {\n id\n browserSessions(\n first: $first\n after: $after\n last: $last\n before: $before\n state: ACTIVE\n ) {\n totalCount\n\n edges {\n cursor\n node {\n id\n ...BrowserSession_session\n }\n }\n\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/

View File

@ -106,6 +106,8 @@ export type BrowserSessionConnection = {
nodes: Array<BrowserSession>;
/** Information to aid in pagination. */
pageInfo: PageInfo;
/** Identifies the total count of items in the connection. */
totalCount: Scalars["Int"]["output"];
};
/** An edge in a connection. */
@ -879,6 +881,7 @@ export type BrowserSessionListQuery = {
id: string;
browserSessions: {
__typename?: "BrowserSessionConnection";
totalCount: number;
edges: Array<{
__typename?: "BrowserSessionEdge";
cursor: string;
@ -1815,6 +1818,10 @@ export const BrowserSessionListDocument = {
selectionSet: {
kind: "SelectionSet",
selections: [
{
kind: "Field",
name: { kind: "Name", value: "totalCount" },
},
{
kind: "Field",
name: { kind: "Name", value: "edges" },

View File

@ -217,6 +217,17 @@ export default {
},
args: [],
},
{
name: "totalCount",
type: {
kind: "NON_NULL",
ofType: {
kind: "SCALAR",
name: "Any",
},
},
args: [],
},
],
interfaces: [],
},