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 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()
|
||||
}
|
||||
|
@ -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.
|
||||
|
@ -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);
|
||||
|
@ -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),
|
||||
)?);
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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?;
|
||||
|
@ -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)?;
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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()
|
||||
|
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::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!(),
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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>;
|
||||
);
|
||||
|
@ -102,6 +102,10 @@ type BrowserSessionConnection {
|
||||
A list of nodes.
|
||||
"""
|
||||
nodes: [BrowserSession!]!
|
||||
"""
|
||||
Identifies the total count of items in the connection.
|
||||
"""
|
||||
totalCount: Int!
|
||||
}
|
||||
|
||||
"""
|
||||
|
@ -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) => (
|
||||
|
@ -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.
|
||||
*/
|
||||
|
@ -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" },
|
||||
|
@ -217,6 +217,17 @@ export default {
|
||||
},
|
||||
args: [],
|
||||
},
|
||||
{
|
||||
name: "totalCount",
|
||||
type: {
|
||||
kind: "NON_NULL",
|
||||
ofType: {
|
||||
kind: "SCALAR",
|
||||
name: "Any",
|
||||
},
|
||||
},
|
||||
args: [],
|
||||
},
|
||||
],
|
||||
interfaces: [],
|
||||
},
|
||||
|
Reference in New Issue
Block a user