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
frontend: Show all compatibilities sessions, not just SSO logins
Also cleans up a bunch of things in the frontend
This commit is contained in:
@ -24,7 +24,10 @@ use crate::state::ContextExt;
|
|||||||
/// 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.
|
||||||
#[derive(Description)]
|
#[derive(Description)]
|
||||||
pub struct CompatSession(pub mas_data_model::CompatSession);
|
pub struct CompatSession(
|
||||||
|
pub mas_data_model::CompatSession,
|
||||||
|
pub Option<mas_data_model::CompatSsoLogin>,
|
||||||
|
);
|
||||||
|
|
||||||
#[Object(use_type_description)]
|
#[Object(use_type_description)]
|
||||||
impl CompatSession {
|
impl CompatSession {
|
||||||
@ -61,6 +64,11 @@ impl CompatSession {
|
|||||||
pub async fn finished_at(&self) -> Option<DateTime<Utc>> {
|
pub async fn finished_at(&self) -> Option<DateTime<Utc>> {
|
||||||
self.0.finished_at()
|
self.0.finished_at()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The associated SSO login, if any.
|
||||||
|
pub async fn sso_login(&self) -> Option<CompatSsoLogin> {
|
||||||
|
self.1.as_ref().map(|l| CompatSsoLogin(l.clone()))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A compat SSO login represents a login done through the legacy Matrix login
|
/// A compat SSO login represents a login done through the legacy Matrix login
|
||||||
@ -114,6 +122,6 @@ impl CompatSsoLogin {
|
|||||||
.context("Could not load compat session")?;
|
.context("Could not load compat session")?;
|
||||||
repo.cancel().await?;
|
repo.cancel().await?;
|
||||||
|
|
||||||
Ok(Some(CompatSession(session)))
|
Ok(Some(CompatSession(session, Some(self.0.clone()))))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -22,14 +22,17 @@ use mas_storage::{
|
|||||||
oauth2::OAuth2SessionRepository,
|
oauth2::OAuth2SessionRepository,
|
||||||
upstream_oauth2::UpstreamOAuthLinkRepository,
|
upstream_oauth2::UpstreamOAuthLinkRepository,
|
||||||
user::{BrowserSessionRepository, UserEmailRepository},
|
user::{BrowserSessionRepository, UserEmailRepository},
|
||||||
Pagination,
|
Pagination, RepositoryAccess,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
compat_sessions::CompatSsoLogin, BrowserSession, Cursor, NodeCursor, NodeType, OAuth2Session,
|
compat_sessions::CompatSsoLogin, BrowserSession, Cursor, NodeCursor, NodeType, OAuth2Session,
|
||||||
UpstreamOAuth2Link,
|
UpstreamOAuth2Link,
|
||||||
};
|
};
|
||||||
use crate::{model::matrix::MatrixUser, state::ContextExt};
|
use crate::{
|
||||||
|
model::{matrix::MatrixUser, CompatSession},
|
||||||
|
state::ContextExt,
|
||||||
|
};
|
||||||
|
|
||||||
#[derive(Description)]
|
#[derive(Description)]
|
||||||
/// A user is an individual's account.
|
/// A user is an individual's account.
|
||||||
@ -129,6 +132,58 @@ impl User {
|
|||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get the list of compatibility sessions, chronologically sorted
|
||||||
|
async fn compat_sessions(
|
||||||
|
&self,
|
||||||
|
ctx: &Context<'_>,
|
||||||
|
|
||||||
|
#[graphql(desc = "Returns the elements in the list that come after the cursor.")]
|
||||||
|
after: Option<String>,
|
||||||
|
#[graphql(desc = "Returns the elements in the list that come before the cursor.")]
|
||||||
|
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, CompatSession>, async_graphql::Error> {
|
||||||
|
let state = ctx.state();
|
||||||
|
let mut repo = state.repository().await?;
|
||||||
|
|
||||||
|
query(
|
||||||
|
after,
|
||||||
|
before,
|
||||||
|
first,
|
||||||
|
last,
|
||||||
|
|after, before, first, last| async move {
|
||||||
|
let after_id = after
|
||||||
|
.map(|x: OpaqueCursor<NodeCursor>| x.extract_for_type(NodeType::CompatSsoLogin))
|
||||||
|
.transpose()?;
|
||||||
|
let before_id = before
|
||||||
|
.map(|x: OpaqueCursor<NodeCursor>| x.extract_for_type(NodeType::CompatSsoLogin))
|
||||||
|
.transpose()?;
|
||||||
|
let pagination = Pagination::try_new(before_id, after_id, first, last)?;
|
||||||
|
|
||||||
|
let page = repo
|
||||||
|
.compat_session()
|
||||||
|
.list_paginated(&self.0, pagination)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
repo.cancel().await?;
|
||||||
|
|
||||||
|
let mut connection = Connection::new(page.has_previous_page, page.has_next_page);
|
||||||
|
connection
|
||||||
|
.edges
|
||||||
|
.extend(page.edges.into_iter().map(|(session, sso_login)| {
|
||||||
|
Edge::new(
|
||||||
|
OpaqueCursor(NodeCursor(NodeType::CompatSession, session.id)),
|
||||||
|
CompatSession(session, sso_login),
|
||||||
|
)
|
||||||
|
}));
|
||||||
|
|
||||||
|
Ok::<_, async_graphql::Error>(connection)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
/// Get the list of active browser sessions, chronologically sorted
|
/// Get the list of active browser sessions, chronologically sorted
|
||||||
async fn browser_sessions(
|
async fn browser_sessions(
|
||||||
&self,
|
&self,
|
||||||
|
@ -66,7 +66,8 @@ impl EndCompatSessionPayload {
|
|||||||
/// Returns the ended session.
|
/// Returns the ended session.
|
||||||
async fn compat_session(&self) -> Option<CompatSession> {
|
async fn compat_session(&self) -> Option<CompatSession> {
|
||||||
match self {
|
match self {
|
||||||
Self::Ended(session) => Some(CompatSession(session.clone())),
|
// XXX: the SSO login is not returned here.
|
||||||
|
Self::Ended(session) => Some(CompatSession(session.clone(), None)),
|
||||||
Self::NotFound => None,
|
Self::NotFound => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -40,7 +40,9 @@ use axum::{
|
|||||||
Router,
|
Router,
|
||||||
};
|
};
|
||||||
use headers::HeaderName;
|
use headers::HeaderName;
|
||||||
use hyper::header::{ACCEPT, ACCEPT_LANGUAGE, AUTHORIZATION, CONTENT_LANGUAGE, CONTENT_TYPE};
|
use hyper::header::{
|
||||||
|
ACCEPT, ACCEPT_LANGUAGE, AUTHORIZATION, CONTENT_LANGUAGE, CONTENT_LENGTH, CONTENT_TYPE,
|
||||||
|
};
|
||||||
use mas_http::CorsLayerExt;
|
use mas_http::CorsLayerExt;
|
||||||
use mas_keystore::{Encrypter, Keystore};
|
use mas_keystore::{Encrypter, Keystore};
|
||||||
use mas_policy::PolicyFactory;
|
use mas_policy::PolicyFactory;
|
||||||
@ -268,7 +270,8 @@ where
|
|||||||
BoxRng: FromRequestParts<S>,
|
BoxRng: FromRequestParts<S>,
|
||||||
{
|
{
|
||||||
Router::new()
|
Router::new()
|
||||||
// TODO: mount this route somewhere else?
|
// XXX: hard-coded redirect from /account to /account/
|
||||||
|
.route("/account", get(|| async { mas_router::Account.go() }))
|
||||||
.route(mas_router::Account::route(), get(self::views::app::get))
|
.route(mas_router::Account::route(), get(self::views::app::get))
|
||||||
.route(
|
.route(
|
||||||
mas_router::AccountWildcard::route(),
|
mas_router::AccountWildcard::route(),
|
||||||
@ -351,6 +354,7 @@ where
|
|||||||
if let Ok(res) = templates.render_error(ctx).await {
|
if let Ok(res) = templates.render_error(ctx).await {
|
||||||
let (mut parts, _original_body) = response.into_parts();
|
let (mut parts, _original_body) = response.into_parts();
|
||||||
parts.headers.remove(CONTENT_TYPE);
|
parts.headers.remove(CONTENT_TYPE);
|
||||||
|
parts.headers.remove(CONTENT_LENGTH);
|
||||||
return Ok((parts, Html(res)).into_response());
|
return Ok((parts, Html(res)).into_response());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -14,14 +14,20 @@
|
|||||||
|
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use mas_data_model::{CompatSession, CompatSessionState, Device, User};
|
use mas_data_model::{
|
||||||
use mas_storage::{compat::CompatSessionRepository, Clock};
|
CompatSession, CompatSessionState, CompatSsoLogin, CompatSsoLoginState, Device, User,
|
||||||
|
};
|
||||||
|
use mas_storage::{compat::CompatSessionRepository, Clock, Page, Pagination};
|
||||||
use rand::RngCore;
|
use rand::RngCore;
|
||||||
use sqlx::PgConnection;
|
use sqlx::{PgConnection, QueryBuilder};
|
||||||
use ulid::Ulid;
|
use ulid::Ulid;
|
||||||
|
use url::Url;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::{tracing::ExecuteExt, DatabaseError, DatabaseInconsistencyError, LookupResultExt};
|
use crate::{
|
||||||
|
pagination::QueryBuilderExt, tracing::ExecuteExt, DatabaseError, DatabaseInconsistencyError,
|
||||||
|
LookupResultExt,
|
||||||
|
};
|
||||||
|
|
||||||
/// An implementation of [`CompatSessionRepository`] for a PostgreSQL connection
|
/// An implementation of [`CompatSessionRepository`] for a PostgreSQL connection
|
||||||
pub struct PgCompatSessionRepository<'c> {
|
pub struct PgCompatSessionRepository<'c> {
|
||||||
@ -75,6 +81,101 @@ impl TryFrom<CompatSessionLookup> for CompatSession {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(sqlx::FromRow)]
|
||||||
|
struct CompatSessionAndSsoLoginLookup {
|
||||||
|
compat_session_id: Uuid,
|
||||||
|
device_id: String,
|
||||||
|
user_id: Uuid,
|
||||||
|
created_at: DateTime<Utc>,
|
||||||
|
finished_at: Option<DateTime<Utc>>,
|
||||||
|
is_synapse_admin: bool,
|
||||||
|
compat_sso_login_id: Option<Uuid>,
|
||||||
|
compat_sso_login_token: Option<String>,
|
||||||
|
compat_sso_login_redirect_uri: Option<String>,
|
||||||
|
compat_sso_login_created_at: Option<DateTime<Utc>>,
|
||||||
|
compat_sso_login_fulfilled_at: Option<DateTime<Utc>>,
|
||||||
|
compat_sso_login_exchanged_at: Option<DateTime<Utc>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<CompatSessionAndSsoLoginLookup> for (CompatSession, Option<CompatSsoLogin>) {
|
||||||
|
type Error = DatabaseInconsistencyError;
|
||||||
|
|
||||||
|
fn try_from(value: CompatSessionAndSsoLoginLookup) -> Result<Self, Self::Error> {
|
||||||
|
let id = value.compat_session_id.into();
|
||||||
|
let device = Device::try_from(value.device_id).map_err(|e| {
|
||||||
|
DatabaseInconsistencyError::on("compat_sessions")
|
||||||
|
.column("device_id")
|
||||||
|
.row(id)
|
||||||
|
.source(e)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let state = match value.finished_at {
|
||||||
|
None => CompatSessionState::Valid,
|
||||||
|
Some(finished_at) => CompatSessionState::Finished { finished_at },
|
||||||
|
};
|
||||||
|
|
||||||
|
let session = CompatSession {
|
||||||
|
id,
|
||||||
|
state,
|
||||||
|
user_id: value.user_id.into(),
|
||||||
|
device,
|
||||||
|
created_at: value.created_at,
|
||||||
|
is_synapse_admin: value.is_synapse_admin,
|
||||||
|
};
|
||||||
|
|
||||||
|
match (
|
||||||
|
value.compat_sso_login_id,
|
||||||
|
value.compat_sso_login_token,
|
||||||
|
value.compat_sso_login_redirect_uri,
|
||||||
|
value.compat_sso_login_created_at,
|
||||||
|
value.compat_sso_login_fulfilled_at,
|
||||||
|
value.compat_sso_login_exchanged_at,
|
||||||
|
) {
|
||||||
|
(None, None, None, None, None, None) => Ok((session, None)),
|
||||||
|
(
|
||||||
|
Some(id),
|
||||||
|
Some(login_token),
|
||||||
|
Some(redirect_uri),
|
||||||
|
Some(created_at),
|
||||||
|
fulfilled_at,
|
||||||
|
exchanged_at,
|
||||||
|
) => {
|
||||||
|
let id = id.into();
|
||||||
|
let redirect_uri = Url::parse(&redirect_uri).map_err(|e| {
|
||||||
|
DatabaseInconsistencyError::on("compat_sso_logins")
|
||||||
|
.column("redirect_uri")
|
||||||
|
.row(id)
|
||||||
|
.source(e)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let state = match (fulfilled_at, exchanged_at) {
|
||||||
|
(Some(fulfilled_at), None) => CompatSsoLoginState::Fulfilled {
|
||||||
|
fulfilled_at,
|
||||||
|
session_id: session.id,
|
||||||
|
},
|
||||||
|
(Some(fulfilled_at), Some(exchanged_at)) => CompatSsoLoginState::Exchanged {
|
||||||
|
fulfilled_at,
|
||||||
|
exchanged_at,
|
||||||
|
session_id: session.id,
|
||||||
|
},
|
||||||
|
_ => return Err(DatabaseInconsistencyError::on("compat_sso_logins").row(id)),
|
||||||
|
};
|
||||||
|
|
||||||
|
let login = CompatSsoLogin {
|
||||||
|
id,
|
||||||
|
redirect_uri,
|
||||||
|
login_token,
|
||||||
|
created_at,
|
||||||
|
state,
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok((session, Some(login)))
|
||||||
|
}
|
||||||
|
_ => Err(DatabaseInconsistencyError::on("compat_sso_logins").row(id)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl<'c> CompatSessionRepository for PgCompatSessionRepository<'c> {
|
impl<'c> CompatSessionRepository for PgCompatSessionRepository<'c> {
|
||||||
type Error = DatabaseError;
|
type Error = DatabaseError;
|
||||||
@ -201,4 +302,53 @@ impl<'c> CompatSessionRepository for PgCompatSessionRepository<'c> {
|
|||||||
|
|
||||||
Ok(compat_session)
|
Ok(compat_session)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(
|
||||||
|
name = "db.compat_session.list_paginated",
|
||||||
|
skip_all,
|
||||||
|
fields(
|
||||||
|
db.statement,
|
||||||
|
%user.id,
|
||||||
|
),
|
||||||
|
err,
|
||||||
|
)]
|
||||||
|
async fn list_paginated(
|
||||||
|
&mut self,
|
||||||
|
user: &User,
|
||||||
|
pagination: Pagination,
|
||||||
|
) -> Result<Page<(CompatSession, Option<CompatSsoLogin>)>, Self::Error> {
|
||||||
|
let mut query = QueryBuilder::new(
|
||||||
|
r#"
|
||||||
|
SELECT cs.compat_session_id
|
||||||
|
, cs.device_id
|
||||||
|
, cs.user_id
|
||||||
|
, cs.created_at
|
||||||
|
, cs.finished_at
|
||||||
|
, cs.is_synapse_admin
|
||||||
|
, cl.compat_sso_login_id
|
||||||
|
, cl.login_token as compat_sso_login_token
|
||||||
|
, cl.redirect_uri as compat_sso_login_redirect_uri
|
||||||
|
, cl.created_at as compat_sso_login_created_at
|
||||||
|
, cl.fulfilled_at as compat_sso_login_fulfilled_at
|
||||||
|
, cl.exchanged_at as compat_sso_login_exchanged_at
|
||||||
|
|
||||||
|
FROM compat_sessions cs
|
||||||
|
LEFT JOIN compat_sso_logins cl USING (compat_session_id)
|
||||||
|
"#,
|
||||||
|
);
|
||||||
|
|
||||||
|
query
|
||||||
|
.push(" WHERE cs.user_id = ")
|
||||||
|
.push_bind(Uuid::from(user.id))
|
||||||
|
.generate_pagination("cs.compat_session_id", pagination);
|
||||||
|
|
||||||
|
let edges: Vec<CompatSessionAndSsoLoginLookup> = query
|
||||||
|
.build_query_as()
|
||||||
|
.traced()
|
||||||
|
.fetch_all(&mut *self.conn)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let page = pagination.process(edges).try_map(TryFrom::try_from)?;
|
||||||
|
Ok(page)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -13,11 +13,11 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use mas_data_model::{CompatSession, Device, User};
|
use mas_data_model::{CompatSession, CompatSsoLogin, Device, User};
|
||||||
use rand_core::RngCore;
|
use rand_core::RngCore;
|
||||||
use ulid::Ulid;
|
use ulid::Ulid;
|
||||||
|
|
||||||
use crate::{repository_impl, Clock};
|
use crate::{repository_impl, Clock, Page, Pagination};
|
||||||
|
|
||||||
/// A [`CompatSessionRepository`] helps interacting with
|
/// A [`CompatSessionRepository`] helps interacting with
|
||||||
/// [`CompatSessionRepository`] saved in the storage backend
|
/// [`CompatSessionRepository`] saved in the storage backend
|
||||||
@ -80,6 +80,24 @@ pub trait CompatSessionRepository: Send + Sync {
|
|||||||
clock: &dyn Clock,
|
clock: &dyn Clock,
|
||||||
compat_session: CompatSession,
|
compat_session: CompatSession,
|
||||||
) -> Result<CompatSession, Self::Error>;
|
) -> Result<CompatSession, Self::Error>;
|
||||||
|
|
||||||
|
/// Get a paginated list of compat sessions for a user
|
||||||
|
///
|
||||||
|
/// Returns a page of compat sessions, with the associated SSO logins if any
|
||||||
|
///
|
||||||
|
/// # Parameters
|
||||||
|
///
|
||||||
|
/// * `user`: The user to get the compat sessions for
|
||||||
|
/// * `pagination`: The pagination parameters
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns [`Self::Error`] if the underlying repository fails
|
||||||
|
async fn list_paginated(
|
||||||
|
&mut self,
|
||||||
|
user: &User,
|
||||||
|
pagination: Pagination,
|
||||||
|
) -> Result<Page<(CompatSession, Option<CompatSsoLogin>)>, Self::Error>;
|
||||||
}
|
}
|
||||||
|
|
||||||
repository_impl!(CompatSessionRepository:
|
repository_impl!(CompatSessionRepository:
|
||||||
@ -99,4 +117,10 @@ repository_impl!(CompatSessionRepository:
|
|||||||
clock: &dyn Clock,
|
clock: &dyn Clock,
|
||||||
compat_session: CompatSession,
|
compat_session: CompatSession,
|
||||||
) -> Result<CompatSession, Self::Error>;
|
) -> Result<CompatSession, Self::Error>;
|
||||||
|
|
||||||
|
async fn list_paginated(
|
||||||
|
&mut self,
|
||||||
|
user: &User,
|
||||||
|
pagination: Pagination,
|
||||||
|
) -> Result<Page<(CompatSession, Option<CompatSsoLogin>)>, Self::Error>;
|
||||||
);
|
);
|
||||||
|
@ -22,15 +22,15 @@ limitations under the License.
|
|||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>matrix-authentication-service</title>
|
<title>matrix-authentication-service</title>
|
||||||
<script>
|
<script type="application/javascript">
|
||||||
window.APP_CONFIG = JSON.parse('{root: "/app/"}');
|
window.APP_CONFIG = JSON.parse('{"root": "/account/"}');
|
||||||
(function () {
|
(function () {
|
||||||
const query = window.matchMedia("(prefers-color-scheme: dark)");
|
const query = window.matchMedia("(prefers-color-scheme: dark)");
|
||||||
function handleChange(list) {
|
function handleChange(list) {
|
||||||
if (list.matches) {
|
if (list.matches) {
|
||||||
document.documentElement.classList.add("dark");
|
document.documentElement.classList.add("cpd-theme-dark");
|
||||||
} else {
|
} else {
|
||||||
document.documentElement.classList.remove("dark");
|
document.documentElement.classList.remove("cpd-theme-dark");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -143,6 +143,39 @@ type CompatSession implements Node & CreationEvent {
|
|||||||
When the session ended.
|
When the session ended.
|
||||||
"""
|
"""
|
||||||
finishedAt: DateTime
|
finishedAt: DateTime
|
||||||
|
"""
|
||||||
|
The associated SSO login, if any.
|
||||||
|
"""
|
||||||
|
ssoLogin: CompatSsoLogin
|
||||||
|
}
|
||||||
|
|
||||||
|
type CompatSessionConnection {
|
||||||
|
"""
|
||||||
|
Information to aid in pagination.
|
||||||
|
"""
|
||||||
|
pageInfo: PageInfo!
|
||||||
|
"""
|
||||||
|
A list of edges.
|
||||||
|
"""
|
||||||
|
edges: [CompatSessionEdge!]!
|
||||||
|
"""
|
||||||
|
A list of nodes.
|
||||||
|
"""
|
||||||
|
nodes: [CompatSession!]!
|
||||||
|
}
|
||||||
|
|
||||||
|
"""
|
||||||
|
An edge in a connection.
|
||||||
|
"""
|
||||||
|
type CompatSessionEdge {
|
||||||
|
"""
|
||||||
|
The item at the end of the edge
|
||||||
|
"""
|
||||||
|
node: CompatSession!
|
||||||
|
"""
|
||||||
|
A cursor for use in pagination
|
||||||
|
"""
|
||||||
|
cursor: String!
|
||||||
}
|
}
|
||||||
|
|
||||||
"""
|
"""
|
||||||
@ -826,6 +859,15 @@ type User implements Node {
|
|||||||
last: Int
|
last: Int
|
||||||
): CompatSsoLoginConnection!
|
): CompatSsoLoginConnection!
|
||||||
"""
|
"""
|
||||||
|
Get the list of compatibility sessions, chronologically sorted
|
||||||
|
"""
|
||||||
|
compatSessions(
|
||||||
|
after: String
|
||||||
|
before: String
|
||||||
|
first: Int
|
||||||
|
last: Int
|
||||||
|
): CompatSessionConnection!
|
||||||
|
"""
|
||||||
Get the list of active browser sessions, chronologically sorted
|
Get the list of active browser sessions, chronologically sorted
|
||||||
"""
|
"""
|
||||||
browserSessions(
|
browserSessions(
|
||||||
|
@ -12,14 +12,13 @@
|
|||||||
// 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.
|
||||||
|
|
||||||
import { Button } from "@vector-im/compound-web";
|
import { Control, Field, Root, Submit } from "@vector-im/compound-web";
|
||||||
import { atom, useAtom } from "jotai";
|
import { atom, useAtom } from "jotai";
|
||||||
import { atomWithMutation } from "jotai-urql";
|
import { atomWithMutation } from "jotai-urql";
|
||||||
import { useRef, useTransition } from "react";
|
import { useTransition } from "react";
|
||||||
|
|
||||||
import { graphql } from "../gql";
|
import { graphql } from "../gql";
|
||||||
|
|
||||||
import Input from "./Input";
|
|
||||||
import Typography from "./Typography";
|
import Typography from "./Typography";
|
||||||
|
|
||||||
const ADD_EMAIL_MUTATION = graphql(/* GraphQL */ `
|
const ADD_EMAIL_MUTATION = graphql(/* GraphQL */ `
|
||||||
@ -45,7 +44,8 @@ const AddEmailForm: React.FC<{ userId: string; onAdd?: () => void }> = ({
|
|||||||
userId,
|
userId,
|
||||||
onAdd,
|
onAdd,
|
||||||
}) => {
|
}) => {
|
||||||
const formRef = useRef<HTMLFormElement>(null);
|
// TODO: there is a problem with refs in compound
|
||||||
|
//const formRef = useRef<HTMLFormElement>(null);
|
||||||
const [addEmailResult, addEmail] = useAtom(addUserEmailAtom);
|
const [addEmailResult, addEmail] = useAtom(addUserEmailAtom);
|
||||||
const [pending, startTransition] = useTransition();
|
const [pending, startTransition] = useTransition();
|
||||||
|
|
||||||
@ -65,7 +65,7 @@ const AddEmailForm: React.FC<{ userId: string; onAdd?: () => void }> = ({
|
|||||||
onAdd?.();
|
onAdd?.();
|
||||||
|
|
||||||
// Reset the form
|
// Reset the form
|
||||||
formRef.current?.reset();
|
//formRef.current?.reset();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@ -97,18 +97,12 @@ const AddEmailForm: React.FC<{ userId: string; onAdd?: () => void }> = ({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<form className="flex" onSubmit={handleSubmit} ref={formRef}>
|
<Root className="flex" onSubmit={handleSubmit}>
|
||||||
<Input
|
<Field name="email" className="flex-1 mr-2">
|
||||||
className="flex-1 mr-2"
|
<Control disabled={pending} type="email" inputMode="email" />
|
||||||
disabled={pending}
|
</Field>
|
||||||
type="email"
|
<Submit disabled={pending}>Add</Submit>
|
||||||
inputMode="email"
|
</Root>
|
||||||
name="email"
|
|
||||||
/>
|
|
||||||
<Button kind="primary" disabled={pending} type="submit">
|
|
||||||
Add
|
|
||||||
</Button>
|
|
||||||
</form>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -17,7 +17,9 @@ type Props = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const BlockList: React.FC<Props> = ({ children }) => {
|
const BlockList: React.FC<Props> = ({ children }) => {
|
||||||
return <div className="my-2">{children}</div>;
|
return (
|
||||||
|
<div className="grid grid-cols-1 gap-4 group content-start">{children}</div>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default BlockList;
|
export default BlockList;
|
||||||
|
@ -92,7 +92,7 @@ const BrowserSession: React.FC<Props> = ({ session, isCurrent }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Block className="my-4 flex items-center">
|
<Block className="flex items-center">
|
||||||
<IconWebBrowser className="mr-4 session-icon" />
|
<IconWebBrowser className="mr-4 session-icon" />
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<Body size="md" weight="medium">
|
<Body size="md" weight="medium">
|
||||||
|
@ -24,27 +24,23 @@ import Block from "./Block";
|
|||||||
import DateTime from "./DateTime";
|
import DateTime from "./DateTime";
|
||||||
import { Body, Bold, Code } from "./Typography";
|
import { Body, Bold, Code } from "./Typography";
|
||||||
|
|
||||||
const FRAGMENT = graphql(/* GraphQL */ `
|
const LOGIN_FRAGMENT = graphql(/* GraphQL */ `
|
||||||
fragment CompatSsoLogin_login on CompatSsoLogin {
|
fragment CompatSession_sso_login on CompatSsoLogin {
|
||||||
id
|
id
|
||||||
redirectUri
|
redirectUri
|
||||||
createdAt
|
|
||||||
session {
|
|
||||||
id
|
|
||||||
...CompatSsoLogin_session
|
|
||||||
createdAt
|
|
||||||
deviceId
|
|
||||||
finishedAt
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
`);
|
`);
|
||||||
|
|
||||||
const SESSION_FRAGMENT = graphql(/* GraphQL */ `
|
const FRAGMENT = graphql(/* GraphQL */ `
|
||||||
fragment CompatSsoLogin_session on CompatSession {
|
fragment CompatSession_session on CompatSession {
|
||||||
id
|
id
|
||||||
createdAt
|
createdAt
|
||||||
deviceId
|
deviceId
|
||||||
finishedAt
|
finishedAt
|
||||||
|
ssoLogin {
|
||||||
|
id
|
||||||
|
...CompatSession_sso_login
|
||||||
|
}
|
||||||
}
|
}
|
||||||
`);
|
`);
|
||||||
|
|
||||||
@ -54,7 +50,7 @@ const END_SESSION_MUTATION = graphql(/* GraphQL */ `
|
|||||||
status
|
status
|
||||||
compatSession {
|
compatSession {
|
||||||
id
|
id
|
||||||
...CompatSsoLogin_session
|
finishedAt
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -72,15 +68,11 @@ const endCompatSessionFamily = atomFamily((id: string) => {
|
|||||||
return endCompatSessionAtom;
|
return endCompatSessionAtom;
|
||||||
});
|
});
|
||||||
|
|
||||||
type Props = {
|
|
||||||
login: FragmentType<typeof FRAGMENT>;
|
|
||||||
};
|
|
||||||
|
|
||||||
const CompatSession: React.FC<{
|
const CompatSession: React.FC<{
|
||||||
session: FragmentType<typeof SESSION_FRAGMENT>;
|
session: FragmentType<typeof FRAGMENT>;
|
||||||
}> = ({ session }) => {
|
}> = ({ session }) => {
|
||||||
const [pending, startTransition] = useTransition();
|
const [pending, startTransition] = useTransition();
|
||||||
const data = useFragment(SESSION_FRAGMENT, session);
|
const data = useFragment(FRAGMENT, session);
|
||||||
const endCompatSession = useSetAtom(endCompatSessionFamily(data.id));
|
const endCompatSession = useSetAtom(endCompatSessionFamily(data.id));
|
||||||
|
|
||||||
const onSessionEnd = (): void => {
|
const onSessionEnd = (): void => {
|
||||||
@ -90,7 +82,7 @@ const CompatSession: React.FC<{
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<Block className="p-4 bg-grey-25 dark:bg-grey-450 rounded-lg">
|
||||||
<Body>
|
<Body>
|
||||||
Started: <DateTime datetime={data.createdAt} />
|
Started: <DateTime datetime={data.createdAt} />
|
||||||
</Body>
|
</Body>
|
||||||
@ -102,6 +94,7 @@ const CompatSession: React.FC<{
|
|||||||
<Body>
|
<Body>
|
||||||
Device ID: <Code>{data.deviceId}</Code>
|
Device ID: <Code>{data.deviceId}</Code>
|
||||||
</Body>
|
</Body>
|
||||||
|
{data.ssoLogin && <CompatSsoLogin login={data.ssoLogin} />}
|
||||||
{data.finishedAt ? null : (
|
{data.finishedAt ? null : (
|
||||||
<Button
|
<Button
|
||||||
className="mt-2"
|
className="mt-2"
|
||||||
@ -113,24 +106,22 @@ const CompatSession: React.FC<{
|
|||||||
End session
|
End session
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const CompatSsoLogin: React.FC<Props> = ({ login }) => {
|
|
||||||
const data = useFragment(FRAGMENT, login);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Block>
|
|
||||||
<Body>
|
|
||||||
Requested: <DateTime datetime={data.createdAt} />
|
|
||||||
</Body>
|
|
||||||
<Body>
|
|
||||||
Redirect URI: <Bold>{data.redirectUri}</Bold>
|
|
||||||
</Body>
|
|
||||||
{data.session && <CompatSession session={data.session} />}
|
|
||||||
</Block>
|
</Block>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default CompatSsoLogin;
|
const CompatSsoLogin: React.FC<{
|
||||||
|
login: FragmentType<typeof LOGIN_FRAGMENT>;
|
||||||
|
}> = ({ login }) => {
|
||||||
|
const data = useFragment(LOGIN_FRAGMENT, login);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Body>
|
||||||
|
Redirect URI: <Bold>{data.redirectUri}</Bold>
|
||||||
|
</Body>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CompatSession;
|
@ -28,13 +28,13 @@ import {
|
|||||||
import { isErr, isOk, unwrapErr, unwrapOk } from "../result";
|
import { isErr, isOk, unwrapErr, unwrapOk } from "../result";
|
||||||
|
|
||||||
import BlockList from "./BlockList";
|
import BlockList from "./BlockList";
|
||||||
import CompatSsoLogin from "./CompatSsoLogin";
|
import CompatSession from "./CompatSession";
|
||||||
import GraphQLError from "./GraphQLError";
|
import GraphQLError from "./GraphQLError";
|
||||||
import PaginationControls from "./PaginationControls";
|
import PaginationControls from "./PaginationControls";
|
||||||
import { Title } from "./Typography";
|
import { Title } from "./Typography";
|
||||||
|
|
||||||
const QUERY = graphql(/* GraphQL */ `
|
const QUERY = graphql(/* GraphQL */ `
|
||||||
query CompatSsoLoginList(
|
query CompatSessionList(
|
||||||
$userId: ID!
|
$userId: ID!
|
||||||
$first: Int
|
$first: Int
|
||||||
$after: String
|
$after: String
|
||||||
@ -43,7 +43,7 @@ const QUERY = graphql(/* GraphQL */ `
|
|||||||
) {
|
) {
|
||||||
user(id: $userId) {
|
user(id: $userId) {
|
||||||
id
|
id
|
||||||
compatSsoLogins(
|
compatSessions(
|
||||||
first: $first
|
first: $first
|
||||||
after: $after
|
after: $after
|
||||||
last: $last
|
last: $last
|
||||||
@ -52,7 +52,7 @@ const QUERY = graphql(/* GraphQL */ `
|
|||||||
edges {
|
edges {
|
||||||
node {
|
node {
|
||||||
id
|
id
|
||||||
...CompatSsoLogin_login
|
...CompatSession_session
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -69,23 +69,23 @@ const QUERY = graphql(/* GraphQL */ `
|
|||||||
|
|
||||||
const currentPaginationAtom = atomForCurrentPagination();
|
const currentPaginationAtom = atomForCurrentPagination();
|
||||||
|
|
||||||
const compatSsoLoginListFamily = atomFamily((userId: string) => {
|
const compatSessionListFamily = atomFamily((userId: string) => {
|
||||||
const compatSsoLoginListQuery = atomWithQuery({
|
const compatSessionListQuery = atomWithQuery({
|
||||||
query: QUERY,
|
query: QUERY,
|
||||||
getVariables: (get) => ({ userId, ...get(currentPaginationAtom) }),
|
getVariables: (get) => ({ userId, ...get(currentPaginationAtom) }),
|
||||||
});
|
});
|
||||||
|
|
||||||
const compatSsoLoginList = mapQueryAtom(
|
const compatSessionList = mapQueryAtom(
|
||||||
compatSsoLoginListQuery,
|
compatSessionListQuery,
|
||||||
(data) => data.user?.compatSsoLogins || null
|
(data) => data.user?.compatSessions || null
|
||||||
);
|
);
|
||||||
|
|
||||||
return compatSsoLoginList;
|
return compatSessionList;
|
||||||
});
|
});
|
||||||
|
|
||||||
const pageInfoFamily = atomFamily((userId: string) => {
|
const pageInfoFamily = atomFamily((userId: string) => {
|
||||||
const pageInfoAtom = atom(async (get): Promise<PageInfo | null> => {
|
const pageInfoAtom = atom(async (get): Promise<PageInfo | null> => {
|
||||||
const result = await get(compatSsoLoginListFamily(userId));
|
const result = await get(compatSessionListFamily(userId));
|
||||||
return (isOk(result) && unwrapOk(result)?.pageInfo) || null;
|
return (isOk(result) && unwrapOk(result)?.pageInfo) || null;
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -100,15 +100,15 @@ const paginationFamily = atomFamily((userId: string) => {
|
|||||||
return paginationAtom;
|
return paginationAtom;
|
||||||
});
|
});
|
||||||
|
|
||||||
const CompatSsoLoginList: React.FC<{ userId: string }> = ({ userId }) => {
|
const CompatSessionList: React.FC<{ userId: string }> = ({ userId }) => {
|
||||||
const [pending, startTransition] = useTransition();
|
const [pending, startTransition] = useTransition();
|
||||||
const result = useAtomValue(compatSsoLoginListFamily(userId));
|
const result = useAtomValue(compatSessionListFamily(userId));
|
||||||
const setPagination = useSetAtom(currentPaginationAtom);
|
const setPagination = useSetAtom(currentPaginationAtom);
|
||||||
const [prevPage, nextPage] = useAtomValue(paginationFamily(userId));
|
const [prevPage, nextPage] = useAtomValue(paginationFamily(userId));
|
||||||
|
|
||||||
if (isErr(result)) return <GraphQLError error={unwrapErr(result)} />;
|
if (isErr(result)) return <GraphQLError error={unwrapErr(result)} />;
|
||||||
const compatSsoLoginList = unwrapOk(result);
|
const compatSessionList = unwrapOk(result);
|
||||||
if (compatSsoLoginList === null)
|
if (compatSessionList === null)
|
||||||
return <>Failed to load list of compatibility sessions.</>;
|
return <>Failed to load list of compatibility sessions.</>;
|
||||||
|
|
||||||
const paginate = (pagination: Pagination): void => {
|
const paginate = (pagination: Pagination): void => {
|
||||||
@ -125,11 +125,11 @@ const CompatSsoLoginList: React.FC<{ userId: string }> = ({ userId }) => {
|
|||||||
onNext={nextPage ? (): void => paginate(nextPage) : null}
|
onNext={nextPage ? (): void => paginate(nextPage) : null}
|
||||||
disabled={pending}
|
disabled={pending}
|
||||||
/>
|
/>
|
||||||
{compatSsoLoginList.edges.map((n) => (
|
{compatSessionList.edges.map((n) => (
|
||||||
<CompatSsoLogin login={n.node} key={n.node.id} />
|
<CompatSession session={n.node} key={n.node.id} />
|
||||||
))}
|
))}
|
||||||
</BlockList>
|
</BlockList>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default CompatSsoLoginList;
|
export default CompatSessionList;
|
@ -1,50 +0,0 @@
|
|||||||
// Copyright 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.
|
|
||||||
|
|
||||||
import type { Meta, StoryObj } from "@storybook/react";
|
|
||||||
|
|
||||||
import Input from "./Input";
|
|
||||||
|
|
||||||
const meta = {
|
|
||||||
title: "UI/Input",
|
|
||||||
component: Input,
|
|
||||||
tags: ["autodocs"],
|
|
||||||
argTypes: {
|
|
||||||
value: {
|
|
||||||
control: "text",
|
|
||||||
},
|
|
||||||
placeholder: {
|
|
||||||
control: "text",
|
|
||||||
},
|
|
||||||
disabled: {
|
|
||||||
control: "boolean",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
args: {
|
|
||||||
value: "",
|
|
||||||
placeholder: "Placeholder",
|
|
||||||
disabled: false,
|
|
||||||
},
|
|
||||||
} satisfies Meta<typeof Input>;
|
|
||||||
|
|
||||||
export default meta;
|
|
||||||
type Story = StoryObj<typeof Input>;
|
|
||||||
|
|
||||||
export const Basic: Story = {};
|
|
||||||
|
|
||||||
export const Disabled: Story = {
|
|
||||||
args: {
|
|
||||||
disabled: true,
|
|
||||||
},
|
|
||||||
};
|
|
@ -1,28 +0,0 @@
|
|||||||
// Copyright 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.
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
disabled?: boolean;
|
|
||||||
className?: string;
|
|
||||||
} & React.HTMLProps<HTMLInputElement>;
|
|
||||||
|
|
||||||
const Input: React.FC<Props> = ({ disabled, className, ...props }) => {
|
|
||||||
const disabledClass = disabled
|
|
||||||
? "bg-grey-100 dark:bg-grey-400"
|
|
||||||
: "bg-white dark:bg-grey-450";
|
|
||||||
const fullClassName = `${className} px-2 py-1 border-2 border-grey-100 dark:border-grey-400 dark:text-white placeholder-grey-100 dark:placeholder-grey-150 rounded-lg ${disabledClass}`;
|
|
||||||
return <input disabled={disabled} className={fullClassName} {...props} />;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Input;
|
|
@ -19,8 +19,8 @@ const Layout: React.FC<{ children?: React.ReactNode }> = ({ children }) => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<NavBar className="nav-bar container">
|
<NavBar className="nav-bar container">
|
||||||
<NavItem route={{ type: "home" }}>Home</NavItem>
|
<NavItem route={{ type: "home" }}>Sessions</NavItem>
|
||||||
<NavItem route={{ type: "account" }}>My Account</NavItem>
|
<NavItem route={{ type: "account" }}>Emails</NavItem>
|
||||||
</NavBar>
|
</NavBar>
|
||||||
|
|
||||||
<hr className="my-2" />
|
<hr className="my-2" />
|
||||||
@ -33,6 +33,8 @@ const Layout: React.FC<{ children?: React.ReactNode }> = ({ children }) => {
|
|||||||
<a href="https://matrix.org" target="_blank" rel="noreferrer noopener">
|
<a href="https://matrix.org" target="_blank" rel="noreferrer noopener">
|
||||||
<img
|
<img
|
||||||
className="inline my-2"
|
className="inline my-2"
|
||||||
|
height={32}
|
||||||
|
width={75}
|
||||||
src="https://matrix.org/images/matrix-logo.svg"
|
src="https://matrix.org/images/matrix-logo.svg"
|
||||||
alt="Matrix.org"
|
alt="Matrix.org"
|
||||||
/>
|
/>
|
||||||
|
@ -100,10 +100,11 @@ const OAuth2Session: React.FC<Props> = ({ session }) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Block
|
<Block
|
||||||
className={
|
className={`p-4 bg-grey-25 dark:bg-grey-450 rounded-lg ${
|
||||||
data.finishedAt &&
|
data.finishedAt
|
||||||
"opacity-50 group-hover:opacity-100 transition-opacity"
|
? "opacity-50 group-hover:opacity-100 transition-opacity"
|
||||||
}
|
: ""
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
<Typography variant="body" bold>
|
<Typography variant="body" bold>
|
||||||
<Link
|
<Link
|
||||||
|
@ -28,7 +28,7 @@ const PaginationControls: React.FC<Props> = ({
|
|||||||
disabled,
|
disabled,
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<div className="grid items-center grid-cols-3 gap-2 my-2">
|
<div className="grid items-center grid-cols-3 gap-2">
|
||||||
<Button
|
<Button
|
||||||
kind="secondary"
|
kind="secondary"
|
||||||
size="sm"
|
size="sm"
|
||||||
|
@ -12,17 +12,24 @@
|
|||||||
// 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.
|
||||||
|
|
||||||
import { Button } from "@vector-im/compound-web";
|
import {
|
||||||
|
Button,
|
||||||
|
Control,
|
||||||
|
Field,
|
||||||
|
Label,
|
||||||
|
Message,
|
||||||
|
Root as Form,
|
||||||
|
Submit,
|
||||||
|
} from "@vector-im/compound-web";
|
||||||
import { atom, useAtom, useSetAtom } from "jotai";
|
import { atom, useAtom, useSetAtom } from "jotai";
|
||||||
import { atomFamily } from "jotai/utils";
|
import { atomFamily } from "jotai/utils";
|
||||||
import { atomWithMutation } from "jotai-urql";
|
import { atomWithMutation } from "jotai-urql";
|
||||||
import { useRef, useTransition } from "react";
|
import { useTransition } from "react";
|
||||||
|
|
||||||
import { FragmentType, graphql, useFragment } from "../gql";
|
import { FragmentType, graphql, useFragment } from "../gql";
|
||||||
|
|
||||||
import Block from "./Block";
|
import Block from "./Block";
|
||||||
import DateTime from "./DateTime";
|
import DateTime from "./DateTime";
|
||||||
import Input from "./Input";
|
|
||||||
import Typography from "./Typography";
|
import Typography from "./Typography";
|
||||||
|
|
||||||
// This component shows a single user email address, with controls to verify it,
|
// This component shows a single user email address, with controls to verify it,
|
||||||
@ -168,7 +175,8 @@ const UserEmail: React.FC<{
|
|||||||
);
|
);
|
||||||
const setPrimaryEmail = useSetAtom(setPrimaryEmailFamily(data.id));
|
const setPrimaryEmail = useSetAtom(setPrimaryEmailFamily(data.id));
|
||||||
const removeEmail = useSetAtom(removeEmailFamily(data.id));
|
const removeEmail = useSetAtom(removeEmailFamily(data.id));
|
||||||
const formRef = useRef<HTMLFormElement>(null);
|
// TODO: compound doesn't forward the refs properly
|
||||||
|
// const fieldRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
const onFormSubmit = (e: React.FormEvent<HTMLFormElement>): void => {
|
const onFormSubmit = (e: React.FormEvent<HTMLFormElement>): void => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@ -177,7 +185,7 @@ const UserEmail: React.FC<{
|
|||||||
startTransition(() => {
|
startTransition(() => {
|
||||||
verifyEmail(code).then((result) => {
|
verifyEmail(code).then((result) => {
|
||||||
// Clear the form
|
// Clear the form
|
||||||
formRef.current?.reset();
|
e.currentTarget?.reset();
|
||||||
|
|
||||||
if (result.data?.verifyEmail.status === "VERIFIED") {
|
if (result.data?.verifyEmail.status === "VERIFIED") {
|
||||||
// Call the onSetPrimary callback if provided
|
// Call the onSetPrimary callback if provided
|
||||||
@ -191,7 +199,7 @@ const UserEmail: React.FC<{
|
|||||||
const onResendClick = (): void => {
|
const onResendClick = (): void => {
|
||||||
startTransition(() => {
|
startTransition(() => {
|
||||||
resendVerificationEmail().then(() => {
|
resendVerificationEmail().then(() => {
|
||||||
formRef.current?.code.focus();
|
// TODO: fieldRef.current?.focus();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@ -220,61 +228,70 @@ const UserEmail: React.FC<{
|
|||||||
verifyEmailResult.data?.verifyEmail.status === "INVALID_CODE";
|
verifyEmailResult.data?.verifyEmail.status === "INVALID_CODE";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Block highlight={highlight}>
|
<Block
|
||||||
|
highlight={highlight}
|
||||||
|
className="grid grid-col-1 gap-2 pb-4 border-b-2 border-b-grey-200"
|
||||||
|
>
|
||||||
{isPrimary && (
|
{isPrimary && (
|
||||||
<Typography variant="body" bold>
|
<Typography variant="body" bold>
|
||||||
Primary
|
Primary
|
||||||
</Typography>
|
</Typography>
|
||||||
)}
|
)}
|
||||||
<div className="flex justify-between items-center">
|
|
||||||
<Typography variant="caption" bold className="flex-1">
|
<Typography variant="caption" bold className="flex-1">
|
||||||
{data.email}
|
{data.email}
|
||||||
</Typography>
|
</Typography>
|
||||||
{!isPrimary && (
|
|
||||||
<>
|
|
||||||
{/* The primary email can only be set if the email was verified */}
|
|
||||||
{data.confirmedAt && (
|
|
||||||
<Button
|
|
||||||
disabled={pending}
|
|
||||||
onClick={onSetPrimaryClick}
|
|
||||||
className="ml-2"
|
|
||||||
>
|
|
||||||
Set primary
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
<Button disabled={pending} onClick={onRemoveClick} className="ml-2">
|
|
||||||
Remove
|
|
||||||
</Button>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{data.confirmedAt ? (
|
{data.confirmedAt ? (
|
||||||
<Typography variant="micro">
|
<Typography variant="micro">
|
||||||
Verified <DateTime datetime={data.confirmedAt} />
|
Verified <DateTime datetime={data.confirmedAt} />
|
||||||
</Typography>
|
</Typography>
|
||||||
) : (
|
) : (
|
||||||
<form
|
<Form onSubmit={onFormSubmit} className="grid grid-cols-2 gap-2">
|
||||||
onSubmit={onFormSubmit}
|
<Field name="code" className="col-span-2">
|
||||||
className="mt-2 grid grid-cols-2 gap-2"
|
<Label>Code</Label>
|
||||||
ref={formRef}
|
<Control
|
||||||
>
|
// ref={fieldRef}
|
||||||
<Input
|
placeholder="xxxxxx"
|
||||||
className="col-span-2"
|
|
||||||
name="code"
|
|
||||||
placeholder="Code"
|
|
||||||
type="text"
|
type="text"
|
||||||
inputMode="numeric"
|
inputMode="numeric"
|
||||||
/>
|
/>
|
||||||
|
</Field>
|
||||||
{invalidCode && (
|
{invalidCode && (
|
||||||
<div className="col-span-2 text-alert font-bold">Invalid code</div>
|
<Message className="col-span-2 text-alert font-bold">
|
||||||
|
Invalid code
|
||||||
|
</Message>
|
||||||
)}
|
)}
|
||||||
<Button type="submit" disabled={pending}>
|
<Submit size="sm" type="submit" disabled={pending}>
|
||||||
Submit
|
Submit
|
||||||
</Button>
|
</Submit>
|
||||||
<Button disabled={pending || emailSent} onClick={onResendClick}>
|
<Button
|
||||||
|
size="sm"
|
||||||
|
kind="secondary"
|
||||||
|
disabled={pending || emailSent}
|
||||||
|
onClick={onResendClick}
|
||||||
|
>
|
||||||
{emailSent ? "Sent!" : "Resend"}
|
{emailSent ? "Sent!" : "Resend"}
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</Form>
|
||||||
|
)}
|
||||||
|
{!isPrimary && (
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
{/* The primary email can only be set if the email was verified */}
|
||||||
|
{data.confirmedAt ? (
|
||||||
|
<Button size="sm" disabled={pending} onClick={onSetPrimaryClick}>
|
||||||
|
Set primary
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<div />
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
kind="destructive"
|
||||||
|
size="sm"
|
||||||
|
disabled={pending}
|
||||||
|
onClick={onRemoveClick}
|
||||||
|
>
|
||||||
|
Remove
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</Block>
|
</Block>
|
||||||
);
|
);
|
||||||
|
@ -25,14 +25,14 @@ const documents = {
|
|||||||
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 ) {\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 CompatSsoLogin_login on CompatSsoLogin {\n id\n redirectUri\n createdAt\n session {\n id\n ...CompatSsoLogin_session\n createdAt\n deviceId\n finishedAt\n }\n }\n":
|
"\n fragment CompatSession_sso_login on CompatSsoLogin {\n id\n redirectUri\n }\n":
|
||||||
types.CompatSsoLogin_LoginFragmentDoc,
|
types.CompatSession_Sso_LoginFragmentDoc,
|
||||||
"\n fragment CompatSsoLogin_session on CompatSession {\n id\n createdAt\n deviceId\n finishedAt\n }\n":
|
"\n fragment CompatSession_session on CompatSession {\n id\n createdAt\n deviceId\n finishedAt\n ssoLogin {\n id\n ...CompatSession_sso_login\n }\n }\n":
|
||||||
types.CompatSsoLogin_SessionFragmentDoc,
|
types.CompatSession_SessionFragmentDoc,
|
||||||
"\n mutation EndCompatSession($id: ID!) {\n endCompatSession(input: { compatSessionId: $id }) {\n status\n compatSession {\n id\n ...CompatSsoLogin_session\n }\n }\n }\n":
|
"\n mutation EndCompatSession($id: ID!) {\n endCompatSession(input: { compatSessionId: $id }) {\n status\n compatSession {\n id\n finishedAt\n }\n }\n }\n":
|
||||||
types.EndCompatSessionDocument,
|
types.EndCompatSessionDocument,
|
||||||
"\n query CompatSsoLoginList(\n $userId: ID!\n $first: Int\n $after: String\n $last: Int\n $before: String\n ) {\n user(id: $userId) {\n id\n compatSsoLogins(\n first: $first\n after: $after\n last: $last\n before: $before\n ) {\n edges {\n node {\n id\n ...CompatSsoLogin_login\n }\n }\n\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n }\n }\n":
|
"\n query CompatSessionList(\n $userId: ID!\n $first: Int\n $after: String\n $last: Int\n $before: String\n ) {\n user(id: $userId) {\n id\n compatSessions(\n first: $first\n after: $after\n last: $last\n before: $before\n ) {\n edges {\n node {\n id\n ...CompatSession_session\n }\n }\n\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n }\n }\n":
|
||||||
types.CompatSsoLoginListDocument,
|
types.CompatSessionListDocument,
|
||||||
"\n fragment OAuth2Session_session on Oauth2Session {\n id\n scope\n createdAt\n finishedAt\n client {\n id\n clientId\n clientName\n clientUri\n }\n }\n":
|
"\n fragment OAuth2Session_session on Oauth2Session {\n id\n scope\n createdAt\n finishedAt\n client {\n id\n clientId\n clientName\n clientUri\n }\n }\n":
|
||||||
types.OAuth2Session_SessionFragmentDoc,
|
types.OAuth2Session_SessionFragmentDoc,
|
||||||
"\n mutation EndOAuth2Session($id: ID!) {\n endOauth2Session(input: { oauth2SessionId: $id }) {\n status\n oauth2Session {\n id\n ...OAuth2Session_session\n }\n }\n }\n":
|
"\n mutation EndOAuth2Session($id: ID!) {\n endOauth2Session(input: { oauth2SessionId: $id }) {\n status\n oauth2Session {\n id\n ...OAuth2Session_session\n }\n }\n }\n":
|
||||||
@ -115,26 +115,26 @@ 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 fragment CompatSsoLogin_login on CompatSsoLogin {\n id\n redirectUri\n createdAt\n session {\n id\n ...CompatSsoLogin_session\n createdAt\n deviceId\n finishedAt\n }\n }\n"
|
source: "\n fragment CompatSession_sso_login on CompatSsoLogin {\n id\n redirectUri\n }\n"
|
||||||
): typeof documents["\n fragment CompatSsoLogin_login on CompatSsoLogin {\n id\n redirectUri\n createdAt\n session {\n id\n ...CompatSsoLogin_session\n createdAt\n deviceId\n finishedAt\n }\n }\n"];
|
): typeof documents["\n fragment CompatSession_sso_login on CompatSsoLogin {\n id\n redirectUri\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.
|
||||||
*/
|
*/
|
||||||
export function graphql(
|
export function graphql(
|
||||||
source: "\n fragment CompatSsoLogin_session on CompatSession {\n id\n createdAt\n deviceId\n finishedAt\n }\n"
|
source: "\n fragment CompatSession_session on CompatSession {\n id\n createdAt\n deviceId\n finishedAt\n ssoLogin {\n id\n ...CompatSession_sso_login\n }\n }\n"
|
||||||
): typeof documents["\n fragment CompatSsoLogin_session on CompatSession {\n id\n createdAt\n deviceId\n finishedAt\n }\n"];
|
): typeof documents["\n fragment CompatSession_session on CompatSession {\n id\n createdAt\n deviceId\n finishedAt\n ssoLogin {\n id\n ...CompatSession_sso_login\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.
|
||||||
*/
|
*/
|
||||||
export function graphql(
|
export function graphql(
|
||||||
source: "\n mutation EndCompatSession($id: ID!) {\n endCompatSession(input: { compatSessionId: $id }) {\n status\n compatSession {\n id\n ...CompatSsoLogin_session\n }\n }\n }\n"
|
source: "\n mutation EndCompatSession($id: ID!) {\n endCompatSession(input: { compatSessionId: $id }) {\n status\n compatSession {\n id\n finishedAt\n }\n }\n }\n"
|
||||||
): typeof documents["\n mutation EndCompatSession($id: ID!) {\n endCompatSession(input: { compatSessionId: $id }) {\n status\n compatSession {\n id\n ...CompatSsoLogin_session\n }\n }\n }\n"];
|
): typeof documents["\n mutation EndCompatSession($id: ID!) {\n endCompatSession(input: { compatSessionId: $id }) {\n status\n compatSession {\n id\n finishedAt\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.
|
||||||
*/
|
*/
|
||||||
export function graphql(
|
export function graphql(
|
||||||
source: "\n query CompatSsoLoginList(\n $userId: ID!\n $first: Int\n $after: String\n $last: Int\n $before: String\n ) {\n user(id: $userId) {\n id\n compatSsoLogins(\n first: $first\n after: $after\n last: $last\n before: $before\n ) {\n edges {\n node {\n id\n ...CompatSsoLogin_login\n }\n }\n\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n }\n }\n"
|
source: "\n query CompatSessionList(\n $userId: ID!\n $first: Int\n $after: String\n $last: Int\n $before: String\n ) {\n user(id: $userId) {\n id\n compatSessions(\n first: $first\n after: $after\n last: $last\n before: $before\n ) {\n edges {\n node {\n id\n ...CompatSession_session\n }\n }\n\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n }\n }\n"
|
||||||
): typeof documents["\n query CompatSsoLoginList(\n $userId: ID!\n $first: Int\n $after: String\n $last: Int\n $before: String\n ) {\n user(id: $userId) {\n id\n compatSsoLogins(\n first: $first\n after: $after\n last: $last\n before: $before\n ) {\n edges {\n node {\n id\n ...CompatSsoLogin_login\n }\n }\n\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n }\n }\n"];
|
): typeof documents["\n query CompatSessionList(\n $userId: ID!\n $first: Int\n $after: String\n $last: Int\n $before: String\n ) {\n user(id: $userId) {\n id\n compatSessions(\n first: $first\n after: $after\n last: $last\n before: $before\n ) {\n edges {\n node {\n id\n ...CompatSession_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.
|
||||||
*/
|
*/
|
||||||
|
@ -132,10 +132,31 @@ export type CompatSession = CreationEvent &
|
|||||||
finishedAt?: Maybe<Scalars["DateTime"]["output"]>;
|
finishedAt?: Maybe<Scalars["DateTime"]["output"]>;
|
||||||
/** ID of the object. */
|
/** ID of the object. */
|
||||||
id: Scalars["ID"]["output"];
|
id: Scalars["ID"]["output"];
|
||||||
|
/** The associated SSO login, if any. */
|
||||||
|
ssoLogin?: Maybe<CompatSsoLogin>;
|
||||||
/** The user authorized for this session. */
|
/** The user authorized for this session. */
|
||||||
user: User;
|
user: User;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type CompatSessionConnection = {
|
||||||
|
__typename?: "CompatSessionConnection";
|
||||||
|
/** A list of edges. */
|
||||||
|
edges: Array<CompatSessionEdge>;
|
||||||
|
/** A list of nodes. */
|
||||||
|
nodes: Array<CompatSession>;
|
||||||
|
/** Information to aid in pagination. */
|
||||||
|
pageInfo: PageInfo;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** An edge in a connection. */
|
||||||
|
export type CompatSessionEdge = {
|
||||||
|
__typename?: "CompatSessionEdge";
|
||||||
|
/** A cursor for use in pagination */
|
||||||
|
cursor: Scalars["String"]["output"];
|
||||||
|
/** The item at the end of the edge */
|
||||||
|
node: CompatSession;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A compat SSO login represents a login done through the legacy Matrix login
|
* A compat SSO login represents a login done through the legacy Matrix login
|
||||||
* API, via the `m.login.sso` login method.
|
* API, via the `m.login.sso` login method.
|
||||||
@ -623,6 +644,8 @@ export type User = Node & {
|
|||||||
__typename?: "User";
|
__typename?: "User";
|
||||||
/** Get the list of active browser sessions, chronologically sorted */
|
/** Get the list of active browser sessions, chronologically sorted */
|
||||||
browserSessions: BrowserSessionConnection;
|
browserSessions: BrowserSessionConnection;
|
||||||
|
/** Get the list of compatibility sessions, chronologically sorted */
|
||||||
|
compatSessions: CompatSessionConnection;
|
||||||
/** Get the list of compatibility SSO logins, chronologically sorted */
|
/** Get the list of compatibility SSO logins, chronologically sorted */
|
||||||
compatSsoLogins: CompatSsoLoginConnection;
|
compatSsoLogins: CompatSsoLoginConnection;
|
||||||
/** Get the list of emails, chronologically sorted */
|
/** Get the list of emails, chronologically sorted */
|
||||||
@ -649,6 +672,14 @@ export type UserBrowserSessionsArgs = {
|
|||||||
last?: InputMaybe<Scalars["Int"]["input"]>;
|
last?: InputMaybe<Scalars["Int"]["input"]>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** A user is an individual's account. */
|
||||||
|
export type UserCompatSessionsArgs = {
|
||||||
|
after?: InputMaybe<Scalars["String"]["input"]>;
|
||||||
|
before?: InputMaybe<Scalars["String"]["input"]>;
|
||||||
|
first?: InputMaybe<Scalars["Int"]["input"]>;
|
||||||
|
last?: InputMaybe<Scalars["Int"]["input"]>;
|
||||||
|
};
|
||||||
|
|
||||||
/** A user is an individual's account. */
|
/** A user is an individual's account. */
|
||||||
export type UserCompatSsoLoginsArgs = {
|
export type UserCompatSsoLoginsArgs = {
|
||||||
after?: InputMaybe<Scalars["String"]["input"]>;
|
after?: InputMaybe<Scalars["String"]["input"]>;
|
||||||
@ -859,33 +890,26 @@ export type BrowserSessionListQuery = {
|
|||||||
} | null;
|
} | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type CompatSsoLogin_LoginFragment = {
|
export type CompatSession_Sso_LoginFragment = {
|
||||||
__typename?: "CompatSsoLogin";
|
__typename?: "CompatSsoLogin";
|
||||||
id: string;
|
id: string;
|
||||||
redirectUri: any;
|
redirectUri: any;
|
||||||
createdAt: any;
|
} & { " $fragmentName"?: "CompatSession_Sso_LoginFragment" };
|
||||||
session?:
|
|
||||||
| ({
|
export type CompatSession_SessionFragment = {
|
||||||
__typename?: "CompatSession";
|
__typename?: "CompatSession";
|
||||||
id: string;
|
id: string;
|
||||||
createdAt: any;
|
createdAt: any;
|
||||||
deviceId: string;
|
deviceId: string;
|
||||||
finishedAt?: any | null;
|
finishedAt?: any | null;
|
||||||
} & {
|
ssoLogin?:
|
||||||
|
| ({ __typename?: "CompatSsoLogin"; id: string } & {
|
||||||
" $fragmentRefs"?: {
|
" $fragmentRefs"?: {
|
||||||
CompatSsoLogin_SessionFragment: CompatSsoLogin_SessionFragment;
|
CompatSession_Sso_LoginFragment: CompatSession_Sso_LoginFragment;
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
| null;
|
| null;
|
||||||
} & { " $fragmentName"?: "CompatSsoLogin_LoginFragment" };
|
} & { " $fragmentName"?: "CompatSession_SessionFragment" };
|
||||||
|
|
||||||
export type CompatSsoLogin_SessionFragment = {
|
|
||||||
__typename?: "CompatSession";
|
|
||||||
id: string;
|
|
||||||
createdAt: any;
|
|
||||||
deviceId: string;
|
|
||||||
finishedAt?: any | null;
|
|
||||||
} & { " $fragmentName"?: "CompatSsoLogin_SessionFragment" };
|
|
||||||
|
|
||||||
export type EndCompatSessionMutationVariables = Exact<{
|
export type EndCompatSessionMutationVariables = Exact<{
|
||||||
id: Scalars["ID"]["input"];
|
id: Scalars["ID"]["input"];
|
||||||
@ -896,17 +920,15 @@ export type EndCompatSessionMutation = {
|
|||||||
endCompatSession: {
|
endCompatSession: {
|
||||||
__typename?: "EndCompatSessionPayload";
|
__typename?: "EndCompatSessionPayload";
|
||||||
status: EndCompatSessionStatus;
|
status: EndCompatSessionStatus;
|
||||||
compatSession?:
|
compatSession?: {
|
||||||
| ({ __typename?: "CompatSession"; id: string } & {
|
__typename?: "CompatSession";
|
||||||
" $fragmentRefs"?: {
|
id: string;
|
||||||
CompatSsoLogin_SessionFragment: CompatSsoLogin_SessionFragment;
|
finishedAt?: any | null;
|
||||||
};
|
} | null;
|
||||||
})
|
|
||||||
| null;
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export type CompatSsoLoginListQueryVariables = Exact<{
|
export type CompatSessionListQueryVariables = Exact<{
|
||||||
userId: Scalars["ID"]["input"];
|
userId: Scalars["ID"]["input"];
|
||||||
first?: InputMaybe<Scalars["Int"]["input"]>;
|
first?: InputMaybe<Scalars["Int"]["input"]>;
|
||||||
after?: InputMaybe<Scalars["String"]["input"]>;
|
after?: InputMaybe<Scalars["String"]["input"]>;
|
||||||
@ -914,18 +936,18 @@ export type CompatSsoLoginListQueryVariables = Exact<{
|
|||||||
before?: InputMaybe<Scalars["String"]["input"]>;
|
before?: InputMaybe<Scalars["String"]["input"]>;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
export type CompatSsoLoginListQuery = {
|
export type CompatSessionListQuery = {
|
||||||
__typename?: "Query";
|
__typename?: "Query";
|
||||||
user?: {
|
user?: {
|
||||||
__typename?: "User";
|
__typename?: "User";
|
||||||
id: string;
|
id: string;
|
||||||
compatSsoLogins: {
|
compatSessions: {
|
||||||
__typename?: "CompatSsoLoginConnection";
|
__typename?: "CompatSessionConnection";
|
||||||
edges: Array<{
|
edges: Array<{
|
||||||
__typename?: "CompatSsoLoginEdge";
|
__typename?: "CompatSessionEdge";
|
||||||
node: { __typename?: "CompatSsoLogin"; id: string } & {
|
node: { __typename?: "CompatSession"; id: string } & {
|
||||||
" $fragmentRefs"?: {
|
" $fragmentRefs"?: {
|
||||||
CompatSsoLogin_LoginFragment: CompatSsoLogin_LoginFragment;
|
CompatSession_SessionFragment: CompatSession_SessionFragment;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}>;
|
}>;
|
||||||
@ -1227,34 +1249,12 @@ export const BrowserSession_SessionFragmentDoc = {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
} as unknown as DocumentNode<BrowserSession_SessionFragment, unknown>;
|
} as unknown as DocumentNode<BrowserSession_SessionFragment, unknown>;
|
||||||
export const CompatSsoLogin_SessionFragmentDoc = {
|
export const CompatSession_Sso_LoginFragmentDoc = {
|
||||||
kind: "Document",
|
kind: "Document",
|
||||||
definitions: [
|
definitions: [
|
||||||
{
|
{
|
||||||
kind: "FragmentDefinition",
|
kind: "FragmentDefinition",
|
||||||
name: { kind: "Name", value: "CompatSsoLogin_session" },
|
name: { kind: "Name", value: "CompatSession_sso_login" },
|
||||||
typeCondition: {
|
|
||||||
kind: "NamedType",
|
|
||||||
name: { kind: "Name", value: "CompatSession" },
|
|
||||||
},
|
|
||||||
selectionSet: {
|
|
||||||
kind: "SelectionSet",
|
|
||||||
selections: [
|
|
||||||
{ kind: "Field", name: { kind: "Name", value: "id" } },
|
|
||||||
{ kind: "Field", name: { kind: "Name", value: "createdAt" } },
|
|
||||||
{ kind: "Field", name: { kind: "Name", value: "deviceId" } },
|
|
||||||
{ kind: "Field", name: { kind: "Name", value: "finishedAt" } },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
} as unknown as DocumentNode<CompatSsoLogin_SessionFragment, unknown>;
|
|
||||||
export const CompatSsoLogin_LoginFragmentDoc = {
|
|
||||||
kind: "Document",
|
|
||||||
definitions: [
|
|
||||||
{
|
|
||||||
kind: "FragmentDefinition",
|
|
||||||
name: { kind: "Name", value: "CompatSsoLogin_login" },
|
|
||||||
typeCondition: {
|
typeCondition: {
|
||||||
kind: "NamedType",
|
kind: "NamedType",
|
||||||
name: { kind: "Name", value: "CompatSsoLogin" },
|
name: { kind: "Name", value: "CompatSsoLogin" },
|
||||||
@ -1264,30 +1264,17 @@ export const CompatSsoLogin_LoginFragmentDoc = {
|
|||||||
selections: [
|
selections: [
|
||||||
{ kind: "Field", name: { kind: "Name", value: "id" } },
|
{ kind: "Field", name: { kind: "Name", value: "id" } },
|
||||||
{ kind: "Field", name: { kind: "Name", value: "redirectUri" } },
|
{ kind: "Field", name: { kind: "Name", value: "redirectUri" } },
|
||||||
{ kind: "Field", name: { kind: "Name", value: "createdAt" } },
|
|
||||||
{
|
|
||||||
kind: "Field",
|
|
||||||
name: { kind: "Name", value: "session" },
|
|
||||||
selectionSet: {
|
|
||||||
kind: "SelectionSet",
|
|
||||||
selections: [
|
|
||||||
{ kind: "Field", name: { kind: "Name", value: "id" } },
|
|
||||||
{
|
|
||||||
kind: "FragmentSpread",
|
|
||||||
name: { kind: "Name", value: "CompatSsoLogin_session" },
|
|
||||||
},
|
|
||||||
{ kind: "Field", name: { kind: "Name", value: "createdAt" } },
|
|
||||||
{ kind: "Field", name: { kind: "Name", value: "deviceId" } },
|
|
||||||
{ kind: "Field", name: { kind: "Name", value: "finishedAt" } },
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
} as unknown as DocumentNode<CompatSession_Sso_LoginFragment, unknown>;
|
||||||
},
|
export const CompatSession_SessionFragmentDoc = {
|
||||||
|
kind: "Document",
|
||||||
|
definitions: [
|
||||||
{
|
{
|
||||||
kind: "FragmentDefinition",
|
kind: "FragmentDefinition",
|
||||||
name: { kind: "Name", value: "CompatSsoLogin_session" },
|
name: { kind: "Name", value: "CompatSession_session" },
|
||||||
typeCondition: {
|
typeCondition: {
|
||||||
kind: "NamedType",
|
kind: "NamedType",
|
||||||
name: { kind: "Name", value: "CompatSession" },
|
name: { kind: "Name", value: "CompatSession" },
|
||||||
@ -1299,11 +1286,40 @@ export const CompatSsoLogin_LoginFragmentDoc = {
|
|||||||
{ kind: "Field", name: { kind: "Name", value: "createdAt" } },
|
{ kind: "Field", name: { kind: "Name", value: "createdAt" } },
|
||||||
{ kind: "Field", name: { kind: "Name", value: "deviceId" } },
|
{ kind: "Field", name: { kind: "Name", value: "deviceId" } },
|
||||||
{ kind: "Field", name: { kind: "Name", value: "finishedAt" } },
|
{ kind: "Field", name: { kind: "Name", value: "finishedAt" } },
|
||||||
|
{
|
||||||
|
kind: "Field",
|
||||||
|
name: { kind: "Name", value: "ssoLogin" },
|
||||||
|
selectionSet: {
|
||||||
|
kind: "SelectionSet",
|
||||||
|
selections: [
|
||||||
|
{ kind: "Field", name: { kind: "Name", value: "id" } },
|
||||||
|
{
|
||||||
|
kind: "FragmentSpread",
|
||||||
|
name: { kind: "Name", value: "CompatSession_sso_login" },
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
} as unknown as DocumentNode<CompatSsoLogin_LoginFragment, unknown>;
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: "FragmentDefinition",
|
||||||
|
name: { kind: "Name", value: "CompatSession_sso_login" },
|
||||||
|
typeCondition: {
|
||||||
|
kind: "NamedType",
|
||||||
|
name: { kind: "Name", value: "CompatSsoLogin" },
|
||||||
|
},
|
||||||
|
selectionSet: {
|
||||||
|
kind: "SelectionSet",
|
||||||
|
selections: [
|
||||||
|
{ kind: "Field", name: { kind: "Name", value: "id" } },
|
||||||
|
{ kind: "Field", name: { kind: "Name", value: "redirectUri" } },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
} as unknown as DocumentNode<CompatSession_SessionFragment, unknown>;
|
||||||
export const OAuth2Session_SessionFragmentDoc = {
|
export const OAuth2Session_SessionFragmentDoc = {
|
||||||
kind: "Document",
|
kind: "Document",
|
||||||
definitions: [
|
definitions: [
|
||||||
@ -1937,8 +1953,8 @@ export const EndCompatSessionDocument = {
|
|||||||
selections: [
|
selections: [
|
||||||
{ kind: "Field", name: { kind: "Name", value: "id" } },
|
{ kind: "Field", name: { kind: "Name", value: "id" } },
|
||||||
{
|
{
|
||||||
kind: "FragmentSpread",
|
kind: "Field",
|
||||||
name: { kind: "Name", value: "CompatSsoLogin_session" },
|
name: { kind: "Name", value: "finishedAt" },
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@ -1949,35 +1965,18 @@ export const EndCompatSessionDocument = {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
kind: "FragmentDefinition",
|
|
||||||
name: { kind: "Name", value: "CompatSsoLogin_session" },
|
|
||||||
typeCondition: {
|
|
||||||
kind: "NamedType",
|
|
||||||
name: { kind: "Name", value: "CompatSession" },
|
|
||||||
},
|
|
||||||
selectionSet: {
|
|
||||||
kind: "SelectionSet",
|
|
||||||
selections: [
|
|
||||||
{ kind: "Field", name: { kind: "Name", value: "id" } },
|
|
||||||
{ kind: "Field", name: { kind: "Name", value: "createdAt" } },
|
|
||||||
{ kind: "Field", name: { kind: "Name", value: "deviceId" } },
|
|
||||||
{ kind: "Field", name: { kind: "Name", value: "finishedAt" } },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
} as unknown as DocumentNode<
|
} as unknown as DocumentNode<
|
||||||
EndCompatSessionMutation,
|
EndCompatSessionMutation,
|
||||||
EndCompatSessionMutationVariables
|
EndCompatSessionMutationVariables
|
||||||
>;
|
>;
|
||||||
export const CompatSsoLoginListDocument = {
|
export const CompatSessionListDocument = {
|
||||||
kind: "Document",
|
kind: "Document",
|
||||||
definitions: [
|
definitions: [
|
||||||
{
|
{
|
||||||
kind: "OperationDefinition",
|
kind: "OperationDefinition",
|
||||||
operation: "query",
|
operation: "query",
|
||||||
name: { kind: "Name", value: "CompatSsoLoginList" },
|
name: { kind: "Name", value: "CompatSessionList" },
|
||||||
variableDefinitions: [
|
variableDefinitions: [
|
||||||
{
|
{
|
||||||
kind: "VariableDefinition",
|
kind: "VariableDefinition",
|
||||||
@ -2042,7 +2041,7 @@ export const CompatSsoLoginListDocument = {
|
|||||||
{ kind: "Field", name: { kind: "Name", value: "id" } },
|
{ kind: "Field", name: { kind: "Name", value: "id" } },
|
||||||
{
|
{
|
||||||
kind: "Field",
|
kind: "Field",
|
||||||
name: { kind: "Name", value: "compatSsoLogins" },
|
name: { kind: "Name", value: "compatSessions" },
|
||||||
arguments: [
|
arguments: [
|
||||||
{
|
{
|
||||||
kind: "Argument",
|
kind: "Argument",
|
||||||
@ -2100,7 +2099,7 @@ export const CompatSsoLoginListDocument = {
|
|||||||
kind: "FragmentSpread",
|
kind: "FragmentSpread",
|
||||||
name: {
|
name: {
|
||||||
kind: "Name",
|
kind: "Name",
|
||||||
value: "CompatSsoLogin_login",
|
value: "CompatSession_session",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@ -2145,7 +2144,22 @@ export const CompatSsoLoginListDocument = {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
kind: "FragmentDefinition",
|
kind: "FragmentDefinition",
|
||||||
name: { kind: "Name", value: "CompatSsoLogin_session" },
|
name: { kind: "Name", value: "CompatSession_sso_login" },
|
||||||
|
typeCondition: {
|
||||||
|
kind: "NamedType",
|
||||||
|
name: { kind: "Name", value: "CompatSsoLogin" },
|
||||||
|
},
|
||||||
|
selectionSet: {
|
||||||
|
kind: "SelectionSet",
|
||||||
|
selections: [
|
||||||
|
{ kind: "Field", name: { kind: "Name", value: "id" } },
|
||||||
|
{ kind: "Field", name: { kind: "Name", value: "redirectUri" } },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: "FragmentDefinition",
|
||||||
|
name: { kind: "Name", value: "CompatSession_session" },
|
||||||
typeCondition: {
|
typeCondition: {
|
||||||
kind: "NamedType",
|
kind: "NamedType",
|
||||||
name: { kind: "Name", value: "CompatSession" },
|
name: { kind: "Name", value: "CompatSession" },
|
||||||
@ -2157,36 +2171,17 @@ export const CompatSsoLoginListDocument = {
|
|||||||
{ kind: "Field", name: { kind: "Name", value: "createdAt" } },
|
{ kind: "Field", name: { kind: "Name", value: "createdAt" } },
|
||||||
{ kind: "Field", name: { kind: "Name", value: "deviceId" } },
|
{ kind: "Field", name: { kind: "Name", value: "deviceId" } },
|
||||||
{ kind: "Field", name: { kind: "Name", value: "finishedAt" } },
|
{ kind: "Field", name: { kind: "Name", value: "finishedAt" } },
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
kind: "FragmentDefinition",
|
|
||||||
name: { kind: "Name", value: "CompatSsoLogin_login" },
|
|
||||||
typeCondition: {
|
|
||||||
kind: "NamedType",
|
|
||||||
name: { kind: "Name", value: "CompatSsoLogin" },
|
|
||||||
},
|
|
||||||
selectionSet: {
|
|
||||||
kind: "SelectionSet",
|
|
||||||
selections: [
|
|
||||||
{ kind: "Field", name: { kind: "Name", value: "id" } },
|
|
||||||
{ kind: "Field", name: { kind: "Name", value: "redirectUri" } },
|
|
||||||
{ kind: "Field", name: { kind: "Name", value: "createdAt" } },
|
|
||||||
{
|
{
|
||||||
kind: "Field",
|
kind: "Field",
|
||||||
name: { kind: "Name", value: "session" },
|
name: { kind: "Name", value: "ssoLogin" },
|
||||||
selectionSet: {
|
selectionSet: {
|
||||||
kind: "SelectionSet",
|
kind: "SelectionSet",
|
||||||
selections: [
|
selections: [
|
||||||
{ kind: "Field", name: { kind: "Name", value: "id" } },
|
{ kind: "Field", name: { kind: "Name", value: "id" } },
|
||||||
{
|
{
|
||||||
kind: "FragmentSpread",
|
kind: "FragmentSpread",
|
||||||
name: { kind: "Name", value: "CompatSsoLogin_session" },
|
name: { kind: "Name", value: "CompatSession_sso_login" },
|
||||||
},
|
},
|
||||||
{ kind: "Field", name: { kind: "Name", value: "createdAt" } },
|
|
||||||
{ kind: "Field", name: { kind: "Name", value: "deviceId" } },
|
|
||||||
{ kind: "Field", name: { kind: "Name", value: "finishedAt" } },
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -2195,8 +2190,8 @@ export const CompatSsoLoginListDocument = {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
} as unknown as DocumentNode<
|
} as unknown as DocumentNode<
|
||||||
CompatSsoLoginListQuery,
|
CompatSessionListQuery,
|
||||||
CompatSsoLoginListQueryVariables
|
CompatSessionListQueryVariables
|
||||||
>;
|
>;
|
||||||
export const EndOAuth2SessionDocument = {
|
export const EndOAuth2SessionDocument = {
|
||||||
kind: "Document",
|
kind: "Document",
|
||||||
|
@ -295,6 +295,15 @@ export default {
|
|||||||
},
|
},
|
||||||
args: [],
|
args: [],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "ssoLogin",
|
||||||
|
type: {
|
||||||
|
kind: "OBJECT",
|
||||||
|
name: "CompatSsoLogin",
|
||||||
|
ofType: null,
|
||||||
|
},
|
||||||
|
args: [],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "user",
|
name: "user",
|
||||||
type: {
|
type: {
|
||||||
@ -319,6 +328,91 @@ export default {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
kind: "OBJECT",
|
||||||
|
name: "CompatSessionConnection",
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: "edges",
|
||||||
|
type: {
|
||||||
|
kind: "NON_NULL",
|
||||||
|
ofType: {
|
||||||
|
kind: "LIST",
|
||||||
|
ofType: {
|
||||||
|
kind: "NON_NULL",
|
||||||
|
ofType: {
|
||||||
|
kind: "OBJECT",
|
||||||
|
name: "CompatSessionEdge",
|
||||||
|
ofType: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
args: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "nodes",
|
||||||
|
type: {
|
||||||
|
kind: "NON_NULL",
|
||||||
|
ofType: {
|
||||||
|
kind: "LIST",
|
||||||
|
ofType: {
|
||||||
|
kind: "NON_NULL",
|
||||||
|
ofType: {
|
||||||
|
kind: "OBJECT",
|
||||||
|
name: "CompatSession",
|
||||||
|
ofType: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
args: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "pageInfo",
|
||||||
|
type: {
|
||||||
|
kind: "NON_NULL",
|
||||||
|
ofType: {
|
||||||
|
kind: "OBJECT",
|
||||||
|
name: "PageInfo",
|
||||||
|
ofType: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
args: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
interfaces: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: "OBJECT",
|
||||||
|
name: "CompatSessionEdge",
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: "cursor",
|
||||||
|
type: {
|
||||||
|
kind: "NON_NULL",
|
||||||
|
ofType: {
|
||||||
|
kind: "SCALAR",
|
||||||
|
name: "Any",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
args: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "node",
|
||||||
|
type: {
|
||||||
|
kind: "NON_NULL",
|
||||||
|
ofType: {
|
||||||
|
kind: "OBJECT",
|
||||||
|
name: "CompatSession",
|
||||||
|
ofType: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
args: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
interfaces: [],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
kind: "OBJECT",
|
kind: "OBJECT",
|
||||||
name: "CompatSsoLogin",
|
name: "CompatSsoLogin",
|
||||||
@ -1878,6 +1972,47 @@ export default {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "compatSessions",
|
||||||
|
type: {
|
||||||
|
kind: "NON_NULL",
|
||||||
|
ofType: {
|
||||||
|
kind: "OBJECT",
|
||||||
|
name: "CompatSessionConnection",
|
||||||
|
ofType: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
args: [
|
||||||
|
{
|
||||||
|
name: "after",
|
||||||
|
type: {
|
||||||
|
kind: "SCALAR",
|
||||||
|
name: "Any",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "before",
|
||||||
|
type: {
|
||||||
|
kind: "SCALAR",
|
||||||
|
name: "Any",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "first",
|
||||||
|
type: {
|
||||||
|
kind: "SCALAR",
|
||||||
|
name: "Any",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "last",
|
||||||
|
type: {
|
||||||
|
kind: "SCALAR",
|
||||||
|
name: "Any",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "compatSsoLogins",
|
name: "compatSsoLogins",
|
||||||
type: {
|
type: {
|
||||||
|
@ -37,11 +37,7 @@ const CurrentUserAccount: React.FC = () => {
|
|||||||
const userId = unwrapOk(result);
|
const userId = unwrapOk(result);
|
||||||
if (userId === null) return <NotLoggedIn />;
|
if (userId === null) return <NotLoggedIn />;
|
||||||
|
|
||||||
return (
|
return <UserAccount id={userId} />;
|
||||||
<div className="w-96 mx-auto">
|
|
||||||
<UserAccount id={userId} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default CurrentUserAccount;
|
export default CurrentUserAccount;
|
||||||
|
@ -16,7 +16,7 @@ import { useAtomValue } from "jotai";
|
|||||||
|
|
||||||
import { currentUserIdAtom } from "../atoms";
|
import { currentUserIdAtom } from "../atoms";
|
||||||
import BrowserSessionList from "../components/BrowserSessionList";
|
import BrowserSessionList from "../components/BrowserSessionList";
|
||||||
import CompatSsoLoginList from "../components/CompatSsoLoginList";
|
import CompatSessionList from "../components/CompatSessionList";
|
||||||
import GraphQLError from "../components/GraphQLError";
|
import GraphQLError from "../components/GraphQLError";
|
||||||
import NotLoggedIn from "../components/NotLoggedIn";
|
import NotLoggedIn from "../components/NotLoggedIn";
|
||||||
import OAuth2SessionList from "../components/OAuth2SessionList";
|
import OAuth2SessionList from "../components/OAuth2SessionList";
|
||||||
@ -33,9 +33,9 @@ const Home: React.FC = () => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<UserGreeting userId={currentUserId} />
|
<UserGreeting userId={currentUserId} />
|
||||||
<div className="mt-4 grid gap-1">
|
<div className="mt-4 grid gap-8">
|
||||||
<OAuth2SessionList userId={currentUserId} />
|
<OAuth2SessionList userId={currentUserId} />
|
||||||
<CompatSsoLoginList userId={currentUserId} />
|
<CompatSessionList userId={currentUserId} />
|
||||||
<BrowserSessionList userId={currentUserId} />
|
<BrowserSessionList userId={currentUserId} />
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
@ -19,7 +19,7 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
mode: "jit",
|
mode: "jit",
|
||||||
content: ["./src/**/*.tsx", "./index.html"],
|
content: ["./src/**/*.tsx", "./index.html"],
|
||||||
darkMode: "class",
|
darkMode: ["class", ".cpd-theme-dark"],
|
||||||
theme: {
|
theme: {
|
||||||
colors: {
|
colors: {
|
||||||
white: "#FFFFFF",
|
white: "#FFFFFF",
|
||||||
|
Reference in New Issue
Block a user