You've already forked authentication-service
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:
@ -60,7 +60,6 @@ pub struct BrowserSession {
|
|||||||
pub user: User,
|
pub user: User,
|
||||||
pub created_at: DateTime<Utc>,
|
pub created_at: DateTime<Utc>,
|
||||||
pub finished_at: Option<DateTime<Utc>>,
|
pub finished_at: Option<DateTime<Utc>>,
|
||||||
pub last_authentication: Option<Authentication>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl BrowserSession {
|
impl BrowserSession {
|
||||||
@ -68,15 +67,6 @@ impl BrowserSession {
|
|||||||
pub fn active(&self) -> bool {
|
pub fn active(&self) -> bool {
|
||||||
self.finished_at.is_none()
|
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 {
|
impl BrowserSession {
|
||||||
@ -89,7 +79,6 @@ impl BrowserSession {
|
|||||||
user,
|
user,
|
||||||
created_at: now,
|
created_at: now,
|
||||||
finished_at: None,
|
finished_at: None,
|
||||||
last_authentication: None,
|
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
@ -12,10 +12,12 @@
|
|||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
use async_graphql::{Description, Enum, Object, ID};
|
use async_graphql::{Context, Description, Enum, Object, ID};
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
|
use mas_storage::{user::BrowserSessionRepository, RepositoryAccess};
|
||||||
|
|
||||||
use super::{NodeType, User};
|
use super::{NodeType, User};
|
||||||
|
use crate::state::ContextExt;
|
||||||
|
|
||||||
/// A browser session represents a logged in user in a browser.
|
/// A browser session represents a logged in user in a browser.
|
||||||
#[derive(Description)]
|
#[derive(Description)]
|
||||||
@ -50,8 +52,21 @@ impl BrowserSession {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// The most recent authentication of this session.
|
/// The most recent authentication of this session.
|
||||||
async fn last_authentication(&self) -> Option<Authentication> {
|
async fn last_authentication(
|
||||||
self.0.last_authentication.clone().map(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.
|
/// When the object was created.
|
||||||
|
@ -198,7 +198,7 @@ impl User {
|
|||||||
before: Option<String>,
|
before: Option<String>,
|
||||||
#[graphql(desc = "Returns the first *n* elements from the list.")] first: Option<i32>,
|
#[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>,
|
#[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 state = ctx.state();
|
||||||
let mut repo = state.repository().await?;
|
let mut repo = state.repository().await?;
|
||||||
|
|
||||||
@ -225,9 +225,20 @@ impl User {
|
|||||||
|
|
||||||
let page = repo.browser_session().list(filter, pagination).await?;
|
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?;
|
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| {
|
connection.edges.extend(page.edges.into_iter().map(|u| {
|
||||||
Edge::new(
|
Edge::new(
|
||||||
OpaqueCursor(NodeCursor(NodeType::BrowserSession, u.id)),
|
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
|
/// A user email address
|
||||||
#[derive(Description)]
|
#[derive(Description)]
|
||||||
pub struct UserEmail(pub mas_data_model::UserEmail);
|
pub struct UserEmail(pub mas_data_model::UserEmail);
|
||||||
|
@ -27,7 +27,8 @@ use mas_policy::PolicyFactory;
|
|||||||
use mas_router::{PostAuthAction, Route, UrlBuilder};
|
use mas_router::{PostAuthAction, Route, UrlBuilder};
|
||||||
use mas_storage::{
|
use mas_storage::{
|
||||||
oauth2::{OAuth2AuthorizationGrantRepository, OAuth2ClientRepository, OAuth2SessionRepository},
|
oauth2::{OAuth2AuthorizationGrantRepository, OAuth2ClientRepository, OAuth2SessionRepository},
|
||||||
BoxClock, BoxRepository, BoxRng,
|
user::BrowserSessionRepository,
|
||||||
|
BoxClock, BoxRepository, BoxRng, RepositoryAccess,
|
||||||
};
|
};
|
||||||
use mas_templates::Templates;
|
use mas_templates::Templates;
|
||||||
use oauth2_types::requests::AuthorizationResponse;
|
use oauth2_types::requests::AuthorizationResponse;
|
||||||
@ -194,10 +195,16 @@ pub(crate) async fn complete(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check if the authentication is fresh enough
|
// 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?;
|
repo.save().await?;
|
||||||
return Err(GrantCompletionError::RequiresReauth);
|
return Err(GrantCompletionError::RequiresReauth);
|
||||||
}
|
};
|
||||||
|
|
||||||
// Run through the policy
|
// Run through the policy
|
||||||
let mut policy = policy_factory.instantiate().await?;
|
let mut policy = policy_factory.instantiate().await?;
|
||||||
@ -257,6 +264,7 @@ pub(crate) async fn complete(
|
|||||||
&grant,
|
&grant,
|
||||||
&browser_session,
|
&browser_session,
|
||||||
None,
|
None,
|
||||||
|
Some(&valid_authentication),
|
||||||
)?);
|
)?);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -16,7 +16,8 @@ use std::collections::HashMap;
|
|||||||
|
|
||||||
use chrono::Duration;
|
use chrono::Duration;
|
||||||
use mas_data_model::{
|
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_iana::jose::JsonWebSignatureAlg;
|
||||||
use mas_jose::{
|
use mas_jose::{
|
||||||
@ -60,6 +61,7 @@ pub(crate) fn generate_id_token(
|
|||||||
grant: &AuthorizationGrant,
|
grant: &AuthorizationGrant,
|
||||||
browser_session: &BrowserSession,
|
browser_session: &BrowserSession,
|
||||||
access_token: Option<&AccessToken>,
|
access_token: Option<&AccessToken>,
|
||||||
|
last_authentication: Option<&Authentication>,
|
||||||
) -> Result<String, IdTokenSignatureError> {
|
) -> Result<String, IdTokenSignatureError> {
|
||||||
let mut claims = HashMap::new();
|
let mut claims = HashMap::new();
|
||||||
let now = clock.now();
|
let now = clock.now();
|
||||||
@ -73,7 +75,7 @@ pub(crate) fn generate_id_token(
|
|||||||
claims::NONCE.insert(&mut claims, nonce.clone())?;
|
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)?;
|
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
|
let access_token = repo
|
||||||
.oauth2_access_token()
|
.oauth2_access_token()
|
||||||
.add(rng, clock, session, access_token_str.clone(), ttl)
|
.add(rng, clock, session, access_token_str, ttl)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let refresh_token = repo
|
let refresh_token = repo
|
||||||
|
@ -302,6 +302,11 @@ async fn authorization_code_grant(
|
|||||||
.await?
|
.await?
|
||||||
.ok_or(RouteError::NoSuchBrowserSession)?;
|
.ok_or(RouteError::NoSuchBrowserSession)?;
|
||||||
|
|
||||||
|
let last_authentication = repo
|
||||||
|
.browser_session()
|
||||||
|
.get_last_authentication(&browser_session)
|
||||||
|
.await?;
|
||||||
|
|
||||||
let ttl = Duration::minutes(5);
|
let ttl = Duration::minutes(5);
|
||||||
let (access_token, refresh_token) =
|
let (access_token, refresh_token) =
|
||||||
generate_token_pair(&mut rng, clock, &mut repo, &session, ttl).await?;
|
generate_token_pair(&mut rng, clock, &mut repo, &session, ttl).await?;
|
||||||
@ -316,6 +321,7 @@ async fn authorization_code_grant(
|
|||||||
&authz_grant,
|
&authz_grant,
|
||||||
&browser_session,
|
&browser_session,
|
||||||
Some(&access_token),
|
Some(&access_token),
|
||||||
|
last_authentication.as_ref(),
|
||||||
)?)
|
)?)
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
|
@ -214,9 +214,8 @@ pub(crate) async fn get(
|
|||||||
.consume(&clock, upstream_session)
|
.consume(&clock, upstream_session)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let session = repo
|
repo.browser_session()
|
||||||
.browser_session()
|
.authenticate_with_upstream(&mut rng, &clock, &session, &link)
|
||||||
.authenticate_with_upstream(&mut rng, &clock, session, &link)
|
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
cookie_jar = cookie_jar.set_session(&session);
|
cookie_jar = cookie_jar.set_session(&session);
|
||||||
@ -509,9 +508,8 @@ pub(crate) async fn post(
|
|||||||
.consume(&clock, upstream_session)
|
.consume(&clock, upstream_session)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let session = repo
|
repo.browser_session()
|
||||||
.browser_session()
|
.authenticate_with_upstream(&mut rng, &clock, &session, &link)
|
||||||
.authenticate_with_upstream(&mut rng, &clock, session, &link)
|
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let cookie_jar = sessions_cookie
|
let cookie_jar = sessions_cookie
|
||||||
|
@ -150,9 +150,8 @@ pub(crate) async fn post(
|
|||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let session = repo
|
repo.browser_session()
|
||||||
.browser_session()
|
.authenticate_with_password(&mut rng, &clock, &session, &user_password)
|
||||||
.authenticate_with_password(&mut rng, &clock, session, &user_password)
|
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let reply = render(&mut rng, &clock, templates.clone(), session, cookie_jar).await?;
|
let reply = render(&mut rng, &clock, templates.clone(), session, cookie_jar).await?;
|
||||||
|
@ -250,9 +250,8 @@ async fn login(
|
|||||||
.map_err(|_| FormError::Internal)?;
|
.map_err(|_| FormError::Internal)?;
|
||||||
|
|
||||||
// And mark it as authenticated by the password
|
// And mark it as authenticated by the password
|
||||||
let user_session = repo
|
repo.browser_session()
|
||||||
.browser_session()
|
.authenticate_with_password(&mut rng, clock, &user_session, &user_password)
|
||||||
.authenticate_with_password(&mut rng, clock, user_session, &user_password)
|
|
||||||
.await
|
.await
|
||||||
.map_err(|_| FormError::Internal)?;
|
.map_err(|_| FormError::Internal)?;
|
||||||
|
|
||||||
|
@ -147,9 +147,8 @@ pub(crate) async fn post(
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Mark the session as authenticated by the password
|
// Mark the session as authenticated by the password
|
||||||
let session = repo
|
repo.browser_session()
|
||||||
.browser_session()
|
.authenticate_with_password(&mut rng, &clock, &session, &user_password)
|
||||||
.authenticate_with_password(&mut rng, &clock, session, &user_password)
|
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let cookie_jar = cookie_jar.set_session(&session);
|
let cookie_jar = cookie_jar.set_session(&session);
|
||||||
|
@ -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().add(&mut rng, &clock, &user).await?;
|
||||||
|
|
||||||
let session = repo
|
repo.browser_session()
|
||||||
.browser_session()
|
.authenticate_with_password(&mut rng, &clock, &session, &user_password)
|
||||||
.authenticate_with_password(&mut rng, &clock, session, &user_password)
|
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
repo.job()
|
repo.job()
|
||||||
|
52
crates/storage-pg/.sqlx/query-25d61a373560556deafe056c8cd2982ac472f5ec2fab08b0b5275c4b78c11a7e.json
generated
Normal file
52
crates/storage-pg/.sqlx/query-25d61a373560556deafe056c8cd2982ac472f5ec2fab08b0b5275c4b78c11a7e.json
generated
Normal 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"
|
||||||
|
}
|
@ -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"
|
|
||||||
}
|
|
@ -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"
|
|
||||||
}
|
|
28
crates/storage-pg/.sqlx/query-ce0dbf84b23f4d5cfbd068811149d88898d4c5df8ab557846e2f9184636f2dcf.json
generated
Normal file
28
crates/storage-pg/.sqlx/query-ce0dbf84b23f4d5cfbd068811149d88898d4c5df8ab557846e2f9184636f2dcf.json
generated
Normal 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"
|
||||||
|
}
|
@ -25,9 +25,9 @@ pub(crate) fn map_values(values: sea_query::Values) -> sqlx::postgres::PgArgumen
|
|||||||
Value::SmallInt(i) => arguments.add(i),
|
Value::SmallInt(i) => arguments.add(i),
|
||||||
Value::Int(i) => arguments.add(i),
|
Value::Int(i) => arguments.add(i),
|
||||||
Value::BigInt(i) => arguments.add(i),
|
Value::BigInt(i) => arguments.add(i),
|
||||||
Value::TinyUnsigned(u) => arguments.add(u.map(|u| u as i16)),
|
Value::TinyUnsigned(u) => arguments.add(u.map(i16::from)),
|
||||||
Value::SmallUnsigned(u) => arguments.add(u.map(|u| u as i32)),
|
Value::SmallUnsigned(u) => arguments.add(u.map(i32::from)),
|
||||||
Value::Unsigned(u) => arguments.add(u.map(|u| u as i64)),
|
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::BigUnsigned(u) => arguments.add(u.map(|u| i64::try_from(u).unwrap_or(i64::MAX))),
|
||||||
Value::Float(f) => arguments.add(f),
|
Value::Float(f) => arguments.add(f),
|
||||||
Value::Double(d) => arguments.add(d),
|
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::ChronoDateTimeLocal(dt) => arguments.add(dt.as_deref()),
|
||||||
Value::ChronoDateTimeWithTimeZone(dt) => arguments.add(dt.as_deref()),
|
Value::ChronoDateTimeWithTimeZone(dt) => arguments.add(dt.as_deref()),
|
||||||
Value::Uuid(u) => arguments.add(u.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!(),
|
_ => unimplemented!(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -18,7 +18,7 @@ use mas_data_model::{Authentication, BrowserSession, Password, UpstreamOAuthLink
|
|||||||
use mas_storage::{user::BrowserSessionRepository, Clock, Page, Pagination};
|
use mas_storage::{user::BrowserSessionRepository, Clock, Page, Pagination};
|
||||||
use rand::RngCore;
|
use rand::RngCore;
|
||||||
use sea_query::{Expr, IntoColumnRef, PostgresQueryBuilder};
|
use sea_query::{Expr, IntoColumnRef, PostgresQueryBuilder};
|
||||||
use sqlx::{PgConnection, QueryBuilder};
|
use sqlx::PgConnection;
|
||||||
use ulid::Ulid;
|
use ulid::Ulid;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
@ -50,8 +50,6 @@ struct SessionLookup {
|
|||||||
user_id: Uuid,
|
user_id: Uuid,
|
||||||
user_username: String,
|
user_username: String,
|
||||||
user_primary_user_email_id: Option<Uuid>,
|
user_primary_user_email_id: Option<Uuid>,
|
||||||
last_authentication_id: Option<Uuid>,
|
|
||||||
last_authd_at: Option<DateTime<Utc>>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(sea_query::Iden)]
|
#[derive(sea_query::Iden)]
|
||||||
@ -71,14 +69,6 @@ enum Users {
|
|||||||
PrimaryUserEmailId,
|
PrimaryUserEmailId,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(sea_query::Iden)]
|
|
||||||
enum SessionAuthentication {
|
|
||||||
Table,
|
|
||||||
UserSessionAuthenticationId,
|
|
||||||
UserSessionId,
|
|
||||||
CreatedAt,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TryFrom<SessionLookup> for BrowserSession {
|
impl TryFrom<SessionLookup> for BrowserSession {
|
||||||
type Error = DatabaseInconsistencyError;
|
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),
|
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 {
|
Ok(BrowserSession {
|
||||||
id: value.user_session_id.into(),
|
id: value.user_session_id.into(),
|
||||||
user,
|
user,
|
||||||
created_at: value.user_session_created_at,
|
created_at: value.user_session_created_at,
|
||||||
finished_at: value.user_session_finished_at,
|
finished_at: value.user_session_finished_at,
|
||||||
last_authentication,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -132,21 +108,15 @@ impl<'c> BrowserSessionRepository for PgBrowserSessionRepository<'c> {
|
|||||||
SessionLookup,
|
SessionLookup,
|
||||||
r#"
|
r#"
|
||||||
SELECT s.user_session_id
|
SELECT s.user_session_id
|
||||||
, s.created_at AS "user_session_created_at"
|
, s.created_at AS "user_session_created_at"
|
||||||
, s.finished_at AS "user_session_finished_at"
|
, s.finished_at AS "user_session_finished_at"
|
||||||
, u.user_id
|
, u.user_id
|
||||||
, u.username AS "user_username"
|
, u.username AS "user_username"
|
||||||
, u.primary_user_email_id AS "user_primary_user_email_id"
|
, 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
|
FROM user_sessions s
|
||||||
INNER JOIN users u
|
INNER JOIN users u
|
||||||
USING (user_id)
|
USING (user_id)
|
||||||
LEFT JOIN user_session_authentications a
|
|
||||||
USING (user_session_id)
|
|
||||||
WHERE s.user_session_id = $1
|
WHERE s.user_session_id = $1
|
||||||
ORDER BY a.created_at DESC
|
|
||||||
LIMIT 1
|
|
||||||
"#,
|
"#,
|
||||||
Uuid::from(id),
|
Uuid::from(id),
|
||||||
)
|
)
|
||||||
@ -199,7 +169,6 @@ impl<'c> BrowserSessionRepository for PgBrowserSessionRepository<'c> {
|
|||||||
user: user.clone(),
|
user: user.clone(),
|
||||||
created_at,
|
created_at,
|
||||||
finished_at: None,
|
finished_at: None,
|
||||||
last_authentication: None,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(session)
|
Ok(session)
|
||||||
@ -278,14 +247,6 @@ impl<'c> BrowserSessionRepository for PgBrowserSessionRepository<'c> {
|
|||||||
Expr::col((Users::Table, Users::PrimaryUserEmailId)),
|
Expr::col((Users::Table, Users::PrimaryUserEmailId)),
|
||||||
SessionLookupIden::UserPrimaryUserEmailId,
|
SessionLookupIden::UserPrimaryUserEmailId,
|
||||||
)
|
)
|
||||||
.expr_as(
|
|
||||||
Expr::value(None::<Uuid>),
|
|
||||||
SessionLookupIden::LastAuthenticationId,
|
|
||||||
)
|
|
||||||
.expr_as(
|
|
||||||
Expr::value(None::<DateTime<Utc>>),
|
|
||||||
SessionLookupIden::LastAuthdAt,
|
|
||||||
)
|
|
||||||
.from(UserSessions::Table)
|
.from(UserSessions::Table)
|
||||||
.inner_join(
|
.inner_join(
|
||||||
Users::Table,
|
Users::Table,
|
||||||
@ -324,6 +285,45 @@ impl<'c> BrowserSessionRepository for PgBrowserSessionRepository<'c> {
|
|||||||
Ok(page)
|
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(
|
#[tracing::instrument(
|
||||||
name = "db.browser_session.authenticate_with_password",
|
name = "db.browser_session.authenticate_with_password",
|
||||||
skip_all,
|
skip_all,
|
||||||
@ -339,9 +339,9 @@ impl<'c> BrowserSessionRepository for PgBrowserSessionRepository<'c> {
|
|||||||
&mut self,
|
&mut self,
|
||||||
rng: &mut (dyn RngCore + Send),
|
rng: &mut (dyn RngCore + Send),
|
||||||
clock: &dyn Clock,
|
clock: &dyn Clock,
|
||||||
mut user_session: BrowserSession,
|
user_session: &BrowserSession,
|
||||||
user_password: &Password,
|
user_password: &Password,
|
||||||
) -> Result<BrowserSession, Self::Error> {
|
) -> Result<Authentication, Self::Error> {
|
||||||
let _user_password = user_password;
|
let _user_password = user_password;
|
||||||
let created_at = clock.now();
|
let created_at = clock.now();
|
||||||
let id = Ulid::from_datetime_with_source(created_at.into(), rng);
|
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)
|
.execute(&mut *self.conn)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
user_session.last_authentication = Some(Authentication { id, created_at });
|
Ok(Authentication { id, created_at })
|
||||||
|
|
||||||
Ok(user_session)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tracing::instrument(
|
#[tracing::instrument(
|
||||||
@ -384,9 +382,9 @@ impl<'c> BrowserSessionRepository for PgBrowserSessionRepository<'c> {
|
|||||||
&mut self,
|
&mut self,
|
||||||
rng: &mut (dyn RngCore + Send),
|
rng: &mut (dyn RngCore + Send),
|
||||||
clock: &dyn Clock,
|
clock: &dyn Clock,
|
||||||
mut user_session: BrowserSession,
|
user_session: &BrowserSession,
|
||||||
upstream_oauth_link: &UpstreamOAuthLink,
|
upstream_oauth_link: &UpstreamOAuthLink,
|
||||||
) -> Result<BrowserSession, Self::Error> {
|
) -> Result<Authentication, Self::Error> {
|
||||||
let _upstream_oauth_link = upstream_oauth_link;
|
let _upstream_oauth_link = upstream_oauth_link;
|
||||||
let created_at = clock.now();
|
let created_at = clock.now();
|
||||||
let id = Ulid::from_datetime_with_source(created_at.into(), rng);
|
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)
|
.execute(&mut *self.conn)
|
||||||
.await?;
|
.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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -15,7 +15,10 @@
|
|||||||
use chrono::Duration;
|
use chrono::Duration;
|
||||||
use mas_storage::{
|
use mas_storage::{
|
||||||
clock::MockClock,
|
clock::MockClock,
|
||||||
user::{BrowserSessionRepository, UserEmailRepository, UserPasswordRepository, UserRepository},
|
user::{
|
||||||
|
BrowserSessionFilter, BrowserSessionRepository, UserEmailRepository,
|
||||||
|
UserPasswordRepository, UserRepository,
|
||||||
|
},
|
||||||
Pagination, Repository, RepositoryAccess,
|
Pagination, Repository, RepositoryAccess,
|
||||||
};
|
};
|
||||||
use rand::SeedableRng;
|
use rand::SeedableRng;
|
||||||
@ -360,7 +363,11 @@ async fn test_user_session(pool: PgPool) {
|
|||||||
.await
|
.await
|
||||||
.unwrap();
|
.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
|
let session = repo
|
||||||
.browser_session()
|
.browser_session()
|
||||||
@ -370,12 +377,12 @@ async fn test_user_session(pool: PgPool) {
|
|||||||
assert_eq!(session.user.id, user.id);
|
assert_eq!(session.user.id, user.id);
|
||||||
assert!(session.finished_at.is_none());
|
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
|
// The session should be in the list of active sessions
|
||||||
let session_list = repo
|
let session_list = repo
|
||||||
.browser_session()
|
.browser_session()
|
||||||
.list_active_paginated(&user, Pagination::first(10))
|
.list(filter, Pagination::first(10))
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert!(!session_list.has_next_page);
|
assert!(!session_list.has_next_page);
|
||||||
@ -400,12 +407,12 @@ async fn test_user_session(pool: PgPool) {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
// The active session counter is back to 0
|
// 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
|
// The session should not be in the list of active sessions anymore
|
||||||
let session_list = repo
|
let session_list = repo
|
||||||
.browser_session()
|
.browser_session()
|
||||||
.list_active_paginated(&user, Pagination::first(10))
|
.list(filter, Pagination::first(10))
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert!(!session_list.has_next_page);
|
assert!(!session_list.has_next_page);
|
||||||
|
@ -13,7 +13,7 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
use async_trait::async_trait;
|
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 rand_core::RngCore;
|
||||||
use ulid::Ulid;
|
use ulid::Ulid;
|
||||||
|
|
||||||
@ -157,9 +157,18 @@ pub trait BrowserSessionRepository: Send + Sync {
|
|||||||
pagination: Pagination,
|
pagination: Pagination,
|
||||||
) -> Result<Page<BrowserSession>, Self::Error>;
|
) -> 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
|
/// # Parameters
|
||||||
///
|
///
|
||||||
@ -175,14 +184,12 @@ pub trait BrowserSessionRepository: Send + Sync {
|
|||||||
&mut self,
|
&mut self,
|
||||||
rng: &mut (dyn RngCore + Send),
|
rng: &mut (dyn RngCore + Send),
|
||||||
clock: &dyn Clock,
|
clock: &dyn Clock,
|
||||||
user_session: BrowserSession,
|
user_session: &BrowserSession,
|
||||||
user_password: &Password,
|
user_password: &Password,
|
||||||
) -> Result<BrowserSession, Self::Error>;
|
) -> Result<Authentication, Self::Error>;
|
||||||
|
|
||||||
/// Authenticate a [`BrowserSession`] with the given [`UpstreamOAuthLink`]
|
/// Authenticate a [`BrowserSession`] with the given [`UpstreamOAuthLink`]
|
||||||
///
|
///
|
||||||
/// Returns the updated [`BrowserSession`]
|
|
||||||
///
|
|
||||||
/// # Parameters
|
/// # Parameters
|
||||||
///
|
///
|
||||||
/// * `rng`: The random number generator to use
|
/// * `rng`: The random number generator to use
|
||||||
@ -198,9 +205,23 @@ pub trait BrowserSessionRepository: Send + Sync {
|
|||||||
&mut self,
|
&mut self,
|
||||||
rng: &mut (dyn RngCore + Send),
|
rng: &mut (dyn RngCore + Send),
|
||||||
clock: &dyn Clock,
|
clock: &dyn Clock,
|
||||||
user_session: BrowserSession,
|
user_session: &BrowserSession,
|
||||||
upstream_oauth_link: &UpstreamOAuthLink,
|
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:
|
repository_impl!(BrowserSessionRepository:
|
||||||
@ -223,19 +244,26 @@ repository_impl!(BrowserSessionRepository:
|
|||||||
pagination: Pagination,
|
pagination: Pagination,
|
||||||
) -> Result<Page<BrowserSession>, Self::Error>;
|
) -> Result<Page<BrowserSession>, Self::Error>;
|
||||||
|
|
||||||
|
async fn count(&mut self, filter: BrowserSessionFilter<'_>) -> Result<usize, Self::Error>;
|
||||||
|
|
||||||
async fn authenticate_with_password(
|
async fn authenticate_with_password(
|
||||||
&mut self,
|
&mut self,
|
||||||
rng: &mut (dyn RngCore + Send),
|
rng: &mut (dyn RngCore + Send),
|
||||||
clock: &dyn Clock,
|
clock: &dyn Clock,
|
||||||
user_session: BrowserSession,
|
user_session: &BrowserSession,
|
||||||
user_password: &Password,
|
user_password: &Password,
|
||||||
) -> Result<BrowserSession, Self::Error>;
|
) -> Result<Authentication, Self::Error>;
|
||||||
|
|
||||||
async fn authenticate_with_upstream(
|
async fn authenticate_with_upstream(
|
||||||
&mut self,
|
&mut self,
|
||||||
rng: &mut (dyn RngCore + Send),
|
rng: &mut (dyn RngCore + Send),
|
||||||
clock: &dyn Clock,
|
clock: &dyn Clock,
|
||||||
user_session: BrowserSession,
|
user_session: &BrowserSession,
|
||||||
upstream_oauth_link: &UpstreamOAuthLink,
|
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>;
|
||||||
);
|
);
|
||||||
|
@ -102,6 +102,10 @@ type BrowserSessionConnection {
|
|||||||
A list of nodes.
|
A list of nodes.
|
||||||
"""
|
"""
|
||||||
nodes: [BrowserSession!]!
|
nodes: [BrowserSession!]!
|
||||||
|
"""
|
||||||
|
Identifies the total count of items in the connection.
|
||||||
|
"""
|
||||||
|
totalCount: Int!
|
||||||
}
|
}
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
@ -50,6 +50,8 @@ const QUERY = graphql(/* GraphQL */ `
|
|||||||
before: $before
|
before: $before
|
||||||
state: ACTIVE
|
state: ACTIVE
|
||||||
) {
|
) {
|
||||||
|
totalCount
|
||||||
|
|
||||||
edges {
|
edges {
|
||||||
cursor
|
cursor
|
||||||
node {
|
node {
|
||||||
@ -129,6 +131,7 @@ const BrowserSessionList: React.FC<{ userId: string }> = ({ userId }) => {
|
|||||||
<PaginationControls
|
<PaginationControls
|
||||||
onPrev={prevPage ? (): void => paginate(prevPage) : null}
|
onPrev={prevPage ? (): void => paginate(prevPage) : null}
|
||||||
onNext={nextPage ? (): void => paginate(nextPage) : null}
|
onNext={nextPage ? (): void => paginate(nextPage) : null}
|
||||||
|
count={browserSessions.totalCount}
|
||||||
disabled={pending}
|
disabled={pending}
|
||||||
/>
|
/>
|
||||||
{browserSessions.edges.map((n) => (
|
{browserSessions.edges.map((n) => (
|
||||||
|
@ -23,7 +23,7 @@ const documents = {
|
|||||||
types.BrowserSession_SessionFragmentDoc,
|
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":
|
"\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,
|
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,
|
types.BrowserSessionListDocument,
|
||||||
"\n fragment CompatSession_sso_login on CompatSsoLogin {\n id\n redirectUri\n }\n":
|
"\n fragment CompatSession_sso_login on CompatSsoLogin {\n id\n redirectUri\n }\n":
|
||||||
types.CompatSession_Sso_LoginFragmentDoc,
|
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.
|
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||||
*/
|
*/
|
||||||
export function graphql(
|
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"
|
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 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.
|
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||||
*/
|
*/
|
||||||
|
@ -106,6 +106,8 @@ export type BrowserSessionConnection = {
|
|||||||
nodes: Array<BrowserSession>;
|
nodes: Array<BrowserSession>;
|
||||||
/** Information to aid in pagination. */
|
/** Information to aid in pagination. */
|
||||||
pageInfo: PageInfo;
|
pageInfo: PageInfo;
|
||||||
|
/** Identifies the total count of items in the connection. */
|
||||||
|
totalCount: Scalars["Int"]["output"];
|
||||||
};
|
};
|
||||||
|
|
||||||
/** An edge in a connection. */
|
/** An edge in a connection. */
|
||||||
@ -879,6 +881,7 @@ export type BrowserSessionListQuery = {
|
|||||||
id: string;
|
id: string;
|
||||||
browserSessions: {
|
browserSessions: {
|
||||||
__typename?: "BrowserSessionConnection";
|
__typename?: "BrowserSessionConnection";
|
||||||
|
totalCount: number;
|
||||||
edges: Array<{
|
edges: Array<{
|
||||||
__typename?: "BrowserSessionEdge";
|
__typename?: "BrowserSessionEdge";
|
||||||
cursor: string;
|
cursor: string;
|
||||||
@ -1815,6 +1818,10 @@ export const BrowserSessionListDocument = {
|
|||||||
selectionSet: {
|
selectionSet: {
|
||||||
kind: "SelectionSet",
|
kind: "SelectionSet",
|
||||||
selections: [
|
selections: [
|
||||||
|
{
|
||||||
|
kind: "Field",
|
||||||
|
name: { kind: "Name", value: "totalCount" },
|
||||||
|
},
|
||||||
{
|
{
|
||||||
kind: "Field",
|
kind: "Field",
|
||||||
name: { kind: "Name", value: "edges" },
|
name: { kind: "Name", value: "edges" },
|
||||||
|
@ -217,6 +217,17 @@ export default {
|
|||||||
},
|
},
|
||||||
args: [],
|
args: [],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "totalCount",
|
||||||
|
type: {
|
||||||
|
kind: "NON_NULL",
|
||||||
|
ofType: {
|
||||||
|
kind: "SCALAR",
|
||||||
|
name: "Any",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
args: [],
|
||||||
|
},
|
||||||
],
|
],
|
||||||
interfaces: [],
|
interfaces: [],
|
||||||
},
|
},
|
||||||
|
Reference in New Issue
Block a user