mirror of
https://github.com/QuantumNous/new-api.git
synced 2026-09-11 14:41:21 +00:00
fix(web): detect iPad login sessions correctly
This commit is contained in:
@@ -0,0 +1,98 @@
|
|||||||
|
/*
|
||||||
|
Copyright (C) 2023-2026 QuantumNous
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as
|
||||||
|
published by the Free Software Foundation, either version 3 of the
|
||||||
|
License, or (at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
For commercial licensing, please contact support@quantumnous.com
|
||||||
|
*/
|
||||||
|
import assert from 'node:assert/strict'
|
||||||
|
import { describe, test } from 'node:test'
|
||||||
|
|
||||||
|
import type { TFunction } from 'i18next'
|
||||||
|
|
||||||
|
import { loginMethodLabel, sessionDevice } from '../login-session-utils'
|
||||||
|
|
||||||
|
const translate = ((key: string) => key) as TFunction
|
||||||
|
|
||||||
|
describe('login session presentation', () => {
|
||||||
|
test('labels built-in and provider OAuth login methods', () => {
|
||||||
|
assert.equal(loginMethodLabel('password', translate), 'Password')
|
||||||
|
assert.equal(
|
||||||
|
loginMethodLabel('2fa', translate),
|
||||||
|
'Two-factor Authentication'
|
||||||
|
)
|
||||||
|
assert.equal(loginMethodLabel('oauth:github', translate), 'OAuth · GitHub')
|
||||||
|
assert.equal(
|
||||||
|
loginMethodLabel('oauth:custom-provider', translate),
|
||||||
|
'OAuth · custom-provider'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('labels iPad Safari as iOS when its user agent also mentions Mac OS X', () => {
|
||||||
|
const userAgent =
|
||||||
|
'Mozilla/5.0 (iPad; CPU OS 17_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Mobile/15E148 Safari/604.1'
|
||||||
|
|
||||||
|
assert.equal(
|
||||||
|
sessionDevice(userAgent, 'Unknown device', 'Browser'),
|
||||||
|
'Safari · iOS'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('labels a touch-capable current iPad session as iOS when its desktop user agent says Macintosh', () => {
|
||||||
|
const userAgent =
|
||||||
|
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Safari/605.1.15'
|
||||||
|
|
||||||
|
assert.equal(
|
||||||
|
sessionDevice(userAgent, 'Unknown device', 'Browser', 5),
|
||||||
|
'Safari · iOS'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('keeps touch-capable Windows Chrome sessions identifiable', () => {
|
||||||
|
const userAgent =
|
||||||
|
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36'
|
||||||
|
|
||||||
|
assert.equal(
|
||||||
|
sessionDevice(userAgent, 'Unknown device', 'Browser', 10),
|
||||||
|
'Chrome · Windows'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('keeps Android Chrome sessions identifiable when their user agent mentions Linux', () => {
|
||||||
|
const userAgent =
|
||||||
|
'Mozilla/5.0 (Linux; Android 14; Pixel 8 Pro) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Mobile Safari/537.36'
|
||||||
|
|
||||||
|
assert.equal(
|
||||||
|
sessionDevice(userAgent, 'Unknown device', 'Browser', 5),
|
||||||
|
'Chrome · Android'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('keeps genuine macOS Safari sessions identifiable', () => {
|
||||||
|
const userAgent =
|
||||||
|
'Mozilla/5.0 (Macintosh; Intel Mac OS X 14_5) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Safari/605.1.15'
|
||||||
|
|
||||||
|
assert.equal(
|
||||||
|
sessionDevice(userAgent, 'Unknown device', 'Browser'),
|
||||||
|
'Safari · macOS'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('falls back to the unknown-device label for an empty user agent', () => {
|
||||||
|
assert.equal(
|
||||||
|
sessionDevice('', 'Unknown device', 'Browser'),
|
||||||
|
'Unknown device'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -34,6 +34,10 @@ interface LoginSessionItemProps {
|
|||||||
|
|
||||||
export function LoginSessionItem({ session, onRevoke }: LoginSessionItemProps) {
|
export function LoginSessionItem({ session, onRevoke }: LoginSessionItemProps) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
const maxTouchPoints =
|
||||||
|
session.current && typeof navigator !== 'undefined'
|
||||||
|
? navigator.maxTouchPoints
|
||||||
|
: 0
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='flex flex-col gap-3 py-4 sm:flex-row sm:items-center'>
|
<div className='flex flex-col gap-3 py-4 sm:flex-row sm:items-center'>
|
||||||
@@ -46,7 +50,8 @@ export function LoginSessionItem({ session, onRevoke }: LoginSessionItemProps) {
|
|||||||
{sessionDevice(
|
{sessionDevice(
|
||||||
session.user_agent,
|
session.user_agent,
|
||||||
t('Unknown device'),
|
t('Unknown device'),
|
||||||
t('Browser')
|
t('Browser'),
|
||||||
|
maxTouchPoints
|
||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
{session.current && <Badge variant='secondary'>{t('Current')}</Badge>}
|
{session.current && <Badge variant='secondary'>{t('Current')}</Badge>}
|
||||||
|
|||||||
@@ -1,56 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright (C) 2023-2026 QuantumNous
|
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
|
||||||
it under the terms of the GNU Affero General Public License as
|
|
||||||
published by the Free Software Foundation, either version 3 of the
|
|
||||||
License, or (at your option) any later version.
|
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
GNU Affero General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU Affero General Public License
|
|
||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
For commercial licensing, please contact support@quantumnous.com
|
|
||||||
*/
|
|
||||||
import assert from 'node:assert/strict'
|
|
||||||
import { describe, test } from 'node:test'
|
|
||||||
|
|
||||||
import type { TFunction } from 'i18next'
|
|
||||||
|
|
||||||
import { loginMethodLabel, sessionDevice } from './login-session-utils'
|
|
||||||
|
|
||||||
const translate = ((key: string) => key) as TFunction
|
|
||||||
|
|
||||||
describe('login session presentation', () => {
|
|
||||||
test('labels built-in and provider OAuth login methods', () => {
|
|
||||||
assert.equal(loginMethodLabel('password', translate), 'Password')
|
|
||||||
assert.equal(
|
|
||||||
loginMethodLabel('2fa', translate),
|
|
||||||
'Two-factor Authentication'
|
|
||||||
)
|
|
||||||
assert.equal(loginMethodLabel('oauth:github', translate), 'OAuth · GitHub')
|
|
||||||
assert.equal(
|
|
||||||
loginMethodLabel('oauth:custom-provider', translate),
|
|
||||||
'OAuth · custom-provider'
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('derives a stable browser and operating-system label', () => {
|
|
||||||
assert.equal(
|
|
||||||
sessionDevice(
|
|
||||||
'Mozilla/5.0 (Macintosh; Intel Mac OS X) AppleWebKit Safari/605.1.15',
|
|
||||||
'Unknown device',
|
|
||||||
'Browser'
|
|
||||||
),
|
|
||||||
'Safari · macOS'
|
|
||||||
)
|
|
||||||
assert.equal(
|
|
||||||
sessionDevice('', 'Unknown device', 'Browser'),
|
|
||||||
'Unknown device'
|
|
||||||
)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -21,7 +21,8 @@ import type { TFunction } from 'i18next'
|
|||||||
export function sessionDevice(
|
export function sessionDevice(
|
||||||
userAgent: string,
|
userAgent: string,
|
||||||
unknownDevice: string,
|
unknownDevice: string,
|
||||||
browserLabel: string
|
browserLabel: string,
|
||||||
|
maxTouchPoints = 0
|
||||||
): string {
|
): string {
|
||||||
if (!userAgent) return unknownDevice
|
if (!userAgent) return unknownDevice
|
||||||
let browser = browserLabel
|
let browser = browserLabel
|
||||||
@@ -31,12 +32,15 @@ export function sessionDevice(
|
|||||||
else if (userAgent.includes('Safari/')) browser = 'Safari'
|
else if (userAgent.includes('Safari/')) browser = 'Safari'
|
||||||
|
|
||||||
let system = ''
|
let system = ''
|
||||||
if (userAgent.includes('Windows')) system = 'Windows'
|
const isIPad =
|
||||||
else if (userAgent.includes('Mac OS')) system = 'macOS'
|
userAgent.includes('iPad') ||
|
||||||
else if (userAgent.includes('Android')) system = 'Android'
|
(userAgent.includes('Macintosh') && maxTouchPoints > 1)
|
||||||
else if (userAgent.includes('iPhone') || userAgent.includes('iPad')) {
|
if (userAgent.includes('iPhone') || isIPad) {
|
||||||
system = 'iOS'
|
system = 'iOS'
|
||||||
} else if (userAgent.includes('Linux')) system = 'Linux'
|
} else if (userAgent.includes('Android')) system = 'Android'
|
||||||
|
else if (userAgent.includes('Windows')) system = 'Windows'
|
||||||
|
else if (userAgent.includes('Mac OS')) system = 'macOS'
|
||||||
|
else if (userAgent.includes('Linux')) system = 'Linux'
|
||||||
return system ? `${browser} · ${system}` : browser
|
return system ? `${browser} · ${system}` : browser
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user