You've already forked authentication-service
mirror of
https://github.com/matrix-org/matrix-authentication-service.git
synced 2025-11-23 11:02:35 +03:00
Parse browser session userAgent for session name (#1685)
* parse browser session user agent and use for session name * move current session badge to session component * simplify browser session name
This commit is contained in:
10
frontend/package-lock.json
generated
10
frontend/package-lock.json
generated
@@ -10,6 +10,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@emotion/react": "^11.11.1",
|
"@emotion/react": "^11.11.1",
|
||||||
"@fontsource/inter": "^5.0.8",
|
"@fontsource/inter": "^5.0.8",
|
||||||
|
"@types/ua-parser-js": "^0.7.37",
|
||||||
"@urql/core": "^4.1.2",
|
"@urql/core": "^4.1.2",
|
||||||
"@urql/devtools": "^2.0.3",
|
"@urql/devtools": "^2.0.3",
|
||||||
"@urql/exchange-graphcache": "^6.3.2",
|
"@urql/exchange-graphcache": "^6.3.2",
|
||||||
@@ -25,7 +26,8 @@
|
|||||||
"jotai-location": "^0.5.1",
|
"jotai-location": "^0.5.1",
|
||||||
"jotai-urql": "^0.7.1",
|
"jotai-urql": "^0.7.1",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0"
|
"react-dom": "^18.2.0",
|
||||||
|
"ua-parser-js": "^1.0.35"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@graphql-codegen/cli": "^5.0.0",
|
"@graphql-codegen/cli": "^5.0.0",
|
||||||
@@ -8829,6 +8831,11 @@
|
|||||||
"@types/node": "*"
|
"@types/node": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/ua-parser-js": {
|
||||||
|
"version": "0.7.37",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/ua-parser-js/-/ua-parser-js-0.7.37.tgz",
|
||||||
|
"integrity": "sha512-4sOxS3ZWXC0uHJLYcWAaLMxTvjRX3hT96eF4YWUh1ovTaenvibaZOE5uXtIp4mksKMLRwo7YDiCBCw6vBiUPVg=="
|
||||||
|
},
|
||||||
"node_modules/@types/unist": {
|
"node_modules/@types/unist": {
|
||||||
"version": "2.0.6",
|
"version": "2.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.6.tgz",
|
||||||
@@ -20697,7 +20704,6 @@
|
|||||||
"version": "1.0.35",
|
"version": "1.0.35",
|
||||||
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.35.tgz",
|
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.35.tgz",
|
||||||
"integrity": "sha512-fKnGuqmTBnIE+/KXSzCn4db8RTigUzw1AN0DmdU6hJovUTbYJKyqj+8Mt1c4VfRDnOVJnENmfYkIPZ946UrSAA==",
|
"integrity": "sha512-fKnGuqmTBnIE+/KXSzCn4db8RTigUzw1AN0DmdU6hJovUTbYJKyqj+8Mt1c4VfRDnOVJnENmfYkIPZ946UrSAA==",
|
||||||
"dev": true,
|
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "opencollective",
|
"type": "opencollective",
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@emotion/react": "^11.11.1",
|
"@emotion/react": "^11.11.1",
|
||||||
"@fontsource/inter": "^5.0.8",
|
"@fontsource/inter": "^5.0.8",
|
||||||
|
"@types/ua-parser-js": "^0.7.37",
|
||||||
"@urql/core": "^4.1.2",
|
"@urql/core": "^4.1.2",
|
||||||
"@urql/devtools": "^2.0.3",
|
"@urql/devtools": "^2.0.3",
|
||||||
"@urql/exchange-graphcache": "^6.3.2",
|
"@urql/exchange-graphcache": "^6.3.2",
|
||||||
@@ -32,7 +33,8 @@
|
|||||||
"jotai-location": "^0.5.1",
|
"jotai-location": "^0.5.1",
|
||||||
"jotai-urql": "^0.7.1",
|
"jotai-urql": "^0.7.1",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0"
|
"react-dom": "^18.2.0",
|
||||||
|
"ua-parser-js": "^1.0.35"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@graphql-codegen/cli": "^5.0.0",
|
"@graphql-codegen/cli": "^5.0.0",
|
||||||
|
|||||||
@@ -1,24 +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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
.session-icon {
|
|
||||||
color: var(--cpd-color-icon-secondary);
|
|
||||||
background: var(--cpd-color-bg-subtle-secondary);
|
|
||||||
height: var(--cpd-space-10x);
|
|
||||||
width: var(--cpd-space-10x);
|
|
||||||
padding: var(--cpd-space-2x);
|
|
||||||
border-radius: var(--cpd-space-1x);
|
|
||||||
margin-right: var(--cpd-space-2x);
|
|
||||||
}
|
|
||||||
@@ -20,6 +20,10 @@ import { useTransition } from "react";
|
|||||||
|
|
||||||
import { currentBrowserSessionIdAtom, currentUserIdAtom } from "../atoms";
|
import { currentBrowserSessionIdAtom, currentUserIdAtom } from "../atoms";
|
||||||
import { FragmentType, graphql, useFragment } from "../gql";
|
import { FragmentType, graphql, useFragment } from "../gql";
|
||||||
|
import {
|
||||||
|
parseUserAgent,
|
||||||
|
sessionNameFromDeviceInformation,
|
||||||
|
} from "../utils/parseUserAgent";
|
||||||
|
|
||||||
import Session from "./Session/Session";
|
import Session from "./Session/Session";
|
||||||
|
|
||||||
@@ -28,6 +32,7 @@ const FRAGMENT = graphql(/* GraphQL */ `
|
|||||||
id
|
id
|
||||||
createdAt
|
createdAt
|
||||||
finishedAt
|
finishedAt
|
||||||
|
userAgent
|
||||||
lastAuthentication {
|
lastAuthentication {
|
||||||
id
|
id
|
||||||
createdAt
|
createdAt
|
||||||
@@ -90,7 +95,9 @@ const BrowserSession: React.FC<Props> = ({ session, isCurrent }) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const sessionName = isCurrent ? "Current browser session" : "Browser session";
|
const deviceInformation = parseUserAgent(data.userAgent || undefined);
|
||||||
|
const sessionName =
|
||||||
|
sessionNameFromDeviceInformation(deviceInformation) || "Browser session";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Session
|
<Session
|
||||||
@@ -98,6 +105,7 @@ const BrowserSession: React.FC<Props> = ({ session, isCurrent }) => {
|
|||||||
name={sessionName}
|
name={sessionName}
|
||||||
createdAt={createdAt}
|
createdAt={createdAt}
|
||||||
finishedAt={data.finishedAt}
|
finishedAt={data.finishedAt}
|
||||||
|
isCurrent={isCurrent}
|
||||||
>
|
>
|
||||||
{!data.finishedAt && (
|
{!data.finishedAt && (
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -33,4 +33,11 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.current-session-badge {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 0 var(--cpd-space-2x);
|
||||||
|
border-radius: var(--cpd-space-2x);
|
||||||
|
background-color: var(--cpd-color-text-action-accent);
|
||||||
}
|
}
|
||||||
@@ -30,6 +30,7 @@ export type SessionProps = {
|
|||||||
createdAt: string;
|
createdAt: string;
|
||||||
finishedAt?: string;
|
finishedAt?: string;
|
||||||
clientName?: string;
|
clientName?: string;
|
||||||
|
isCurrent?: boolean;
|
||||||
};
|
};
|
||||||
const Session: React.FC<React.PropsWithChildren<SessionProps>> = ({
|
const Session: React.FC<React.PropsWithChildren<SessionProps>> = ({
|
||||||
id,
|
id,
|
||||||
@@ -37,10 +38,21 @@ const Session: React.FC<React.PropsWithChildren<SessionProps>> = ({
|
|||||||
createdAt,
|
createdAt,
|
||||||
finishedAt,
|
finishedAt,
|
||||||
clientName,
|
clientName,
|
||||||
|
isCurrent,
|
||||||
children,
|
children,
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<Block>
|
<Block>
|
||||||
|
{isCurrent && (
|
||||||
|
<Body
|
||||||
|
as="span"
|
||||||
|
size="sm"
|
||||||
|
className={styles.currentSessionBadge}
|
||||||
|
weight="semibold"
|
||||||
|
>
|
||||||
|
Current
|
||||||
|
</Body>
|
||||||
|
)}
|
||||||
<H6 className={styles.sessionName} title={id}>
|
<H6 className={styles.sessionName} title={id}>
|
||||||
{name || id}
|
{name || id}
|
||||||
</H6>
|
</H6>
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ const documents = {
|
|||||||
types.CurrentViewerQueryDocument,
|
types.CurrentViewerQueryDocument,
|
||||||
"\n query CurrentViewerSessionQuery {\n viewerSession {\n __typename\n ... on BrowserSession {\n id\n }\n\n ... on Anonymous {\n id\n }\n }\n }\n":
|
"\n query CurrentViewerSessionQuery {\n viewerSession {\n __typename\n ... on BrowserSession {\n id\n }\n\n ... on Anonymous {\n id\n }\n }\n }\n":
|
||||||
types.CurrentViewerSessionQueryDocument,
|
types.CurrentViewerSessionQueryDocument,
|
||||||
"\n fragment BrowserSession_session on BrowserSession {\n id\n createdAt\n finishedAt\n lastAuthentication {\n id\n createdAt\n }\n }\n":
|
"\n fragment BrowserSession_session on BrowserSession {\n id\n createdAt\n finishedAt\n userAgent\n lastAuthentication {\n id\n createdAt\n }\n }\n":
|
||||||
types.BrowserSession_SessionFragmentDoc,
|
types.BrowserSession_SessionFragmentDoc,
|
||||||
"\n mutation EndBrowserSession($id: ID!) {\n endBrowserSession(input: { browserSessionId: $id }) {\n status\n browserSession {\n id\n ...BrowserSession_session\n }\n }\n }\n":
|
"\n mutation EndBrowserSession($id: ID!) {\n endBrowserSession(input: { browserSessionId: $id }) {\n status\n browserSession {\n id\n ...BrowserSession_session\n }\n }\n }\n":
|
||||||
types.EndBrowserSessionDocument,
|
types.EndBrowserSessionDocument,
|
||||||
@@ -103,8 +103,8 @@ export function graphql(
|
|||||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||||
*/
|
*/
|
||||||
export function graphql(
|
export function graphql(
|
||||||
source: "\n fragment BrowserSession_session on BrowserSession {\n id\n createdAt\n finishedAt\n lastAuthentication {\n id\n createdAt\n }\n }\n",
|
source: "\n fragment BrowserSession_session on BrowserSession {\n id\n createdAt\n finishedAt\n userAgent\n lastAuthentication {\n id\n createdAt\n }\n }\n",
|
||||||
): (typeof documents)["\n fragment BrowserSession_session on BrowserSession {\n id\n createdAt\n finishedAt\n lastAuthentication {\n id\n createdAt\n }\n }\n"];
|
): (typeof documents)["\n fragment BrowserSession_session on BrowserSession {\n id\n createdAt\n finishedAt\n userAgent\n lastAuthentication {\n id\n createdAt\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.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1017,6 +1017,7 @@ export type BrowserSession_SessionFragment = {
|
|||||||
id: string;
|
id: string;
|
||||||
createdAt: any;
|
createdAt: any;
|
||||||
finishedAt?: any | null;
|
finishedAt?: any | null;
|
||||||
|
userAgent?: string | null;
|
||||||
lastAuthentication?: {
|
lastAuthentication?: {
|
||||||
__typename?: "Authentication";
|
__typename?: "Authentication";
|
||||||
id: string;
|
id: string;
|
||||||
@@ -1551,6 +1552,7 @@ export const BrowserSession_SessionFragmentDoc = {
|
|||||||
{ kind: "Field", name: { kind: "Name", value: "id" } },
|
{ kind: "Field", name: { kind: "Name", value: "id" } },
|
||||||
{ kind: "Field", name: { kind: "Name", value: "createdAt" } },
|
{ kind: "Field", name: { kind: "Name", value: "createdAt" } },
|
||||||
{ kind: "Field", name: { kind: "Name", value: "finishedAt" } },
|
{ kind: "Field", name: { kind: "Name", value: "finishedAt" } },
|
||||||
|
{ kind: "Field", name: { kind: "Name", value: "userAgent" } },
|
||||||
{
|
{
|
||||||
kind: "Field",
|
kind: "Field",
|
||||||
name: { kind: "Name", value: "lastAuthentication" },
|
name: { kind: "Name", value: "lastAuthentication" },
|
||||||
@@ -2043,6 +2045,7 @@ export const EndBrowserSessionDocument = {
|
|||||||
{ kind: "Field", name: { kind: "Name", value: "id" } },
|
{ kind: "Field", name: { kind: "Name", value: "id" } },
|
||||||
{ kind: "Field", name: { kind: "Name", value: "createdAt" } },
|
{ kind: "Field", name: { kind: "Name", value: "createdAt" } },
|
||||||
{ kind: "Field", name: { kind: "Name", value: "finishedAt" } },
|
{ kind: "Field", name: { kind: "Name", value: "finishedAt" } },
|
||||||
|
{ kind: "Field", name: { kind: "Name", value: "userAgent" } },
|
||||||
{
|
{
|
||||||
kind: "Field",
|
kind: "Field",
|
||||||
name: { kind: "Name", value: "lastAuthentication" },
|
name: { kind: "Name", value: "lastAuthentication" },
|
||||||
@@ -2274,6 +2277,7 @@ export const BrowserSessionListDocument = {
|
|||||||
{ kind: "Field", name: { kind: "Name", value: "id" } },
|
{ kind: "Field", name: { kind: "Name", value: "id" } },
|
||||||
{ kind: "Field", name: { kind: "Name", value: "createdAt" } },
|
{ kind: "Field", name: { kind: "Name", value: "createdAt" } },
|
||||||
{ kind: "Field", name: { kind: "Name", value: "finishedAt" } },
|
{ kind: "Field", name: { kind: "Name", value: "finishedAt" } },
|
||||||
|
{ kind: "Field", name: { kind: "Name", value: "userAgent" } },
|
||||||
{
|
{
|
||||||
kind: "Field",
|
kind: "Field",
|
||||||
name: { kind: "Name", value: "lastAuthentication" },
|
name: { kind: "Name", value: "lastAuthentication" },
|
||||||
|
|||||||
389
frontend/src/utils/parseUserAgent.test.ts
Normal file
389
frontend/src/utils/parseUserAgent.test.ts
Normal file
@@ -0,0 +1,389 @@
|
|||||||
|
/*
|
||||||
|
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 { describe, it, expect } from "vitest";
|
||||||
|
|
||||||
|
import {
|
||||||
|
DeviceType,
|
||||||
|
DeviceInformation,
|
||||||
|
parseUserAgent,
|
||||||
|
sessionNameFromDeviceInformation,
|
||||||
|
} from "./parseUserAgent";
|
||||||
|
|
||||||
|
const makeDeviceExtendedInfo = (
|
||||||
|
deviceType: DeviceType,
|
||||||
|
deviceModel?: string,
|
||||||
|
deviceModelVersion?: string,
|
||||||
|
deviceOperatingSystem?: string,
|
||||||
|
deviceOperatingSystemVersion?: string,
|
||||||
|
clientName?: string,
|
||||||
|
clientVersion?: string,
|
||||||
|
): DeviceInformation => ({
|
||||||
|
deviceType,
|
||||||
|
deviceModel,
|
||||||
|
deviceModelVersion,
|
||||||
|
deviceOperatingSystem,
|
||||||
|
deviceOperatingSystemVersion,
|
||||||
|
client: clientName,
|
||||||
|
clientVersion,
|
||||||
|
});
|
||||||
|
|
||||||
|
/* eslint-disable max-len */
|
||||||
|
const ANDROID_UA = [
|
||||||
|
// New User Agent Implementation
|
||||||
|
"Element dbg/1.5.0-dev (Xiaomi Mi 9T; Android 11; RKQ1.200826.002 test-keys; Flavour GooglePlay; MatrixAndroidSdk2 1.5.2)",
|
||||||
|
"Element/1.5.0 (Samsung SM-G960F; Android 6.0.1; RKQ1.200826.002; Flavour FDroid; MatrixAndroidSdk2 1.5.2)",
|
||||||
|
"Element/1.5.0 (Google Nexus 5; Android 7.0; RKQ1.200826.002 test test; Flavour FDroid; MatrixAndroidSdk2 1.5.2)",
|
||||||
|
"Element/1.5.0 (Google (Nexus) 5; Android 7.0; RKQ1.200826.002 test test; Flavour FDroid; MatrixAndroidSdk2 1.5.2)",
|
||||||
|
"Element/1.5.0 (Google (Nexus) (5); Android 7.0; RKQ1.200826.002 test test; Flavour FDroid; MatrixAndroidSdk2 1.5.2)",
|
||||||
|
// Legacy User Agent Implementation
|
||||||
|
"Element/1.0.0 (Linux; U; Android 6.0.1; SM-A510F Build/MMB29; Flavour GPlay; MatrixAndroidSdk2 1.0)",
|
||||||
|
"Element/1.0.0 (Linux; Android 7.0; SM-G610M Build/NRD90M; Flavour GPlay; MatrixAndroidSdk2 1.0)",
|
||||||
|
];
|
||||||
|
|
||||||
|
const ANDROID_EXPECTED_RESULT = [
|
||||||
|
makeDeviceExtendedInfo(
|
||||||
|
DeviceType.Mobile,
|
||||||
|
"Xiaomi Mi 9T",
|
||||||
|
undefined,
|
||||||
|
"Android",
|
||||||
|
"11",
|
||||||
|
),
|
||||||
|
makeDeviceExtendedInfo(
|
||||||
|
DeviceType.Mobile,
|
||||||
|
"Samsung",
|
||||||
|
"SM-G960F",
|
||||||
|
"Android",
|
||||||
|
"6.0.1",
|
||||||
|
),
|
||||||
|
makeDeviceExtendedInfo(DeviceType.Mobile, "LG", "Nexus 5", "Android", "7.0"),
|
||||||
|
makeDeviceExtendedInfo(
|
||||||
|
DeviceType.Mobile,
|
||||||
|
"Google (Nexus) 5",
|
||||||
|
undefined,
|
||||||
|
"Android",
|
||||||
|
"7.0",
|
||||||
|
),
|
||||||
|
makeDeviceExtendedInfo(
|
||||||
|
DeviceType.Mobile,
|
||||||
|
"Google (Nexus) (5)",
|
||||||
|
undefined,
|
||||||
|
"Android",
|
||||||
|
"7.0",
|
||||||
|
),
|
||||||
|
makeDeviceExtendedInfo(
|
||||||
|
DeviceType.Mobile,
|
||||||
|
"Samsung",
|
||||||
|
"SM-A510F",
|
||||||
|
"Android",
|
||||||
|
"6.0.1",
|
||||||
|
),
|
||||||
|
makeDeviceExtendedInfo(
|
||||||
|
DeviceType.Mobile,
|
||||||
|
"Samsung",
|
||||||
|
"SM-G610M",
|
||||||
|
"Android",
|
||||||
|
"7.0",
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
const IOS_UA = [
|
||||||
|
"Element/1.8.21 (iPhone; iOS 15.2; Scale/3.00)",
|
||||||
|
"Element/1.8.21 (iPhone XS Max; iOS 15.2; Scale/3.00)",
|
||||||
|
"Element/1.8.21 (iPad Pro (11-inch); iOS 15.2; Scale/3.00)",
|
||||||
|
"Element/1.8.21 (iPad Pro (12.9-inch) (3rd generation); iOS 15.2; Scale/3.00)",
|
||||||
|
];
|
||||||
|
const IOS_EXPECTED_RESULT = [
|
||||||
|
makeDeviceExtendedInfo(DeviceType.Mobile, "Apple", "iPhone", "iOS 15.2"),
|
||||||
|
makeDeviceExtendedInfo(
|
||||||
|
DeviceType.Mobile,
|
||||||
|
"Apple",
|
||||||
|
"iPhone XS Max",
|
||||||
|
"iOS 15.2",
|
||||||
|
),
|
||||||
|
makeDeviceExtendedInfo(
|
||||||
|
DeviceType.Mobile,
|
||||||
|
"iPad Pro (11-inch)",
|
||||||
|
undefined,
|
||||||
|
"iOS 15.2",
|
||||||
|
),
|
||||||
|
makeDeviceExtendedInfo(
|
||||||
|
DeviceType.Mobile,
|
||||||
|
"iPad Pro (12.9-inch) (3rd generation)",
|
||||||
|
undefined,
|
||||||
|
"iOS 15.2",
|
||||||
|
),
|
||||||
|
];
|
||||||
|
const DESKTOP_UA = [
|
||||||
|
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) ElementNightly/2022091301 Chrome/104.0.5112.102" +
|
||||||
|
" Electron/20.1.1 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36 (KHTML, like Gecko) ElementNightly/2022091301 Chrome/104.0.5112.102 Electron/20.1.1 Safari/537.36",
|
||||||
|
];
|
||||||
|
const DESKTOP_EXPECTED_RESULT = [
|
||||||
|
makeDeviceExtendedInfo(
|
||||||
|
DeviceType.Desktop,
|
||||||
|
"Apple",
|
||||||
|
"Macintosh",
|
||||||
|
"Mac OS",
|
||||||
|
undefined,
|
||||||
|
"Electron",
|
||||||
|
"20.1.1",
|
||||||
|
),
|
||||||
|
makeDeviceExtendedInfo(
|
||||||
|
DeviceType.Desktop,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
"Windows",
|
||||||
|
undefined,
|
||||||
|
"Electron",
|
||||||
|
"20.1.1",
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
const WEB_UA = [
|
||||||
|
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.5112.102 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.5112.102 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:39.0) Gecko/20100101 Firefox/39.0",
|
||||||
|
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_2) AppleWebKit/600.3.18 (KHTML, like Gecko) Version/8.0.3 Safari/600.3.18",
|
||||||
|
"Mozilla/5.0 (Windows NT 6.0; rv:40.0) Gecko/20100101 Firefox/40.0",
|
||||||
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.135 Safari/537.36 Edge/12.246",
|
||||||
|
// using mobile browser
|
||||||
|
"Mozilla/5.0 (iPad; CPU OS 8_4_1 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) Version/8.0 Mobile/12H321 Safari/600.1.4",
|
||||||
|
"Mozilla/5.0 (iPhone; CPU iPhone OS 8_4_1 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) Version/8.0 Mobile/12H321 Safari/600.1.4",
|
||||||
|
"Mozilla/5.0 (Linux; Android 9; SM-G973U Build/PPR1.180610.011) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Mobile Safari/537.36",
|
||||||
|
];
|
||||||
|
|
||||||
|
const WEB_EXPECTED_RESULT = [
|
||||||
|
makeDeviceExtendedInfo(
|
||||||
|
DeviceType.Web,
|
||||||
|
"Apple",
|
||||||
|
"Macintosh",
|
||||||
|
"Mac OS",
|
||||||
|
undefined,
|
||||||
|
"Chrome",
|
||||||
|
"104.0.5112.102",
|
||||||
|
),
|
||||||
|
makeDeviceExtendedInfo(
|
||||||
|
DeviceType.Web,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
"Windows",
|
||||||
|
undefined,
|
||||||
|
"Chrome",
|
||||||
|
"104.0.5112.102",
|
||||||
|
),
|
||||||
|
makeDeviceExtendedInfo(
|
||||||
|
DeviceType.Web,
|
||||||
|
"Apple",
|
||||||
|
"Macintosh",
|
||||||
|
"Mac OS",
|
||||||
|
undefined,
|
||||||
|
"Firefox",
|
||||||
|
"39.0",
|
||||||
|
),
|
||||||
|
makeDeviceExtendedInfo(
|
||||||
|
DeviceType.Web,
|
||||||
|
"Apple",
|
||||||
|
"Macintosh",
|
||||||
|
"Mac OS",
|
||||||
|
undefined,
|
||||||
|
"Safari",
|
||||||
|
"8.0.3",
|
||||||
|
),
|
||||||
|
makeDeviceExtendedInfo(
|
||||||
|
DeviceType.Web,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
"Windows",
|
||||||
|
undefined,
|
||||||
|
"Firefox",
|
||||||
|
"40.0",
|
||||||
|
),
|
||||||
|
makeDeviceExtendedInfo(
|
||||||
|
DeviceType.Web,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
"Windows",
|
||||||
|
undefined,
|
||||||
|
"Edge",
|
||||||
|
"12.246",
|
||||||
|
),
|
||||||
|
// using mobile browser
|
||||||
|
makeDeviceExtendedInfo(
|
||||||
|
DeviceType.Web,
|
||||||
|
"Apple",
|
||||||
|
"iPad",
|
||||||
|
"iOS",
|
||||||
|
undefined,
|
||||||
|
"Mobile Safari",
|
||||||
|
"8.0",
|
||||||
|
),
|
||||||
|
makeDeviceExtendedInfo(
|
||||||
|
DeviceType.Web,
|
||||||
|
"Apple",
|
||||||
|
"iPhone",
|
||||||
|
"iOS",
|
||||||
|
undefined,
|
||||||
|
"Mobile Safari",
|
||||||
|
"8.0",
|
||||||
|
),
|
||||||
|
makeDeviceExtendedInfo(
|
||||||
|
DeviceType.Web,
|
||||||
|
"Samsung",
|
||||||
|
"SM-G973U",
|
||||||
|
"Android",
|
||||||
|
undefined,
|
||||||
|
"Chrome",
|
||||||
|
"69.0.3497.100",
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
const MISC_UA = [
|
||||||
|
"AppleTV11,1/11.1",
|
||||||
|
"Curl Client/1.0",
|
||||||
|
"banana",
|
||||||
|
"",
|
||||||
|
// fluffy chat ios
|
||||||
|
"Dart/2.18 (dart:io)",
|
||||||
|
];
|
||||||
|
|
||||||
|
const MISC_EXPECTED_RESULT = [
|
||||||
|
makeDeviceExtendedInfo(
|
||||||
|
DeviceType.Unknown,
|
||||||
|
"Apple",
|
||||||
|
"Apple TV",
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
),
|
||||||
|
makeDeviceExtendedInfo(
|
||||||
|
DeviceType.Unknown,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
),
|
||||||
|
makeDeviceExtendedInfo(
|
||||||
|
DeviceType.Unknown,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
),
|
||||||
|
makeDeviceExtendedInfo(
|
||||||
|
DeviceType.Unknown,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
),
|
||||||
|
makeDeviceExtendedInfo(
|
||||||
|
DeviceType.Unknown,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
),
|
||||||
|
];
|
||||||
|
/* eslint-disable max-len */
|
||||||
|
|
||||||
|
describe("parseUserAgent()", () => {
|
||||||
|
it("returns deviceType unknown when user agent is falsy", () => {
|
||||||
|
expect(parseUserAgent(undefined)).toEqual({
|
||||||
|
deviceType: DeviceType.Unknown,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
type TestCase = [string, DeviceInformation];
|
||||||
|
|
||||||
|
const testPlatform = (
|
||||||
|
platform: string,
|
||||||
|
userAgents: string[],
|
||||||
|
results: DeviceInformation[],
|
||||||
|
): void => {
|
||||||
|
const testCases: TestCase[] = userAgents.map((userAgent, index) => [
|
||||||
|
userAgent,
|
||||||
|
results[index],
|
||||||
|
]);
|
||||||
|
|
||||||
|
describe(`on platform ${platform}`, () => {
|
||||||
|
it.each(testCases)(
|
||||||
|
"should parse the user agent correctly - %s",
|
||||||
|
(userAgent, expectedResult) => {
|
||||||
|
expect(parseUserAgent(userAgent)).toEqual(expectedResult);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
testPlatform("Android", ANDROID_UA, ANDROID_EXPECTED_RESULT);
|
||||||
|
testPlatform("iOS", IOS_UA, IOS_EXPECTED_RESULT);
|
||||||
|
testPlatform("Desktop", DESKTOP_UA, DESKTOP_EXPECTED_RESULT);
|
||||||
|
testPlatform("Web", WEB_UA, WEB_EXPECTED_RESULT);
|
||||||
|
testPlatform("Misc", MISC_UA, MISC_EXPECTED_RESULT);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("sessionNameFromDeviceInformation", () => {
|
||||||
|
const deviceInfo = {
|
||||||
|
client: "Chrome",
|
||||||
|
clientVersion: "123",
|
||||||
|
deviceModel: "Apple Macintosh",
|
||||||
|
deviceOperatingSystem: "Mac OS",
|
||||||
|
deviceType: DeviceType.Web,
|
||||||
|
};
|
||||||
|
|
||||||
|
it("should concatenate device info", () => {
|
||||||
|
expect(sessionNameFromDeviceInformation(deviceInfo)).toEqual(
|
||||||
|
"Chrome on Mac OS",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should use device model when deviceOS is falsy", () => {
|
||||||
|
expect(
|
||||||
|
sessionNameFromDeviceInformation({
|
||||||
|
...deviceInfo,
|
||||||
|
deviceOperatingSystem: undefined,
|
||||||
|
}),
|
||||||
|
).toEqual("Chrome on Apple Macintosh");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should exclude device model and OS when both are falsy", () => {
|
||||||
|
expect(
|
||||||
|
sessionNameFromDeviceInformation({
|
||||||
|
...deviceInfo,
|
||||||
|
deviceOperatingSystem: undefined,
|
||||||
|
deviceModel: undefined,
|
||||||
|
}),
|
||||||
|
).toEqual("Chrome");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should exclude client when falsy", () => {
|
||||||
|
expect(
|
||||||
|
sessionNameFromDeviceInformation({
|
||||||
|
...deviceInfo,
|
||||||
|
client: undefined,
|
||||||
|
}),
|
||||||
|
).toEqual("Mac OS");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return an empty string when no info", () => {
|
||||||
|
expect(
|
||||||
|
sessionNameFromDeviceInformation({
|
||||||
|
deviceType: DeviceType.Unknown,
|
||||||
|
}),
|
||||||
|
).toEqual("");
|
||||||
|
});
|
||||||
|
});
|
||||||
145
frontend/src/utils/parseUserAgent.ts
Normal file
145
frontend/src/utils/parseUserAgent.ts
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2022 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 UAParser from "ua-parser-js";
|
||||||
|
|
||||||
|
export enum DeviceType {
|
||||||
|
Desktop = "Desktop",
|
||||||
|
Mobile = "Mobile",
|
||||||
|
Web = "Web",
|
||||||
|
Unknown = "Unknown",
|
||||||
|
}
|
||||||
|
export type DeviceInformation = {
|
||||||
|
deviceType: DeviceType;
|
||||||
|
// eg Google Pixel 6
|
||||||
|
deviceModel?: string;
|
||||||
|
deviceModelVersion?: string;
|
||||||
|
// eg Android 11
|
||||||
|
deviceOperatingSystem?: string;
|
||||||
|
deviceOperatingSystemVersion?: string;
|
||||||
|
// eg Firefox 1.1.0
|
||||||
|
client?: string;
|
||||||
|
clientVersion?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Element/1.8.21 (iPhone XS Max; iOS 15.2; Scale/3.00)
|
||||||
|
const IOS_KEYWORD = "; iOS ";
|
||||||
|
const BROWSER_KEYWORD = "Mozilla/";
|
||||||
|
|
||||||
|
const getDeviceType = (
|
||||||
|
userAgent: string,
|
||||||
|
device: UAParser.IDevice,
|
||||||
|
browser: UAParser.IBrowser,
|
||||||
|
operatingSystem: UAParser.IOS,
|
||||||
|
): DeviceType => {
|
||||||
|
if (browser.name === "Electron") {
|
||||||
|
return DeviceType.Desktop;
|
||||||
|
}
|
||||||
|
if (browser.name) {
|
||||||
|
return DeviceType.Web;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
device.type === "mobile" ||
|
||||||
|
operatingSystem.name?.includes("Android") ||
|
||||||
|
userAgent.indexOf(IOS_KEYWORD) > -1
|
||||||
|
) {
|
||||||
|
return DeviceType.Mobile;
|
||||||
|
}
|
||||||
|
return DeviceType.Unknown;
|
||||||
|
};
|
||||||
|
|
||||||
|
interface CustomValues {
|
||||||
|
customDeviceModel?: string;
|
||||||
|
customDeviceOS?: string;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Some mobile model and OS strings are not recognised
|
||||||
|
* by the UA parsing library
|
||||||
|
* check they exist by hand
|
||||||
|
*/
|
||||||
|
const checkForCustomValues = (userAgent: string): CustomValues => {
|
||||||
|
if (userAgent.includes(BROWSER_KEYWORD)) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const mightHaveDevice = userAgent.includes("(");
|
||||||
|
if (!mightHaveDevice) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
const deviceInfoSegments = userAgent
|
||||||
|
.substring(userAgent.indexOf("(") + 1)
|
||||||
|
.split("; ");
|
||||||
|
const customDeviceModel = deviceInfoSegments[0] || undefined;
|
||||||
|
const customDeviceOS = deviceInfoSegments[1] || undefined;
|
||||||
|
return { customDeviceModel, customDeviceOS };
|
||||||
|
};
|
||||||
|
|
||||||
|
export const parseUserAgent = (userAgent?: string): DeviceInformation => {
|
||||||
|
if (!userAgent) {
|
||||||
|
return {
|
||||||
|
deviceType: DeviceType.Unknown,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const parser = new UAParser(userAgent);
|
||||||
|
|
||||||
|
const browser = parser.getBrowser();
|
||||||
|
const device = parser.getDevice();
|
||||||
|
const operatingSystem = parser.getOS();
|
||||||
|
|
||||||
|
const deviceType = getDeviceType(userAgent, device, browser, operatingSystem);
|
||||||
|
|
||||||
|
// OSX versions are frozen at 10.15.17 in UA strings https://chromestatus.com/feature/5452592194781184
|
||||||
|
// ignore OS version in browser based sessions
|
||||||
|
const shouldIgnoreOSVersion =
|
||||||
|
deviceType === DeviceType.Web || deviceType === DeviceType.Desktop;
|
||||||
|
const deviceOperatingSystem = operatingSystem.name;
|
||||||
|
const deviceOperatingSystemVersion = shouldIgnoreOSVersion
|
||||||
|
? undefined
|
||||||
|
: operatingSystem.version;
|
||||||
|
const deviceModel = device.vendor;
|
||||||
|
const deviceModelVersion = device.model;
|
||||||
|
const client = browser.name;
|
||||||
|
const clientVersion = browser.version;
|
||||||
|
|
||||||
|
// only try to parse custom model and OS when device type is known
|
||||||
|
const { customDeviceModel, customDeviceOS } =
|
||||||
|
deviceType !== DeviceType.Unknown
|
||||||
|
? checkForCustomValues(userAgent)
|
||||||
|
: ({} as CustomValues);
|
||||||
|
|
||||||
|
return {
|
||||||
|
deviceType,
|
||||||
|
deviceModel: deviceModel || customDeviceModel,
|
||||||
|
deviceModelVersion,
|
||||||
|
deviceOperatingSystem: deviceOperatingSystem || customDeviceOS,
|
||||||
|
deviceOperatingSystemVersion,
|
||||||
|
client,
|
||||||
|
clientVersion,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const sessionNameFromDeviceInformation = ({
|
||||||
|
deviceModel,
|
||||||
|
deviceOperatingSystem,
|
||||||
|
client,
|
||||||
|
}: DeviceInformation): string | undefined => {
|
||||||
|
const description = [client, deviceOperatingSystem || deviceModel]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(" on ");
|
||||||
|
|
||||||
|
return description;
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user