1
0
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:
Quentin Gliech
2023-07-06 17:49:50 +02:00
parent 76653f9638
commit ca520dfd9a
25 changed files with 708 additions and 369 deletions

View File

@ -24,7 +24,10 @@ use crate::state::ContextExt;
/// A compat session represents a client session which used the legacy Matrix
/// login API.
#[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)]
impl CompatSession {
@ -61,6 +64,11 @@ impl CompatSession {
pub async fn finished_at(&self) -> Option<DateTime<Utc>> {
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
@ -114,6 +122,6 @@ impl CompatSsoLogin {
.context("Could not load compat session")?;
repo.cancel().await?;
Ok(Some(CompatSession(session)))
Ok(Some(CompatSession(session, Some(self.0.clone()))))
}
}

View File

@ -22,14 +22,17 @@ use mas_storage::{
oauth2::OAuth2SessionRepository,
upstream_oauth2::UpstreamOAuthLinkRepository,
user::{BrowserSessionRepository, UserEmailRepository},
Pagination,
Pagination, RepositoryAccess,
};
use super::{
compat_sessions::CompatSsoLogin, BrowserSession, Cursor, NodeCursor, NodeType, OAuth2Session,
UpstreamOAuth2Link,
};
use crate::{model::matrix::MatrixUser, state::ContextExt};
use crate::{
model::{matrix::MatrixUser, CompatSession},
state::ContextExt,
};
#[derive(Description)]
/// A user is an individual's account.
@ -129,6 +132,58 @@ impl User {
.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
async fn browser_sessions(
&self,

View File

@ -66,7 +66,8 @@ impl EndCompatSessionPayload {
/// Returns the ended session.
async fn compat_session(&self) -> Option<CompatSession> {
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,
}
}

View File

@ -40,7 +40,9 @@ use axum::{
Router,
};
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_keystore::{Encrypter, Keystore};
use mas_policy::PolicyFactory;
@ -268,7 +270,8 @@ where
BoxRng: FromRequestParts<S>,
{
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::AccountWildcard::route(),
@ -351,6 +354,7 @@ where
if let Ok(res) = templates.render_error(ctx).await {
let (mut parts, _original_body) = response.into_parts();
parts.headers.remove(CONTENT_TYPE);
parts.headers.remove(CONTENT_LENGTH);
return Ok((parts, Html(res)).into_response());
}
}

View File

@ -14,14 +14,20 @@
use async_trait::async_trait;
use chrono::{DateTime, Utc};
use mas_data_model::{CompatSession, CompatSessionState, Device, User};
use mas_storage::{compat::CompatSessionRepository, Clock};
use mas_data_model::{
CompatSession, CompatSessionState, CompatSsoLogin, CompatSsoLoginState, Device, User,
};
use mas_storage::{compat::CompatSessionRepository, Clock, Page, Pagination};
use rand::RngCore;
use sqlx::PgConnection;
use sqlx::{PgConnection, QueryBuilder};
use ulid::Ulid;
use url::Url;
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
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]
impl<'c> CompatSessionRepository for PgCompatSessionRepository<'c> {
type Error = DatabaseError;
@ -201,4 +302,53 @@ impl<'c> CompatSessionRepository for PgCompatSessionRepository<'c> {
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)
}
}

View File

@ -13,11 +13,11 @@
// limitations under the License.
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 ulid::Ulid;
use crate::{repository_impl, Clock};
use crate::{repository_impl, Clock, Page, Pagination};
/// A [`CompatSessionRepository`] helps interacting with
/// [`CompatSessionRepository`] saved in the storage backend
@ -80,6 +80,24 @@ pub trait CompatSessionRepository: Send + Sync {
clock: &dyn Clock,
compat_session: CompatSession,
) -> 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:
@ -99,4 +117,10 @@ repository_impl!(CompatSessionRepository:
clock: &dyn Clock,
compat_session: CompatSession,
) -> Result<CompatSession, Self::Error>;
async fn list_paginated(
&mut self,
user: &User,
pagination: Pagination,
) -> Result<Page<(CompatSession, Option<CompatSsoLogin>)>, Self::Error>;
);

View File

@ -22,15 +22,15 @@ limitations under the License.
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>matrix-authentication-service</title>
<script>
window.APP_CONFIG = JSON.parse('{root: "/app/"}');
<script type="application/javascript">
window.APP_CONFIG = JSON.parse('{"root": "/account/"}');
(function () {
const query = window.matchMedia("(prefers-color-scheme: dark)");
function handleChange(list) {
if (list.matches) {
document.documentElement.classList.add("dark");
document.documentElement.classList.add("cpd-theme-dark");
} else {
document.documentElement.classList.remove("dark");
document.documentElement.classList.remove("cpd-theme-dark");
}
}

View File

@ -143,6 +143,39 @@ type CompatSession implements Node & CreationEvent {
When the session ended.
"""
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
): 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
"""
browserSessions(

View File

@ -12,14 +12,13 @@
// See the License for the specific language governing permissions and
// 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 { atomWithMutation } from "jotai-urql";
import { useRef, useTransition } from "react";
import { useTransition } from "react";
import { graphql } from "../gql";
import Input from "./Input";
import Typography from "./Typography";
const ADD_EMAIL_MUTATION = graphql(/* GraphQL */ `
@ -45,7 +44,8 @@ const AddEmailForm: React.FC<{ userId: string; onAdd?: () => void }> = ({
userId,
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 [pending, startTransition] = useTransition();
@ -65,7 +65,7 @@ const AddEmailForm: React.FC<{ userId: string; onAdd?: () => void }> = ({
onAdd?.();
// Reset the form
formRef.current?.reset();
//formRef.current?.reset();
});
});
};
@ -97,18 +97,12 @@ const AddEmailForm: React.FC<{ userId: string; onAdd?: () => void }> = ({
</div>
)}
<form className="flex" onSubmit={handleSubmit} ref={formRef}>
<Input
className="flex-1 mr-2"
disabled={pending}
type="email"
inputMode="email"
name="email"
/>
<Button kind="primary" disabled={pending} type="submit">
Add
</Button>
</form>
<Root className="flex" onSubmit={handleSubmit}>
<Field name="email" className="flex-1 mr-2">
<Control disabled={pending} type="email" inputMode="email" />
</Field>
<Submit disabled={pending}>Add</Submit>
</Root>
</>
);
};

View File

@ -17,7 +17,9 @@ type Props = {
};
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;

View File

@ -92,7 +92,7 @@ const BrowserSession: React.FC<Props> = ({ session, isCurrent }) => {
};
return (
<Block className="my-4 flex items-center">
<Block className="flex items-center">
<IconWebBrowser className="mr-4 session-icon" />
<div className="flex-1">
<Body size="md" weight="medium">

View File

@ -24,27 +24,23 @@ import Block from "./Block";
import DateTime from "./DateTime";
import { Body, Bold, Code } from "./Typography";
const FRAGMENT = graphql(/* GraphQL */ `
fragment CompatSsoLogin_login on CompatSsoLogin {
const LOGIN_FRAGMENT = graphql(/* GraphQL */ `
fragment CompatSession_sso_login on CompatSsoLogin {
id
redirectUri
createdAt
session {
id
...CompatSsoLogin_session
createdAt
deviceId
finishedAt
}
}
`);
const SESSION_FRAGMENT = graphql(/* GraphQL */ `
fragment CompatSsoLogin_session on CompatSession {
const FRAGMENT = graphql(/* GraphQL */ `
fragment CompatSession_session on CompatSession {
id
createdAt
deviceId
finishedAt
ssoLogin {
id
...CompatSession_sso_login
}
}
`);
@ -54,7 +50,7 @@ const END_SESSION_MUTATION = graphql(/* GraphQL */ `
status
compatSession {
id
...CompatSsoLogin_session
finishedAt
}
}
}
@ -72,15 +68,11 @@ const endCompatSessionFamily = atomFamily((id: string) => {
return endCompatSessionAtom;
});
type Props = {
login: FragmentType<typeof FRAGMENT>;
};
const CompatSession: React.FC<{
session: FragmentType<typeof SESSION_FRAGMENT>;
session: FragmentType<typeof FRAGMENT>;
}> = ({ session }) => {
const [pending, startTransition] = useTransition();
const data = useFragment(SESSION_FRAGMENT, session);
const data = useFragment(FRAGMENT, session);
const endCompatSession = useSetAtom(endCompatSessionFamily(data.id));
const onSessionEnd = (): void => {
@ -90,7 +82,7 @@ const CompatSession: React.FC<{
};
return (
<>
<Block className="p-4 bg-grey-25 dark:bg-grey-450 rounded-lg">
<Body>
Started: <DateTime datetime={data.createdAt} />
</Body>
@ -102,6 +94,7 @@ const CompatSession: React.FC<{
<Body>
Device ID: <Code>{data.deviceId}</Code>
</Body>
{data.ssoLogin && <CompatSsoLogin login={data.ssoLogin} />}
{data.finishedAt ? null : (
<Button
className="mt-2"
@ -113,24 +106,22 @@ const CompatSession: React.FC<{
End session
</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>
);
};
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;

View File

@ -28,13 +28,13 @@ import {
import { isErr, isOk, unwrapErr, unwrapOk } from "../result";
import BlockList from "./BlockList";
import CompatSsoLogin from "./CompatSsoLogin";
import CompatSession from "./CompatSession";
import GraphQLError from "./GraphQLError";
import PaginationControls from "./PaginationControls";
import { Title } from "./Typography";
const QUERY = graphql(/* GraphQL */ `
query CompatSsoLoginList(
query CompatSessionList(
$userId: ID!
$first: Int
$after: String
@ -43,7 +43,7 @@ const QUERY = graphql(/* GraphQL */ `
) {
user(id: $userId) {
id
compatSsoLogins(
compatSessions(
first: $first
after: $after
last: $last
@ -52,7 +52,7 @@ const QUERY = graphql(/* GraphQL */ `
edges {
node {
id
...CompatSsoLogin_login
...CompatSession_session
}
}
@ -69,23 +69,23 @@ const QUERY = graphql(/* GraphQL */ `
const currentPaginationAtom = atomForCurrentPagination();
const compatSsoLoginListFamily = atomFamily((userId: string) => {
const compatSsoLoginListQuery = atomWithQuery({
const compatSessionListFamily = atomFamily((userId: string) => {
const compatSessionListQuery = atomWithQuery({
query: QUERY,
getVariables: (get) => ({ userId, ...get(currentPaginationAtom) }),
});
const compatSsoLoginList = mapQueryAtom(
compatSsoLoginListQuery,
(data) => data.user?.compatSsoLogins || null
const compatSessionList = mapQueryAtom(
compatSessionListQuery,
(data) => data.user?.compatSessions || null
);
return compatSsoLoginList;
return compatSessionList;
});
const pageInfoFamily = atomFamily((userId: string) => {
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;
});
@ -100,15 +100,15 @@ const paginationFamily = atomFamily((userId: string) => {
return paginationAtom;
});
const CompatSsoLoginList: React.FC<{ userId: string }> = ({ userId }) => {
const CompatSessionList: React.FC<{ userId: string }> = ({ userId }) => {
const [pending, startTransition] = useTransition();
const result = useAtomValue(compatSsoLoginListFamily(userId));
const result = useAtomValue(compatSessionListFamily(userId));
const setPagination = useSetAtom(currentPaginationAtom);
const [prevPage, nextPage] = useAtomValue(paginationFamily(userId));
if (isErr(result)) return <GraphQLError error={unwrapErr(result)} />;
const compatSsoLoginList = unwrapOk(result);
if (compatSsoLoginList === null)
const compatSessionList = unwrapOk(result);
if (compatSessionList === null)
return <>Failed to load list of compatibility sessions.</>;
const paginate = (pagination: Pagination): void => {
@ -125,11 +125,11 @@ const CompatSsoLoginList: React.FC<{ userId: string }> = ({ userId }) => {
onNext={nextPage ? (): void => paginate(nextPage) : null}
disabled={pending}
/>
{compatSsoLoginList.edges.map((n) => (
<CompatSsoLogin login={n.node} key={n.node.id} />
{compatSessionList.edges.map((n) => (
<CompatSession session={n.node} key={n.node.id} />
))}
</BlockList>
);
};
export default CompatSsoLoginList;
export default CompatSessionList;

View File

@ -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,
},
};

View File

@ -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;

View File

@ -19,8 +19,8 @@ const Layout: React.FC<{ children?: React.ReactNode }> = ({ children }) => {
return (
<>
<NavBar className="nav-bar container">
<NavItem route={{ type: "home" }}>Home</NavItem>
<NavItem route={{ type: "account" }}>My Account</NavItem>
<NavItem route={{ type: "home" }}>Sessions</NavItem>
<NavItem route={{ type: "account" }}>Emails</NavItem>
</NavBar>
<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">
<img
className="inline my-2"
height={32}
width={75}
src="https://matrix.org/images/matrix-logo.svg"
alt="Matrix.org"
/>

View File

@ -100,10 +100,11 @@ const OAuth2Session: React.FC<Props> = ({ session }) => {
return (
<Block
className={
data.finishedAt &&
"opacity-50 group-hover:opacity-100 transition-opacity"
}
className={`p-4 bg-grey-25 dark:bg-grey-450 rounded-lg ${
data.finishedAt
? "opacity-50 group-hover:opacity-100 transition-opacity"
: ""
}`}
>
<Typography variant="body" bold>
<Link

View File

@ -28,7 +28,7 @@ const PaginationControls: React.FC<Props> = ({
disabled,
}) => {
return (
<div className="grid items-center grid-cols-3 gap-2 my-2">
<div className="grid items-center grid-cols-3 gap-2">
<Button
kind="secondary"
size="sm"

View File

@ -12,17 +12,24 @@
// See the License for the specific language governing permissions and
// 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 { atomFamily } from "jotai/utils";
import { atomWithMutation } from "jotai-urql";
import { useRef, useTransition } from "react";
import { useTransition } from "react";
import { FragmentType, graphql, useFragment } from "../gql";
import Block from "./Block";
import DateTime from "./DateTime";
import Input from "./Input";
import Typography from "./Typography";
// 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 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 => {
e.preventDefault();
@ -177,7 +185,7 @@ const UserEmail: React.FC<{
startTransition(() => {
verifyEmail(code).then((result) => {
// Clear the form
formRef.current?.reset();
e.currentTarget?.reset();
if (result.data?.verifyEmail.status === "VERIFIED") {
// Call the onSetPrimary callback if provided
@ -191,7 +199,7 @@ const UserEmail: React.FC<{
const onResendClick = (): void => {
startTransition(() => {
resendVerificationEmail().then(() => {
formRef.current?.code.focus();
// TODO: fieldRef.current?.focus();
});
});
};
@ -220,61 +228,70 @@ const UserEmail: React.FC<{
verifyEmailResult.data?.verifyEmail.status === "INVALID_CODE";
return (
<Block highlight={highlight}>
<Block
highlight={highlight}
className="grid grid-col-1 gap-2 pb-4 border-b-2 border-b-grey-200"
>
{isPrimary && (
<Typography variant="body" bold>
Primary
</Typography>
)}
<div className="flex justify-between items-center">
<Typography variant="caption" bold className="flex-1">
{data.email}
</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 ? (
<Typography variant="micro">
Verified <DateTime datetime={data.confirmedAt} />
</Typography>
) : (
<form
onSubmit={onFormSubmit}
className="mt-2 grid grid-cols-2 gap-2"
ref={formRef}
>
<Input
className="col-span-2"
name="code"
placeholder="Code"
<Form onSubmit={onFormSubmit} className="grid grid-cols-2 gap-2">
<Field name="code" className="col-span-2">
<Label>Code</Label>
<Control
// ref={fieldRef}
placeholder="xxxxxx"
type="text"
inputMode="numeric"
/>
</Field>
{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
</Button>
<Button disabled={pending || emailSent} onClick={onResendClick}>
</Submit>
<Button
size="sm"
kind="secondary"
disabled={pending || emailSent}
onClick={onResendClick}
>
{emailSent ? "Sent!" : "Resend"}
</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>
);

View File

@ -25,14 +25,14 @@ const documents = {
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":
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":
types.CompatSsoLogin_LoginFragmentDoc,
"\n fragment CompatSsoLogin_session on CompatSession {\n id\n createdAt\n deviceId\n finishedAt\n }\n":
types.CompatSsoLogin_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 fragment CompatSession_sso_login on CompatSsoLogin {\n id\n redirectUri\n }\n":
types.CompatSession_Sso_LoginFragmentDoc,
"\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.CompatSession_SessionFragmentDoc,
"\n mutation EndCompatSession($id: ID!) {\n endCompatSession(input: { compatSessionId: $id }) {\n status\n compatSession {\n id\n finishedAt\n }\n }\n }\n":
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":
types.CompatSsoLoginListDocument,
"\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.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":
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":
@ -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.
*/
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"
): 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"];
source: "\n fragment CompatSession_sso_login on CompatSsoLogin {\n id\n redirectUri\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.
*/
export function graphql(
source: "\n fragment CompatSsoLogin_session on CompatSession {\n id\n createdAt\n deviceId\n finishedAt\n }\n"
): typeof documents["\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 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.
*/
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"
): 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"];
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 finishedAt\n }\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(
source: "\n query 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 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 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.
*/

View File

@ -132,10 +132,31 @@ export type CompatSession = CreationEvent &
finishedAt?: Maybe<Scalars["DateTime"]["output"]>;
/** ID of the object. */
id: Scalars["ID"]["output"];
/** The associated SSO login, if any. */
ssoLogin?: Maybe<CompatSsoLogin>;
/** The user authorized for this session. */
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
* API, via the `m.login.sso` login method.
@ -623,6 +644,8 @@ export type User = Node & {
__typename?: "User";
/** Get the list of active browser sessions, chronologically sorted */
browserSessions: BrowserSessionConnection;
/** Get the list of compatibility sessions, chronologically sorted */
compatSessions: CompatSessionConnection;
/** Get the list of compatibility SSO logins, chronologically sorted */
compatSsoLogins: CompatSsoLoginConnection;
/** Get the list of emails, chronologically sorted */
@ -649,6 +672,14 @@ export type UserBrowserSessionsArgs = {
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. */
export type UserCompatSsoLoginsArgs = {
after?: InputMaybe<Scalars["String"]["input"]>;
@ -859,33 +890,26 @@ export type BrowserSessionListQuery = {
} | null;
};
export type CompatSsoLogin_LoginFragment = {
export type CompatSession_Sso_LoginFragment = {
__typename?: "CompatSsoLogin";
id: string;
redirectUri: any;
createdAt: any;
session?:
| ({
} & { " $fragmentName"?: "CompatSession_Sso_LoginFragment" };
export type CompatSession_SessionFragment = {
__typename?: "CompatSession";
id: string;
createdAt: any;
deviceId: string;
finishedAt?: any | null;
} & {
ssoLogin?:
| ({ __typename?: "CompatSsoLogin"; id: string } & {
" $fragmentRefs"?: {
CompatSsoLogin_SessionFragment: CompatSsoLogin_SessionFragment;
CompatSession_Sso_LoginFragment: CompatSession_Sso_LoginFragment;
};
})
| null;
} & { " $fragmentName"?: "CompatSsoLogin_LoginFragment" };
export type CompatSsoLogin_SessionFragment = {
__typename?: "CompatSession";
id: string;
createdAt: any;
deviceId: string;
finishedAt?: any | null;
} & { " $fragmentName"?: "CompatSsoLogin_SessionFragment" };
} & { " $fragmentName"?: "CompatSession_SessionFragment" };
export type EndCompatSessionMutationVariables = Exact<{
id: Scalars["ID"]["input"];
@ -896,17 +920,15 @@ export type EndCompatSessionMutation = {
endCompatSession: {
__typename?: "EndCompatSessionPayload";
status: EndCompatSessionStatus;
compatSession?:
| ({ __typename?: "CompatSession"; id: string } & {
" $fragmentRefs"?: {
CompatSsoLogin_SessionFragment: CompatSsoLogin_SessionFragment;
};
})
| null;
compatSession?: {
__typename?: "CompatSession";
id: string;
finishedAt?: any | null;
} | null;
};
};
export type CompatSsoLoginListQueryVariables = Exact<{
export type CompatSessionListQueryVariables = Exact<{
userId: Scalars["ID"]["input"];
first?: InputMaybe<Scalars["Int"]["input"]>;
after?: InputMaybe<Scalars["String"]["input"]>;
@ -914,18 +936,18 @@ export type CompatSsoLoginListQueryVariables = Exact<{
before?: InputMaybe<Scalars["String"]["input"]>;
}>;
export type CompatSsoLoginListQuery = {
export type CompatSessionListQuery = {
__typename?: "Query";
user?: {
__typename?: "User";
id: string;
compatSsoLogins: {
__typename?: "CompatSsoLoginConnection";
compatSessions: {
__typename?: "CompatSessionConnection";
edges: Array<{
__typename?: "CompatSsoLoginEdge";
node: { __typename?: "CompatSsoLogin"; id: string } & {
__typename?: "CompatSessionEdge";
node: { __typename?: "CompatSession"; id: string } & {
" $fragmentRefs"?: {
CompatSsoLogin_LoginFragment: CompatSsoLogin_LoginFragment;
CompatSession_SessionFragment: CompatSession_SessionFragment;
};
};
}>;
@ -1227,34 +1249,12 @@ export const BrowserSession_SessionFragmentDoc = {
},
],
} as unknown as DocumentNode<BrowserSession_SessionFragment, unknown>;
export const CompatSsoLogin_SessionFragmentDoc = {
export const CompatSession_Sso_LoginFragmentDoc = {
kind: "Document",
definitions: [
{
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<CompatSsoLogin_SessionFragment, unknown>;
export const CompatSsoLogin_LoginFragmentDoc = {
kind: "Document",
definitions: [
{
kind: "FragmentDefinition",
name: { kind: "Name", value: "CompatSsoLogin_login" },
name: { kind: "Name", value: "CompatSession_sso_login" },
typeCondition: {
kind: "NamedType",
name: { kind: "Name", value: "CompatSsoLogin" },
@ -1264,30 +1264,17 @@ export const CompatSsoLogin_LoginFragmentDoc = {
selections: [
{ kind: "Field", name: { kind: "Name", value: "id" } },
{ 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",
name: { kind: "Name", value: "CompatSsoLogin_session" },
name: { kind: "Name", value: "CompatSession_session" },
typeCondition: {
kind: "NamedType",
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: "deviceId" } },
{ 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 = {
kind: "Document",
definitions: [
@ -1937,8 +1953,8 @@ export const EndCompatSessionDocument = {
selections: [
{ kind: "Field", name: { kind: "Name", value: "id" } },
{
kind: "FragmentSpread",
name: { kind: "Name", value: "CompatSsoLogin_session" },
kind: "Field",
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<
EndCompatSessionMutation,
EndCompatSessionMutationVariables
>;
export const CompatSsoLoginListDocument = {
export const CompatSessionListDocument = {
kind: "Document",
definitions: [
{
kind: "OperationDefinition",
operation: "query",
name: { kind: "Name", value: "CompatSsoLoginList" },
name: { kind: "Name", value: "CompatSessionList" },
variableDefinitions: [
{
kind: "VariableDefinition",
@ -2042,7 +2041,7 @@ export const CompatSsoLoginListDocument = {
{ kind: "Field", name: { kind: "Name", value: "id" } },
{
kind: "Field",
name: { kind: "Name", value: "compatSsoLogins" },
name: { kind: "Name", value: "compatSessions" },
arguments: [
{
kind: "Argument",
@ -2100,7 +2099,7 @@ export const CompatSsoLoginListDocument = {
kind: "FragmentSpread",
name: {
kind: "Name",
value: "CompatSsoLogin_login",
value: "CompatSession_session",
},
},
],
@ -2145,7 +2144,22 @@ export const CompatSsoLoginListDocument = {
},
{
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: {
kind: "NamedType",
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: "deviceId" } },
{ 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",
name: { kind: "Name", value: "session" },
name: { kind: "Name", value: "ssoLogin" },
selectionSet: {
kind: "SelectionSet",
selections: [
{ kind: "Field", name: { kind: "Name", value: "id" } },
{
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<
CompatSsoLoginListQuery,
CompatSsoLoginListQueryVariables
CompatSessionListQuery,
CompatSessionListQueryVariables
>;
export const EndOAuth2SessionDocument = {
kind: "Document",

View File

@ -295,6 +295,15 @@ export default {
},
args: [],
},
{
name: "ssoLogin",
type: {
kind: "OBJECT",
name: "CompatSsoLogin",
ofType: null,
},
args: [],
},
{
name: "user",
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",
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",
type: {

View File

@ -37,11 +37,7 @@ const CurrentUserAccount: React.FC = () => {
const userId = unwrapOk(result);
if (userId === null) return <NotLoggedIn />;
return (
<div className="w-96 mx-auto">
<UserAccount id={userId} />
</div>
);
return <UserAccount id={userId} />;
};
export default CurrentUserAccount;

View File

@ -16,7 +16,7 @@ import { useAtomValue } from "jotai";
import { currentUserIdAtom } from "../atoms";
import BrowserSessionList from "../components/BrowserSessionList";
import CompatSsoLoginList from "../components/CompatSsoLoginList";
import CompatSessionList from "../components/CompatSessionList";
import GraphQLError from "../components/GraphQLError";
import NotLoggedIn from "../components/NotLoggedIn";
import OAuth2SessionList from "../components/OAuth2SessionList";
@ -33,9 +33,9 @@ const Home: React.FC = () => {
return (
<>
<UserGreeting userId={currentUserId} />
<div className="mt-4 grid gap-1">
<div className="mt-4 grid gap-8">
<OAuth2SessionList userId={currentUserId} />
<CompatSsoLoginList userId={currentUserId} />
<CompatSessionList userId={currentUserId} />
<BrowserSessionList userId={currentUserId} />
</div>
</>

View File

@ -19,7 +19,7 @@
module.exports = {
mode: "jit",
content: ["./src/**/*.tsx", "./index.html"],
darkMode: "class",
darkMode: ["class", ".cpd-theme-dark"],
theme: {
colors: {
white: "#FFFFFF",