You've already forked authentication-service
mirror of
https://github.com/matrix-org/matrix-authentication-service.git
synced 2025-11-20 12:02:22 +03:00
frontend: split the various lists off the home page
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
// Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
// Copyright 2022, 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.
|
||||
@@ -25,17 +25,23 @@ type Location = {
|
||||
};
|
||||
|
||||
type HomeRoute = { type: "home" };
|
||||
type AccountRoute = { type: "account" };
|
||||
type EmailListRoute = { type: "email-list" };
|
||||
type OAuth2ClientRoute = { type: "client"; id: string };
|
||||
type OAuth2SessionList = { type: "oauth2-session-list" };
|
||||
type BrowserSessionRoute = { type: "session"; id: string };
|
||||
type BrowserSessionListRoute = { type: "browser-session-list" };
|
||||
type CompatSessionListRoute = { type: "compat-session-list" };
|
||||
type VerifyEmailRoute = { type: "verify-email"; id: string };
|
||||
type UnknownRoute = { type: "unknown"; segments: string[] };
|
||||
|
||||
export type Route =
|
||||
| HomeRoute
|
||||
| AccountRoute
|
||||
| EmailListRoute
|
||||
| OAuth2ClientRoute
|
||||
| OAuth2SessionList
|
||||
| BrowserSessionRoute
|
||||
| BrowserSessionListRoute
|
||||
| CompatSessionListRoute
|
||||
| VerifyEmailRoute
|
||||
| UnknownRoute;
|
||||
|
||||
@@ -43,40 +49,84 @@ const routeToSegments = (route: Route): string[] => {
|
||||
switch (route.type) {
|
||||
case "home":
|
||||
return [];
|
||||
case "account":
|
||||
return ["account"];
|
||||
case "client":
|
||||
return ["client", route.id];
|
||||
case "session":
|
||||
return ["session", route.id];
|
||||
case "email-list":
|
||||
return ["emails"];
|
||||
case "verify-email":
|
||||
return ["verify-email", route.id];
|
||||
return ["emails", route.id, "verify"];
|
||||
case "client":
|
||||
return ["clients", route.id];
|
||||
case "browser-session-list":
|
||||
return ["sessions"];
|
||||
case "session":
|
||||
return ["sessions", route.id];
|
||||
case "oauth2-session-list":
|
||||
return ["oauth2-sessions"];
|
||||
case "compat-session-list":
|
||||
return ["compat-sessions"];
|
||||
case "unknown":
|
||||
return route.segments;
|
||||
}
|
||||
};
|
||||
|
||||
const P = Symbol();
|
||||
type PatternItem = string | typeof P;
|
||||
|
||||
// Returns true if the segments match the pattern, where P is a parameter
|
||||
const segmentMatches = (
|
||||
segments: string[],
|
||||
...pattern: PatternItem[]
|
||||
): boolean => {
|
||||
// Quick check to see if the lengths match
|
||||
if (segments.length !== pattern.length) return false;
|
||||
|
||||
// Check each segment
|
||||
for (let i = 0; i < segments.length; i++) {
|
||||
// If the pattern is P, then it's a parameter and we can skip it
|
||||
if (pattern[i] === P) continue;
|
||||
// Otherwise, check that the segment matches the pattern
|
||||
if (segments[i] !== pattern[i]) return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
const segmentsToRoute = (segments: string[]): Route => {
|
||||
const matches = (...pattern: PatternItem[]): boolean =>
|
||||
segmentMatches(segments, ...pattern);
|
||||
|
||||
// Special case for the home page
|
||||
if (segments.length === 0 || (segments.length === 1 && segments[0] === "")) {
|
||||
return { type: "home" };
|
||||
}
|
||||
|
||||
if (segments.length === 1 && segments[0] === "account") {
|
||||
return { type: "account" };
|
||||
if (matches("emails")) {
|
||||
return { type: "email-list" };
|
||||
}
|
||||
|
||||
if (segments.length === 2 && segments[0] === "client") {
|
||||
if (matches("sessions")) {
|
||||
return { type: "browser-session-list" };
|
||||
}
|
||||
|
||||
if (matches("oauth2-sessions")) {
|
||||
return { type: "oauth2-session-list" };
|
||||
}
|
||||
|
||||
if (matches("compat-sessions")) {
|
||||
return { type: "compat-session-list" };
|
||||
}
|
||||
|
||||
if (matches("emails", P, "verify")) {
|
||||
return { type: "verify-email", id: segments[1] };
|
||||
}
|
||||
|
||||
if (matches("clients", P)) {
|
||||
return { type: "client", id: segments[1] };
|
||||
}
|
||||
|
||||
if (segments.length === 2 && segments[0] === "session") {
|
||||
if (matches("sessions", P)) {
|
||||
return { type: "session", id: segments[1] };
|
||||
}
|
||||
|
||||
if (segments.length === 2 && segments[0] === "verify-email") {
|
||||
return { type: "verify-email", id: segments[1] };
|
||||
}
|
||||
|
||||
return { type: "unknown", segments };
|
||||
};
|
||||
|
||||
@@ -117,9 +167,12 @@ export const routeAtom = atom(
|
||||
);
|
||||
|
||||
const Home = lazy(() => import("./pages/Home"));
|
||||
const Account = lazy(() => import("./pages/Account"));
|
||||
const EmailList = lazy(() => import("./pages/EmailList"));
|
||||
const OAuth2Client = lazy(() => import("./pages/OAuth2Client"));
|
||||
const BrowserSession = lazy(() => import("./pages/BrowserSession"));
|
||||
const BrowserSessionList = lazy(() => import("./pages/BrowserSessionList"));
|
||||
const CompatSessionList = lazy(() => import("./pages/CompatSessionList"));
|
||||
const OAuth2SessionList = lazy(() => import("./pages/OAuth2SessionList"));
|
||||
const VerifyEmail = lazy(() => import("./pages/VerifyEmail"));
|
||||
|
||||
const InnerRouter: React.FC = () => {
|
||||
@@ -128,8 +181,14 @@ const InnerRouter: React.FC = () => {
|
||||
switch (route.type) {
|
||||
case "home":
|
||||
return <Home />;
|
||||
case "account":
|
||||
return <Account />;
|
||||
case "email-list":
|
||||
return <EmailList />;
|
||||
case "oauth2-session-list":
|
||||
return <OAuth2SessionList />;
|
||||
case "browser-session-list":
|
||||
return <BrowserSessionList />;
|
||||
case "compat-session-list":
|
||||
return <CompatSessionList />;
|
||||
case "client":
|
||||
return <OAuth2Client id={route.id} />;
|
||||
case "session":
|
||||
@@ -158,7 +217,6 @@ const shouldHandleClick = (e: React.MouseEvent): boolean =>
|
||||
export const Link: React.FC<
|
||||
{
|
||||
route: Route;
|
||||
children: React.ReactNode;
|
||||
} & React.HTMLProps<HTMLAnchorElement>
|
||||
> = ({ route, children, ...props }) => {
|
||||
const config = useAtomValue(appConfigAtom);
|
||||
|
||||
@@ -42,8 +42,8 @@ const Layout: React.FC<{ children?: React.ReactNode }> = ({ children }) => {
|
||||
<UserGreeting userId={userId} />
|
||||
|
||||
<NavBar>
|
||||
<NavItem route={{ type: "home" }}>Sessions</NavItem>
|
||||
<NavItem route={{ type: "account" }}>Emails</NavItem>
|
||||
<NavItem route={{ type: "home" }}>Home</NavItem>
|
||||
<NavItem route={{ type: "email-list" }}>Emails</NavItem>
|
||||
</NavBar>
|
||||
|
||||
<main>{children}</main>
|
||||
|
||||
@@ -30,7 +30,7 @@ const meta = {
|
||||
<WithHomePage>
|
||||
<NavBar {...props}>
|
||||
<NavItem route={{ type: "home" }}>Home</NavItem>
|
||||
<NavItem route={{ type: "account" }}>Account</NavItem>
|
||||
<NavItem route={{ type: "email-list" }}>Emails</NavItem>
|
||||
<ExternalLink href="https://example.com">External</ExternalLink>
|
||||
</NavBar>
|
||||
</WithHomePage>
|
||||
|
||||
@@ -53,7 +53,7 @@ export const Active: Story = {
|
||||
|
||||
export const Inactive: Story = {
|
||||
args: {
|
||||
route: { type: "account" },
|
||||
children: "Account",
|
||||
route: { type: "email-list" },
|
||||
children: "Emails",
|
||||
},
|
||||
};
|
||||
|
||||
@@ -77,7 +77,7 @@ describe("NavItem", () => {
|
||||
it("renders a different route", () => {
|
||||
const component = create(
|
||||
<WithLocation path="/">
|
||||
<NavItem route={{ type: "account" }}>Account</NavItem>
|
||||
<NavItem route={{ type: "email-list" }}>Emails</NavItem>
|
||||
</WithLocation>,
|
||||
);
|
||||
const tree = component.toJSON();
|
||||
|
||||
@@ -8,7 +8,7 @@ exports[`NavItem > render an active <NavItem /> 1`] = `
|
||||
<a
|
||||
aria-current="page"
|
||||
className="_navItem_8603fc"
|
||||
href=""
|
||||
href="/"
|
||||
onClick={[Function]}
|
||||
>
|
||||
Active
|
||||
@@ -22,7 +22,7 @@ exports[`NavItem > render an inactive <NavItem /> 1`] = `
|
||||
>
|
||||
<a
|
||||
className="_navItem_8603fc"
|
||||
href=""
|
||||
href="/"
|
||||
onClick={[Function]}
|
||||
>
|
||||
Inactive
|
||||
@@ -36,10 +36,10 @@ exports[`NavItem > renders a different route 1`] = `
|
||||
>
|
||||
<a
|
||||
className="_navItem_8603fc"
|
||||
href="account"
|
||||
href="/emails"
|
||||
onClick={[Function]}
|
||||
>
|
||||
Account
|
||||
Emails
|
||||
</a>
|
||||
</li>
|
||||
`;
|
||||
|
||||
101
frontend/src/components/UserHome/UserHome.tsx
Normal file
101
frontend/src/components/UserHome/UserHome.tsx
Normal file
@@ -0,0 +1,101 @@
|
||||
// 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 { Alert, Body } from "@vector-im/compound-web";
|
||||
import { useState } from "react";
|
||||
|
||||
import { Link } from "../../Router";
|
||||
import { FragmentType, graphql, useFragment } from "../../gql";
|
||||
import UserEmail from "../UserEmail";
|
||||
|
||||
const FRAGMENT = graphql(/* GraphQL */ `
|
||||
fragment UserHome_user on User {
|
||||
id
|
||||
|
||||
primaryEmail {
|
||||
id
|
||||
...UserEmail_email
|
||||
}
|
||||
|
||||
confirmedEmails: emails(first: 0, state: CONFIRMED) {
|
||||
totalCount
|
||||
}
|
||||
|
||||
unverifiedEmails: emails(first: 0, state: PENDING) {
|
||||
totalCount
|
||||
}
|
||||
|
||||
browserSessions(first: 0, state: ACTIVE) {
|
||||
totalCount
|
||||
}
|
||||
|
||||
oauth2Sessions(first: 0, state: ACTIVE) {
|
||||
totalCount
|
||||
}
|
||||
|
||||
compatSessions(first: 0, state: ACTIVE) {
|
||||
totalCount
|
||||
}
|
||||
}
|
||||
`);
|
||||
|
||||
const UserHome: React.FC<{
|
||||
user: FragmentType<typeof FRAGMENT>;
|
||||
}> = ({ user }) => {
|
||||
const data = useFragment(FRAGMENT, user);
|
||||
const [dismiss, setDismiss] = useState(false);
|
||||
|
||||
const doDismiss = (): void => {
|
||||
setDismiss(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{data.unverifiedEmails.totalCount > 0 && !dismiss && (
|
||||
<Alert type="critical" title="Unverified email" onClose={doDismiss}>
|
||||
You have {data.unverifiedEmails.totalCount} unverified email
|
||||
address(es). <Link route={{ type: "email-list" }}>Check</Link>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{data.primaryEmail ? (
|
||||
<UserEmail email={data.primaryEmail} isPrimary />
|
||||
) : (
|
||||
<Alert type="critical" title="No primary email adress" />
|
||||
)}
|
||||
|
||||
{data.confirmedEmails.totalCount > 1 && (
|
||||
<Body>
|
||||
{data.confirmedEmails.totalCount} additional emails.{" "}
|
||||
<Link route={{ type: "email-list" }}>View all</Link>
|
||||
</Body>
|
||||
)}
|
||||
|
||||
<Body>
|
||||
{data.browserSessions.totalCount} active browser session(s).{" "}
|
||||
<Link route={{ type: "browser-session-list" }}>View all</Link>
|
||||
</Body>
|
||||
<Body>
|
||||
{data.oauth2Sessions.totalCount} active OAuth2 session(s).{" "}
|
||||
<Link route={{ type: "oauth2-session-list" }}>View all</Link>
|
||||
</Body>
|
||||
<Body>
|
||||
{data.compatSessions.totalCount} active compatibility layer session(s).{" "}
|
||||
<Link route={{ type: "compat-session-list" }}>View all</Link>
|
||||
</Body>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default UserHome;
|
||||
15
frontend/src/components/UserHome/index.ts
Normal file
15
frontend/src/components/UserHome/index.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
// 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.
|
||||
|
||||
export { default } from "./UserHome";
|
||||
@@ -127,7 +127,7 @@ const VerifyEmail: React.FC<{
|
||||
e.currentTarget?.reset();
|
||||
|
||||
if (result.data?.verifyEmail.status === "VERIFIED") {
|
||||
setRoute({ type: "account" });
|
||||
setRoute({ type: "email-list" });
|
||||
} else {
|
||||
fieldRef.current?.focus();
|
||||
fieldRef.current?.select();
|
||||
|
||||
@@ -51,6 +51,8 @@ const documents = {
|
||||
types.UserPrimaryEmailDocument,
|
||||
"\n query UserGreeting($userId: ID!) {\n user(id: $userId) {\n id\n username\n matrix {\n mxid\n displayName\n }\n }\n }\n":
|
||||
types.UserGreetingDocument,
|
||||
"\n fragment UserHome_user on User {\n id\n\n primaryEmail {\n id\n ...UserEmail_email\n }\n\n confirmedEmails: emails(first: 0, state: CONFIRMED) {\n totalCount\n }\n\n unverifiedEmails: emails(first: 0, state: PENDING) {\n totalCount\n }\n\n browserSessions(first: 0, state: ACTIVE) {\n totalCount\n }\n\n oauth2Sessions(first: 0, state: ACTIVE) {\n totalCount\n }\n\n compatSessions(first: 0, state: ACTIVE) {\n totalCount\n }\n }\n":
|
||||
types.UserHome_UserFragmentDoc,
|
||||
"\n fragment UserEmail_verifyEmail on UserEmail {\n id\n email\n }\n":
|
||||
types.UserEmail_VerifyEmailFragmentDoc,
|
||||
"\n mutation VerifyEmail($id: ID!, $code: String!) {\n verifyEmail(input: { userEmailId: $id, code: $code }) {\n status\n\n user {\n id\n primaryEmail {\n id\n }\n }\n\n email {\n id\n ...UserEmail_email\n }\n }\n }\n":
|
||||
@@ -59,6 +61,8 @@ const documents = {
|
||||
types.ResendVerificationEmailDocument,
|
||||
"\n query BrowserSessionQuery($id: ID!) {\n browserSession(id: $id) {\n id\n createdAt\n lastAuthentication {\n id\n createdAt\n }\n user {\n id\n username\n }\n }\n }\n":
|
||||
types.BrowserSessionQueryDocument,
|
||||
"\n query HomeQuery {\n viewer {\n __typename\n\n ... on User {\n id\n ...UserHome_user\n }\n }\n }\n":
|
||||
types.HomeQueryDocument,
|
||||
"\n query OAuth2ClientQuery($id: ID!) {\n oauth2Client(id: $id) {\n id\n clientId\n clientName\n clientUri\n tosUri\n policyUri\n redirectUris\n }\n }\n":
|
||||
types.OAuth2ClientQueryDocument,
|
||||
"\n query VerifyEmailQuery($id: ID!) {\n userEmail(id: $id) {\n ...UserEmail_verifyEmail\n }\n }\n":
|
||||
@@ -193,6 +197,12 @@ export function graphql(
|
||||
export function graphql(
|
||||
source: "\n query UserGreeting($userId: ID!) {\n user(id: $userId) {\n id\n username\n matrix {\n mxid\n displayName\n }\n }\n }\n",
|
||||
): (typeof documents)["\n query UserGreeting($userId: ID!) {\n user(id: $userId) {\n id\n username\n matrix {\n mxid\n displayName\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 fragment UserHome_user on User {\n id\n\n primaryEmail {\n id\n ...UserEmail_email\n }\n\n confirmedEmails: emails(first: 0, state: CONFIRMED) {\n totalCount\n }\n\n unverifiedEmails: emails(first: 0, state: PENDING) {\n totalCount\n }\n\n browserSessions(first: 0, state: ACTIVE) {\n totalCount\n }\n\n oauth2Sessions(first: 0, state: ACTIVE) {\n totalCount\n }\n\n compatSessions(first: 0, state: ACTIVE) {\n totalCount\n }\n }\n",
|
||||
): (typeof documents)["\n fragment UserHome_user on User {\n id\n\n primaryEmail {\n id\n ...UserEmail_email\n }\n\n confirmedEmails: emails(first: 0, state: CONFIRMED) {\n totalCount\n }\n\n unverifiedEmails: emails(first: 0, state: PENDING) {\n totalCount\n }\n\n browserSessions(first: 0, state: ACTIVE) {\n totalCount\n }\n\n oauth2Sessions(first: 0, state: ACTIVE) {\n totalCount\n }\n\n compatSessions(first: 0, state: ACTIVE) {\n totalCount\n }\n }\n"];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
@@ -217,6 +227,12 @@ export function graphql(
|
||||
export function graphql(
|
||||
source: "\n query BrowserSessionQuery($id: ID!) {\n browserSession(id: $id) {\n id\n createdAt\n lastAuthentication {\n id\n createdAt\n }\n user {\n id\n username\n }\n }\n }\n",
|
||||
): (typeof documents)["\n query BrowserSessionQuery($id: ID!) {\n browserSession(id: $id) {\n id\n createdAt\n lastAuthentication {\n id\n createdAt\n }\n user {\n id\n username\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 HomeQuery {\n viewer {\n __typename\n\n ... on User {\n id\n ...UserHome_user\n }\n }\n }\n",
|
||||
): (typeof documents)["\n query HomeQuery {\n viewer {\n __typename\n\n ... on User {\n id\n ...UserHome_user\n }\n }\n }\n"];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
|
||||
@@ -1231,6 +1231,30 @@ export type UserGreetingQuery = {
|
||||
} | null;
|
||||
};
|
||||
|
||||
export type UserHome_UserFragment = {
|
||||
__typename?: "User";
|
||||
id: string;
|
||||
primaryEmail?:
|
||||
| ({ __typename?: "UserEmail"; id: string } & {
|
||||
" $fragmentRefs"?: { UserEmail_EmailFragment: UserEmail_EmailFragment };
|
||||
})
|
||||
| null;
|
||||
confirmedEmails: { __typename?: "UserEmailConnection"; totalCount: number };
|
||||
unverifiedEmails: { __typename?: "UserEmailConnection"; totalCount: number };
|
||||
browserSessions: {
|
||||
__typename?: "BrowserSessionConnection";
|
||||
totalCount: number;
|
||||
};
|
||||
oauth2Sessions: {
|
||||
__typename?: "Oauth2SessionConnection";
|
||||
totalCount: number;
|
||||
};
|
||||
compatSessions: {
|
||||
__typename?: "CompatSessionConnection";
|
||||
totalCount: number;
|
||||
};
|
||||
} & { " $fragmentName"?: "UserHome_UserFragment" };
|
||||
|
||||
export type UserEmail_VerifyEmailFragment = {
|
||||
__typename?: "UserEmail";
|
||||
id: string;
|
||||
@@ -1301,6 +1325,17 @@ export type BrowserSessionQueryQuery = {
|
||||
} | null;
|
||||
};
|
||||
|
||||
export type HomeQueryQueryVariables = Exact<{ [key: string]: never }>;
|
||||
|
||||
export type HomeQueryQuery = {
|
||||
__typename?: "Query";
|
||||
viewer:
|
||||
| { __typename: "Anonymous" }
|
||||
| ({ __typename: "User"; id: string } & {
|
||||
" $fragmentRefs"?: { UserHome_UserFragment: UserHome_UserFragment };
|
||||
});
|
||||
};
|
||||
|
||||
export type OAuth2ClientQueryQueryVariables = Exact<{
|
||||
id: Scalars["ID"]["input"];
|
||||
}>;
|
||||
@@ -1492,6 +1527,167 @@ export const UserEmail_EmailFragmentDoc = {
|
||||
},
|
||||
],
|
||||
} as unknown as DocumentNode<UserEmail_EmailFragment, unknown>;
|
||||
export const UserHome_UserFragmentDoc = {
|
||||
kind: "Document",
|
||||
definitions: [
|
||||
{
|
||||
kind: "FragmentDefinition",
|
||||
name: { kind: "Name", value: "UserHome_user" },
|
||||
typeCondition: {
|
||||
kind: "NamedType",
|
||||
name: { kind: "Name", value: "User" },
|
||||
},
|
||||
selectionSet: {
|
||||
kind: "SelectionSet",
|
||||
selections: [
|
||||
{ kind: "Field", name: { kind: "Name", value: "id" } },
|
||||
{
|
||||
kind: "Field",
|
||||
name: { kind: "Name", value: "primaryEmail" },
|
||||
selectionSet: {
|
||||
kind: "SelectionSet",
|
||||
selections: [
|
||||
{ kind: "Field", name: { kind: "Name", value: "id" } },
|
||||
{
|
||||
kind: "FragmentSpread",
|
||||
name: { kind: "Name", value: "UserEmail_email" },
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: "Field",
|
||||
alias: { kind: "Name", value: "confirmedEmails" },
|
||||
name: { kind: "Name", value: "emails" },
|
||||
arguments: [
|
||||
{
|
||||
kind: "Argument",
|
||||
name: { kind: "Name", value: "first" },
|
||||
value: { kind: "IntValue", value: "0" },
|
||||
},
|
||||
{
|
||||
kind: "Argument",
|
||||
name: { kind: "Name", value: "state" },
|
||||
value: { kind: "EnumValue", value: "CONFIRMED" },
|
||||
},
|
||||
],
|
||||
selectionSet: {
|
||||
kind: "SelectionSet",
|
||||
selections: [
|
||||
{ kind: "Field", name: { kind: "Name", value: "totalCount" } },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: "Field",
|
||||
alias: { kind: "Name", value: "unverifiedEmails" },
|
||||
name: { kind: "Name", value: "emails" },
|
||||
arguments: [
|
||||
{
|
||||
kind: "Argument",
|
||||
name: { kind: "Name", value: "first" },
|
||||
value: { kind: "IntValue", value: "0" },
|
||||
},
|
||||
{
|
||||
kind: "Argument",
|
||||
name: { kind: "Name", value: "state" },
|
||||
value: { kind: "EnumValue", value: "PENDING" },
|
||||
},
|
||||
],
|
||||
selectionSet: {
|
||||
kind: "SelectionSet",
|
||||
selections: [
|
||||
{ kind: "Field", name: { kind: "Name", value: "totalCount" } },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: "Field",
|
||||
name: { kind: "Name", value: "browserSessions" },
|
||||
arguments: [
|
||||
{
|
||||
kind: "Argument",
|
||||
name: { kind: "Name", value: "first" },
|
||||
value: { kind: "IntValue", value: "0" },
|
||||
},
|
||||
{
|
||||
kind: "Argument",
|
||||
name: { kind: "Name", value: "state" },
|
||||
value: { kind: "EnumValue", value: "ACTIVE" },
|
||||
},
|
||||
],
|
||||
selectionSet: {
|
||||
kind: "SelectionSet",
|
||||
selections: [
|
||||
{ kind: "Field", name: { kind: "Name", value: "totalCount" } },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: "Field",
|
||||
name: { kind: "Name", value: "oauth2Sessions" },
|
||||
arguments: [
|
||||
{
|
||||
kind: "Argument",
|
||||
name: { kind: "Name", value: "first" },
|
||||
value: { kind: "IntValue", value: "0" },
|
||||
},
|
||||
{
|
||||
kind: "Argument",
|
||||
name: { kind: "Name", value: "state" },
|
||||
value: { kind: "EnumValue", value: "ACTIVE" },
|
||||
},
|
||||
],
|
||||
selectionSet: {
|
||||
kind: "SelectionSet",
|
||||
selections: [
|
||||
{ kind: "Field", name: { kind: "Name", value: "totalCount" } },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: "Field",
|
||||
name: { kind: "Name", value: "compatSessions" },
|
||||
arguments: [
|
||||
{
|
||||
kind: "Argument",
|
||||
name: { kind: "Name", value: "first" },
|
||||
value: { kind: "IntValue", value: "0" },
|
||||
},
|
||||
{
|
||||
kind: "Argument",
|
||||
name: { kind: "Name", value: "state" },
|
||||
value: { kind: "EnumValue", value: "ACTIVE" },
|
||||
},
|
||||
],
|
||||
selectionSet: {
|
||||
kind: "SelectionSet",
|
||||
selections: [
|
||||
{ kind: "Field", name: { kind: "Name", value: "totalCount" } },
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: "FragmentDefinition",
|
||||
name: { kind: "Name", value: "UserEmail_email" },
|
||||
typeCondition: {
|
||||
kind: "NamedType",
|
||||
name: { kind: "Name", value: "UserEmail" },
|
||||
},
|
||||
selectionSet: {
|
||||
kind: "SelectionSet",
|
||||
selections: [
|
||||
{ kind: "Field", name: { kind: "Name", value: "id" } },
|
||||
{ kind: "Field", name: { kind: "Name", value: "email" } },
|
||||
{ kind: "Field", name: { kind: "Name", value: "confirmedAt" } },
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
} as unknown as DocumentNode<UserHome_UserFragment, unknown>;
|
||||
export const UserEmail_VerifyEmailFragmentDoc = {
|
||||
kind: "Document",
|
||||
definitions: [
|
||||
@@ -3490,6 +3686,204 @@ export const BrowserSessionQueryDocument = {
|
||||
BrowserSessionQueryQuery,
|
||||
BrowserSessionQueryQueryVariables
|
||||
>;
|
||||
export const HomeQueryDocument = {
|
||||
kind: "Document",
|
||||
definitions: [
|
||||
{
|
||||
kind: "OperationDefinition",
|
||||
operation: "query",
|
||||
name: { kind: "Name", value: "HomeQuery" },
|
||||
selectionSet: {
|
||||
kind: "SelectionSet",
|
||||
selections: [
|
||||
{
|
||||
kind: "Field",
|
||||
name: { kind: "Name", value: "viewer" },
|
||||
selectionSet: {
|
||||
kind: "SelectionSet",
|
||||
selections: [
|
||||
{ kind: "Field", name: { kind: "Name", value: "__typename" } },
|
||||
{
|
||||
kind: "InlineFragment",
|
||||
typeCondition: {
|
||||
kind: "NamedType",
|
||||
name: { kind: "Name", value: "User" },
|
||||
},
|
||||
selectionSet: {
|
||||
kind: "SelectionSet",
|
||||
selections: [
|
||||
{ kind: "Field", name: { kind: "Name", value: "id" } },
|
||||
{
|
||||
kind: "FragmentSpread",
|
||||
name: { kind: "Name", value: "UserHome_user" },
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: "FragmentDefinition",
|
||||
name: { kind: "Name", value: "UserEmail_email" },
|
||||
typeCondition: {
|
||||
kind: "NamedType",
|
||||
name: { kind: "Name", value: "UserEmail" },
|
||||
},
|
||||
selectionSet: {
|
||||
kind: "SelectionSet",
|
||||
selections: [
|
||||
{ kind: "Field", name: { kind: "Name", value: "id" } },
|
||||
{ kind: "Field", name: { kind: "Name", value: "email" } },
|
||||
{ kind: "Field", name: { kind: "Name", value: "confirmedAt" } },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: "FragmentDefinition",
|
||||
name: { kind: "Name", value: "UserHome_user" },
|
||||
typeCondition: {
|
||||
kind: "NamedType",
|
||||
name: { kind: "Name", value: "User" },
|
||||
},
|
||||
selectionSet: {
|
||||
kind: "SelectionSet",
|
||||
selections: [
|
||||
{ kind: "Field", name: { kind: "Name", value: "id" } },
|
||||
{
|
||||
kind: "Field",
|
||||
name: { kind: "Name", value: "primaryEmail" },
|
||||
selectionSet: {
|
||||
kind: "SelectionSet",
|
||||
selections: [
|
||||
{ kind: "Field", name: { kind: "Name", value: "id" } },
|
||||
{
|
||||
kind: "FragmentSpread",
|
||||
name: { kind: "Name", value: "UserEmail_email" },
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: "Field",
|
||||
alias: { kind: "Name", value: "confirmedEmails" },
|
||||
name: { kind: "Name", value: "emails" },
|
||||
arguments: [
|
||||
{
|
||||
kind: "Argument",
|
||||
name: { kind: "Name", value: "first" },
|
||||
value: { kind: "IntValue", value: "0" },
|
||||
},
|
||||
{
|
||||
kind: "Argument",
|
||||
name: { kind: "Name", value: "state" },
|
||||
value: { kind: "EnumValue", value: "CONFIRMED" },
|
||||
},
|
||||
],
|
||||
selectionSet: {
|
||||
kind: "SelectionSet",
|
||||
selections: [
|
||||
{ kind: "Field", name: { kind: "Name", value: "totalCount" } },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: "Field",
|
||||
alias: { kind: "Name", value: "unverifiedEmails" },
|
||||
name: { kind: "Name", value: "emails" },
|
||||
arguments: [
|
||||
{
|
||||
kind: "Argument",
|
||||
name: { kind: "Name", value: "first" },
|
||||
value: { kind: "IntValue", value: "0" },
|
||||
},
|
||||
{
|
||||
kind: "Argument",
|
||||
name: { kind: "Name", value: "state" },
|
||||
value: { kind: "EnumValue", value: "PENDING" },
|
||||
},
|
||||
],
|
||||
selectionSet: {
|
||||
kind: "SelectionSet",
|
||||
selections: [
|
||||
{ kind: "Field", name: { kind: "Name", value: "totalCount" } },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: "Field",
|
||||
name: { kind: "Name", value: "browserSessions" },
|
||||
arguments: [
|
||||
{
|
||||
kind: "Argument",
|
||||
name: { kind: "Name", value: "first" },
|
||||
value: { kind: "IntValue", value: "0" },
|
||||
},
|
||||
{
|
||||
kind: "Argument",
|
||||
name: { kind: "Name", value: "state" },
|
||||
value: { kind: "EnumValue", value: "ACTIVE" },
|
||||
},
|
||||
],
|
||||
selectionSet: {
|
||||
kind: "SelectionSet",
|
||||
selections: [
|
||||
{ kind: "Field", name: { kind: "Name", value: "totalCount" } },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: "Field",
|
||||
name: { kind: "Name", value: "oauth2Sessions" },
|
||||
arguments: [
|
||||
{
|
||||
kind: "Argument",
|
||||
name: { kind: "Name", value: "first" },
|
||||
value: { kind: "IntValue", value: "0" },
|
||||
},
|
||||
{
|
||||
kind: "Argument",
|
||||
name: { kind: "Name", value: "state" },
|
||||
value: { kind: "EnumValue", value: "ACTIVE" },
|
||||
},
|
||||
],
|
||||
selectionSet: {
|
||||
kind: "SelectionSet",
|
||||
selections: [
|
||||
{ kind: "Field", name: { kind: "Name", value: "totalCount" } },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: "Field",
|
||||
name: { kind: "Name", value: "compatSessions" },
|
||||
arguments: [
|
||||
{
|
||||
kind: "Argument",
|
||||
name: { kind: "Name", value: "first" },
|
||||
value: { kind: "IntValue", value: "0" },
|
||||
},
|
||||
{
|
||||
kind: "Argument",
|
||||
name: { kind: "Name", value: "state" },
|
||||
value: { kind: "EnumValue", value: "ACTIVE" },
|
||||
},
|
||||
],
|
||||
selectionSet: {
|
||||
kind: "SelectionSet",
|
||||
selections: [
|
||||
{ kind: "Field", name: { kind: "Name", value: "totalCount" } },
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
} as unknown as DocumentNode<HomeQueryQuery, HomeQueryQueryVariables>;
|
||||
export const OAuth2ClientQueryDocument = {
|
||||
kind: "Document",
|
||||
definitions: [
|
||||
|
||||
33
frontend/src/pages/BrowserSessionList.tsx
Normal file
33
frontend/src/pages/BrowserSessionList.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
// 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 { useAtomValue } from "jotai";
|
||||
|
||||
import { currentUserIdAtom } from "../atoms";
|
||||
import List from "../components/BrowserSessionList";
|
||||
import GraphQLError from "../components/GraphQLError";
|
||||
import NotLoggedIn from "../components/NotLoggedIn";
|
||||
import { isErr, unwrapErr, unwrapOk } from "../result";
|
||||
|
||||
const BrowserSessionList: React.FC = () => {
|
||||
const result = useAtomValue(currentUserIdAtom);
|
||||
if (isErr(result)) return <GraphQLError error={unwrapErr(result)} />;
|
||||
|
||||
const userId = unwrapOk(result);
|
||||
if (userId === null) return <NotLoggedIn />;
|
||||
|
||||
return <List userId={userId} />;
|
||||
};
|
||||
|
||||
export default BrowserSessionList;
|
||||
33
frontend/src/pages/CompatSessionList.tsx
Normal file
33
frontend/src/pages/CompatSessionList.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
// 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 { useAtomValue } from "jotai";
|
||||
|
||||
import { currentUserIdAtom } from "../atoms";
|
||||
import List from "../components/CompatSessionList";
|
||||
import GraphQLError from "../components/GraphQLError";
|
||||
import NotLoggedIn from "../components/NotLoggedIn";
|
||||
import { isErr, unwrapErr, unwrapOk } from "../result";
|
||||
|
||||
const CompatSessionList: React.FC = () => {
|
||||
const result = useAtomValue(currentUserIdAtom);
|
||||
if (isErr(result)) return <GraphQLError error={unwrapErr(result)} />;
|
||||
|
||||
const userId = unwrapOk(result);
|
||||
if (userId === null) return <NotLoggedIn />;
|
||||
|
||||
return <List userId={userId} />;
|
||||
};
|
||||
|
||||
export default CompatSessionList;
|
||||
@@ -20,18 +20,14 @@ import NotLoggedIn from "../components/NotLoggedIn";
|
||||
import UserEmailList from "../components/UserEmailList";
|
||||
import { isErr, unwrapErr, unwrapOk } from "../result";
|
||||
|
||||
const UserAccount: React.FC<{ id: string }> = ({ id }) => {
|
||||
return <UserEmailList userId={id} />;
|
||||
};
|
||||
|
||||
const CurrentUserAccount: React.FC = () => {
|
||||
const EmailList: React.FC = () => {
|
||||
const result = useAtomValue(currentUserIdAtom);
|
||||
if (isErr(result)) return <GraphQLError error={unwrapErr(result)} />;
|
||||
|
||||
const userId = unwrapOk(result);
|
||||
if (userId === null) return <NotLoggedIn />;
|
||||
|
||||
return <UserAccount id={userId} />;
|
||||
return <UserEmailList userId={userId} />;
|
||||
};
|
||||
|
||||
export default CurrentUserAccount;
|
||||
export default EmailList;
|
||||
@@ -13,31 +13,48 @@
|
||||
// limitations under the License.
|
||||
|
||||
import { useAtomValue } from "jotai";
|
||||
import { atomWithQuery } from "jotai-urql";
|
||||
|
||||
import { currentUserIdAtom } from "../atoms";
|
||||
import BrowserSessionList from "../components/BrowserSessionList";
|
||||
import CompatSessionList from "../components/CompatSessionList";
|
||||
import { mapQueryAtom } from "../atoms";
|
||||
import GraphQLError from "../components/GraphQLError";
|
||||
import NotLoggedIn from "../components/NotLoggedIn";
|
||||
import OAuth2SessionList from "../components/OAuth2SessionList";
|
||||
import UserHome from "../components/UserHome";
|
||||
import { graphql } from "../gql";
|
||||
import { isErr, unwrapErr, unwrapOk } from "../result";
|
||||
|
||||
const QUERY = graphql(/* GraphQL */ `
|
||||
query HomeQuery {
|
||||
viewer {
|
||||
__typename
|
||||
|
||||
... on User {
|
||||
id
|
||||
...UserHome_user
|
||||
}
|
||||
}
|
||||
}
|
||||
`);
|
||||
|
||||
const homeQueryAtom = atomWithQuery({
|
||||
query: QUERY,
|
||||
});
|
||||
|
||||
const homeAtom = mapQueryAtom(homeQueryAtom, (data) => {
|
||||
if (data.viewer?.__typename === "User") {
|
||||
return data.viewer;
|
||||
}
|
||||
|
||||
return null;
|
||||
});
|
||||
|
||||
const Home: React.FC = () => {
|
||||
const result = useAtomValue(currentUserIdAtom);
|
||||
const result = useAtomValue(homeAtom);
|
||||
if (isErr(result)) return <GraphQLError error={unwrapErr(result)} />;
|
||||
|
||||
const currentUserId = unwrapOk(result);
|
||||
if (currentUserId === null) return <NotLoggedIn />;
|
||||
const data = unwrapOk(result);
|
||||
if (data === null) return <NotLoggedIn />;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="mt-4 grid gap-8">
|
||||
<OAuth2SessionList userId={currentUserId} />
|
||||
<CompatSessionList userId={currentUserId} />
|
||||
<BrowserSessionList userId={currentUserId} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
return <UserHome user={data} />;
|
||||
};
|
||||
|
||||
export default Home;
|
||||
|
||||
33
frontend/src/pages/OAuth2SessionList.tsx
Normal file
33
frontend/src/pages/OAuth2SessionList.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
// 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 { useAtomValue } from "jotai";
|
||||
|
||||
import { currentUserIdAtom } from "../atoms";
|
||||
import GraphQLError from "../components/GraphQLError";
|
||||
import NotLoggedIn from "../components/NotLoggedIn";
|
||||
import List from "../components/OAuth2SessionList";
|
||||
import { isErr, unwrapErr, unwrapOk } from "../result";
|
||||
|
||||
const OAuth2SessionList: React.FC = () => {
|
||||
const result = useAtomValue(currentUserIdAtom);
|
||||
if (isErr(result)) return <GraphQLError error={unwrapErr(result)} />;
|
||||
|
||||
const userId = unwrapOk(result);
|
||||
if (userId === null) return <NotLoggedIn />;
|
||||
|
||||
return <List userId={userId} />;
|
||||
};
|
||||
|
||||
export default OAuth2SessionList;
|
||||
Reference in New Issue
Block a user