You've already forked authentication-service
mirror of
https://github.com/matrix-org/matrix-authentication-service.git
synced 2025-07-31 09:24:31 +03:00
WIP: use sea-query for dynamic paginated queries
This commit is contained in:
38
Cargo.lock
generated
38
Cargo.lock
generated
@ -3559,6 +3559,7 @@ dependencies = [
|
|||||||
"oauth2-types",
|
"oauth2-types",
|
||||||
"rand 0.8.5",
|
"rand 0.8.5",
|
||||||
"rand_chacha 0.3.1",
|
"rand_chacha 0.3.1",
|
||||||
|
"sea-query",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"sqlx",
|
"sqlx",
|
||||||
@ -5211,6 +5212,43 @@ dependencies = [
|
|||||||
"untrusted",
|
"untrusted",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sea-query"
|
||||||
|
version = "0.28.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bbab99b8cd878ab7786157b7eb8df96333a6807cc6e45e8888c85b51534b401a"
|
||||||
|
dependencies = [
|
||||||
|
"chrono",
|
||||||
|
"sea-query-attr",
|
||||||
|
"sea-query-derive",
|
||||||
|
"uuid 1.4.1",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sea-query-attr"
|
||||||
|
version = "0.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "878cf3d57f0e5bfacd425cdaccc58b4c06d68a7b71c63fc28710a20c88676808"
|
||||||
|
dependencies = [
|
||||||
|
"darling 0.14.4",
|
||||||
|
"heck",
|
||||||
|
"quote 1.0.31",
|
||||||
|
"syn 1.0.109",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sea-query-derive"
|
||||||
|
version = "0.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "63f62030c60f3a691f5fe251713b4e220b306e50a71e1d6f9cce1f24bb781978"
|
||||||
|
dependencies = [
|
||||||
|
"heck",
|
||||||
|
"proc-macro2 1.0.66",
|
||||||
|
"quote 1.0.31",
|
||||||
|
"syn 1.0.109",
|
||||||
|
"thiserror",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sec1"
|
name = "sec1"
|
||||||
version = "0.7.3"
|
version = "0.7.3"
|
||||||
|
@ -12,7 +12,7 @@
|
|||||||
// 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, Object, ID};
|
use async_graphql::{Description, Enum, Object, ID};
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
|
|
||||||
use super::{NodeType, User};
|
use super::{NodeType, User};
|
||||||
@ -21,6 +21,16 @@ use super::{NodeType, User};
|
|||||||
#[derive(Description)]
|
#[derive(Description)]
|
||||||
pub struct BrowserSession(pub mas_data_model::BrowserSession);
|
pub struct BrowserSession(pub mas_data_model::BrowserSession);
|
||||||
|
|
||||||
|
/// The state of a browser session.
|
||||||
|
#[derive(Enum, Copy, Clone, Eq, PartialEq)]
|
||||||
|
pub enum BrowserSessionState {
|
||||||
|
/// The session is active.
|
||||||
|
Active,
|
||||||
|
|
||||||
|
/// The session is no longer active.
|
||||||
|
Finished,
|
||||||
|
}
|
||||||
|
|
||||||
impl From<mas_data_model::BrowserSession> for BrowserSession {
|
impl From<mas_data_model::BrowserSession> for BrowserSession {
|
||||||
fn from(v: mas_data_model::BrowserSession) -> Self {
|
fn from(v: mas_data_model::BrowserSession) -> Self {
|
||||||
Self(v)
|
Self(v)
|
||||||
|
@ -21,7 +21,7 @@ use mas_storage::{
|
|||||||
compat::CompatSsoLoginRepository,
|
compat::CompatSsoLoginRepository,
|
||||||
oauth2::OAuth2SessionRepository,
|
oauth2::OAuth2SessionRepository,
|
||||||
upstream_oauth2::UpstreamOAuthLinkRepository,
|
upstream_oauth2::UpstreamOAuthLinkRepository,
|
||||||
user::{BrowserSessionRepository, UserEmailRepository},
|
user::{BrowserSessionFilter, BrowserSessionRepository, UserEmailRepository},
|
||||||
Pagination, RepositoryAccess,
|
Pagination, RepositoryAccess,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -30,7 +30,7 @@ use super::{
|
|||||||
UpstreamOAuth2Link,
|
UpstreamOAuth2Link,
|
||||||
};
|
};
|
||||||
use crate::{
|
use crate::{
|
||||||
model::{matrix::MatrixUser, CompatSession},
|
model::{browser_sessions::BrowserSessionState, matrix::MatrixUser, CompatSession},
|
||||||
state::ContextExt,
|
state::ContextExt,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -189,6 +189,9 @@ impl User {
|
|||||||
&self,
|
&self,
|
||||||
ctx: &Context<'_>,
|
ctx: &Context<'_>,
|
||||||
|
|
||||||
|
#[graphql(name = "state", desc = "List only sessions in the given state.")]
|
||||||
|
state_param: Option<BrowserSessionState>,
|
||||||
|
|
||||||
#[graphql(desc = "Returns the elements in the list that come after the cursor.")]
|
#[graphql(desc = "Returns the elements in the list that come after the cursor.")]
|
||||||
after: Option<String>,
|
after: Option<String>,
|
||||||
#[graphql(desc = "Returns the elements in the list that come before the cursor.")]
|
#[graphql(desc = "Returns the elements in the list that come before the cursor.")]
|
||||||
@ -213,10 +216,14 @@ impl User {
|
|||||||
.transpose()?;
|
.transpose()?;
|
||||||
let pagination = Pagination::try_new(before_id, after_id, first, last)?;
|
let pagination = Pagination::try_new(before_id, after_id, first, last)?;
|
||||||
|
|
||||||
let page = repo
|
let filter = BrowserSessionFilter::new().for_user(&self.0);
|
||||||
.browser_session()
|
let filter = match state_param {
|
||||||
.list_active_paginated(&self.0, pagination)
|
Some(BrowserSessionState::Active) => filter.active_only(),
|
||||||
.await?;
|
Some(BrowserSessionState::Finished) => filter.finished_only(),
|
||||||
|
None => filter,
|
||||||
|
};
|
||||||
|
|
||||||
|
let page = repo.browser_session().list(filter, pagination).await?;
|
||||||
|
|
||||||
repo.cancel().await?;
|
repo.cancel().await?;
|
||||||
|
|
||||||
|
@ -8,6 +8,7 @@ license = "Apache-2.0"
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
async-trait = "0.1.71"
|
async-trait = "0.1.71"
|
||||||
sqlx = { version = "0.7.1", features = ["runtime-tokio-rustls", "postgres", "migrate", "chrono", "json", "uuid"] }
|
sqlx = { version = "0.7.1", features = ["runtime-tokio-rustls", "postgres", "migrate", "chrono", "json", "uuid"] }
|
||||||
|
sea-query = { version = "0.28.5", features = ["derive", "attr", "with-uuid", "with-chrono"] }
|
||||||
chrono = { version = "0.4.26", features = ["serde"] }
|
chrono = { version = "0.4.26", features = ["serde"] }
|
||||||
serde = { version = "1.0.171", features = ["derive"] }
|
serde = { version = "1.0.171", features = ["derive"] }
|
||||||
serde_json = "1.0.103"
|
serde_json = "1.0.103"
|
||||||
|
@ -217,6 +217,7 @@ pub mod user;
|
|||||||
mod errors;
|
mod errors;
|
||||||
pub(crate) mod pagination;
|
pub(crate) mod pagination;
|
||||||
pub(crate) mod repository;
|
pub(crate) mod repository;
|
||||||
|
mod sea_query_sqlx;
|
||||||
pub(crate) mod tracing;
|
pub(crate) mod tracing;
|
||||||
|
|
||||||
pub(crate) use self::errors::DatabaseInconsistencyError;
|
pub(crate) use self::errors::DatabaseInconsistencyError;
|
||||||
|
@ -21,9 +21,11 @@ use uuid::Uuid;
|
|||||||
/// An extension trait to the `sqlx` [`QueryBuilder`], to help adding pagination
|
/// An extension trait to the `sqlx` [`QueryBuilder`], to help adding pagination
|
||||||
/// to a query
|
/// to a query
|
||||||
pub trait QueryBuilderExt {
|
pub trait QueryBuilderExt {
|
||||||
|
type Iden;
|
||||||
|
|
||||||
/// Add cursor-based pagination to a query, as used in paginated GraphQL
|
/// Add cursor-based pagination to a query, as used in paginated GraphQL
|
||||||
/// connections
|
/// connections
|
||||||
fn generate_pagination(&mut self, id_field: &'static str, pagination: Pagination) -> &mut Self;
|
fn generate_pagination(&mut self, id_field: Self::Iden, pagination: Pagination) -> &mut Self;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a, DB> QueryBuilderExt for QueryBuilder<'a, DB>
|
impl<'a, DB> QueryBuilderExt for QueryBuilder<'a, DB>
|
||||||
@ -32,6 +34,8 @@ where
|
|||||||
Uuid: sqlx::Type<DB> + sqlx::Encode<'a, DB>,
|
Uuid: sqlx::Type<DB> + sqlx::Encode<'a, DB>,
|
||||||
i64: sqlx::Type<DB> + sqlx::Encode<'a, DB>,
|
i64: sqlx::Type<DB> + sqlx::Encode<'a, DB>,
|
||||||
{
|
{
|
||||||
|
type Iden = &'static str;
|
||||||
|
|
||||||
fn generate_pagination(&mut self, id_field: &'static str, pagination: Pagination) -> &mut Self {
|
fn generate_pagination(&mut self, id_field: &'static str, pagination: Pagination) -> &mut Self {
|
||||||
// ref: https://github.com/graphql/graphql-relay-js/issues/94#issuecomment-232410564
|
// ref: https://github.com/graphql/graphql-relay-js/issues/94#issuecomment-232410564
|
||||||
// 1. Start from the greedy query: SELECT * FROM table
|
// 1. Start from the greedy query: SELECT * FROM table
|
||||||
@ -76,3 +80,40 @@ where
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl QueryBuilderExt for sea_query::SelectStatement {
|
||||||
|
type Iden = sea_query::ColumnRef;
|
||||||
|
fn generate_pagination(&mut self, id_field: Self::Iden, pagination: Pagination) -> &mut Self {
|
||||||
|
// ref: https://github.com/graphql/graphql-relay-js/issues/94#issuecomment-232410564
|
||||||
|
// 1. Start from the greedy query: SELECT * FROM table
|
||||||
|
|
||||||
|
// 2. If the after argument is provided, add `id > parsed_cursor` to the `WHERE`
|
||||||
|
// clause
|
||||||
|
if let Some(after) = pagination.after {
|
||||||
|
self.and_where(sea_query::Expr::col(id_field.clone()).gt(Uuid::from(after)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. If the before argument is provided, add `id < parsed_cursor` to the
|
||||||
|
// `WHERE` clause
|
||||||
|
if let Some(before) = pagination.before {
|
||||||
|
self.and_where(sea_query::Expr::col(id_field.clone()).lt(Uuid::from(before)));
|
||||||
|
}
|
||||||
|
|
||||||
|
match pagination.direction {
|
||||||
|
// 4. If the first argument is provided, add `ORDER BY id ASC LIMIT first+1` to the
|
||||||
|
// query
|
||||||
|
PaginationDirection::Forward => {
|
||||||
|
self.order_by(id_field, sea_query::Order::Asc)
|
||||||
|
.limit((pagination.count + 1) as u64);
|
||||||
|
}
|
||||||
|
// 5. If the first argument is provided, add `ORDER BY id DESC LIMIT last+1` to the
|
||||||
|
// query
|
||||||
|
PaginationDirection::Backward => {
|
||||||
|
self.order_by(id_field, sea_query::Order::Desc)
|
||||||
|
.limit((pagination.count + 1) as u64);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
49
crates/storage-pg/src/sea_query_sqlx.rs
Normal file
49
crates/storage-pg/src/sea_query_sqlx.rs
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
// Copyright 2021-2023 The Matrix.org Foundation C.I.C.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
use sea_query::Value;
|
||||||
|
use sqlx::Arguments;
|
||||||
|
|
||||||
|
pub(crate) fn map_values(values: sea_query::Values) -> sqlx::postgres::PgArguments {
|
||||||
|
let mut arguments = sqlx::postgres::PgArguments::default();
|
||||||
|
|
||||||
|
for value in values {
|
||||||
|
match value {
|
||||||
|
Value::Bool(b) => arguments.add(b),
|
||||||
|
Value::TinyInt(i) => arguments.add(i),
|
||||||
|
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::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),
|
||||||
|
Value::String(s) => arguments.add(s.as_deref()),
|
||||||
|
Value::Char(c) => arguments.add(c.map(|c| c.to_string())),
|
||||||
|
Value::Bytes(b) => arguments.add(b.as_deref()),
|
||||||
|
Value::ChronoDate(d) => arguments.add(d.as_deref()),
|
||||||
|
Value::ChronoTime(t) => arguments.add(t.as_deref()),
|
||||||
|
Value::ChronoDateTime(dt) => arguments.add(dt.as_deref()),
|
||||||
|
Value::ChronoDateTimeUtc(dt) => arguments.add(dt.as_deref()),
|
||||||
|
Value::ChronoDateTimeLocal(dt) => arguments.add(dt.as_deref()),
|
||||||
|
Value::ChronoDateTimeWithTimeZone(dt) => arguments.add(dt.as_deref()),
|
||||||
|
Value::Uuid(u) => arguments.add(u.as_deref()),
|
||||||
|
_ => unimplemented!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
arguments
|
||||||
|
}
|
@ -17,13 +17,14 @@ use chrono::{DateTime, Utc};
|
|||||||
use mas_data_model::{Authentication, BrowserSession, Password, UpstreamOAuthLink, User};
|
use mas_data_model::{Authentication, BrowserSession, Password, UpstreamOAuthLink, User};
|
||||||
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 sqlx::{PgConnection, QueryBuilder};
|
use sqlx::{PgConnection, QueryBuilder};
|
||||||
use ulid::Ulid;
|
use ulid::Ulid;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
pagination::QueryBuilderExt, tracing::ExecuteExt, DatabaseError, DatabaseInconsistencyError,
|
pagination::QueryBuilderExt, sea_query_sqlx::map_values, tracing::ExecuteExt, DatabaseError,
|
||||||
LookupResultExt,
|
DatabaseInconsistencyError, LookupResultExt,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// An implementation of [`BrowserSessionRepository`] for a PostgreSQL
|
/// An implementation of [`BrowserSessionRepository`] for a PostgreSQL
|
||||||
@ -41,6 +42,7 @@ impl<'c> PgBrowserSessionRepository<'c> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(sqlx::FromRow)]
|
#[derive(sqlx::FromRow)]
|
||||||
|
#[sea_query::enum_def]
|
||||||
struct SessionLookup {
|
struct SessionLookup {
|
||||||
user_session_id: Uuid,
|
user_session_id: Uuid,
|
||||||
user_session_created_at: DateTime<Utc>,
|
user_session_created_at: DateTime<Utc>,
|
||||||
@ -52,6 +54,31 @@ struct SessionLookup {
|
|||||||
last_authd_at: Option<DateTime<Utc>>,
|
last_authd_at: Option<DateTime<Utc>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(sea_query::Iden)]
|
||||||
|
enum UserSessions {
|
||||||
|
Table,
|
||||||
|
UserSessionId,
|
||||||
|
CreatedAt,
|
||||||
|
FinishedAt,
|
||||||
|
UserId,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(sea_query::Iden)]
|
||||||
|
enum Users {
|
||||||
|
Table,
|
||||||
|
UserId,
|
||||||
|
Username,
|
||||||
|
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;
|
||||||
|
|
||||||
@ -214,46 +241,78 @@ impl<'c> BrowserSessionRepository for PgBrowserSessionRepository<'c> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tracing::instrument(
|
#[tracing::instrument(
|
||||||
name = "db.browser_session.list_active_paginated",
|
name = "db.browser_session.list",
|
||||||
skip_all,
|
skip_all,
|
||||||
fields(
|
fields(
|
||||||
db.statement,
|
db.statement,
|
||||||
%user.id,
|
|
||||||
),
|
),
|
||||||
err,
|
err,
|
||||||
)]
|
)]
|
||||||
async fn list_active_paginated(
|
async fn list(
|
||||||
&mut self,
|
&mut self,
|
||||||
user: &User,
|
filter: mas_storage::user::BrowserSessionFilter<'_>,
|
||||||
pagination: Pagination,
|
pagination: Pagination,
|
||||||
) -> Result<Page<BrowserSession>, Self::Error> {
|
) -> Result<Page<BrowserSession>, Self::Error> {
|
||||||
// TODO: ordering of last authentication is wrong
|
let (sql, values) = sea_query::Query::select()
|
||||||
let mut query = QueryBuilder::new(
|
.expr_as(
|
||||||
r#"
|
Expr::col((UserSessions::Table, UserSessions::UserSessionId)),
|
||||||
SELECT DISTINCT ON (s.user_session_id)
|
SessionLookupIden::UserSessionId,
|
||||||
s.user_session_id,
|
)
|
||||||
s.created_at AS "user_session_created_at",
|
.expr_as(
|
||||||
s.finished_at AS "user_session_finished_at",
|
Expr::col((UserSessions::Table, UserSessions::CreatedAt)),
|
||||||
u.user_id,
|
SessionLookupIden::UserSessionCreatedAt,
|
||||||
u.username AS "user_username",
|
)
|
||||||
u.primary_user_email_id AS "user_primary_user_email_id",
|
.expr_as(
|
||||||
a.user_session_authentication_id AS "last_authentication_id",
|
Expr::col((UserSessions::Table, UserSessions::FinishedAt)),
|
||||||
a.created_at AS "last_authd_at"
|
SessionLookupIden::UserSessionFinishedAt,
|
||||||
FROM user_sessions s
|
)
|
||||||
INNER JOIN users u
|
.expr_as(
|
||||||
USING (user_id)
|
Expr::col((Users::Table, Users::UserId)),
|
||||||
LEFT JOIN user_session_authentications a
|
SessionLookupIden::UserId,
|
||||||
USING (user_session_id)
|
)
|
||||||
"#,
|
.expr_as(
|
||||||
);
|
Expr::col((Users::Table, Users::Username)),
|
||||||
|
SessionLookupIden::UserUsername,
|
||||||
|
)
|
||||||
|
.expr_as(
|
||||||
|
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,
|
||||||
|
Expr::col((UserSessions::Table, UserSessions::UserId))
|
||||||
|
.equals((Users::Table, Users::UserId)),
|
||||||
|
)
|
||||||
|
.and_where_option(
|
||||||
|
filter
|
||||||
|
.user()
|
||||||
|
.map(|user| Expr::col((Users::Table, Users::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()
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
.generate_pagination(
|
||||||
|
(UserSessions::Table, UserSessions::UserSessionId).into_column_ref(),
|
||||||
|
pagination,
|
||||||
|
)
|
||||||
|
.build(PostgresQueryBuilder);
|
||||||
|
|
||||||
query
|
let arguments = map_values(values);
|
||||||
.push(" WHERE s.finished_at IS NULL AND s.user_id = ")
|
|
||||||
.push_bind(Uuid::from(user.id))
|
|
||||||
.generate_pagination("s.user_session_id", pagination);
|
|
||||||
|
|
||||||
let edges: Vec<SessionLookup> = query
|
let edges: Vec<SessionLookup> = sqlx::query_as_with(&sql, arguments)
|
||||||
.build_query_as()
|
|
||||||
.traced()
|
.traced()
|
||||||
.fetch_all(&mut *self.conn)
|
.fetch_all(&mut *self.conn)
|
||||||
.await?;
|
.await?;
|
||||||
@ -261,34 +320,10 @@ impl<'c> BrowserSessionRepository for PgBrowserSessionRepository<'c> {
|
|||||||
let page = pagination
|
let page = pagination
|
||||||
.process(edges)
|
.process(edges)
|
||||||
.try_map(BrowserSession::try_from)?;
|
.try_map(BrowserSession::try_from)?;
|
||||||
|
|
||||||
Ok(page)
|
Ok(page)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tracing::instrument(
|
|
||||||
name = "db.browser_session.count_active",
|
|
||||||
skip_all,
|
|
||||||
fields(
|
|
||||||
db.statement,
|
|
||||||
%user.id,
|
|
||||||
),
|
|
||||||
err,
|
|
||||||
)]
|
|
||||||
async fn count_active(&mut self, user: &User) -> Result<usize, Self::Error> {
|
|
||||||
let res = sqlx::query_scalar!(
|
|
||||||
r#"
|
|
||||||
SELECT COUNT(*) as "count!"
|
|
||||||
FROM user_sessions s
|
|
||||||
WHERE s.user_id = $1 AND s.finished_at IS NULL
|
|
||||||
"#,
|
|
||||||
Uuid::from(user.id),
|
|
||||||
)
|
|
||||||
.traced()
|
|
||||||
.fetch_one(&mut *self.conn)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
res.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,
|
||||||
|
@ -26,7 +26,9 @@ mod password;
|
|||||||
mod session;
|
mod session;
|
||||||
|
|
||||||
pub use self::{
|
pub use self::{
|
||||||
email::UserEmailRepository, password::UserPasswordRepository, session::BrowserSessionRepository,
|
email::UserEmailRepository,
|
||||||
|
password::UserPasswordRepository,
|
||||||
|
session::{BrowserSessionFilter, BrowserSessionRepository},
|
||||||
};
|
};
|
||||||
|
|
||||||
/// A [`UserRepository`] helps interacting with [`User`] saved in the storage
|
/// A [`UserRepository`] helps interacting with [`User`] saved in the storage
|
||||||
|
@ -19,6 +19,70 @@ use ulid::Ulid;
|
|||||||
|
|
||||||
use crate::{pagination::Page, repository_impl, Clock, Pagination};
|
use crate::{pagination::Page, repository_impl, Clock, Pagination};
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
|
pub enum BrowserSessionState {
|
||||||
|
Active,
|
||||||
|
Finished,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BrowserSessionState {
|
||||||
|
pub fn is_active(self) -> bool {
|
||||||
|
matches!(self, Self::Active)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_finished(self) -> bool {
|
||||||
|
matches!(self, Self::Finished)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Filter parameters for listing browser sessions
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
|
||||||
|
pub struct BrowserSessionFilter<'a> {
|
||||||
|
user: Option<&'a User>,
|
||||||
|
state: Option<BrowserSessionState>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> BrowserSessionFilter<'a> {
|
||||||
|
/// Create a new [`BrowserSessionFilter`] with default values
|
||||||
|
#[must_use]
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the user who owns the browser sessions
|
||||||
|
#[must_use]
|
||||||
|
pub fn for_user(mut self, user: &'a User) -> Self {
|
||||||
|
self.user = Some(user);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the user filter
|
||||||
|
#[must_use]
|
||||||
|
pub fn user(&self) -> Option<&User> {
|
||||||
|
self.user
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Only return active browser sessions
|
||||||
|
#[must_use]
|
||||||
|
pub fn active_only(mut self) -> Self {
|
||||||
|
self.state = Some(BrowserSessionState::Active);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Only return finished browser sessions
|
||||||
|
#[must_use]
|
||||||
|
pub fn finished_only(mut self) -> Self {
|
||||||
|
self.state = Some(BrowserSessionState::Finished);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the state filter
|
||||||
|
#[must_use]
|
||||||
|
pub fn state(&self) -> Option<BrowserSessionState> {
|
||||||
|
self.state
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// A [`BrowserSessionRepository`] helps interacting with [`BrowserSession`]
|
/// A [`BrowserSessionRepository`] helps interacting with [`BrowserSession`]
|
||||||
/// saved in the storage backend
|
/// saved in the storage backend
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
@ -77,33 +141,22 @@ pub trait BrowserSessionRepository: Send + Sync {
|
|||||||
user_session: BrowserSession,
|
user_session: BrowserSession,
|
||||||
) -> Result<BrowserSession, Self::Error>;
|
) -> Result<BrowserSession, Self::Error>;
|
||||||
|
|
||||||
/// List active [`BrowserSession`] for a [`User`] with the given pagination
|
/// List [`BrowserSession`] with the given filter and pagination
|
||||||
///
|
///
|
||||||
/// # Parameters
|
/// # Parameters
|
||||||
///
|
///
|
||||||
/// * `user`: The user to list the sessions for
|
/// * `filter`: The filter to apply
|
||||||
/// * `pagination`: The pagination parameters
|
/// * `pagination`: The pagination parameters
|
||||||
///
|
///
|
||||||
/// # Errors
|
/// # Errors
|
||||||
///
|
///
|
||||||
/// Returns [`Self::Error`] if the underlying repository fails
|
/// Returns [`Self::Error`] if the underlying repository fails
|
||||||
async fn list_active_paginated(
|
async fn list(
|
||||||
&mut self,
|
&mut self,
|
||||||
user: &User,
|
filter: BrowserSessionFilter<'_>,
|
||||||
pagination: Pagination,
|
pagination: Pagination,
|
||||||
) -> Result<Page<BrowserSession>, Self::Error>;
|
) -> Result<Page<BrowserSession>, Self::Error>;
|
||||||
|
|
||||||
/// Count active [`BrowserSession`] for a [`User`]
|
|
||||||
///
|
|
||||||
/// # Parameters
|
|
||||||
///
|
|
||||||
/// * `user`: The user to count the sessions for
|
|
||||||
///
|
|
||||||
/// # Errors
|
|
||||||
///
|
|
||||||
/// Returns [`Self::Error`] if the underlying repository fails
|
|
||||||
async fn count_active(&mut self, user: &User) -> Result<usize, Self::Error>;
|
|
||||||
|
|
||||||
/// Authenticate a [`BrowserSession`] with the given [`Password`]
|
/// Authenticate a [`BrowserSession`] with the given [`Password`]
|
||||||
///
|
///
|
||||||
/// Returns the updated [`BrowserSession`]
|
/// Returns the updated [`BrowserSession`]
|
||||||
@ -163,12 +216,12 @@ repository_impl!(BrowserSessionRepository:
|
|||||||
clock: &dyn Clock,
|
clock: &dyn Clock,
|
||||||
user_session: BrowserSession,
|
user_session: BrowserSession,
|
||||||
) -> Result<BrowserSession, Self::Error>;
|
) -> Result<BrowserSession, Self::Error>;
|
||||||
async fn list_active_paginated(
|
|
||||||
|
async fn list(
|
||||||
&mut self,
|
&mut self,
|
||||||
user: &User,
|
filter: BrowserSessionFilter<'_>,
|
||||||
pagination: Pagination,
|
pagination: Pagination,
|
||||||
) -> Result<Page<BrowserSession>, Self::Error>;
|
) -> Result<Page<BrowserSession>, Self::Error>;
|
||||||
async fn count_active(&mut self, user: &User) -> Result<usize, Self::Error>;
|
|
||||||
|
|
||||||
async fn authenticate_with_password(
|
async fn authenticate_with_password(
|
||||||
&mut self,
|
&mut self,
|
||||||
|
@ -118,6 +118,20 @@ type BrowserSessionEdge {
|
|||||||
cursor: String!
|
cursor: String!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
"""
|
||||||
|
The state of a browser session.
|
||||||
|
"""
|
||||||
|
enum BrowserSessionState {
|
||||||
|
"""
|
||||||
|
The session is active.
|
||||||
|
"""
|
||||||
|
ACTIVE
|
||||||
|
"""
|
||||||
|
The session is no longer active.
|
||||||
|
"""
|
||||||
|
FINISHED
|
||||||
|
}
|
||||||
|
|
||||||
"""
|
"""
|
||||||
A compat session represents a client session which used the legacy Matrix
|
A compat session represents a client session which used the legacy Matrix
|
||||||
login API.
|
login API.
|
||||||
@ -871,6 +885,7 @@ type User implements Node {
|
|||||||
Get the list of active browser sessions, chronologically sorted
|
Get the list of active browser sessions, chronologically sorted
|
||||||
"""
|
"""
|
||||||
browserSessions(
|
browserSessions(
|
||||||
|
state: BrowserSessionState
|
||||||
after: String
|
after: String
|
||||||
before: String
|
before: String
|
||||||
first: Int
|
first: Int
|
||||||
|
@ -48,6 +48,7 @@ const QUERY = graphql(/* GraphQL */ `
|
|||||||
after: $after
|
after: $after
|
||||||
last: $last
|
last: $last
|
||||||
before: $before
|
before: $before
|
||||||
|
state: ACTIVE
|
||||||
) {
|
) {
|
||||||
edges {
|
edges {
|
||||||
cursor
|
cursor
|
||||||
|
@ -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 ) {\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 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 ) {\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 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 ) {\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"];
|
||||||
/**
|
/**
|
||||||
* 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.
|
||||||
*/
|
*/
|
||||||
|
@ -117,6 +117,14 @@ export type BrowserSessionEdge = {
|
|||||||
node: BrowserSession;
|
node: BrowserSession;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** The state of a browser session. */
|
||||||
|
export enum BrowserSessionState {
|
||||||
|
/** The session is active. */
|
||||||
|
Active = "ACTIVE",
|
||||||
|
/** The session is no longer active. */
|
||||||
|
Finished = "FINISHED",
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A compat session represents a client session which used the legacy Matrix
|
* A compat session represents a client session which used the legacy Matrix
|
||||||
* login API.
|
* login API.
|
||||||
@ -670,6 +678,7 @@ export type UserBrowserSessionsArgs = {
|
|||||||
before?: InputMaybe<Scalars["String"]["input"]>;
|
before?: InputMaybe<Scalars["String"]["input"]>;
|
||||||
first?: InputMaybe<Scalars["Int"]["input"]>;
|
first?: InputMaybe<Scalars["Int"]["input"]>;
|
||||||
last?: InputMaybe<Scalars["Int"]["input"]>;
|
last?: InputMaybe<Scalars["Int"]["input"]>;
|
||||||
|
state?: InputMaybe<BrowserSessionState>;
|
||||||
};
|
};
|
||||||
|
|
||||||
/** A user is an individual's account. */
|
/** A user is an individual's account. */
|
||||||
@ -1797,6 +1806,11 @@ export const BrowserSessionListDocument = {
|
|||||||
name: { kind: "Name", value: "before" },
|
name: { kind: "Name", value: "before" },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
kind: "Argument",
|
||||||
|
name: { kind: "Name", value: "state" },
|
||||||
|
value: { kind: "EnumValue", value: "ACTIVE" },
|
||||||
|
},
|
||||||
],
|
],
|
||||||
selectionSet: {
|
selectionSet: {
|
||||||
kind: "SelectionSet",
|
kind: "SelectionSet",
|
||||||
|
@ -1970,6 +1970,13 @@ export default {
|
|||||||
name: "Any",
|
name: "Any",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "state",
|
||||||
|
type: {
|
||||||
|
kind: "SCALAR",
|
||||||
|
name: "Any",
|
||||||
|
},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
Reference in New Issue
Block a user