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": {
|
||||
"@emotion/react": "^11.11.1",
|
||||
"@fontsource/inter": "^5.0.8",
|
||||
"@types/ua-parser-js": "^0.7.37",
|
||||
"@urql/core": "^4.1.2",
|
||||
"@urql/devtools": "^2.0.3",
|
||||
"@urql/exchange-graphcache": "^6.3.2",
|
||||
@@ -25,7 +26,8 @@
|
||||
"jotai-location": "^0.5.1",
|
||||
"jotai-urql": "^0.7.1",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0"
|
||||
"react-dom": "^18.2.0",
|
||||
"ua-parser-js": "^1.0.35"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@graphql-codegen/cli": "^5.0.0",
|
||||
@@ -8829,6 +8831,11 @@
|
||||
"@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": {
|
||||
"version": "2.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.6.tgz",
|
||||
@@ -20697,7 +20704,6 @@
|
||||
"version": "1.0.35",
|
||||
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.35.tgz",
|
||||
"integrity": "sha512-fKnGuqmTBnIE+/KXSzCn4db8RTigUzw1AN0DmdU6hJovUTbYJKyqj+8Mt1c4VfRDnOVJnENmfYkIPZ946UrSAA==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
"dependencies": {
|
||||
"@emotion/react": "^11.11.1",
|
||||
"@fontsource/inter": "^5.0.8",
|
||||
"@types/ua-parser-js": "^0.7.37",
|
||||
"@urql/core": "^4.1.2",
|
||||
"@urql/devtools": "^2.0.3",
|
||||
"@urql/exchange-graphcache": "^6.3.2",
|
||||
@@ -32,7 +33,8 @@
|
||||
"jotai-location": "^0.5.1",
|
||||
"jotai-urql": "^0.7.1",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0"
|
||||
"react-dom": "^18.2.0",
|
||||
"ua-parser-js": "^1.0.35"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@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 { FragmentType, graphql, useFragment } from "../gql";
|
||||
import {
|
||||
parseUserAgent,
|
||||
sessionNameFromDeviceInformation,
|
||||
} from "../utils/parseUserAgent";
|
||||
|
||||
import Session from "./Session/Session";
|
||||
|
||||
@@ -28,6 +32,7 @@ const FRAGMENT = graphql(/* GraphQL */ `
|
||||
id
|
||||
createdAt
|
||||
finishedAt
|
||||
userAgent
|
||||
lastAuthentication {
|
||||
id
|
||||
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 (
|
||||
<Session
|
||||
@@ -98,6 +105,7 @@ const BrowserSession: React.FC<Props> = ({ session, isCurrent }) => {
|
||||
name={sessionName}
|
||||
createdAt={createdAt}
|
||||
finishedAt={data.finishedAt}
|
||||
isCurrent={isCurrent}
|
||||
>
|
||||
{!data.finishedAt && (
|
||||
<Button
|
||||
|
||||
@@ -34,3 +34,10 @@
|
||||
flex-direction: row;
|
||||
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;
|
||||
finishedAt?: string;
|
||||
clientName?: string;
|
||||
isCurrent?: boolean;
|
||||
};
|
||||
const Session: React.FC<React.PropsWithChildren<SessionProps>> = ({
|
||||
id,
|
||||
@@ -37,10 +38,21 @@ const Session: React.FC<React.PropsWithChildren<SessionProps>> = ({
|
||||
createdAt,
|
||||
finishedAt,
|
||||
clientName,
|
||||
isCurrent,
|
||||
children,
|
||||
}) => {
|
||||
return (
|
||||
<Block>
|
||||
{isCurrent && (
|
||||
<Body
|
||||
as="span"
|
||||
size="sm"
|
||||
className={styles.currentSessionBadge}
|
||||
weight="semibold"
|
||||
>
|
||||
Current
|
||||
</Body>
|
||||
)}
|
||||
<H6 className={styles.sessionName} title={id}>
|
||||
{name || id}
|
||||
</H6>
|
||||
|
||||
@@ -17,7 +17,7 @@ const documents = {
|
||||
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":
|
||||
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,
|
||||
"\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,
|
||||
@@ -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.
|
||||
*/
|
||||
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",
|
||||
): (typeof documents)["\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 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.
|
||||
*/
|
||||
|
||||
@@ -1017,6 +1017,7 @@ export type BrowserSession_SessionFragment = {
|
||||
id: string;
|
||||
createdAt: any;
|
||||
finishedAt?: any | null;
|
||||
userAgent?: string | null;
|
||||
lastAuthentication?: {
|
||||
__typename?: "Authentication";
|
||||
id: string;
|
||||
@@ -1551,6 +1552,7 @@ export const BrowserSession_SessionFragmentDoc = {
|
||||
{ kind: "Field", name: { kind: "Name", value: "id" } },
|
||||
{ kind: "Field", name: { kind: "Name", value: "createdAt" } },
|
||||
{ kind: "Field", name: { kind: "Name", value: "finishedAt" } },
|
||||
{ kind: "Field", name: { kind: "Name", value: "userAgent" } },
|
||||
{
|
||||
kind: "Field",
|
||||
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: "createdAt" } },
|
||||
{ kind: "Field", name: { kind: "Name", value: "finishedAt" } },
|
||||
{ kind: "Field", name: { kind: "Name", value: "userAgent" } },
|
||||
{
|
||||
kind: "Field",
|
||||
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: "createdAt" } },
|
||||
{ kind: "Field", name: { kind: "Name", value: "finishedAt" } },
|
||||
{ kind: "Field", name: { kind: "Name", value: "userAgent" } },
|
||||
{
|
||||
kind: "Field",
|
||||
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