You've already forked authentication-service
mirror of
https://github.com/matrix-org/matrix-authentication-service.git
synced 2025-07-31 09:24:31 +03:00
Parse User Agents on the backend side (#2388)
* Parse user agents on the server side * Parse and expose user agents on the backend * Use the parsed user agent in the device consent page * Fix the device icon tests * Fix clippy warnings * Box stuff to avoid large enum variants * Ignore a clippy warning * Fix the requester boxing
This commit is contained in:
12
Cargo.lock
generated
12
Cargo.lock
generated
@ -3070,10 +3070,12 @@ dependencies = [
|
|||||||
"oauth2-types",
|
"oauth2-types",
|
||||||
"rand 0.8.5",
|
"rand 0.8.5",
|
||||||
"rand_chacha 0.3.1",
|
"rand_chacha 0.3.1",
|
||||||
|
"regex",
|
||||||
"serde",
|
"serde",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"ulid",
|
"ulid",
|
||||||
"url",
|
"url",
|
||||||
|
"woothee",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -7198,6 +7200,16 @@ dependencies = [
|
|||||||
"unicode-xid",
|
"unicode-xid",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "woothee"
|
||||||
|
version = "0.13.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "896174c6a4779d4d7d4523dd27aef7d46609eda2497e370f6c998325c6bf6971"
|
||||||
|
dependencies = [
|
||||||
|
"lazy_static",
|
||||||
|
"regex",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "writeable"
|
name = "writeable"
|
||||||
version = "0.5.4"
|
version = "0.5.4"
|
||||||
|
@ -20,6 +20,8 @@ crc = "3.0.1"
|
|||||||
ulid.workspace = true
|
ulid.workspace = true
|
||||||
rand.workspace = true
|
rand.workspace = true
|
||||||
rand_chacha = "0.3.1"
|
rand_chacha = "0.3.1"
|
||||||
|
regex = "1.10.3"
|
||||||
|
woothee = "0.13.0"
|
||||||
|
|
||||||
mas-iana.workspace = true
|
mas-iana.workspace = true
|
||||||
mas-jose.workspace = true
|
mas-jose.workspace = true
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
// Copyright 2023 The Matrix.org Foundation C.I.C.
|
// Copyright 2024 The Matrix.org Foundation C.I.C.
|
||||||
//
|
//
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
// you may not use this file except in compliance with the License.
|
// you may not use this file except in compliance with the License.
|
||||||
@ -12,4 +12,15 @@
|
|||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
export * from "./useCurrentBrowserSessionId";
|
use mas_data_model::UserAgent;
|
||||||
|
|
||||||
|
/// Simple command-line tool to try out user-agent parsing
|
||||||
|
///
|
||||||
|
/// It parses user-agents from stdin and prints the parsed user-agent to stdout.
|
||||||
|
fn main() {
|
||||||
|
for line in std::io::stdin().lines() {
|
||||||
|
let user_agent = line.unwrap();
|
||||||
|
let user_agent = UserAgent::parse(user_agent);
|
||||||
|
println!("{user_agent:?}");
|
||||||
|
}
|
||||||
|
}
|
@ -19,7 +19,7 @@ use serde::Serialize;
|
|||||||
use ulid::Ulid;
|
use ulid::Ulid;
|
||||||
|
|
||||||
use super::Device;
|
use super::Device;
|
||||||
use crate::InvalidTransitionError;
|
use crate::{InvalidTransitionError, UserAgent};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)]
|
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)]
|
||||||
pub enum CompatSessionState {
|
pub enum CompatSessionState {
|
||||||
@ -83,7 +83,7 @@ pub struct CompatSession {
|
|||||||
pub user_session_id: Option<Ulid>,
|
pub user_session_id: Option<Ulid>,
|
||||||
pub created_at: DateTime<Utc>,
|
pub created_at: DateTime<Utc>,
|
||||||
pub is_synapse_admin: bool,
|
pub is_synapse_admin: bool,
|
||||||
pub user_agent: Option<String>,
|
pub user_agent: Option<UserAgent>,
|
||||||
pub last_active_at: Option<DateTime<Utc>>,
|
pub last_active_at: Option<DateTime<Utc>>,
|
||||||
pub last_active_ip: Option<IpAddr>,
|
pub last_active_ip: Option<IpAddr>,
|
||||||
}
|
}
|
||||||
|
@ -20,6 +20,7 @@ pub(crate) mod compat;
|
|||||||
pub(crate) mod oauth2;
|
pub(crate) mod oauth2;
|
||||||
pub(crate) mod tokens;
|
pub(crate) mod tokens;
|
||||||
pub(crate) mod upstream_oauth2;
|
pub(crate) mod upstream_oauth2;
|
||||||
|
pub(crate) mod user_agent;
|
||||||
pub(crate) mod users;
|
pub(crate) mod users;
|
||||||
|
|
||||||
/// Error when an invalid state transition is attempted.
|
/// Error when an invalid state transition is attempted.
|
||||||
@ -46,6 +47,7 @@ pub use self::{
|
|||||||
UpstreamOAuthProviderImportAction, UpstreamOAuthProviderImportPreference,
|
UpstreamOAuthProviderImportAction, UpstreamOAuthProviderImportPreference,
|
||||||
UpstreamOAuthProviderPkceMode, UpstreamOAuthProviderSubjectPreference,
|
UpstreamOAuthProviderPkceMode, UpstreamOAuthProviderSubjectPreference,
|
||||||
},
|
},
|
||||||
|
user_agent::{DeviceType, UserAgent},
|
||||||
users::{
|
users::{
|
||||||
Authentication, AuthenticationMethod, BrowserSession, Password, User, UserEmail,
|
Authentication, AuthenticationMethod, BrowserSession, Password, User, UserEmail,
|
||||||
UserEmailVerification, UserEmailVerificationState,
|
UserEmailVerification, UserEmailVerificationState,
|
||||||
|
@ -19,7 +19,7 @@ use oauth2_types::scope::Scope;
|
|||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use ulid::Ulid;
|
use ulid::Ulid;
|
||||||
|
|
||||||
use crate::{BrowserSession, InvalidTransitionError, Session};
|
use crate::{BrowserSession, InvalidTransitionError, Session, UserAgent};
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
|
||||||
#[serde(rename_all = "snake_case", tag = "state")]
|
#[serde(rename_all = "snake_case", tag = "state")]
|
||||||
@ -200,7 +200,7 @@ pub struct DeviceCodeGrant {
|
|||||||
pub ip_address: Option<IpAddr>,
|
pub ip_address: Option<IpAddr>,
|
||||||
|
|
||||||
/// The user agent used to request this device code grant.
|
/// The user agent used to request this device code grant.
|
||||||
pub user_agent: Option<String>,
|
pub user_agent: Option<UserAgent>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::ops::Deref for DeviceCodeGrant {
|
impl std::ops::Deref for DeviceCodeGrant {
|
||||||
|
@ -19,7 +19,7 @@ use oauth2_types::scope::Scope;
|
|||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use ulid::Ulid;
|
use ulid::Ulid;
|
||||||
|
|
||||||
use crate::InvalidTransitionError;
|
use crate::{InvalidTransitionError, UserAgent};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)]
|
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)]
|
||||||
pub enum SessionState {
|
pub enum SessionState {
|
||||||
@ -75,7 +75,7 @@ pub struct Session {
|
|||||||
pub user_session_id: Option<Ulid>,
|
pub user_session_id: Option<Ulid>,
|
||||||
pub client_id: Ulid,
|
pub client_id: Ulid,
|
||||||
pub scope: Scope,
|
pub scope: Scope,
|
||||||
pub user_agent: Option<String>,
|
pub user_agent: Option<UserAgent>,
|
||||||
pub last_active_at: Option<DateTime<Utc>>,
|
pub last_active_at: Option<DateTime<Utc>>,
|
||||||
pub last_active_ip: Option<IpAddr>,
|
pub last_active_ip: Option<IpAddr>,
|
||||||
}
|
}
|
||||||
|
217
crates/data-model/src/user_agent.rs
Normal file
217
crates/data-model/src/user_agent.rs
Normal file
@ -0,0 +1,217 @@
|
|||||||
|
// Copyright 2024 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.
|
||||||
|
|
||||||
|
use serde::Serialize;
|
||||||
|
use woothee::{parser::Parser, woothee::VALUE_UNKNOWN};
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Clone, PartialEq, Eq)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub enum DeviceType {
|
||||||
|
Pc,
|
||||||
|
Mobile,
|
||||||
|
Tablet,
|
||||||
|
Unknown,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Clone, PartialEq, Eq)]
|
||||||
|
pub struct UserAgent {
|
||||||
|
pub name: Option<String>,
|
||||||
|
pub version: Option<String>,
|
||||||
|
pub os: Option<String>,
|
||||||
|
pub os_version: Option<String>,
|
||||||
|
pub model: Option<String>,
|
||||||
|
pub device_type: DeviceType,
|
||||||
|
pub raw: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::ops::Deref for UserAgent {
|
||||||
|
type Target = str;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.raw
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UserAgent {
|
||||||
|
fn parse_custom(user_agent: &str) -> Option<(&str, &str, &str, &str, Option<&str>)> {
|
||||||
|
let regex = regex::Regex::new(r"^(?P<name>[^/]+)/(?P<version>[^ ]+) \((?P<segments>.+)\)$")
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let captures = regex.captures(user_agent)?;
|
||||||
|
let name = captures.name("name")?.as_str();
|
||||||
|
let version = captures.name("version")?.as_str();
|
||||||
|
let segments: Vec<&str> = captures
|
||||||
|
.name("segments")?
|
||||||
|
.as_str()
|
||||||
|
.split(';')
|
||||||
|
.map(str::trim)
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
match segments[..] {
|
||||||
|
["Linux", "U", os, model, ..] | [model, os, ..] => {
|
||||||
|
// Most android model have a `/[build version]` suffix we don't care about
|
||||||
|
let model = model.split_once('/').map_or(model, |(model, _)| model);
|
||||||
|
// Some android version also have `Build/[build version]` suffix we don't care
|
||||||
|
// about
|
||||||
|
let model = model.strip_suffix("Build").unwrap_or(model);
|
||||||
|
// And let's trim any leftovers
|
||||||
|
let model = model.trim();
|
||||||
|
|
||||||
|
let (os, os_version) = if let Some((os, version)) = os.split_once(' ') {
|
||||||
|
(os, Some(version))
|
||||||
|
} else {
|
||||||
|
(os, None)
|
||||||
|
};
|
||||||
|
|
||||||
|
Some((name, version, model, os, os_version))
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn parse(user_agent: String) -> Self {
|
||||||
|
if !user_agent.contains("Mozilla/") {
|
||||||
|
if let Some((name, version, model, os, os_version)) =
|
||||||
|
UserAgent::parse_custom(&user_agent)
|
||||||
|
{
|
||||||
|
let mut device_type = DeviceType::Unknown;
|
||||||
|
|
||||||
|
// Handle mobile simple mobile devices
|
||||||
|
if os == "Android" || os == "iOS" {
|
||||||
|
device_type = DeviceType::Mobile;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle iPads
|
||||||
|
if model.contains("iPad") {
|
||||||
|
device_type = DeviceType::Tablet;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Self {
|
||||||
|
name: Some(name.to_owned()),
|
||||||
|
version: Some(version.to_owned()),
|
||||||
|
os: Some(os.to_owned()),
|
||||||
|
os_version: os_version.map(std::borrow::ToOwned::to_owned),
|
||||||
|
model: Some(model.to_owned()),
|
||||||
|
device_type,
|
||||||
|
raw: user_agent,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut model = None;
|
||||||
|
let Some(mut result) = Parser::new().parse(&user_agent) else {
|
||||||
|
return Self {
|
||||||
|
raw: user_agent,
|
||||||
|
name: None,
|
||||||
|
version: None,
|
||||||
|
os: None,
|
||||||
|
os_version: None,
|
||||||
|
model: None,
|
||||||
|
device_type: DeviceType::Unknown,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut device_type = match result.category {
|
||||||
|
"pc" => DeviceType::Pc,
|
||||||
|
"smartphone" | "mobilephone" => DeviceType::Mobile,
|
||||||
|
_ => DeviceType::Unknown,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Special handling for Chrome user-agent reduction cases
|
||||||
|
// https://www.chromium.org/updates/ua-reduction/
|
||||||
|
match (result.os, &*result.os_version) {
|
||||||
|
// Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/533.88 (KHTML, like Gecko)
|
||||||
|
// Chrome/109.1.2342.76 Safari/533.88
|
||||||
|
("Windows 10", "NT 10.0") if user_agent.contains("Windows NT 10.0; Win64; x64") => {
|
||||||
|
result.os = "Windows";
|
||||||
|
result.os_version = VALUE_UNKNOWN.into();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like
|
||||||
|
// Gecko) Chrome/100.0.4896.133 Safari/537.36
|
||||||
|
("Mac OSX", "10.15.7") if user_agent.contains("Macintosh; Intel Mac OS X 10_15_7") => {
|
||||||
|
result.os = "macOS";
|
||||||
|
result.os_version = VALUE_UNKNOWN.into();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko)
|
||||||
|
// Chrome/100.0.0.0 Safari/537.36
|
||||||
|
("Linux", _) if user_agent.contains("X11; Linux x86_64") => {
|
||||||
|
result.os = "Linux";
|
||||||
|
result.os_version = VALUE_UNKNOWN.into();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mozilla/5.0 (X11; CrOS x86_64 14541.0.0) AppleWebKit/537.36 (KHTML, like Gecko)
|
||||||
|
// Chrome/107.0.0.0 Safari/537.36
|
||||||
|
("ChromeOS", _) if user_agent.contains("X11; CrOS x86_64 14541.0.0") => {
|
||||||
|
result.os = "Chrome OS";
|
||||||
|
result.os_version = VALUE_UNKNOWN.into();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko)
|
||||||
|
// Chrome/100.0.0.0 Mobile Safari/537.36
|
||||||
|
("Android", "10") if user_agent.contains("Linux; Android 10; K") => {
|
||||||
|
result.os = "Android";
|
||||||
|
result.os_version = VALUE_UNKNOWN.into();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Safari also freezes the OS version
|
||||||
|
// Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like
|
||||||
|
// Gecko) Version/17.3.1 Safari/605.1.15
|
||||||
|
("Mac OSX", "10.15.7") if user_agent.contains("Macintosh; Intel Mac OS X 10_15_7") => {
|
||||||
|
result.os = "macOS";
|
||||||
|
result.os_version = VALUE_UNKNOWN.into();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Woothee identifies iPhone and iPod in the OS, but we want to map them to iOS and use
|
||||||
|
// them as model
|
||||||
|
("iPhone" | "iPod", _) => {
|
||||||
|
model = Some(result.os.to_owned());
|
||||||
|
result.os = "iOS";
|
||||||
|
}
|
||||||
|
|
||||||
|
("iPad", _) => {
|
||||||
|
model = Some(result.os.to_owned());
|
||||||
|
device_type = DeviceType::Tablet;
|
||||||
|
result.os = "iPadOS";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also map `Mac OSX` to `macOS`
|
||||||
|
("Mac OSX", _) => {
|
||||||
|
result.os = "macOS";
|
||||||
|
}
|
||||||
|
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// For some reason, the version on Windows is on the OS field
|
||||||
|
// This transforms `Windows 10` into `Windows` and `10`
|
||||||
|
if let Some(version) = result.os.strip_prefix("Windows ") {
|
||||||
|
result.os = "Windows";
|
||||||
|
result.os_version = version.into();
|
||||||
|
}
|
||||||
|
|
||||||
|
Self {
|
||||||
|
name: (result.name != VALUE_UNKNOWN).then(|| result.name.to_owned()),
|
||||||
|
version: (result.version != VALUE_UNKNOWN).then(|| result.version.to_owned()),
|
||||||
|
os: (result.os != VALUE_UNKNOWN).then(|| result.os.to_owned()),
|
||||||
|
os_version: (result.os_version != VALUE_UNKNOWN)
|
||||||
|
.then(|| result.os_version.into_owned()),
|
||||||
|
device_type,
|
||||||
|
model,
|
||||||
|
raw: user_agent,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -19,6 +19,8 @@ use rand::{Rng, SeedableRng};
|
|||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use ulid::Ulid;
|
use ulid::Ulid;
|
||||||
|
|
||||||
|
use crate::UserAgent;
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
|
||||||
pub struct User {
|
pub struct User {
|
||||||
pub id: Ulid,
|
pub id: Ulid,
|
||||||
@ -83,7 +85,7 @@ pub struct BrowserSession {
|
|||||||
pub user: User,
|
pub user: User,
|
||||||
pub created_at: DateTime<Utc>,
|
pub created_at: DateTime<Utc>,
|
||||||
pub finished_at: Option<DateTime<Utc>>,
|
pub finished_at: Option<DateTime<Utc>>,
|
||||||
pub user_agent: Option<String>,
|
pub user_agent: Option<UserAgent>,
|
||||||
pub last_active_at: Option<DateTime<Utc>>,
|
pub last_active_at: Option<DateTime<Utc>>,
|
||||||
pub last_active_ip: Option<IpAddr>,
|
pub last_active_ip: Option<IpAddr>,
|
||||||
}
|
}
|
||||||
@ -105,7 +107,9 @@ impl BrowserSession {
|
|||||||
user,
|
user,
|
||||||
created_at: now,
|
created_at: now,
|
||||||
finished_at: None,
|
finished_at: None,
|
||||||
user_agent: Some("Mozilla/5.0".to_owned()),
|
user_agent: Some(UserAgent::parse(
|
||||||
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.0.0 Safari/537.36".to_owned()
|
||||||
|
)),
|
||||||
last_active_at: Some(now),
|
last_active_at: Some(now),
|
||||||
last_active_ip: None,
|
last_active_ip: None,
|
||||||
})
|
})
|
||||||
|
@ -49,10 +49,10 @@ pub enum Requester {
|
|||||||
Anonymous,
|
Anonymous,
|
||||||
|
|
||||||
/// The requester is a browser session, stored in a cookie.
|
/// The requester is a browser session, stored in a cookie.
|
||||||
BrowserSession(BrowserSession),
|
BrowserSession(Box<BrowserSession>),
|
||||||
|
|
||||||
/// The requester is a OAuth2 session, with an access token.
|
/// The requester is a OAuth2 session, with an access token.
|
||||||
OAuth2Session(Session, Option<User>),
|
OAuth2Session(Box<(Session, Option<User>)>),
|
||||||
}
|
}
|
||||||
|
|
||||||
trait OwnerId {
|
trait OwnerId {
|
||||||
@ -108,21 +108,21 @@ impl Requester {
|
|||||||
fn browser_session(&self) -> Option<&BrowserSession> {
|
fn browser_session(&self) -> Option<&BrowserSession> {
|
||||||
match self {
|
match self {
|
||||||
Self::BrowserSession(session) => Some(session),
|
Self::BrowserSession(session) => Some(session),
|
||||||
Self::OAuth2Session(_, _) | Self::Anonymous => None,
|
Self::OAuth2Session(_) | Self::Anonymous => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn user(&self) -> Option<&User> {
|
fn user(&self) -> Option<&User> {
|
||||||
match self {
|
match self {
|
||||||
Self::BrowserSession(session) => Some(&session.user),
|
Self::BrowserSession(session) => Some(&session.user),
|
||||||
Self::OAuth2Session(_session, user) => user.as_ref(),
|
Self::OAuth2Session(tuple) => tuple.1.as_ref(),
|
||||||
Self::Anonymous => None,
|
Self::Anonymous => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn oauth2_session(&self) -> Option<&Session> {
|
fn oauth2_session(&self) -> Option<&Session> {
|
||||||
match self {
|
match self {
|
||||||
Self::OAuth2Session(session, _) => Some(session),
|
Self::OAuth2Session(tuple) => Some(&tuple.0),
|
||||||
Self::BrowserSession(_) | Self::Anonymous => None,
|
Self::BrowserSession(_) | Self::Anonymous => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -148,10 +148,10 @@ impl Requester {
|
|||||||
|
|
||||||
fn is_admin(&self) -> bool {
|
fn is_admin(&self) -> bool {
|
||||||
match self {
|
match self {
|
||||||
Self::OAuth2Session(session, _user) => {
|
Self::OAuth2Session(tuple) => {
|
||||||
// TODO: is this the right scope?
|
// TODO: is this the right scope?
|
||||||
// This has to be in sync with the policy
|
// This has to be in sync with the policy
|
||||||
session.scope.contains("urn:mas:admin")
|
tuple.0.scope.contains("urn:mas:admin")
|
||||||
}
|
}
|
||||||
Self::BrowserSession(_) | Self::Anonymous => false,
|
Self::BrowserSession(_) | Self::Anonymous => false,
|
||||||
}
|
}
|
||||||
@ -160,7 +160,7 @@ impl Requester {
|
|||||||
|
|
||||||
impl From<BrowserSession> for Requester {
|
impl From<BrowserSession> for Requester {
|
||||||
fn from(session: BrowserSession) -> Self {
|
fn from(session: BrowserSession) -> Self {
|
||||||
Self::BrowserSession(session)
|
Self::BrowserSession(Box::new(session))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -24,7 +24,7 @@ use mas_storage::{
|
|||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
AppSession, CompatSession, Cursor, NodeCursor, NodeType, OAuth2Session, PreloadedTotalCount,
|
AppSession, CompatSession, Cursor, NodeCursor, NodeType, OAuth2Session, PreloadedTotalCount,
|
||||||
SessionState, User,
|
SessionState, User, UserAgent,
|
||||||
};
|
};
|
||||||
use crate::state::ContextExt;
|
use crate::state::ContextExt;
|
||||||
|
|
||||||
@ -87,9 +87,9 @@ impl BrowserSession {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The user-agent string with which the session was created.
|
/// The user-agent with which the session was created.
|
||||||
pub async fn user_agent(&self) -> Option<&str> {
|
pub async fn user_agent(&self) -> Option<UserAgent> {
|
||||||
self.0.user_agent.as_deref()
|
self.0.user_agent.clone().map(UserAgent::from)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The last IP address used by the session.
|
/// The last IP address used by the session.
|
||||||
|
@ -18,7 +18,7 @@ use chrono::{DateTime, Utc};
|
|||||||
use mas_storage::{compat::CompatSessionRepository, user::UserRepository};
|
use mas_storage::{compat::CompatSessionRepository, user::UserRepository};
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
use super::{BrowserSession, NodeType, SessionState, User};
|
use super::{BrowserSession, NodeType, SessionState, User, UserAgent};
|
||||||
use crate::state::ContextExt;
|
use crate::state::ContextExt;
|
||||||
|
|
||||||
/// Lazy-loaded reverse reference.
|
/// Lazy-loaded reverse reference.
|
||||||
@ -103,6 +103,11 @@ impl CompatSession {
|
|||||||
self.session.finished_at()
|
self.session.finished_at()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The user-agent with which the session was created.
|
||||||
|
pub async fn user_agent(&self) -> Option<UserAgent> {
|
||||||
|
self.session.user_agent.clone().map(UserAgent::from)
|
||||||
|
}
|
||||||
|
|
||||||
/// The associated SSO login, if any.
|
/// The associated SSO login, if any.
|
||||||
pub async fn sso_login(
|
pub async fn sso_login(
|
||||||
&self,
|
&self,
|
||||||
|
@ -12,7 +12,7 @@
|
|||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
use async_graphql::{Enum, Interface, Object};
|
use async_graphql::{Enum, Interface, Object, SimpleObject};
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
|
|
||||||
mod browser_sessions;
|
mod browser_sessions;
|
||||||
@ -73,3 +73,69 @@ pub enum SessionState {
|
|||||||
/// The session is no longer active.
|
/// The session is no longer active.
|
||||||
Finished,
|
Finished,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The type of a user agent
|
||||||
|
#[derive(Enum, Copy, Clone, Eq, PartialEq)]
|
||||||
|
pub enum DeviceType {
|
||||||
|
/// A personal computer, laptop or desktop
|
||||||
|
Pc,
|
||||||
|
|
||||||
|
/// A mobile phone. Can also sometimes be a tablet.
|
||||||
|
Mobile,
|
||||||
|
|
||||||
|
/// A tablet
|
||||||
|
Tablet,
|
||||||
|
|
||||||
|
/// Unknown device type
|
||||||
|
Unknown,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<mas_data_model::DeviceType> for DeviceType {
|
||||||
|
fn from(device_type: mas_data_model::DeviceType) -> Self {
|
||||||
|
match device_type {
|
||||||
|
mas_data_model::DeviceType::Pc => Self::Pc,
|
||||||
|
mas_data_model::DeviceType::Mobile => Self::Mobile,
|
||||||
|
mas_data_model::DeviceType::Tablet => Self::Tablet,
|
||||||
|
mas_data_model::DeviceType::Unknown => Self::Unknown,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A parsed user agent string
|
||||||
|
#[derive(SimpleObject)]
|
||||||
|
pub struct UserAgent {
|
||||||
|
/// The user agent string
|
||||||
|
pub raw: String,
|
||||||
|
|
||||||
|
/// The name of the browser
|
||||||
|
pub name: Option<String>,
|
||||||
|
|
||||||
|
/// The version of the browser
|
||||||
|
pub version: Option<String>,
|
||||||
|
|
||||||
|
/// The operating system name
|
||||||
|
pub os: Option<String>,
|
||||||
|
|
||||||
|
/// The operating system version
|
||||||
|
pub os_version: Option<String>,
|
||||||
|
|
||||||
|
/// The device model
|
||||||
|
pub model: Option<String>,
|
||||||
|
|
||||||
|
/// The device type
|
||||||
|
pub device_type: DeviceType,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<mas_data_model::UserAgent> for UserAgent {
|
||||||
|
fn from(ua: mas_data_model::UserAgent) -> Self {
|
||||||
|
Self {
|
||||||
|
raw: ua.raw,
|
||||||
|
name: ua.name,
|
||||||
|
version: ua.version,
|
||||||
|
os: ua.os,
|
||||||
|
os_version: ua.os_version,
|
||||||
|
model: ua.model,
|
||||||
|
device_type: ua.device_type.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -20,7 +20,7 @@ use oauth2_types::{oidc::ApplicationType, scope::Scope};
|
|||||||
use ulid::Ulid;
|
use ulid::Ulid;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
use super::{BrowserSession, NodeType, SessionState, User};
|
use super::{BrowserSession, NodeType, SessionState, User, UserAgent};
|
||||||
use crate::{state::ContextExt, UserId};
|
use crate::{state::ContextExt, UserId};
|
||||||
|
|
||||||
/// An OAuth 2.0 session represents a client session which used the OAuth APIs
|
/// An OAuth 2.0 session represents a client session which used the OAuth APIs
|
||||||
@ -67,6 +67,11 @@ impl OAuth2Session {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The user-agent with which the session was created.
|
||||||
|
pub async fn user_agent(&self) -> Option<UserAgent> {
|
||||||
|
self.0.user_agent.clone().map(UserAgent::from)
|
||||||
|
}
|
||||||
|
|
||||||
/// The state of the session.
|
/// The state of the session.
|
||||||
pub async fn state(&self) -> SessionState {
|
pub async fn state(&self) -> SessionState {
|
||||||
match &self.0.state {
|
match &self.0.state {
|
||||||
|
@ -40,7 +40,7 @@ pub struct EndCompatSessionInput {
|
|||||||
/// The payload of the `endCompatSession` mutation.
|
/// The payload of the `endCompatSession` mutation.
|
||||||
pub enum EndCompatSessionPayload {
|
pub enum EndCompatSessionPayload {
|
||||||
NotFound,
|
NotFound,
|
||||||
Ended(mas_data_model::CompatSession),
|
Ended(Box<mas_data_model::CompatSession>),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The status of the `endCompatSession` mutation.
|
/// The status of the `endCompatSession` mutation.
|
||||||
@ -66,7 +66,7 @@ impl EndCompatSessionPayload {
|
|||||||
/// Returns the ended session.
|
/// Returns the ended session.
|
||||||
async fn compat_session(&self) -> Option<CompatSession> {
|
async fn compat_session(&self) -> Option<CompatSession> {
|
||||||
match self {
|
match self {
|
||||||
Self::Ended(session) => Some(CompatSession::new(session.clone())),
|
Self::Ended(session) => Some(CompatSession::new(*session.clone())),
|
||||||
Self::NotFound => None,
|
Self::NotFound => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -110,6 +110,6 @@ impl CompatSessionMutations {
|
|||||||
|
|
||||||
repo.save().await?;
|
repo.save().await?;
|
||||||
|
|
||||||
Ok(EndCompatSessionPayload::Ended(session))
|
Ok(EndCompatSessionPayload::Ended(Box::new(session)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -31,8 +31,11 @@ impl ViewerQuery {
|
|||||||
|
|
||||||
match requester {
|
match requester {
|
||||||
Requester::BrowserSession(session) => Viewer::user(session.user.clone()),
|
Requester::BrowserSession(session) => Viewer::user(session.user.clone()),
|
||||||
Requester::OAuth2Session(_session, Some(user)) => Viewer::user(user.clone()),
|
Requester::OAuth2Session(tuple) => match &tuple.1 {
|
||||||
Requester::OAuth2Session(_, None) | Requester::Anonymous => Viewer::anonymous(),
|
Some(user) => Viewer::user(user.clone()),
|
||||||
|
None => Viewer::anonymous(),
|
||||||
|
},
|
||||||
|
Requester::Anonymous => Viewer::anonymous(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -41,10 +44,8 @@ impl ViewerQuery {
|
|||||||
let requester = ctx.requester();
|
let requester = ctx.requester();
|
||||||
|
|
||||||
match requester {
|
match requester {
|
||||||
Requester::BrowserSession(session) => ViewerSession::browser_session(session.clone()),
|
Requester::BrowserSession(session) => ViewerSession::browser_session(*session.clone()),
|
||||||
Requester::OAuth2Session(session, _user) => {
|
Requester::OAuth2Session(tuple) => ViewerSession::oauth2_session(tuple.0.clone()),
|
||||||
ViewerSession::oauth2_session(session.clone())
|
|
||||||
}
|
|
||||||
Requester::Anonymous => ViewerSession::anonymous(),
|
Requester::Anonymous => ViewerSession::anonymous(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -16,7 +16,7 @@ use axum::{extract::State, response::IntoResponse, Json, TypedHeader};
|
|||||||
use chrono::Duration;
|
use chrono::Duration;
|
||||||
use hyper::StatusCode;
|
use hyper::StatusCode;
|
||||||
use mas_axum_utils::sentry::SentryEventID;
|
use mas_axum_utils::sentry::SentryEventID;
|
||||||
use mas_data_model::{CompatSession, CompatSsoLoginState, Device, TokenType, User};
|
use mas_data_model::{CompatSession, CompatSsoLoginState, Device, TokenType, User, UserAgent};
|
||||||
use mas_storage::{
|
use mas_storage::{
|
||||||
compat::{
|
compat::{
|
||||||
CompatAccessTokenRepository, CompatRefreshTokenRepository, CompatSessionRepository,
|
CompatAccessTokenRepository, CompatRefreshTokenRepository, CompatSessionRepository,
|
||||||
@ -220,7 +220,7 @@ pub(crate) async fn post(
|
|||||||
user_agent: Option<TypedHeader<headers::UserAgent>>,
|
user_agent: Option<TypedHeader<headers::UserAgent>>,
|
||||||
Json(input): Json<RequestBody>,
|
Json(input): Json<RequestBody>,
|
||||||
) -> Result<impl IntoResponse, RouteError> {
|
) -> Result<impl IntoResponse, RouteError> {
|
||||||
let user_agent = user_agent.map(|ua| ua.to_string());
|
let user_agent = user_agent.map(|ua| UserAgent::parse(ua.as_str().to_owned()));
|
||||||
let (mut session, user) = match (password_manager.is_enabled(), input.credentials) {
|
let (mut session, user) = match (password_manager.is_enabled(), input.credentials) {
|
||||||
(
|
(
|
||||||
true,
|
true,
|
||||||
|
@ -237,7 +237,7 @@ async fn get_requester(
|
|||||||
return Err(RouteError::MissingScope);
|
return Err(RouteError::MissingScope);
|
||||||
}
|
}
|
||||||
|
|
||||||
Requester::OAuth2Session(session, user)
|
Requester::OAuth2Session(Box::new((session, user)))
|
||||||
} else {
|
} else {
|
||||||
let maybe_session = session_info.load_session(&mut repo).await?;
|
let maybe_session = session_info.load_session(&mut repo).await?;
|
||||||
|
|
||||||
|
@ -14,13 +14,14 @@
|
|||||||
|
|
||||||
use axum::{extract::State, response::IntoResponse, Json, TypedHeader};
|
use axum::{extract::State, response::IntoResponse, Json, TypedHeader};
|
||||||
use chrono::Duration;
|
use chrono::Duration;
|
||||||
use headers::{CacheControl, Pragma, UserAgent};
|
use headers::{CacheControl, Pragma};
|
||||||
use hyper::StatusCode;
|
use hyper::StatusCode;
|
||||||
use mas_axum_utils::{
|
use mas_axum_utils::{
|
||||||
client_authorization::{ClientAuthorization, CredentialsVerificationError},
|
client_authorization::{ClientAuthorization, CredentialsVerificationError},
|
||||||
http_client_factory::HttpClientFactory,
|
http_client_factory::HttpClientFactory,
|
||||||
sentry::SentryEventID,
|
sentry::SentryEventID,
|
||||||
};
|
};
|
||||||
|
use mas_data_model::UserAgent;
|
||||||
use mas_keystore::Encrypter;
|
use mas_keystore::Encrypter;
|
||||||
use mas_router::UrlBuilder;
|
use mas_router::UrlBuilder;
|
||||||
use mas_storage::{oauth2::OAuth2DeviceCodeGrantParams, BoxClock, BoxRepository, BoxRng};
|
use mas_storage::{oauth2::OAuth2DeviceCodeGrantParams, BoxClock, BoxRepository, BoxRng};
|
||||||
@ -84,7 +85,7 @@ pub(crate) async fn post(
|
|||||||
mut rng: BoxRng,
|
mut rng: BoxRng,
|
||||||
clock: BoxClock,
|
clock: BoxClock,
|
||||||
mut repo: BoxRepository,
|
mut repo: BoxRepository,
|
||||||
user_agent: Option<TypedHeader<UserAgent>>,
|
user_agent: Option<TypedHeader<headers::UserAgent>>,
|
||||||
activity_tracker: BoundActivityTracker,
|
activity_tracker: BoundActivityTracker,
|
||||||
State(url_builder): State<UrlBuilder>,
|
State(url_builder): State<UrlBuilder>,
|
||||||
State(http_client_factory): State<HttpClientFactory>,
|
State(http_client_factory): State<HttpClientFactory>,
|
||||||
@ -125,7 +126,7 @@ pub(crate) async fn post(
|
|||||||
|
|
||||||
let expires_in = Duration::minutes(20);
|
let expires_in = Duration::minutes(20);
|
||||||
|
|
||||||
let user_agent = user_agent.map(|ua| ua.0.to_string());
|
let user_agent = user_agent.map(|ua| UserAgent::parse(ua.as_str().to_owned()));
|
||||||
let ip_address = activity_tracker.ip();
|
let ip_address = activity_tracker.ip();
|
||||||
|
|
||||||
let device_code = Alphanumeric.sample_string(&mut rng, 32);
|
let device_code = Alphanumeric.sample_string(&mut rng, 32);
|
||||||
|
@ -21,7 +21,9 @@ use mas_axum_utils::{
|
|||||||
http_client_factory::HttpClientFactory,
|
http_client_factory::HttpClientFactory,
|
||||||
sentry::SentryEventID,
|
sentry::SentryEventID,
|
||||||
};
|
};
|
||||||
use mas_data_model::{AuthorizationGrantStage, Client, Device, DeviceCodeGrantState, TokenType};
|
use mas_data_model::{
|
||||||
|
AuthorizationGrantStage, Client, Device, DeviceCodeGrantState, TokenType, UserAgent,
|
||||||
|
};
|
||||||
use mas_keystore::{Encrypter, Keystore};
|
use mas_keystore::{Encrypter, Keystore};
|
||||||
use mas_oidc_client::types::scope::ScopeToken;
|
use mas_oidc_client::types::scope::ScopeToken;
|
||||||
use mas_policy::Policy;
|
use mas_policy::Policy;
|
||||||
@ -233,7 +235,7 @@ pub(crate) async fn post(
|
|||||||
user_agent: Option<TypedHeader<headers::UserAgent>>,
|
user_agent: Option<TypedHeader<headers::UserAgent>>,
|
||||||
client_authorization: ClientAuthorization<AccessTokenRequest>,
|
client_authorization: ClientAuthorization<AccessTokenRequest>,
|
||||||
) -> Result<impl IntoResponse, RouteError> {
|
) -> Result<impl IntoResponse, RouteError> {
|
||||||
let user_agent = user_agent.map(|ua| ua.to_string());
|
let user_agent = user_agent.map(|ua| UserAgent::parse(ua.as_str().to_owned()));
|
||||||
let client = client_authorization
|
let client = client_authorization
|
||||||
.credentials
|
.credentials
|
||||||
.fetch(&mut repo)
|
.fetch(&mut repo)
|
||||||
@ -335,7 +337,7 @@ async fn authorization_code_grant(
|
|||||||
url_builder: &UrlBuilder,
|
url_builder: &UrlBuilder,
|
||||||
site_config: &SiteConfig,
|
site_config: &SiteConfig,
|
||||||
mut repo: BoxRepository,
|
mut repo: BoxRepository,
|
||||||
user_agent: Option<String>,
|
user_agent: Option<UserAgent>,
|
||||||
) -> Result<(AccessTokenResponse, BoxRepository), RouteError> {
|
) -> Result<(AccessTokenResponse, BoxRepository), RouteError> {
|
||||||
// Check that the client is allowed to use this grant type
|
// Check that the client is allowed to use this grant type
|
||||||
if !client.grant_types.contains(&GrantType::AuthorizationCode) {
|
if !client.grant_types.contains(&GrantType::AuthorizationCode) {
|
||||||
@ -504,7 +506,7 @@ async fn refresh_token_grant(
|
|||||||
client: &Client,
|
client: &Client,
|
||||||
site_config: &SiteConfig,
|
site_config: &SiteConfig,
|
||||||
mut repo: BoxRepository,
|
mut repo: BoxRepository,
|
||||||
user_agent: Option<String>,
|
user_agent: Option<UserAgent>,
|
||||||
) -> Result<(AccessTokenResponse, BoxRepository), RouteError> {
|
) -> Result<(AccessTokenResponse, BoxRepository), RouteError> {
|
||||||
// Check that the client is allowed to use this grant type
|
// Check that the client is allowed to use this grant type
|
||||||
if !client.grant_types.contains(&GrantType::RefreshToken) {
|
if !client.grant_types.contains(&GrantType::RefreshToken) {
|
||||||
@ -587,7 +589,7 @@ async fn client_credentials_grant(
|
|||||||
site_config: &SiteConfig,
|
site_config: &SiteConfig,
|
||||||
mut repo: BoxRepository,
|
mut repo: BoxRepository,
|
||||||
mut policy: Policy,
|
mut policy: Policy,
|
||||||
user_agent: Option<String>,
|
user_agent: Option<UserAgent>,
|
||||||
) -> Result<(AccessTokenResponse, BoxRepository), RouteError> {
|
) -> Result<(AccessTokenResponse, BoxRepository), RouteError> {
|
||||||
// Check that the client is allowed to use this grant type
|
// Check that the client is allowed to use this grant type
|
||||||
if !client.grant_types.contains(&GrantType::ClientCredentials) {
|
if !client.grant_types.contains(&GrantType::ClientCredentials) {
|
||||||
@ -656,7 +658,7 @@ async fn device_code_grant(
|
|||||||
url_builder: &UrlBuilder,
|
url_builder: &UrlBuilder,
|
||||||
site_config: &SiteConfig,
|
site_config: &SiteConfig,
|
||||||
mut repo: BoxRepository,
|
mut repo: BoxRepository,
|
||||||
user_agent: Option<String>,
|
user_agent: Option<UserAgent>,
|
||||||
) -> Result<(AccessTokenResponse, BoxRepository), RouteError> {
|
) -> Result<(AccessTokenResponse, BoxRepository), RouteError> {
|
||||||
// Check that the client is allowed to use this grant type
|
// Check that the client is allowed to use this grant type
|
||||||
if !client.grant_types.contains(&GrantType::DeviceCode) {
|
if !client.grant_types.contains(&GrantType::DeviceCode) {
|
||||||
|
@ -24,7 +24,7 @@ use mas_axum_utils::{
|
|||||||
sentry::SentryEventID,
|
sentry::SentryEventID,
|
||||||
FancyError, SessionInfoExt,
|
FancyError, SessionInfoExt,
|
||||||
};
|
};
|
||||||
use mas_data_model::User;
|
use mas_data_model::{User, UserAgent};
|
||||||
use mas_jose::jwt::Jwt;
|
use mas_jose::jwt::Jwt;
|
||||||
use mas_policy::Policy;
|
use mas_policy::Policy;
|
||||||
use mas_router::UrlBuilder;
|
use mas_router::UrlBuilder;
|
||||||
@ -200,7 +200,7 @@ pub(crate) async fn get(
|
|||||||
user_agent: Option<TypedHeader<headers::UserAgent>>,
|
user_agent: Option<TypedHeader<headers::UserAgent>>,
|
||||||
Path(link_id): Path<Ulid>,
|
Path(link_id): Path<Ulid>,
|
||||||
) -> Result<impl IntoResponse, RouteError> {
|
) -> Result<impl IntoResponse, RouteError> {
|
||||||
let user_agent = user_agent.map(|ua| ua.as_str().to_owned());
|
let user_agent = user_agent.map(|ua| UserAgent::parse(ua.as_str().to_owned()));
|
||||||
let sessions_cookie = UpstreamSessionsCookie::load(&cookie_jar);
|
let sessions_cookie = UpstreamSessionsCookie::load(&cookie_jar);
|
||||||
let (session_id, post_auth_action) = sessions_cookie
|
let (session_id, post_auth_action) = sessions_cookie
|
||||||
.lookup_link(link_id)
|
.lookup_link(link_id)
|
||||||
@ -481,7 +481,7 @@ pub(crate) async fn post(
|
|||||||
Path(link_id): Path<Ulid>,
|
Path(link_id): Path<Ulid>,
|
||||||
Form(form): Form<ProtectedForm<FormData>>,
|
Form(form): Form<ProtectedForm<FormData>>,
|
||||||
) -> Result<Response, RouteError> {
|
) -> Result<Response, RouteError> {
|
||||||
let user_agent = user_agent.map(|ua| ua.as_str().to_owned());
|
let user_agent = user_agent.map(|ua| UserAgent::parse(ua.as_str().to_owned()));
|
||||||
let form = cookie_jar.verify_form(&clock, form)?;
|
let form = cookie_jar.verify_form(&clock, form)?;
|
||||||
|
|
||||||
let sessions_cookie = UpstreamSessionsCookie::load(&cookie_jar);
|
let sessions_cookie = UpstreamSessionsCookie::load(&cookie_jar);
|
||||||
|
@ -17,14 +17,13 @@ use axum::{
|
|||||||
response::{Html, IntoResponse, Response},
|
response::{Html, IntoResponse, Response},
|
||||||
TypedHeader,
|
TypedHeader,
|
||||||
};
|
};
|
||||||
use headers::UserAgent;
|
|
||||||
use hyper::StatusCode;
|
use hyper::StatusCode;
|
||||||
use mas_axum_utils::{
|
use mas_axum_utils::{
|
||||||
cookies::CookieJar,
|
cookies::CookieJar,
|
||||||
csrf::{CsrfExt, CsrfToken, ProtectedForm},
|
csrf::{CsrfExt, CsrfToken, ProtectedForm},
|
||||||
FancyError, SessionInfoExt,
|
FancyError, SessionInfoExt,
|
||||||
};
|
};
|
||||||
use mas_data_model::BrowserSession;
|
use mas_data_model::{BrowserSession, UserAgent};
|
||||||
use mas_i18n::DataLocale;
|
use mas_i18n::DataLocale;
|
||||||
use mas_router::{UpstreamOAuth2Authorize, UrlBuilder};
|
use mas_router::{UpstreamOAuth2Authorize, UrlBuilder};
|
||||||
use mas_storage::{
|
use mas_storage::{
|
||||||
@ -123,10 +122,10 @@ pub(crate) async fn post(
|
|||||||
activity_tracker: BoundActivityTracker,
|
activity_tracker: BoundActivityTracker,
|
||||||
Query(query): Query<OptionalPostAuthAction>,
|
Query(query): Query<OptionalPostAuthAction>,
|
||||||
cookie_jar: CookieJar,
|
cookie_jar: CookieJar,
|
||||||
user_agent: Option<TypedHeader<UserAgent>>,
|
user_agent: Option<TypedHeader<headers::UserAgent>>,
|
||||||
Form(form): Form<ProtectedForm<LoginForm>>,
|
Form(form): Form<ProtectedForm<LoginForm>>,
|
||||||
) -> Result<Response, FancyError> {
|
) -> Result<Response, FancyError> {
|
||||||
let user_agent = user_agent.map(|ua| ua.as_str().to_owned());
|
let user_agent = user_agent.map(|ua| UserAgent::parse(ua.as_str().to_owned()));
|
||||||
if !password_manager.is_enabled() {
|
if !password_manager.is_enabled() {
|
||||||
// XXX: is it necessary to have better errors here?
|
// XXX: is it necessary to have better errors here?
|
||||||
return Ok(StatusCode::METHOD_NOT_ALLOWED.into_response());
|
return Ok(StatusCode::METHOD_NOT_ALLOWED.into_response());
|
||||||
@ -216,7 +215,7 @@ async fn login(
|
|||||||
clock: &impl Clock,
|
clock: &impl Clock,
|
||||||
username: &str,
|
username: &str,
|
||||||
password: &str,
|
password: &str,
|
||||||
user_agent: Option<String>,
|
user_agent: Option<UserAgent>,
|
||||||
) -> Result<BrowserSession, FormError> {
|
) -> Result<BrowserSession, FormError> {
|
||||||
// XXX: we're loosing the error context here
|
// XXX: we're loosing the error context here
|
||||||
// First, lookup the user
|
// First, lookup the user
|
||||||
|
@ -19,7 +19,6 @@ use axum::{
|
|||||||
response::{Html, IntoResponse, Response},
|
response::{Html, IntoResponse, Response},
|
||||||
TypedHeader,
|
TypedHeader,
|
||||||
};
|
};
|
||||||
use headers::UserAgent;
|
|
||||||
use hyper::StatusCode;
|
use hyper::StatusCode;
|
||||||
use lettre::Address;
|
use lettre::Address;
|
||||||
use mas_axum_utils::{
|
use mas_axum_utils::{
|
||||||
@ -27,6 +26,7 @@ use mas_axum_utils::{
|
|||||||
csrf::{CsrfExt, CsrfToken, ProtectedForm},
|
csrf::{CsrfExt, CsrfToken, ProtectedForm},
|
||||||
FancyError, SessionInfoExt,
|
FancyError, SessionInfoExt,
|
||||||
};
|
};
|
||||||
|
use mas_data_model::UserAgent;
|
||||||
use mas_i18n::DataLocale;
|
use mas_i18n::DataLocale;
|
||||||
use mas_policy::Policy;
|
use mas_policy::Policy;
|
||||||
use mas_router::UrlBuilder;
|
use mas_router::UrlBuilder;
|
||||||
@ -116,10 +116,10 @@ pub(crate) async fn post(
|
|||||||
activity_tracker: BoundActivityTracker,
|
activity_tracker: BoundActivityTracker,
|
||||||
Query(query): Query<OptionalPostAuthAction>,
|
Query(query): Query<OptionalPostAuthAction>,
|
||||||
cookie_jar: CookieJar,
|
cookie_jar: CookieJar,
|
||||||
user_agent: Option<TypedHeader<UserAgent>>,
|
user_agent: Option<TypedHeader<headers::UserAgent>>,
|
||||||
Form(form): Form<ProtectedForm<RegisterForm>>,
|
Form(form): Form<ProtectedForm<RegisterForm>>,
|
||||||
) -> Result<Response, FancyError> {
|
) -> Result<Response, FancyError> {
|
||||||
let user_agent = user_agent.map(|ua| ua.as_str().to_owned());
|
let user_agent = user_agent.map(|ua| UserAgent::parse(ua.as_str().to_owned()));
|
||||||
if !password_manager.is_enabled() {
|
if !password_manager.is_enabled() {
|
||||||
return Ok(StatusCode::METHOD_NOT_ALLOWED.into_response());
|
return Ok(StatusCode::METHOD_NOT_ALLOWED.into_response());
|
||||||
}
|
}
|
||||||
|
@ -15,7 +15,7 @@
|
|||||||
//! A module containing PostgreSQL implementation of repositories for sessions
|
//! A module containing PostgreSQL implementation of repositories for sessions
|
||||||
|
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use mas_data_model::{CompatSession, CompatSessionState, Device, Session, SessionState};
|
use mas_data_model::{CompatSession, CompatSessionState, Device, Session, SessionState, UserAgent};
|
||||||
use mas_storage::{
|
use mas_storage::{
|
||||||
app_session::{AppSession, AppSessionFilter, AppSessionRepository},
|
app_session::{AppSession, AppSessionFilter, AppSessionRepository},
|
||||||
Page, Pagination,
|
Page, Pagination,
|
||||||
@ -84,6 +84,7 @@ use priv_::{AppSessionLookup, AppSessionLookupIden};
|
|||||||
impl TryFrom<AppSessionLookup> for AppSession {
|
impl TryFrom<AppSessionLookup> for AppSession {
|
||||||
type Error = DatabaseError;
|
type Error = DatabaseError;
|
||||||
|
|
||||||
|
#[allow(clippy::too_many_lines)]
|
||||||
fn try_from(value: AppSessionLookup) -> Result<Self, Self::Error> {
|
fn try_from(value: AppSessionLookup) -> Result<Self, Self::Error> {
|
||||||
// This is annoying to do, but we have to match on all the fields to determine
|
// This is annoying to do, but we have to match on all the fields to determine
|
||||||
// whether it's a compat session or an oauth2 session
|
// whether it's a compat session or an oauth2 session
|
||||||
@ -104,6 +105,7 @@ impl TryFrom<AppSessionLookup> for AppSession {
|
|||||||
last_active_ip,
|
last_active_ip,
|
||||||
} = value;
|
} = value;
|
||||||
|
|
||||||
|
let user_agent = user_agent.map(UserAgent::parse);
|
||||||
let user_session_id = user_session_id.map(Ulid::from);
|
let user_session_id = user_session_id.map(Ulid::from);
|
||||||
|
|
||||||
match (
|
match (
|
||||||
|
@ -28,7 +28,7 @@ pub use self::{
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use chrono::Duration;
|
use chrono::Duration;
|
||||||
use mas_data_model::Device;
|
use mas_data_model::{Device, UserAgent};
|
||||||
use mas_storage::{
|
use mas_storage::{
|
||||||
clock::MockClock,
|
clock::MockClock,
|
||||||
compat::{
|
compat::{
|
||||||
@ -133,7 +133,7 @@ mod tests {
|
|||||||
assert!(session_lookup.user_agent.is_none());
|
assert!(session_lookup.user_agent.is_none());
|
||||||
let session = repo
|
let session = repo
|
||||||
.compat_session()
|
.compat_session()
|
||||||
.record_user_agent(session_lookup, "Mozilla/5.0".to_owned())
|
.record_user_agent(session_lookup, UserAgent::parse("Mozilla/5.0".to_owned()))
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(session.user_agent.as_deref(), Some("Mozilla/5.0"));
|
assert_eq!(session.user_agent.as_deref(), Some("Mozilla/5.0"));
|
||||||
|
@ -18,7 +18,7 @@ use async_trait::async_trait;
|
|||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use mas_data_model::{
|
use mas_data_model::{
|
||||||
BrowserSession, CompatSession, CompatSessionState, CompatSsoLogin, CompatSsoLoginState, Device,
|
BrowserSession, CompatSession, CompatSessionState, CompatSsoLogin, CompatSsoLoginState, Device,
|
||||||
User,
|
User, UserAgent,
|
||||||
};
|
};
|
||||||
use mas_storage::{
|
use mas_storage::{
|
||||||
compat::{CompatSessionFilter, CompatSessionRepository},
|
compat::{CompatSessionFilter, CompatSessionRepository},
|
||||||
@ -90,7 +90,7 @@ impl TryFrom<CompatSessionLookup> for CompatSession {
|
|||||||
device,
|
device,
|
||||||
created_at: value.created_at,
|
created_at: value.created_at,
|
||||||
is_synapse_admin: value.is_synapse_admin,
|
is_synapse_admin: value.is_synapse_admin,
|
||||||
user_agent: value.user_agent,
|
user_agent: value.user_agent.map(UserAgent::parse),
|
||||||
last_active_at: value.last_active_at,
|
last_active_at: value.last_active_at,
|
||||||
last_active_ip: value.last_active_ip,
|
last_active_ip: value.last_active_ip,
|
||||||
};
|
};
|
||||||
@ -145,7 +145,7 @@ impl TryFrom<CompatSessionAndSsoLoginLookup> for (CompatSession, Option<CompatSs
|
|||||||
user_session_id: value.user_session_id.map(Ulid::from),
|
user_session_id: value.user_session_id.map(Ulid::from),
|
||||||
created_at: value.created_at,
|
created_at: value.created_at,
|
||||||
is_synapse_admin: value.is_synapse_admin,
|
is_synapse_admin: value.is_synapse_admin,
|
||||||
user_agent: value.user_agent,
|
user_agent: value.user_agent.map(UserAgent::parse),
|
||||||
last_active_at: value.last_active_at,
|
last_active_at: value.last_active_at,
|
||||||
last_active_ip: value.last_active_ip,
|
last_active_ip: value.last_active_ip,
|
||||||
};
|
};
|
||||||
@ -575,7 +575,7 @@ impl<'c> CompatSessionRepository for PgCompatSessionRepository<'c> {
|
|||||||
async fn record_user_agent(
|
async fn record_user_agent(
|
||||||
&mut self,
|
&mut self,
|
||||||
mut compat_session: CompatSession,
|
mut compat_session: CompatSession,
|
||||||
user_agent: String,
|
user_agent: UserAgent,
|
||||||
) -> Result<CompatSession, Self::Error> {
|
) -> Result<CompatSession, Self::Error> {
|
||||||
let res = sqlx::query!(
|
let res = sqlx::query!(
|
||||||
r#"
|
r#"
|
||||||
@ -584,7 +584,7 @@ impl<'c> CompatSessionRepository for PgCompatSessionRepository<'c> {
|
|||||||
WHERE compat_session_id = $1
|
WHERE compat_session_id = $1
|
||||||
"#,
|
"#,
|
||||||
Uuid::from(compat_session.id),
|
Uuid::from(compat_session.id),
|
||||||
user_agent,
|
&*user_agent,
|
||||||
)
|
)
|
||||||
.traced()
|
.traced()
|
||||||
.execute(&mut *self.conn)
|
.execute(&mut *self.conn)
|
||||||
|
@ -16,7 +16,7 @@ use std::net::IpAddr;
|
|||||||
|
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use mas_data_model::{BrowserSession, DeviceCodeGrant, DeviceCodeGrantState, Session};
|
use mas_data_model::{BrowserSession, DeviceCodeGrant, DeviceCodeGrantState, Session, UserAgent};
|
||||||
use mas_storage::{
|
use mas_storage::{
|
||||||
oauth2::{OAuth2DeviceCodeGrantParams, OAuth2DeviceCodeGrantRepository},
|
oauth2::{OAuth2DeviceCodeGrantParams, OAuth2DeviceCodeGrantRepository},
|
||||||
Clock,
|
Clock,
|
||||||
@ -140,7 +140,7 @@ impl TryFrom<OAuth2DeviceGrantLookup> for DeviceCodeGrant {
|
|||||||
created_at,
|
created_at,
|
||||||
expires_at,
|
expires_at,
|
||||||
ip_address,
|
ip_address,
|
||||||
user_agent,
|
user_agent: user_agent.map(UserAgent::parse),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -32,7 +32,7 @@ pub use self::{
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use chrono::Duration;
|
use chrono::Duration;
|
||||||
use mas_data_model::AuthorizationCode;
|
use mas_data_model::{AuthorizationCode, UserAgent};
|
||||||
use mas_storage::{
|
use mas_storage::{
|
||||||
clock::MockClock,
|
clock::MockClock,
|
||||||
oauth2::{OAuth2DeviceCodeGrantParams, OAuth2SessionFilter, OAuth2SessionRepository},
|
oauth2::{OAuth2DeviceCodeGrantParams, OAuth2SessionFilter, OAuth2SessionRepository},
|
||||||
@ -371,7 +371,7 @@ mod tests {
|
|||||||
assert!(session.user_agent.is_none());
|
assert!(session.user_agent.is_none());
|
||||||
let session = repo
|
let session = repo
|
||||||
.oauth2_session()
|
.oauth2_session()
|
||||||
.record_user_agent(session, "Mozilla/5.0".to_owned())
|
.record_user_agent(session, UserAgent::parse("Mozilla/5.0".to_owned()))
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(session.user_agent.as_deref(), Some("Mozilla/5.0"));
|
assert_eq!(session.user_agent.as_deref(), Some("Mozilla/5.0"));
|
||||||
|
@ -16,7 +16,7 @@ use std::net::IpAddr;
|
|||||||
|
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use mas_data_model::{BrowserSession, Client, Session, SessionState, User};
|
use mas_data_model::{BrowserSession, Client, Session, SessionState, User, UserAgent};
|
||||||
use mas_storage::{
|
use mas_storage::{
|
||||||
oauth2::{OAuth2SessionFilter, OAuth2SessionRepository},
|
oauth2::{OAuth2SessionFilter, OAuth2SessionRepository},
|
||||||
Clock, Page, Pagination,
|
Clock, Page, Pagination,
|
||||||
@ -94,7 +94,7 @@ impl TryFrom<OAuthSessionLookup> for Session {
|
|||||||
user_id: value.user_id.map(Ulid::from),
|
user_id: value.user_id.map(Ulid::from),
|
||||||
user_session_id: value.user_session_id.map(Ulid::from),
|
user_session_id: value.user_session_id.map(Ulid::from),
|
||||||
scope,
|
scope,
|
||||||
user_agent: value.user_agent,
|
user_agent: value.user_agent.map(UserAgent::parse),
|
||||||
last_active_at: value.last_active_at,
|
last_active_at: value.last_active_at,
|
||||||
last_active_ip: value.last_active_ip,
|
last_active_ip: value.last_active_ip,
|
||||||
})
|
})
|
||||||
@ -444,14 +444,14 @@ impl<'c> OAuth2SessionRepository for PgOAuth2SessionRepository<'c> {
|
|||||||
%session.id,
|
%session.id,
|
||||||
%session.scope,
|
%session.scope,
|
||||||
client.id = %session.client_id,
|
client.id = %session.client_id,
|
||||||
session.user_agent = %user_agent,
|
session.user_agent = %user_agent.raw,
|
||||||
),
|
),
|
||||||
err,
|
err,
|
||||||
)]
|
)]
|
||||||
async fn record_user_agent(
|
async fn record_user_agent(
|
||||||
&mut self,
|
&mut self,
|
||||||
mut session: Session,
|
mut session: Session,
|
||||||
user_agent: String,
|
user_agent: UserAgent,
|
||||||
) -> Result<Session, Self::Error> {
|
) -> Result<Session, Self::Error> {
|
||||||
let res = sqlx::query!(
|
let res = sqlx::query!(
|
||||||
r#"
|
r#"
|
||||||
@ -460,7 +460,7 @@ impl<'c> OAuth2SessionRepository for PgOAuth2SessionRepository<'c> {
|
|||||||
WHERE oauth2_session_id = $1
|
WHERE oauth2_session_id = $1
|
||||||
"#,
|
"#,
|
||||||
Uuid::from(session.id),
|
Uuid::from(session.id),
|
||||||
user_agent,
|
&*user_agent,
|
||||||
)
|
)
|
||||||
.traced()
|
.traced()
|
||||||
.execute(&mut *self.conn)
|
.execute(&mut *self.conn)
|
||||||
|
@ -18,7 +18,7 @@ use async_trait::async_trait;
|
|||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use mas_data_model::{
|
use mas_data_model::{
|
||||||
Authentication, AuthenticationMethod, BrowserSession, Password,
|
Authentication, AuthenticationMethod, BrowserSession, Password,
|
||||||
UpstreamOAuthAuthorizationSession, User,
|
UpstreamOAuthAuthorizationSession, User, UserAgent,
|
||||||
};
|
};
|
||||||
use mas_storage::{user::BrowserSessionRepository, Clock, Page, Pagination};
|
use mas_storage::{user::BrowserSessionRepository, Clock, Page, Pagination};
|
||||||
use rand::RngCore;
|
use rand::RngCore;
|
||||||
@ -86,7 +86,7 @@ impl TryFrom<SessionLookup> for BrowserSession {
|
|||||||
user,
|
user,
|
||||||
created_at: value.user_session_created_at,
|
created_at: value.user_session_created_at,
|
||||||
finished_at: value.user_session_finished_at,
|
finished_at: value.user_session_finished_at,
|
||||||
user_agent: value.user_session_user_agent,
|
user_agent: value.user_session_user_agent.map(UserAgent::parse),
|
||||||
last_active_at: value.user_session_last_active_at,
|
last_active_at: value.user_session_last_active_at,
|
||||||
last_active_ip: value.user_session_last_active_ip,
|
last_active_ip: value.user_session_last_active_ip,
|
||||||
})
|
})
|
||||||
@ -189,7 +189,7 @@ impl<'c> BrowserSessionRepository for PgBrowserSessionRepository<'c> {
|
|||||||
rng: &mut (dyn RngCore + Send),
|
rng: &mut (dyn RngCore + Send),
|
||||||
clock: &dyn Clock,
|
clock: &dyn Clock,
|
||||||
user: &User,
|
user: &User,
|
||||||
user_agent: Option<String>,
|
user_agent: Option<UserAgent>,
|
||||||
) -> Result<BrowserSession, Self::Error> {
|
) -> Result<BrowserSession, Self::Error> {
|
||||||
let created_at = clock.now();
|
let created_at = clock.now();
|
||||||
let id = Ulid::from_datetime_with_source(created_at.into(), rng);
|
let id = Ulid::from_datetime_with_source(created_at.into(), rng);
|
||||||
@ -203,7 +203,7 @@ impl<'c> BrowserSessionRepository for PgBrowserSessionRepository<'c> {
|
|||||||
Uuid::from(id),
|
Uuid::from(id),
|
||||||
Uuid::from(user.id),
|
Uuid::from(user.id),
|
||||||
created_at,
|
created_at,
|
||||||
user_agent,
|
user_agent.as_deref(),
|
||||||
)
|
)
|
||||||
.traced()
|
.traced()
|
||||||
.execute(&mut *self.conn)
|
.execute(&mut *self.conn)
|
||||||
|
@ -16,7 +16,7 @@ use std::net::IpAddr;
|
|||||||
|
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use mas_data_model::{BrowserSession, CompatSession, CompatSsoLogin, Device, User};
|
use mas_data_model::{BrowserSession, CompatSession, CompatSsoLogin, Device, User, UserAgent};
|
||||||
use rand_core::RngCore;
|
use rand_core::RngCore;
|
||||||
use ulid::Ulid;
|
use ulid::Ulid;
|
||||||
|
|
||||||
@ -266,7 +266,7 @@ pub trait CompatSessionRepository: Send + Sync {
|
|||||||
async fn record_user_agent(
|
async fn record_user_agent(
|
||||||
&mut self,
|
&mut self,
|
||||||
compat_session: CompatSession,
|
compat_session: CompatSession,
|
||||||
user_agent: String,
|
user_agent: UserAgent,
|
||||||
) -> Result<CompatSession, Self::Error>;
|
) -> Result<CompatSession, Self::Error>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -305,6 +305,6 @@ repository_impl!(CompatSessionRepository:
|
|||||||
async fn record_user_agent(
|
async fn record_user_agent(
|
||||||
&mut self,
|
&mut self,
|
||||||
compat_session: CompatSession,
|
compat_session: CompatSession,
|
||||||
user_agent: String,
|
user_agent: UserAgent,
|
||||||
) -> Result<CompatSession, Self::Error>;
|
) -> Result<CompatSession, Self::Error>;
|
||||||
);
|
);
|
||||||
|
@ -16,7 +16,7 @@ use std::net::IpAddr;
|
|||||||
|
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use chrono::Duration;
|
use chrono::Duration;
|
||||||
use mas_data_model::{BrowserSession, Client, DeviceCodeGrant, Session};
|
use mas_data_model::{BrowserSession, Client, DeviceCodeGrant, Session, UserAgent};
|
||||||
use oauth2_types::scope::Scope;
|
use oauth2_types::scope::Scope;
|
||||||
use rand_core::RngCore;
|
use rand_core::RngCore;
|
||||||
use ulid::Ulid;
|
use ulid::Ulid;
|
||||||
@ -44,7 +44,7 @@ pub struct OAuth2DeviceCodeGrantParams<'a> {
|
|||||||
pub ip_address: Option<IpAddr>,
|
pub ip_address: Option<IpAddr>,
|
||||||
|
|
||||||
/// The user agent from which the request was made
|
/// The user agent from which the request was made
|
||||||
pub user_agent: Option<String>,
|
pub user_agent: Option<UserAgent>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// An [`OAuth2DeviceCodeGrantRepository`] helps interacting with
|
/// An [`OAuth2DeviceCodeGrantRepository`] helps interacting with
|
||||||
|
@ -16,7 +16,7 @@ use std::net::IpAddr;
|
|||||||
|
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use mas_data_model::{BrowserSession, Client, Session, User};
|
use mas_data_model::{BrowserSession, Client, Session, User, UserAgent};
|
||||||
use oauth2_types::scope::Scope;
|
use oauth2_types::scope::Scope;
|
||||||
use rand_core::RngCore;
|
use rand_core::RngCore;
|
||||||
use ulid::Ulid;
|
use ulid::Ulid;
|
||||||
@ -296,7 +296,7 @@ pub trait OAuth2SessionRepository: Send + Sync {
|
|||||||
async fn record_user_agent(
|
async fn record_user_agent(
|
||||||
&mut self,
|
&mut self,
|
||||||
session: Session,
|
session: Session,
|
||||||
user_agent: String,
|
user_agent: UserAgent,
|
||||||
) -> Result<Session, Self::Error>;
|
) -> Result<Session, Self::Error>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -349,6 +349,6 @@ repository_impl!(OAuth2SessionRepository:
|
|||||||
async fn record_user_agent(
|
async fn record_user_agent(
|
||||||
&mut self,
|
&mut self,
|
||||||
session: Session,
|
session: Session,
|
||||||
user_agent: String,
|
user_agent: UserAgent,
|
||||||
) -> Result<Session, Self::Error>;
|
) -> Result<Session, Self::Error>;
|
||||||
);
|
);
|
||||||
|
@ -17,7 +17,7 @@ use std::net::IpAddr;
|
|||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use mas_data_model::{
|
use mas_data_model::{
|
||||||
Authentication, BrowserSession, Password, UpstreamOAuthAuthorizationSession, User,
|
Authentication, BrowserSession, Password, UpstreamOAuthAuthorizationSession, User, UserAgent,
|
||||||
};
|
};
|
||||||
use rand_core::RngCore;
|
use rand_core::RngCore;
|
||||||
use ulid::Ulid;
|
use ulid::Ulid;
|
||||||
@ -127,7 +127,7 @@ pub trait BrowserSessionRepository: Send + Sync {
|
|||||||
rng: &mut (dyn RngCore + Send),
|
rng: &mut (dyn RngCore + Send),
|
||||||
clock: &dyn Clock,
|
clock: &dyn Clock,
|
||||||
user: &User,
|
user: &User,
|
||||||
user_agent: Option<String>,
|
user_agent: Option<UserAgent>,
|
||||||
) -> Result<BrowserSession, Self::Error>;
|
) -> Result<BrowserSession, Self::Error>;
|
||||||
|
|
||||||
/// Finish a [`BrowserSession`]
|
/// Finish a [`BrowserSession`]
|
||||||
@ -254,7 +254,7 @@ repository_impl!(BrowserSessionRepository:
|
|||||||
rng: &mut (dyn RngCore + Send),
|
rng: &mut (dyn RngCore + Send),
|
||||||
clock: &dyn Clock,
|
clock: &dyn Clock,
|
||||||
user: &User,
|
user: &User,
|
||||||
user_agent: Option<String>,
|
user_agent: Option<UserAgent>,
|
||||||
) -> Result<BrowserSession, Self::Error>;
|
) -> Result<BrowserSession, Self::Error>;
|
||||||
async fn finish(
|
async fn finish(
|
||||||
&mut self,
|
&mut self,
|
||||||
|
@ -25,7 +25,7 @@ use chrono::{DateTime, Duration, Utc};
|
|||||||
use http::{Method, Uri, Version};
|
use http::{Method, Uri, Version};
|
||||||
use mas_data_model::{
|
use mas_data_model::{
|
||||||
AuthorizationGrant, BrowserSession, Client, CompatSsoLogin, CompatSsoLoginState,
|
AuthorizationGrant, BrowserSession, Client, CompatSsoLogin, CompatSsoLoginState,
|
||||||
DeviceCodeGrant, UpstreamOAuthLink, UpstreamOAuthProvider, User, UserEmail,
|
DeviceCodeGrant, UpstreamOAuthLink, UpstreamOAuthProvider, User, UserAgent, UserEmail,
|
||||||
UserEmailVerification,
|
UserEmailVerification,
|
||||||
};
|
};
|
||||||
use mas_i18n::DataLocale;
|
use mas_i18n::DataLocale;
|
||||||
@ -1164,7 +1164,7 @@ impl TemplateContext for DeviceConsentContext {
|
|||||||
created_at: now - Duration::minutes(5),
|
created_at: now - Duration::minutes(5),
|
||||||
expires_at: now + Duration::minutes(25),
|
expires_at: now + Duration::minutes(25),
|
||||||
ip_address: Some(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1))),
|
ip_address: Some(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1))),
|
||||||
user_agent: Some("Mozilla/5.0".to_owned()),
|
user_agent: Some(UserAgent::parse("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.0.0 Safari/537.36".to_owned())),
|
||||||
};
|
};
|
||||||
Self { grant, client }
|
Self { grant, client }
|
||||||
})
|
})
|
||||||
|
@ -102,7 +102,7 @@ module.exports = {
|
|||||||
exceptions: {
|
exceptions: {
|
||||||
// The '*Connection', '*Edge', '*Payload' and 'PageInfo' types don't have IDs
|
// The '*Connection', '*Edge', '*Payload' and 'PageInfo' types don't have IDs
|
||||||
// XXX: Maybe the MatrixUser type should have an ID?
|
// XXX: Maybe the MatrixUser type should have an ID?
|
||||||
types: ["PageInfo", "MatrixUser"],
|
types: ["PageInfo", "MatrixUser", "UserAgent"],
|
||||||
suffixes: ["Connection", "Edge", "Payload"],
|
suffixes: ["Connection", "Edge", "Payload"],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -55,10 +55,10 @@
|
|||||||
"session_details_title": "Session"
|
"session_details_title": "Session"
|
||||||
},
|
},
|
||||||
"device_type_icon_label": {
|
"device_type_icon_label": {
|
||||||
"desktop": "Desktop",
|
|
||||||
"mobile": "Mobile",
|
"mobile": "Mobile",
|
||||||
"unknown": "Unknown device type",
|
"pc": "Computer",
|
||||||
"web": "Web"
|
"tablet": "Tablet",
|
||||||
|
"unknown": "Unknown device type"
|
||||||
},
|
},
|
||||||
"end_session_button": {
|
"end_session_button": {
|
||||||
"confirmation_modal_title": "Are you sure you want to end this session?",
|
"confirmation_modal_title": "Are you sure you want to end this session?",
|
||||||
|
@ -212,9 +212,9 @@ type BrowserSession implements Node & CreationEvent {
|
|||||||
"""
|
"""
|
||||||
state: SessionState!
|
state: SessionState!
|
||||||
"""
|
"""
|
||||||
The user-agent string with which the session was created.
|
The user-agent with which the session was created.
|
||||||
"""
|
"""
|
||||||
userAgent: String
|
userAgent: UserAgent
|
||||||
"""
|
"""
|
||||||
The last IP address used by the session.
|
The last IP address used by the session.
|
||||||
"""
|
"""
|
||||||
@ -314,6 +314,10 @@ type CompatSession implements Node & CreationEvent {
|
|||||||
"""
|
"""
|
||||||
finishedAt: DateTime
|
finishedAt: DateTime
|
||||||
"""
|
"""
|
||||||
|
The user-agent with which the session was created.
|
||||||
|
"""
|
||||||
|
userAgent: UserAgent
|
||||||
|
"""
|
||||||
The associated SSO login, if any.
|
The associated SSO login, if any.
|
||||||
"""
|
"""
|
||||||
ssoLogin: CompatSsoLogin
|
ssoLogin: CompatSsoLogin
|
||||||
@ -500,6 +504,28 @@ The input/output is a string in RFC3339 format.
|
|||||||
"""
|
"""
|
||||||
scalar DateTime
|
scalar DateTime
|
||||||
|
|
||||||
|
"""
|
||||||
|
The type of a user agent
|
||||||
|
"""
|
||||||
|
enum DeviceType {
|
||||||
|
"""
|
||||||
|
A personal computer, laptop or desktop
|
||||||
|
"""
|
||||||
|
PC
|
||||||
|
"""
|
||||||
|
A mobile phone. Can also sometimes be a tablet.
|
||||||
|
"""
|
||||||
|
MOBILE
|
||||||
|
"""
|
||||||
|
A tablet
|
||||||
|
"""
|
||||||
|
TABLET
|
||||||
|
"""
|
||||||
|
Unknown device type
|
||||||
|
"""
|
||||||
|
UNKNOWN
|
||||||
|
}
|
||||||
|
|
||||||
"""
|
"""
|
||||||
The input of the `endBrowserSession` mutation.
|
The input of the `endBrowserSession` mutation.
|
||||||
"""
|
"""
|
||||||
@ -822,6 +848,10 @@ type Oauth2Session implements Node & CreationEvent {
|
|||||||
"""
|
"""
|
||||||
finishedAt: DateTime
|
finishedAt: DateTime
|
||||||
"""
|
"""
|
||||||
|
The user-agent with which the session was created.
|
||||||
|
"""
|
||||||
|
userAgent: UserAgent
|
||||||
|
"""
|
||||||
The state of the session.
|
The state of the session.
|
||||||
"""
|
"""
|
||||||
state: SessionState!
|
state: SessionState!
|
||||||
@ -1529,6 +1559,40 @@ type User implements Node {
|
|||||||
): AppSessionConnection!
|
): AppSessionConnection!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
"""
|
||||||
|
A parsed user agent string
|
||||||
|
"""
|
||||||
|
type UserAgent {
|
||||||
|
"""
|
||||||
|
The user agent string
|
||||||
|
"""
|
||||||
|
raw: String!
|
||||||
|
"""
|
||||||
|
The name of the browser
|
||||||
|
"""
|
||||||
|
name: String
|
||||||
|
"""
|
||||||
|
The version of the browser
|
||||||
|
"""
|
||||||
|
version: String
|
||||||
|
"""
|
||||||
|
The operating system name
|
||||||
|
"""
|
||||||
|
os: String
|
||||||
|
"""
|
||||||
|
The operating system version
|
||||||
|
"""
|
||||||
|
osVersion: String
|
||||||
|
"""
|
||||||
|
The device model
|
||||||
|
"""
|
||||||
|
model: String
|
||||||
|
"""
|
||||||
|
The device type
|
||||||
|
"""
|
||||||
|
deviceType: DeviceType!
|
||||||
|
}
|
||||||
|
|
||||||
"""
|
"""
|
||||||
A user email address
|
A user email address
|
||||||
"""
|
"""
|
||||||
|
@ -17,10 +17,6 @@ import { useCallback } from "react";
|
|||||||
import { useMutation } from "urql";
|
import { useMutation } from "urql";
|
||||||
|
|
||||||
import { FragmentType, graphql, useFragment } from "../gql";
|
import { FragmentType, graphql, useFragment } from "../gql";
|
||||||
import {
|
|
||||||
parseUserAgent,
|
|
||||||
sessionNameFromDeviceInformation,
|
|
||||||
} from "../utils/parseUserAgent";
|
|
||||||
|
|
||||||
import EndSessionButton from "./Session/EndSessionButton";
|
import EndSessionButton from "./Session/EndSessionButton";
|
||||||
import Session from "./Session/Session";
|
import Session from "./Session/Session";
|
||||||
@ -30,7 +26,13 @@ const FRAGMENT = graphql(/* GraphQL */ `
|
|||||||
id
|
id
|
||||||
createdAt
|
createdAt
|
||||||
finishedAt
|
finishedAt
|
||||||
userAgent
|
userAgent {
|
||||||
|
raw
|
||||||
|
name
|
||||||
|
os
|
||||||
|
model
|
||||||
|
deviceType
|
||||||
|
}
|
||||||
lastActiveIp
|
lastActiveIp
|
||||||
lastActiveAt
|
lastActiveAt
|
||||||
lastAuthentication {
|
lastAuthentication {
|
||||||
@ -83,9 +85,16 @@ const BrowserSession: React.FC<Props> = ({ session, isCurrent }) => {
|
|||||||
const lastActiveAt = data.lastActiveAt
|
const lastActiveAt = data.lastActiveAt
|
||||||
? parseISO(data.lastActiveAt)
|
? parseISO(data.lastActiveAt)
|
||||||
: undefined;
|
: undefined;
|
||||||
const deviceInformation = parseUserAgent(data.userAgent || undefined);
|
let sessionName = "Browser session";
|
||||||
const sessionName =
|
if (data.userAgent) {
|
||||||
sessionNameFromDeviceInformation(deviceInformation) || "Browser session";
|
if (data.userAgent.model && data.userAgent.name) {
|
||||||
|
sessionName = `${data.userAgent.name} on ${data.userAgent.model}`;
|
||||||
|
} else if (data.userAgent.name && data.userAgent.os) {
|
||||||
|
sessionName = `${data.userAgent.name} on ${data.userAgent.os}`;
|
||||||
|
} else if (data.userAgent.name) {
|
||||||
|
sessionName = data.userAgent.name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Session
|
<Session
|
||||||
@ -94,7 +103,7 @@ const BrowserSession: React.FC<Props> = ({ session, isCurrent }) => {
|
|||||||
createdAt={createdAt}
|
createdAt={createdAt}
|
||||||
finishedAt={finishedAt}
|
finishedAt={finishedAt}
|
||||||
isCurrent={isCurrent}
|
isCurrent={isCurrent}
|
||||||
deviceType={deviceInformation?.deviceType}
|
deviceType={data.userAgent?.deviceType}
|
||||||
lastActiveIp={data.lastActiveIp || undefined}
|
lastActiveIp={data.lastActiveIp || undefined}
|
||||||
lastActiveAt={lastActiveAt}
|
lastActiveAt={lastActiveAt}
|
||||||
>
|
>
|
||||||
|
@ -28,6 +28,13 @@ export const FRAGMENT = graphql(/* GraphQL */ `
|
|||||||
finishedAt
|
finishedAt
|
||||||
lastActiveIp
|
lastActiveIp
|
||||||
lastActiveAt
|
lastActiveAt
|
||||||
|
userAgent {
|
||||||
|
raw
|
||||||
|
name
|
||||||
|
os
|
||||||
|
model
|
||||||
|
deviceType
|
||||||
|
}
|
||||||
ssoLogin {
|
ssoLogin {
|
||||||
id
|
id
|
||||||
redirectUri
|
redirectUri
|
||||||
@ -78,10 +85,20 @@ const CompatSession: React.FC<{
|
|||||||
await endCompatSession({ id: data.id });
|
await endCompatSession({ id: data.id });
|
||||||
};
|
};
|
||||||
|
|
||||||
const clientName = data.ssoLogin?.redirectUri
|
let clientName = data.ssoLogin?.redirectUri
|
||||||
? simplifyUrl(data.ssoLogin.redirectUri)
|
? simplifyUrl(data.ssoLogin.redirectUri)
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
|
if (data.userAgent) {
|
||||||
|
if (data.userAgent.model && data.userAgent.name) {
|
||||||
|
clientName = `${data.userAgent.name} on ${data.userAgent.model}`;
|
||||||
|
} else if (data.userAgent.name && data.userAgent.os) {
|
||||||
|
clientName = `${data.userAgent.name} on ${data.userAgent.os}`;
|
||||||
|
} else if (data.userAgent.name) {
|
||||||
|
clientName = data.userAgent.name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const createdAt = parseISO(data.createdAt);
|
const createdAt = parseISO(data.createdAt);
|
||||||
const finishedAt = data.finishedAt ? parseISO(data.finishedAt) : undefined;
|
const finishedAt = data.finishedAt ? parseISO(data.finishedAt) : undefined;
|
||||||
const lastActiveAt = data.lastActiveAt
|
const lastActiveAt = data.lastActiveAt
|
||||||
@ -95,6 +112,7 @@ const CompatSession: React.FC<{
|
|||||||
createdAt={createdAt}
|
createdAt={createdAt}
|
||||||
finishedAt={finishedAt}
|
finishedAt={finishedAt}
|
||||||
clientName={clientName}
|
clientName={clientName}
|
||||||
|
deviceType={data.userAgent?.deviceType}
|
||||||
lastActiveIp={data.lastActiveIp || undefined}
|
lastActiveIp={data.lastActiveIp || undefined}
|
||||||
lastActiveAt={lastActiveAt}
|
lastActiveAt={lastActiveAt}
|
||||||
>
|
>
|
||||||
|
@ -16,9 +16,8 @@ import { parseISO } from "date-fns";
|
|||||||
import { useMutation } from "urql";
|
import { useMutation } from "urql";
|
||||||
|
|
||||||
import { FragmentType, graphql, useFragment } from "../gql";
|
import { FragmentType, graphql, useFragment } from "../gql";
|
||||||
import { Oauth2ApplicationType } from "../gql/graphql";
|
import { DeviceType, Oauth2ApplicationType } from "../gql/graphql";
|
||||||
import { getDeviceIdFromScope } from "../utils/deviceIdFromScope";
|
import { getDeviceIdFromScope } from "../utils/deviceIdFromScope";
|
||||||
import { DeviceType } from "../utils/parseUserAgent";
|
|
||||||
|
|
||||||
import { Session } from "./Session";
|
import { Session } from "./Session";
|
||||||
import EndSessionButton from "./Session/EndSessionButton";
|
import EndSessionButton from "./Session/EndSessionButton";
|
||||||
@ -31,6 +30,14 @@ export const FRAGMENT = graphql(/* GraphQL */ `
|
|||||||
finishedAt
|
finishedAt
|
||||||
lastActiveIp
|
lastActiveIp
|
||||||
lastActiveAt
|
lastActiveAt
|
||||||
|
|
||||||
|
userAgent {
|
||||||
|
model
|
||||||
|
os
|
||||||
|
osVersion
|
||||||
|
deviceType
|
||||||
|
}
|
||||||
|
|
||||||
client {
|
client {
|
||||||
id
|
id
|
||||||
clientId
|
clientId
|
||||||
@ -57,7 +64,7 @@ const getDeviceTypeFromClientAppType = (
|
|||||||
appType?: Oauth2ApplicationType | null,
|
appType?: Oauth2ApplicationType | null,
|
||||||
): DeviceType => {
|
): DeviceType => {
|
||||||
if (appType === Oauth2ApplicationType.Web) {
|
if (appType === Oauth2ApplicationType.Web) {
|
||||||
return DeviceType.Web;
|
return DeviceType.Pc;
|
||||||
}
|
}
|
||||||
if (appType === Oauth2ApplicationType.Native) {
|
if (appType === Oauth2ApplicationType.Native) {
|
||||||
return DeviceType.Mobile;
|
return DeviceType.Mobile;
|
||||||
@ -85,9 +92,17 @@ const OAuth2Session: React.FC<Props> = ({ session }) => {
|
|||||||
? parseISO(data.lastActiveAt)
|
? parseISO(data.lastActiveAt)
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
const deviceType = getDeviceTypeFromClientAppType(
|
const deviceType =
|
||||||
data.client.applicationType,
|
(data.userAgent?.deviceType === DeviceType.Unknown
|
||||||
);
|
? null
|
||||||
|
: data.userAgent?.deviceType) ??
|
||||||
|
getDeviceTypeFromClientAppType(data.client.applicationType);
|
||||||
|
|
||||||
|
let clientName = data.client.clientName || data.client.clientId || undefined;
|
||||||
|
|
||||||
|
if (data.userAgent?.model) {
|
||||||
|
clientName = `${clientName} on ${data.userAgent.model}`;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Session
|
<Session
|
||||||
@ -95,7 +110,7 @@ const OAuth2Session: React.FC<Props> = ({ session }) => {
|
|||||||
name={deviceId}
|
name={deviceId}
|
||||||
createdAt={createdAt}
|
createdAt={createdAt}
|
||||||
finishedAt={finishedAt}
|
finishedAt={finishedAt}
|
||||||
clientName={data.client.clientName || data.client.clientId || undefined}
|
clientName={clientName}
|
||||||
clientLogoUri={data.client.logoUri || undefined}
|
clientLogoUri={data.client.logoUri || undefined}
|
||||||
deviceType={deviceType}
|
deviceType={deviceType}
|
||||||
lastActiveIp={data.lastActiveIp || undefined}
|
lastActiveIp={data.lastActiveIp || undefined}
|
||||||
|
@ -14,7 +14,7 @@
|
|||||||
|
|
||||||
import type { Meta, StoryObj } from "@storybook/react";
|
import type { Meta, StoryObj } from "@storybook/react";
|
||||||
|
|
||||||
import { DeviceType } from "../../utils/parseUserAgent";
|
import { DeviceType } from "../../gql/graphql";
|
||||||
|
|
||||||
import DeviceTypeIcon from "./DeviceTypeIcon";
|
import DeviceTypeIcon from "./DeviceTypeIcon";
|
||||||
|
|
||||||
@ -30,9 +30,9 @@ const meta = {
|
|||||||
control: "select",
|
control: "select",
|
||||||
options: [
|
options: [
|
||||||
DeviceType.Unknown,
|
DeviceType.Unknown,
|
||||||
DeviceType.Desktop,
|
DeviceType.Pc,
|
||||||
DeviceType.Mobile,
|
DeviceType.Mobile,
|
||||||
DeviceType.Web,
|
DeviceType.Tablet,
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -43,9 +43,9 @@ type Story = StoryObj<typeof DeviceTypeIcon>;
|
|||||||
|
|
||||||
export const Unknown: Story = {};
|
export const Unknown: Story = {};
|
||||||
|
|
||||||
export const Desktop: Story = {
|
export const Pc: Story = {
|
||||||
args: {
|
args: {
|
||||||
deviceType: DeviceType.Desktop,
|
deviceType: DeviceType.Pc,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
export const Mobile: Story = {
|
export const Mobile: Story = {
|
||||||
@ -53,8 +53,8 @@ export const Mobile: Story = {
|
|||||||
deviceType: DeviceType.Mobile,
|
deviceType: DeviceType.Mobile,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
export const Web: Story = {
|
export const Tablet: Story = {
|
||||||
args: {
|
args: {
|
||||||
deviceType: DeviceType.Web,
|
deviceType: DeviceType.Tablet,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -18,7 +18,7 @@ import { composeStory } from "@storybook/react";
|
|||||||
import { render, cleanup } from "@testing-library/react";
|
import { render, cleanup } from "@testing-library/react";
|
||||||
import { describe, it, expect, afterEach } from "vitest";
|
import { describe, it, expect, afterEach } from "vitest";
|
||||||
|
|
||||||
import Meta, { Unknown, Desktop, Mobile, Web } from "./DeviceTypeIcon.stories";
|
import Meta, { Unknown, Pc, Mobile, Tablet } from "./DeviceTypeIcon.stories";
|
||||||
|
|
||||||
describe("<DeviceTypeIcon />", () => {
|
describe("<DeviceTypeIcon />", () => {
|
||||||
afterEach(cleanup);
|
afterEach(cleanup);
|
||||||
@ -33,13 +33,13 @@ describe("<DeviceTypeIcon />", () => {
|
|||||||
const { container } = render(<Component />);
|
const { container } = render(<Component />);
|
||||||
expect(container).toMatchSnapshot();
|
expect(container).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
it("renders desktop device type", () => {
|
it("renders pc device type", () => {
|
||||||
const Component = composeStory(Desktop, Meta);
|
const Component = composeStory(Pc, Meta);
|
||||||
const { container } = render(<Component />);
|
const { container } = render(<Component />);
|
||||||
expect(container).toMatchSnapshot();
|
expect(container).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
it("renders Web device type", () => {
|
it("renders tablet device type", () => {
|
||||||
const Component = composeStory(Web, Meta);
|
const Component = composeStory(Tablet, Meta);
|
||||||
const { container } = render(<Component />);
|
const { container } = render(<Component />);
|
||||||
expect(container).toMatchSnapshot();
|
expect(container).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
@ -19,7 +19,7 @@ import IconBrowser from "@vector-im/compound-design-tokens/icons/web-browser.svg
|
|||||||
import { FunctionComponent, SVGProps } from "react";
|
import { FunctionComponent, SVGProps } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import { DeviceType } from "../../utils/parseUserAgent";
|
import { DeviceType } from "../../gql/graphql";
|
||||||
|
|
||||||
import styles from "./DeviceTypeIcon.module.css";
|
import styles from "./DeviceTypeIcon.module.css";
|
||||||
|
|
||||||
@ -28,9 +28,9 @@ const deviceTypeToIcon: Record<
|
|||||||
FunctionComponent<SVGProps<SVGSVGElement> & { title?: string | undefined }>
|
FunctionComponent<SVGProps<SVGSVGElement> & { title?: string | undefined }>
|
||||||
> = {
|
> = {
|
||||||
[DeviceType.Unknown]: IconUnknown,
|
[DeviceType.Unknown]: IconUnknown,
|
||||||
[DeviceType.Desktop]: IconComputer,
|
[DeviceType.Pc]: IconComputer,
|
||||||
[DeviceType.Mobile]: IconMobile,
|
[DeviceType.Mobile]: IconMobile,
|
||||||
[DeviceType.Web]: IconBrowser,
|
[DeviceType.Tablet]: IconBrowser,
|
||||||
};
|
};
|
||||||
|
|
||||||
const DeviceTypeIcon: React.FC<{ deviceType: DeviceType }> = ({
|
const DeviceTypeIcon: React.FC<{ deviceType: DeviceType }> = ({
|
||||||
@ -42,9 +42,9 @@ const DeviceTypeIcon: React.FC<{ deviceType: DeviceType }> = ({
|
|||||||
|
|
||||||
const deviceTypeToLabel: Record<DeviceType, string> = {
|
const deviceTypeToLabel: Record<DeviceType, string> = {
|
||||||
[DeviceType.Unknown]: t("frontend.device_type_icon_label.unknown"),
|
[DeviceType.Unknown]: t("frontend.device_type_icon_label.unknown"),
|
||||||
[DeviceType.Desktop]: t("frontend.device_type_icon_label.desktop"),
|
[DeviceType.Pc]: t("frontend.device_type_icon_label.pc"),
|
||||||
[DeviceType.Mobile]: t("frontend.device_type_icon_label.mobile"),
|
[DeviceType.Mobile]: t("frontend.device_type_icon_label.mobile"),
|
||||||
[DeviceType.Web]: t("frontend.device_type_icon_label.web"),
|
[DeviceType.Tablet]: t("frontend.device_type_icon_label.tablet"),
|
||||||
};
|
};
|
||||||
|
|
||||||
const label = deviceTypeToLabel[deviceType];
|
const label = deviceTypeToLabel[deviceType];
|
||||||
|
@ -16,7 +16,7 @@ import { Link } from "@tanstack/react-router";
|
|||||||
import { H6, Text, Badge } from "@vector-im/compound-web";
|
import { H6, Text, Badge } from "@vector-im/compound-web";
|
||||||
import { Trans, useTranslation } from "react-i18next";
|
import { Trans, useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import { DeviceType } from "../../utils/parseUserAgent";
|
import { DeviceType } from "../../gql/graphql";
|
||||||
import Block from "../Block";
|
import Block from "../Block";
|
||||||
import DateTime from "../DateTime";
|
import DateTime from "../DateTime";
|
||||||
|
|
||||||
|
@ -1,41 +1,5 @@
|
|||||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||||
|
|
||||||
exports[`<DeviceTypeIcon /> > renders Web device type 1`] = `
|
|
||||||
<div>
|
|
||||||
<svg
|
|
||||||
aria-label="Web"
|
|
||||||
class="_icon_e677aa"
|
|
||||||
fill="currentColor"
|
|
||||||
height="1em"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
width="1em"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M4 20c-.55 0-1.02-.196-1.413-.587A1.926 1.926 0 0 1 2 18V6c0-.55.196-1.02.587-1.412A1.926 1.926 0 0 1 4 4h16c.55 0 1.02.196 1.413.588.391.391.587.862.587 1.412v12c0 .55-.196 1.02-.587 1.413A1.926 1.926 0 0 1 20 20H4Zm0-2h16V8H4v10Z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`<DeviceTypeIcon /> > renders desktop device type 1`] = `
|
|
||||||
<div>
|
|
||||||
<svg
|
|
||||||
aria-label="Desktop"
|
|
||||||
class="_icon_e677aa"
|
|
||||||
fill="currentColor"
|
|
||||||
height="1em"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
width="1em"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M4 18c-.55 0-1.02-.196-1.413-.587A1.926 1.926 0 0 1 2 16V5c0-.55.196-1.02.587-1.413A1.926 1.926 0 0 1 4 3h16c.55 0 1.02.196 1.413.587.39.393.587.863.587 1.413v11c0 .55-.196 1.02-.587 1.413A1.926 1.926 0 0 1 20 18H4Zm0-2h16V5H4v11Zm-2 5a.967.967 0 0 1-.712-.288A.968.968 0 0 1 1 20c0-.283.096-.52.288-.712A.967.967 0 0 1 2 19h20c.283 0 .52.096.712.288.192.191.288.429.288.712s-.096.52-.288.712A.968.968 0 0 1 22 21H2Z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`<DeviceTypeIcon /> > renders mobile device type 1`] = `
|
exports[`<DeviceTypeIcon /> > renders mobile device type 1`] = `
|
||||||
<div>
|
<div>
|
||||||
<svg
|
<svg
|
||||||
@ -54,6 +18,42 @@ exports[`<DeviceTypeIcon /> > renders mobile device type 1`] = `
|
|||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports[`<DeviceTypeIcon /> > renders pc device type 1`] = `
|
||||||
|
<div>
|
||||||
|
<svg
|
||||||
|
aria-label="Computer"
|
||||||
|
class="_icon_e677aa"
|
||||||
|
fill="currentColor"
|
||||||
|
height="1em"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width="1em"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M4 18c-.55 0-1.02-.196-1.413-.587A1.926 1.926 0 0 1 2 16V5c0-.55.196-1.02.587-1.413A1.926 1.926 0 0 1 4 3h16c.55 0 1.02.196 1.413.587.39.393.587.863.587 1.413v11c0 .55-.196 1.02-.587 1.413A1.926 1.926 0 0 1 20 18H4Zm0-2h16V5H4v11Zm-2 5a.967.967 0 0 1-.712-.288A.968.968 0 0 1 1 20c0-.283.096-.52.288-.712A.967.967 0 0 1 2 19h20c.283 0 .52.096.712.288.192.191.288.429.288.712s-.096.52-.288.712A.968.968 0 0 1 22 21H2Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`<DeviceTypeIcon /> > renders tablet device type 1`] = `
|
||||||
|
<div>
|
||||||
|
<svg
|
||||||
|
aria-label="Tablet"
|
||||||
|
class="_icon_e677aa"
|
||||||
|
fill="currentColor"
|
||||||
|
height="1em"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width="1em"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M4 20c-.55 0-1.02-.196-1.413-.587A1.926 1.926 0 0 1 2 18V6c0-.55.196-1.02.587-1.412A1.926 1.926 0 0 1 4 4h16c.55 0 1.02.196 1.413.588.391.391.587.862.587 1.412v12c0 .55-.196 1.02-.587 1.413A1.926 1.926 0 0 1 20 20H4Zm0-2h16V8H4v10Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
exports[`<DeviceTypeIcon /> > renders unknown device type 1`] = `
|
exports[`<DeviceTypeIcon /> > renders unknown device type 1`] = `
|
||||||
<div>
|
<div>
|
||||||
<svg
|
<svg
|
||||||
|
@ -17,10 +17,6 @@ import { parseISO } from "date-fns";
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import { FragmentType, graphql, useFragment } from "../../gql";
|
import { FragmentType, graphql, useFragment } from "../../gql";
|
||||||
import {
|
|
||||||
parseUserAgent,
|
|
||||||
sessionNameFromDeviceInformation,
|
|
||||||
} from "../../utils/parseUserAgent";
|
|
||||||
import BlockList from "../BlockList/BlockList";
|
import BlockList from "../BlockList/BlockList";
|
||||||
import { useEndBrowserSession } from "../BrowserSession";
|
import { useEndBrowserSession } from "../BrowserSession";
|
||||||
import DateTime from "../DateTime";
|
import DateTime from "../DateTime";
|
||||||
@ -36,7 +32,11 @@ const FRAGMENT = graphql(/* GraphQL */ `
|
|||||||
id
|
id
|
||||||
createdAt
|
createdAt
|
||||||
finishedAt
|
finishedAt
|
||||||
userAgent
|
userAgent {
|
||||||
|
name
|
||||||
|
model
|
||||||
|
os
|
||||||
|
}
|
||||||
lastActiveIp
|
lastActiveIp
|
||||||
lastActiveAt
|
lastActiveAt
|
||||||
lastAuthentication {
|
lastAuthentication {
|
||||||
@ -61,9 +61,16 @@ const BrowserSessionDetail: React.FC<Props> = ({ session, isCurrent }) => {
|
|||||||
|
|
||||||
const onSessionEnd = useEndBrowserSession(data.id, isCurrent);
|
const onSessionEnd = useEndBrowserSession(data.id, isCurrent);
|
||||||
|
|
||||||
const deviceInformation = parseUserAgent(data.userAgent || undefined);
|
let sessionName = "Browser session";
|
||||||
const sessionName =
|
if (data.userAgent) {
|
||||||
sessionNameFromDeviceInformation(deviceInformation) || "Browser session";
|
if (data.userAgent.model && data.userAgent.name) {
|
||||||
|
sessionName = `${data.userAgent.name} on ${data.userAgent.model}`;
|
||||||
|
} else if (data.userAgent.name && data.userAgent.os) {
|
||||||
|
sessionName = `${data.userAgent.name} on ${data.userAgent.os}`;
|
||||||
|
} else if (data.userAgent.name) {
|
||||||
|
sessionName = data.userAgent.name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const finishedAt = data.finishedAt
|
const finishedAt = data.finishedAt
|
||||||
? [
|
? [
|
||||||
|
@ -35,6 +35,11 @@ export const FRAGMENT = graphql(/* GraphQL */ `
|
|||||||
finishedAt
|
finishedAt
|
||||||
lastActiveIp
|
lastActiveIp
|
||||||
lastActiveAt
|
lastActiveAt
|
||||||
|
userAgent {
|
||||||
|
name
|
||||||
|
os
|
||||||
|
model
|
||||||
|
}
|
||||||
ssoLogin {
|
ssoLogin {
|
||||||
id
|
id
|
||||||
redirectUri
|
redirectUri
|
||||||
@ -102,7 +107,7 @@ const CompatSessionDetail: React.FC<Props> = ({ session }) => {
|
|||||||
if (data.ssoLogin?.redirectUri) {
|
if (data.ssoLogin?.redirectUri) {
|
||||||
clientDetails.push({
|
clientDetails.push({
|
||||||
label: t("frontend.compat_session_detail.name"),
|
label: t("frontend.compat_session_detail.name"),
|
||||||
value: simplifyUrl(data.ssoLogin.redirectUri),
|
value: data.userAgent?.name ?? simplifyUrl(data.ssoLogin.redirectUri),
|
||||||
});
|
});
|
||||||
clientDetails.push({
|
clientDetails.push({
|
||||||
label: t("frontend.session.uri_label"),
|
label: t("frontend.session.uri_label"),
|
||||||
|
@ -5,7 +5,7 @@ exports[`<OAuth2Session /> > renders a finished session 1`] = `
|
|||||||
className="_block_17898c _session_634806"
|
className="_block_17898c _session_634806"
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
aria-label="Web"
|
aria-label="Computer"
|
||||||
className="_icon_e677aa"
|
className="_icon_e677aa"
|
||||||
fill="currentColor"
|
fill="currentColor"
|
||||||
height="1em"
|
height="1em"
|
||||||
@ -14,7 +14,7 @@ exports[`<OAuth2Session /> > renders a finished session 1`] = `
|
|||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
d="M4 20c-.55 0-1.02-.196-1.413-.587A1.926 1.926 0 0 1 2 18V6c0-.55.196-1.02.587-1.412A1.926 1.926 0 0 1 4 4h16c.55 0 1.02.196 1.413.588.391.391.587.862.587 1.412v12c0 .55-.196 1.02-.587 1.413A1.926 1.926 0 0 1 20 20H4Zm0-2h16V8H4v10Z"
|
d="M4 18c-.55 0-1.02-.196-1.413-.587A1.926 1.926 0 0 1 2 16V5c0-.55.196-1.02.587-1.413A1.926 1.926 0 0 1 4 3h16c.55 0 1.02.196 1.413.587.39.393.587.863.587 1.413v11c0 .55-.196 1.02-.587 1.413A1.926 1.926 0 0 1 20 18H4Zm0-2h16V5H4v11Zm-2 5a.967.967 0 0 1-.712-.288A.968.968 0 0 1 1 20c0-.283.096-.52.288-.712A.967.967 0 0 1 2 19h20c.283 0 .52.096.712.288.192.191.288.429.288.712s-.096.52-.288.712A.968.968 0 0 1 22 21H2Z"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
<div
|
<div
|
||||||
|
@ -13,23 +13,23 @@ import { TypedDocumentNode as DocumentNode } from "@graphql-typed-document-node/
|
|||||||
* Therefore it is highly recommended to use the babel or swc plugin for production.
|
* Therefore it is highly recommended to use the babel or swc plugin for production.
|
||||||
*/
|
*/
|
||||||
const documents = {
|
const documents = {
|
||||||
"\n fragment BrowserSession_session on BrowserSession {\n id\n createdAt\n finishedAt\n userAgent\n lastActiveIp\n lastActiveAt\n lastAuthentication {\n id\n createdAt\n }\n }\n":
|
"\n fragment BrowserSession_session on BrowserSession {\n id\n createdAt\n finishedAt\n userAgent {\n raw\n name\n os\n model\n deviceType\n }\n lastActiveIp\n lastActiveAt\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,
|
||||||
"\n fragment OAuth2Client_detail on Oauth2Client {\n id\n clientId\n clientName\n clientUri\n logoUri\n tosUri\n policyUri\n redirectUris\n }\n":
|
"\n fragment OAuth2Client_detail on Oauth2Client {\n id\n clientId\n clientName\n clientUri\n logoUri\n tosUri\n policyUri\n redirectUris\n }\n":
|
||||||
types.OAuth2Client_DetailFragmentDoc,
|
types.OAuth2Client_DetailFragmentDoc,
|
||||||
"\n fragment CompatSession_session on CompatSession {\n id\n createdAt\n deviceId\n finishedAt\n lastActiveIp\n lastActiveAt\n ssoLogin {\n id\n redirectUri\n }\n }\n":
|
"\n fragment CompatSession_session on CompatSession {\n id\n createdAt\n deviceId\n finishedAt\n lastActiveIp\n lastActiveAt\n userAgent {\n raw\n name\n os\n model\n deviceType\n }\n ssoLogin {\n id\n redirectUri\n }\n }\n":
|
||||||
types.CompatSession_SessionFragmentDoc,
|
types.CompatSession_SessionFragmentDoc,
|
||||||
"\n mutation EndCompatSession($id: ID!) {\n endCompatSession(input: { compatSessionId: $id }) {\n status\n compatSession {\n id\n finishedAt\n }\n }\n }\n":
|
"\n mutation EndCompatSession($id: ID!) {\n endCompatSession(input: { compatSessionId: $id }) {\n status\n compatSession {\n id\n finishedAt\n }\n }\n }\n":
|
||||||
types.EndCompatSessionDocument,
|
types.EndCompatSessionDocument,
|
||||||
"\n fragment OAuth2Session_session on Oauth2Session {\n id\n scope\n createdAt\n finishedAt\n lastActiveIp\n lastActiveAt\n client {\n id\n clientId\n clientName\n applicationType\n logoUri\n }\n }\n":
|
"\n fragment OAuth2Session_session on Oauth2Session {\n id\n scope\n createdAt\n finishedAt\n lastActiveIp\n lastActiveAt\n\n userAgent {\n model\n os\n osVersion\n deviceType\n }\n\n client {\n id\n clientId\n clientName\n applicationType\n logoUri\n }\n }\n":
|
||||||
types.OAuth2Session_SessionFragmentDoc,
|
types.OAuth2Session_SessionFragmentDoc,
|
||||||
"\n mutation EndOAuth2Session($id: ID!) {\n endOauth2Session(input: { oauth2SessionId: $id }) {\n status\n oauth2Session {\n id\n ...OAuth2Session_session\n }\n }\n }\n":
|
"\n mutation EndOAuth2Session($id: ID!) {\n endOauth2Session(input: { oauth2SessionId: $id }) {\n status\n oauth2Session {\n id\n ...OAuth2Session_session\n }\n }\n }\n":
|
||||||
types.EndOAuth2SessionDocument,
|
types.EndOAuth2SessionDocument,
|
||||||
"\n fragment BrowserSession_detail on BrowserSession {\n id\n createdAt\n finishedAt\n userAgent\n lastActiveIp\n lastActiveAt\n lastAuthentication {\n id\n createdAt\n }\n user {\n id\n username\n }\n }\n":
|
"\n fragment BrowserSession_detail on BrowserSession {\n id\n createdAt\n finishedAt\n userAgent {\n name\n model\n os\n }\n lastActiveIp\n lastActiveAt\n lastAuthentication {\n id\n createdAt\n }\n user {\n id\n username\n }\n }\n":
|
||||||
types.BrowserSession_DetailFragmentDoc,
|
types.BrowserSession_DetailFragmentDoc,
|
||||||
"\n fragment CompatSession_detail on CompatSession {\n id\n createdAt\n deviceId\n finishedAt\n lastActiveIp\n lastActiveAt\n ssoLogin {\n id\n redirectUri\n }\n }\n":
|
"\n fragment CompatSession_detail on CompatSession {\n id\n createdAt\n deviceId\n finishedAt\n lastActiveIp\n lastActiveAt\n userAgent {\n name\n os\n model\n }\n ssoLogin {\n id\n redirectUri\n }\n }\n":
|
||||||
types.CompatSession_DetailFragmentDoc,
|
types.CompatSession_DetailFragmentDoc,
|
||||||
"\n fragment OAuth2Session_detail on Oauth2Session {\n id\n scope\n createdAt\n finishedAt\n lastActiveIp\n lastActiveAt\n client {\n id\n clientId\n clientName\n clientUri\n logoUri\n }\n }\n":
|
"\n fragment OAuth2Session_detail on Oauth2Session {\n id\n scope\n createdAt\n finishedAt\n lastActiveIp\n lastActiveAt\n client {\n id\n clientId\n clientName\n clientUri\n logoUri\n }\n }\n":
|
||||||
types.OAuth2Session_DetailFragmentDoc,
|
types.OAuth2Session_DetailFragmentDoc,
|
||||||
@ -83,8 +83,6 @@ const documents = {
|
|||||||
types.VerifyEmailQueryDocument,
|
types.VerifyEmailQueryDocument,
|
||||||
"\n mutation AllowCrossSigningReset($userId: ID!) {\n allowUserCrossSigningReset(input: { userId: $userId }) {\n user {\n id\n }\n }\n }\n":
|
"\n mutation AllowCrossSigningReset($userId: ID!) {\n allowUserCrossSigningReset(input: { userId: $userId }) {\n user {\n id\n }\n }\n }\n":
|
||||||
types.AllowCrossSigningResetDocument,
|
types.AllowCrossSigningResetDocument,
|
||||||
"\n query CurrentViewerSessionQuery {\n viewerSession {\n __typename\n ... on BrowserSession {\n id\n }\n }\n }\n":
|
|
||||||
types.CurrentViewerSessionQueryDocument,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -105,8 +103,8 @@ export function graphql(source: string): unknown;
|
|||||||
* 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 userAgent\n lastActiveIp\n lastActiveAt\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 raw\n name\n os\n model\n deviceType\n }\n lastActiveIp\n lastActiveAt\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 lastActiveIp\n lastActiveAt\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 raw\n name\n os\n model\n deviceType\n }\n lastActiveIp\n lastActiveAt\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.
|
||||||
*/
|
*/
|
||||||
@ -123,8 +121,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 CompatSession_session on CompatSession {\n id\n createdAt\n deviceId\n finishedAt\n lastActiveIp\n lastActiveAt\n ssoLogin {\n id\n redirectUri\n }\n }\n",
|
source: "\n fragment CompatSession_session on CompatSession {\n id\n createdAt\n deviceId\n finishedAt\n lastActiveIp\n lastActiveAt\n userAgent {\n raw\n name\n os\n model\n deviceType\n }\n ssoLogin {\n id\n redirectUri\n }\n }\n",
|
||||||
): (typeof documents)["\n fragment CompatSession_session on CompatSession {\n id\n createdAt\n deviceId\n finishedAt\n lastActiveIp\n lastActiveAt\n ssoLogin {\n id\n redirectUri\n }\n }\n"];
|
): (typeof documents)["\n fragment CompatSession_session on CompatSession {\n id\n createdAt\n deviceId\n finishedAt\n lastActiveIp\n lastActiveAt\n userAgent {\n raw\n name\n os\n model\n deviceType\n }\n ssoLogin {\n id\n redirectUri\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.
|
||||||
*/
|
*/
|
||||||
@ -135,8 +133,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 OAuth2Session_session on Oauth2Session {\n id\n scope\n createdAt\n finishedAt\n lastActiveIp\n lastActiveAt\n client {\n id\n clientId\n clientName\n applicationType\n logoUri\n }\n }\n",
|
source: "\n fragment OAuth2Session_session on Oauth2Session {\n id\n scope\n createdAt\n finishedAt\n lastActiveIp\n lastActiveAt\n\n userAgent {\n model\n os\n osVersion\n deviceType\n }\n\n client {\n id\n clientId\n clientName\n applicationType\n logoUri\n }\n }\n",
|
||||||
): (typeof documents)["\n fragment OAuth2Session_session on Oauth2Session {\n id\n scope\n createdAt\n finishedAt\n lastActiveIp\n lastActiveAt\n client {\n id\n clientId\n clientName\n applicationType\n logoUri\n }\n }\n"];
|
): (typeof documents)["\n fragment OAuth2Session_session on Oauth2Session {\n id\n scope\n createdAt\n finishedAt\n lastActiveIp\n lastActiveAt\n\n userAgent {\n model\n os\n osVersion\n deviceType\n }\n\n client {\n id\n clientId\n clientName\n applicationType\n logoUri\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.
|
||||||
*/
|
*/
|
||||||
@ -147,14 +145,14 @@ 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_detail on BrowserSession {\n id\n createdAt\n finishedAt\n userAgent\n lastActiveIp\n lastActiveAt\n lastAuthentication {\n id\n createdAt\n }\n user {\n id\n username\n }\n }\n",
|
source: "\n fragment BrowserSession_detail on BrowserSession {\n id\n createdAt\n finishedAt\n userAgent {\n name\n model\n os\n }\n lastActiveIp\n lastActiveAt\n lastAuthentication {\n id\n createdAt\n }\n user {\n id\n username\n }\n }\n",
|
||||||
): (typeof documents)["\n fragment BrowserSession_detail on BrowserSession {\n id\n createdAt\n finishedAt\n userAgent\n lastActiveIp\n lastActiveAt\n lastAuthentication {\n id\n createdAt\n }\n user {\n id\n username\n }\n }\n"];
|
): (typeof documents)["\n fragment BrowserSession_detail on BrowserSession {\n id\n createdAt\n finishedAt\n userAgent {\n name\n model\n os\n }\n lastActiveIp\n lastActiveAt\n lastAuthentication {\n id\n createdAt\n }\n user {\n id\n username\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.
|
||||||
*/
|
*/
|
||||||
export function graphql(
|
export function graphql(
|
||||||
source: "\n fragment CompatSession_detail on CompatSession {\n id\n createdAt\n deviceId\n finishedAt\n lastActiveIp\n lastActiveAt\n ssoLogin {\n id\n redirectUri\n }\n }\n",
|
source: "\n fragment CompatSession_detail on CompatSession {\n id\n createdAt\n deviceId\n finishedAt\n lastActiveIp\n lastActiveAt\n userAgent {\n name\n os\n model\n }\n ssoLogin {\n id\n redirectUri\n }\n }\n",
|
||||||
): (typeof documents)["\n fragment CompatSession_detail on CompatSession {\n id\n createdAt\n deviceId\n finishedAt\n lastActiveIp\n lastActiveAt\n ssoLogin {\n id\n redirectUri\n }\n }\n"];
|
): (typeof documents)["\n fragment CompatSession_detail on CompatSession {\n id\n createdAt\n deviceId\n finishedAt\n lastActiveIp\n lastActiveAt\n userAgent {\n name\n os\n model\n }\n ssoLogin {\n id\n redirectUri\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.
|
||||||
*/
|
*/
|
||||||
@ -311,12 +309,6 @@ export function graphql(
|
|||||||
export function graphql(
|
export function graphql(
|
||||||
source: "\n mutation AllowCrossSigningReset($userId: ID!) {\n allowUserCrossSigningReset(input: { userId: $userId }) {\n user {\n id\n }\n }\n }\n",
|
source: "\n mutation AllowCrossSigningReset($userId: ID!) {\n allowUserCrossSigningReset(input: { userId: $userId }) {\n user {\n id\n }\n }\n }\n",
|
||||||
): (typeof documents)["\n mutation AllowCrossSigningReset($userId: ID!) {\n allowUserCrossSigningReset(input: { userId: $userId }) {\n user {\n id\n }\n }\n }\n"];
|
): (typeof documents)["\n mutation AllowCrossSigningReset($userId: ID!) {\n allowUserCrossSigningReset(input: { userId: $userId }) {\n user {\n id\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 CurrentViewerSessionQuery {\n viewerSession {\n __typename\n ... on BrowserSession {\n id\n }\n }\n }\n",
|
|
||||||
): (typeof documents)["\n query CurrentViewerSessionQuery {\n viewerSession {\n __typename\n ... on BrowserSession {\n id\n }\n }\n }\n"];
|
|
||||||
|
|
||||||
export function graphql(source: string) {
|
export function graphql(source: string) {
|
||||||
return (documents as any)[source] ?? {};
|
return (documents as any)[source] ?? {};
|
||||||
|
@ -179,8 +179,8 @@ export type BrowserSession = CreationEvent &
|
|||||||
state: SessionState;
|
state: SessionState;
|
||||||
/** The user logged in this session. */
|
/** The user logged in this session. */
|
||||||
user: User;
|
user: User;
|
||||||
/** The user-agent string with which the session was created. */
|
/** The user-agent with which the session was created. */
|
||||||
userAgent?: Maybe<Scalars["String"]["output"]>;
|
userAgent?: Maybe<UserAgent>;
|
||||||
};
|
};
|
||||||
|
|
||||||
/** A browser session represents a logged in user in a browser. */
|
/** A browser session represents a logged in user in a browser. */
|
||||||
@ -241,6 +241,8 @@ export type CompatSession = CreationEvent &
|
|||||||
state: SessionState;
|
state: SessionState;
|
||||||
/** The user authorized for this session. */
|
/** The user authorized for this session. */
|
||||||
user: User;
|
user: User;
|
||||||
|
/** The user-agent with which the session was created. */
|
||||||
|
userAgent?: Maybe<UserAgent>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type CompatSessionConnection = {
|
export type CompatSessionConnection = {
|
||||||
@ -343,6 +345,18 @@ export type CreationEvent = {
|
|||||||
createdAt: Scalars["DateTime"]["output"];
|
createdAt: Scalars["DateTime"]["output"];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** The type of a user agent */
|
||||||
|
export enum DeviceType {
|
||||||
|
/** A mobile phone. Can also sometimes be a tablet. */
|
||||||
|
Mobile = "MOBILE",
|
||||||
|
/** A personal computer, laptop or desktop */
|
||||||
|
Pc = "PC",
|
||||||
|
/** A tablet */
|
||||||
|
Tablet = "TABLET",
|
||||||
|
/** Unknown device type */
|
||||||
|
Unknown = "UNKNOWN",
|
||||||
|
}
|
||||||
|
|
||||||
/** The input of the `endBrowserSession` mutation. */
|
/** The input of the `endBrowserSession` mutation. */
|
||||||
export type EndBrowserSessionInput = {
|
export type EndBrowserSessionInput = {
|
||||||
/** The ID of the session to end. */
|
/** The ID of the session to end. */
|
||||||
@ -617,6 +631,8 @@ export type Oauth2Session = CreationEvent &
|
|||||||
state: SessionState;
|
state: SessionState;
|
||||||
/** User authorized for this session. */
|
/** User authorized for this session. */
|
||||||
user?: Maybe<User>;
|
user?: Maybe<User>;
|
||||||
|
/** The user-agent with which the session was created. */
|
||||||
|
userAgent?: Maybe<UserAgent>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Oauth2SessionConnection = {
|
export type Oauth2SessionConnection = {
|
||||||
@ -1058,6 +1074,25 @@ export type UserUpstreamOauth2LinksArgs = {
|
|||||||
last?: InputMaybe<Scalars["Int"]["input"]>;
|
last?: InputMaybe<Scalars["Int"]["input"]>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** A parsed user agent string */
|
||||||
|
export type UserAgent = {
|
||||||
|
__typename?: "UserAgent";
|
||||||
|
/** The device type */
|
||||||
|
deviceType: DeviceType;
|
||||||
|
/** The device model */
|
||||||
|
model?: Maybe<Scalars["String"]["output"]>;
|
||||||
|
/** The name of the browser */
|
||||||
|
name?: Maybe<Scalars["String"]["output"]>;
|
||||||
|
/** The operating system name */
|
||||||
|
os?: Maybe<Scalars["String"]["output"]>;
|
||||||
|
/** The operating system version */
|
||||||
|
osVersion?: Maybe<Scalars["String"]["output"]>;
|
||||||
|
/** The user agent string */
|
||||||
|
raw: Scalars["String"]["output"];
|
||||||
|
/** The version of the browser */
|
||||||
|
version?: Maybe<Scalars["String"]["output"]>;
|
||||||
|
};
|
||||||
|
|
||||||
/** A user email address */
|
/** A user email address */
|
||||||
export type UserEmail = CreationEvent &
|
export type UserEmail = CreationEvent &
|
||||||
Node & {
|
Node & {
|
||||||
@ -1144,9 +1179,16 @@ export type BrowserSession_SessionFragment = {
|
|||||||
id: string;
|
id: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
finishedAt?: string | null;
|
finishedAt?: string | null;
|
||||||
userAgent?: string | null;
|
|
||||||
lastActiveIp?: string | null;
|
lastActiveIp?: string | null;
|
||||||
lastActiveAt?: string | null;
|
lastActiveAt?: string | null;
|
||||||
|
userAgent?: {
|
||||||
|
__typename?: "UserAgent";
|
||||||
|
raw: string;
|
||||||
|
name?: string | null;
|
||||||
|
os?: string | null;
|
||||||
|
model?: string | null;
|
||||||
|
deviceType: DeviceType;
|
||||||
|
} | null;
|
||||||
lastAuthentication?: {
|
lastAuthentication?: {
|
||||||
__typename?: "Authentication";
|
__typename?: "Authentication";
|
||||||
id: string;
|
id: string;
|
||||||
@ -1193,6 +1235,14 @@ export type CompatSession_SessionFragment = {
|
|||||||
finishedAt?: string | null;
|
finishedAt?: string | null;
|
||||||
lastActiveIp?: string | null;
|
lastActiveIp?: string | null;
|
||||||
lastActiveAt?: string | null;
|
lastActiveAt?: string | null;
|
||||||
|
userAgent?: {
|
||||||
|
__typename?: "UserAgent";
|
||||||
|
raw: string;
|
||||||
|
name?: string | null;
|
||||||
|
os?: string | null;
|
||||||
|
model?: string | null;
|
||||||
|
deviceType: DeviceType;
|
||||||
|
} | null;
|
||||||
ssoLogin?: {
|
ssoLogin?: {
|
||||||
__typename?: "CompatSsoLogin";
|
__typename?: "CompatSsoLogin";
|
||||||
id: string;
|
id: string;
|
||||||
@ -1225,6 +1275,13 @@ export type OAuth2Session_SessionFragment = {
|
|||||||
finishedAt?: string | null;
|
finishedAt?: string | null;
|
||||||
lastActiveIp?: string | null;
|
lastActiveIp?: string | null;
|
||||||
lastActiveAt?: string | null;
|
lastActiveAt?: string | null;
|
||||||
|
userAgent?: {
|
||||||
|
__typename?: "UserAgent";
|
||||||
|
model?: string | null;
|
||||||
|
os?: string | null;
|
||||||
|
osVersion?: string | null;
|
||||||
|
deviceType: DeviceType;
|
||||||
|
} | null;
|
||||||
client: {
|
client: {
|
||||||
__typename?: "Oauth2Client";
|
__typename?: "Oauth2Client";
|
||||||
id: string;
|
id: string;
|
||||||
@ -1259,9 +1316,14 @@ export type BrowserSession_DetailFragment = {
|
|||||||
id: string;
|
id: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
finishedAt?: string | null;
|
finishedAt?: string | null;
|
||||||
userAgent?: string | null;
|
|
||||||
lastActiveIp?: string | null;
|
lastActiveIp?: string | null;
|
||||||
lastActiveAt?: string | null;
|
lastActiveAt?: string | null;
|
||||||
|
userAgent?: {
|
||||||
|
__typename?: "UserAgent";
|
||||||
|
name?: string | null;
|
||||||
|
model?: string | null;
|
||||||
|
os?: string | null;
|
||||||
|
} | null;
|
||||||
lastAuthentication?: {
|
lastAuthentication?: {
|
||||||
__typename?: "Authentication";
|
__typename?: "Authentication";
|
||||||
id: string;
|
id: string;
|
||||||
@ -1278,6 +1340,12 @@ export type CompatSession_DetailFragment = {
|
|||||||
finishedAt?: string | null;
|
finishedAt?: string | null;
|
||||||
lastActiveIp?: string | null;
|
lastActiveIp?: string | null;
|
||||||
lastActiveAt?: string | null;
|
lastActiveAt?: string | null;
|
||||||
|
userAgent?: {
|
||||||
|
__typename?: "UserAgent";
|
||||||
|
name?: string | null;
|
||||||
|
os?: string | null;
|
||||||
|
model?: string | null;
|
||||||
|
} | null;
|
||||||
ssoLogin?: {
|
ssoLogin?: {
|
||||||
__typename?: "CompatSsoLogin";
|
__typename?: "CompatSsoLogin";
|
||||||
id: string;
|
id: string;
|
||||||
@ -1736,18 +1804,6 @@ export type AllowCrossSigningResetMutation = {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export type CurrentViewerSessionQueryQueryVariables = Exact<{
|
|
||||||
[key: string]: never;
|
|
||||||
}>;
|
|
||||||
|
|
||||||
export type CurrentViewerSessionQueryQuery = {
|
|
||||||
__typename?: "Query";
|
|
||||||
viewerSession:
|
|
||||||
| { __typename: "Anonymous" }
|
|
||||||
| { __typename: "BrowserSession"; id: string }
|
|
||||||
| { __typename: "Oauth2Session" };
|
|
||||||
};
|
|
||||||
|
|
||||||
export const BrowserSession_SessionFragmentDoc = {
|
export const BrowserSession_SessionFragmentDoc = {
|
||||||
kind: "Document",
|
kind: "Document",
|
||||||
definitions: [
|
definitions: [
|
||||||
@ -1764,7 +1820,20 @@ 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",
|
||||||
|
name: { kind: "Name", value: "userAgent" },
|
||||||
|
selectionSet: {
|
||||||
|
kind: "SelectionSet",
|
||||||
|
selections: [
|
||||||
|
{ kind: "Field", name: { kind: "Name", value: "raw" } },
|
||||||
|
{ kind: "Field", name: { kind: "Name", value: "name" } },
|
||||||
|
{ kind: "Field", name: { kind: "Name", value: "os" } },
|
||||||
|
{ kind: "Field", name: { kind: "Name", value: "model" } },
|
||||||
|
{ kind: "Field", name: { kind: "Name", value: "deviceType" } },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
{ kind: "Field", name: { kind: "Name", value: "lastActiveIp" } },
|
{ kind: "Field", name: { kind: "Name", value: "lastActiveIp" } },
|
||||||
{ kind: "Field", name: { kind: "Name", value: "lastActiveAt" } },
|
{ kind: "Field", name: { kind: "Name", value: "lastActiveAt" } },
|
||||||
{
|
{
|
||||||
@ -1828,6 +1897,20 @@ export const CompatSession_SessionFragmentDoc = {
|
|||||||
{ kind: "Field", name: { kind: "Name", value: "finishedAt" } },
|
{ kind: "Field", name: { kind: "Name", value: "finishedAt" } },
|
||||||
{ kind: "Field", name: { kind: "Name", value: "lastActiveIp" } },
|
{ kind: "Field", name: { kind: "Name", value: "lastActiveIp" } },
|
||||||
{ kind: "Field", name: { kind: "Name", value: "lastActiveAt" } },
|
{ kind: "Field", name: { kind: "Name", value: "lastActiveAt" } },
|
||||||
|
{
|
||||||
|
kind: "Field",
|
||||||
|
name: { kind: "Name", value: "userAgent" },
|
||||||
|
selectionSet: {
|
||||||
|
kind: "SelectionSet",
|
||||||
|
selections: [
|
||||||
|
{ kind: "Field", name: { kind: "Name", value: "raw" } },
|
||||||
|
{ kind: "Field", name: { kind: "Name", value: "name" } },
|
||||||
|
{ kind: "Field", name: { kind: "Name", value: "os" } },
|
||||||
|
{ kind: "Field", name: { kind: "Name", value: "model" } },
|
||||||
|
{ kind: "Field", name: { kind: "Name", value: "deviceType" } },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
kind: "Field",
|
kind: "Field",
|
||||||
name: { kind: "Name", value: "ssoLogin" },
|
name: { kind: "Name", value: "ssoLogin" },
|
||||||
@ -1863,6 +1946,19 @@ export const OAuth2Session_SessionFragmentDoc = {
|
|||||||
{ kind: "Field", name: { kind: "Name", value: "finishedAt" } },
|
{ kind: "Field", name: { kind: "Name", value: "finishedAt" } },
|
||||||
{ kind: "Field", name: { kind: "Name", value: "lastActiveIp" } },
|
{ kind: "Field", name: { kind: "Name", value: "lastActiveIp" } },
|
||||||
{ kind: "Field", name: { kind: "Name", value: "lastActiveAt" } },
|
{ kind: "Field", name: { kind: "Name", value: "lastActiveAt" } },
|
||||||
|
{
|
||||||
|
kind: "Field",
|
||||||
|
name: { kind: "Name", value: "userAgent" },
|
||||||
|
selectionSet: {
|
||||||
|
kind: "SelectionSet",
|
||||||
|
selections: [
|
||||||
|
{ kind: "Field", name: { kind: "Name", value: "model" } },
|
||||||
|
{ kind: "Field", name: { kind: "Name", value: "os" } },
|
||||||
|
{ kind: "Field", name: { kind: "Name", value: "osVersion" } },
|
||||||
|
{ kind: "Field", name: { kind: "Name", value: "deviceType" } },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
kind: "Field",
|
kind: "Field",
|
||||||
name: { kind: "Name", value: "client" },
|
name: { kind: "Name", value: "client" },
|
||||||
@ -1901,7 +1997,18 @@ export const BrowserSession_DetailFragmentDoc = {
|
|||||||
{ 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",
|
||||||
|
name: { kind: "Name", value: "userAgent" },
|
||||||
|
selectionSet: {
|
||||||
|
kind: "SelectionSet",
|
||||||
|
selections: [
|
||||||
|
{ kind: "Field", name: { kind: "Name", value: "name" } },
|
||||||
|
{ kind: "Field", name: { kind: "Name", value: "model" } },
|
||||||
|
{ kind: "Field", name: { kind: "Name", value: "os" } },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
{ kind: "Field", name: { kind: "Name", value: "lastActiveIp" } },
|
{ kind: "Field", name: { kind: "Name", value: "lastActiveIp" } },
|
||||||
{ kind: "Field", name: { kind: "Name", value: "lastActiveAt" } },
|
{ kind: "Field", name: { kind: "Name", value: "lastActiveAt" } },
|
||||||
{
|
{
|
||||||
@ -1950,6 +2057,18 @@ export const CompatSession_DetailFragmentDoc = {
|
|||||||
{ kind: "Field", name: { kind: "Name", value: "finishedAt" } },
|
{ kind: "Field", name: { kind: "Name", value: "finishedAt" } },
|
||||||
{ kind: "Field", name: { kind: "Name", value: "lastActiveIp" } },
|
{ kind: "Field", name: { kind: "Name", value: "lastActiveIp" } },
|
||||||
{ kind: "Field", name: { kind: "Name", value: "lastActiveAt" } },
|
{ kind: "Field", name: { kind: "Name", value: "lastActiveAt" } },
|
||||||
|
{
|
||||||
|
kind: "Field",
|
||||||
|
name: { kind: "Name", value: "userAgent" },
|
||||||
|
selectionSet: {
|
||||||
|
kind: "SelectionSet",
|
||||||
|
selections: [
|
||||||
|
{ kind: "Field", name: { kind: "Name", value: "name" } },
|
||||||
|
{ kind: "Field", name: { kind: "Name", value: "os" } },
|
||||||
|
{ kind: "Field", name: { kind: "Name", value: "model" } },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
kind: "Field",
|
kind: "Field",
|
||||||
name: { kind: "Name", value: "ssoLogin" },
|
name: { kind: "Name", value: "ssoLogin" },
|
||||||
@ -2337,7 +2456,20 @@ 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",
|
||||||
|
name: { kind: "Name", value: "userAgent" },
|
||||||
|
selectionSet: {
|
||||||
|
kind: "SelectionSet",
|
||||||
|
selections: [
|
||||||
|
{ kind: "Field", name: { kind: "Name", value: "raw" } },
|
||||||
|
{ kind: "Field", name: { kind: "Name", value: "name" } },
|
||||||
|
{ kind: "Field", name: { kind: "Name", value: "os" } },
|
||||||
|
{ kind: "Field", name: { kind: "Name", value: "model" } },
|
||||||
|
{ kind: "Field", name: { kind: "Name", value: "deviceType" } },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
{ kind: "Field", name: { kind: "Name", value: "lastActiveIp" } },
|
{ kind: "Field", name: { kind: "Name", value: "lastActiveIp" } },
|
||||||
{ kind: "Field", name: { kind: "Name", value: "lastActiveAt" } },
|
{ kind: "Field", name: { kind: "Name", value: "lastActiveAt" } },
|
||||||
{
|
{
|
||||||
@ -2512,6 +2644,19 @@ export const EndOAuth2SessionDocument = {
|
|||||||
{ kind: "Field", name: { kind: "Name", value: "finishedAt" } },
|
{ kind: "Field", name: { kind: "Name", value: "finishedAt" } },
|
||||||
{ kind: "Field", name: { kind: "Name", value: "lastActiveIp" } },
|
{ kind: "Field", name: { kind: "Name", value: "lastActiveIp" } },
|
||||||
{ kind: "Field", name: { kind: "Name", value: "lastActiveAt" } },
|
{ kind: "Field", name: { kind: "Name", value: "lastActiveAt" } },
|
||||||
|
{
|
||||||
|
kind: "Field",
|
||||||
|
name: { kind: "Name", value: "userAgent" },
|
||||||
|
selectionSet: {
|
||||||
|
kind: "SelectionSet",
|
||||||
|
selections: [
|
||||||
|
{ kind: "Field", name: { kind: "Name", value: "model" } },
|
||||||
|
{ kind: "Field", name: { kind: "Name", value: "os" } },
|
||||||
|
{ kind: "Field", name: { kind: "Name", value: "osVersion" } },
|
||||||
|
{ kind: "Field", name: { kind: "Name", value: "deviceType" } },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
kind: "Field",
|
kind: "Field",
|
||||||
name: { kind: "Name", value: "client" },
|
name: { kind: "Name", value: "client" },
|
||||||
@ -3518,6 +3663,18 @@ export const SessionDetailQueryDocument = {
|
|||||||
{ kind: "Field", name: { kind: "Name", value: "finishedAt" } },
|
{ kind: "Field", name: { kind: "Name", value: "finishedAt" } },
|
||||||
{ kind: "Field", name: { kind: "Name", value: "lastActiveIp" } },
|
{ kind: "Field", name: { kind: "Name", value: "lastActiveIp" } },
|
||||||
{ kind: "Field", name: { kind: "Name", value: "lastActiveAt" } },
|
{ kind: "Field", name: { kind: "Name", value: "lastActiveAt" } },
|
||||||
|
{
|
||||||
|
kind: "Field",
|
||||||
|
name: { kind: "Name", value: "userAgent" },
|
||||||
|
selectionSet: {
|
||||||
|
kind: "SelectionSet",
|
||||||
|
selections: [
|
||||||
|
{ kind: "Field", name: { kind: "Name", value: "name" } },
|
||||||
|
{ kind: "Field", name: { kind: "Name", value: "os" } },
|
||||||
|
{ kind: "Field", name: { kind: "Name", value: "model" } },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
kind: "Field",
|
kind: "Field",
|
||||||
name: { kind: "Name", value: "ssoLogin" },
|
name: { kind: "Name", value: "ssoLogin" },
|
||||||
@ -3578,7 +3735,18 @@ export const SessionDetailQueryDocument = {
|
|||||||
{ 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",
|
||||||
|
name: { kind: "Name", value: "userAgent" },
|
||||||
|
selectionSet: {
|
||||||
|
kind: "SelectionSet",
|
||||||
|
selections: [
|
||||||
|
{ kind: "Field", name: { kind: "Name", value: "name" } },
|
||||||
|
{ kind: "Field", name: { kind: "Name", value: "model" } },
|
||||||
|
{ kind: "Field", name: { kind: "Name", value: "os" } },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
{ kind: "Field", name: { kind: "Name", value: "lastActiveIp" } },
|
{ kind: "Field", name: { kind: "Name", value: "lastActiveIp" } },
|
||||||
{ kind: "Field", name: { kind: "Name", value: "lastActiveAt" } },
|
{ kind: "Field", name: { kind: "Name", value: "lastActiveAt" } },
|
||||||
{
|
{
|
||||||
@ -3833,7 +4001,20 @@ 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",
|
||||||
|
name: { kind: "Name", value: "userAgent" },
|
||||||
|
selectionSet: {
|
||||||
|
kind: "SelectionSet",
|
||||||
|
selections: [
|
||||||
|
{ kind: "Field", name: { kind: "Name", value: "raw" } },
|
||||||
|
{ kind: "Field", name: { kind: "Name", value: "name" } },
|
||||||
|
{ kind: "Field", name: { kind: "Name", value: "os" } },
|
||||||
|
{ kind: "Field", name: { kind: "Name", value: "model" } },
|
||||||
|
{ kind: "Field", name: { kind: "Name", value: "deviceType" } },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
{ kind: "Field", name: { kind: "Name", value: "lastActiveIp" } },
|
{ kind: "Field", name: { kind: "Name", value: "lastActiveIp" } },
|
||||||
{ kind: "Field", name: { kind: "Name", value: "lastActiveAt" } },
|
{ kind: "Field", name: { kind: "Name", value: "lastActiveAt" } },
|
||||||
{
|
{
|
||||||
@ -4151,6 +4332,20 @@ export const AppSessionsListQueryDocument = {
|
|||||||
{ kind: "Field", name: { kind: "Name", value: "finishedAt" } },
|
{ kind: "Field", name: { kind: "Name", value: "finishedAt" } },
|
||||||
{ kind: "Field", name: { kind: "Name", value: "lastActiveIp" } },
|
{ kind: "Field", name: { kind: "Name", value: "lastActiveIp" } },
|
||||||
{ kind: "Field", name: { kind: "Name", value: "lastActiveAt" } },
|
{ kind: "Field", name: { kind: "Name", value: "lastActiveAt" } },
|
||||||
|
{
|
||||||
|
kind: "Field",
|
||||||
|
name: { kind: "Name", value: "userAgent" },
|
||||||
|
selectionSet: {
|
||||||
|
kind: "SelectionSet",
|
||||||
|
selections: [
|
||||||
|
{ kind: "Field", name: { kind: "Name", value: "raw" } },
|
||||||
|
{ kind: "Field", name: { kind: "Name", value: "name" } },
|
||||||
|
{ kind: "Field", name: { kind: "Name", value: "os" } },
|
||||||
|
{ kind: "Field", name: { kind: "Name", value: "model" } },
|
||||||
|
{ kind: "Field", name: { kind: "Name", value: "deviceType" } },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
kind: "Field",
|
kind: "Field",
|
||||||
name: { kind: "Name", value: "ssoLogin" },
|
name: { kind: "Name", value: "ssoLogin" },
|
||||||
@ -4181,6 +4376,19 @@ export const AppSessionsListQueryDocument = {
|
|||||||
{ kind: "Field", name: { kind: "Name", value: "finishedAt" } },
|
{ kind: "Field", name: { kind: "Name", value: "finishedAt" } },
|
||||||
{ kind: "Field", name: { kind: "Name", value: "lastActiveIp" } },
|
{ kind: "Field", name: { kind: "Name", value: "lastActiveIp" } },
|
||||||
{ kind: "Field", name: { kind: "Name", value: "lastActiveAt" } },
|
{ kind: "Field", name: { kind: "Name", value: "lastActiveAt" } },
|
||||||
|
{
|
||||||
|
kind: "Field",
|
||||||
|
name: { kind: "Name", value: "userAgent" },
|
||||||
|
selectionSet: {
|
||||||
|
kind: "SelectionSet",
|
||||||
|
selections: [
|
||||||
|
{ kind: "Field", name: { kind: "Name", value: "model" } },
|
||||||
|
{ kind: "Field", name: { kind: "Name", value: "os" } },
|
||||||
|
{ kind: "Field", name: { kind: "Name", value: "osVersion" } },
|
||||||
|
{ kind: "Field", name: { kind: "Name", value: "deviceType" } },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
kind: "Field",
|
kind: "Field",
|
||||||
name: { kind: "Name", value: "client" },
|
name: { kind: "Name", value: "client" },
|
||||||
@ -4639,44 +4847,3 @@ export const AllowCrossSigningResetDocument = {
|
|||||||
AllowCrossSigningResetMutation,
|
AllowCrossSigningResetMutation,
|
||||||
AllowCrossSigningResetMutationVariables
|
AllowCrossSigningResetMutationVariables
|
||||||
>;
|
>;
|
||||||
export const CurrentViewerSessionQueryDocument = {
|
|
||||||
kind: "Document",
|
|
||||||
definitions: [
|
|
||||||
{
|
|
||||||
kind: "OperationDefinition",
|
|
||||||
operation: "query",
|
|
||||||
name: { kind: "Name", value: "CurrentViewerSessionQuery" },
|
|
||||||
selectionSet: {
|
|
||||||
kind: "SelectionSet",
|
|
||||||
selections: [
|
|
||||||
{
|
|
||||||
kind: "Field",
|
|
||||||
name: { kind: "Name", value: "viewerSession" },
|
|
||||||
selectionSet: {
|
|
||||||
kind: "SelectionSet",
|
|
||||||
selections: [
|
|
||||||
{ kind: "Field", name: { kind: "Name", value: "__typename" } },
|
|
||||||
{
|
|
||||||
kind: "InlineFragment",
|
|
||||||
typeCondition: {
|
|
||||||
kind: "NamedType",
|
|
||||||
name: { kind: "Name", value: "BrowserSession" },
|
|
||||||
},
|
|
||||||
selectionSet: {
|
|
||||||
kind: "SelectionSet",
|
|
||||||
selections: [
|
|
||||||
{ kind: "Field", name: { kind: "Name", value: "id" } },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
} as unknown as DocumentNode<
|
|
||||||
CurrentViewerSessionQueryQuery,
|
|
||||||
CurrentViewerSessionQueryQueryVariables
|
|
||||||
>;
|
|
||||||
|
@ -413,8 +413,9 @@ export default {
|
|||||||
{
|
{
|
||||||
name: "userAgent",
|
name: "userAgent",
|
||||||
type: {
|
type: {
|
||||||
kind: "SCALAR",
|
kind: "OBJECT",
|
||||||
name: "Any",
|
name: "UserAgent",
|
||||||
|
ofType: null,
|
||||||
},
|
},
|
||||||
args: [],
|
args: [],
|
||||||
},
|
},
|
||||||
@ -628,6 +629,15 @@ export default {
|
|||||||
},
|
},
|
||||||
args: [],
|
args: [],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "userAgent",
|
||||||
|
type: {
|
||||||
|
kind: "OBJECT",
|
||||||
|
name: "UserAgent",
|
||||||
|
ofType: null,
|
||||||
|
},
|
||||||
|
args: [],
|
||||||
|
},
|
||||||
],
|
],
|
||||||
interfaces: [
|
interfaces: [
|
||||||
{
|
{
|
||||||
@ -1741,6 +1751,15 @@ export default {
|
|||||||
},
|
},
|
||||||
args: [],
|
args: [],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "userAgent",
|
||||||
|
type: {
|
||||||
|
kind: "OBJECT",
|
||||||
|
name: "UserAgent",
|
||||||
|
ofType: null,
|
||||||
|
},
|
||||||
|
args: [],
|
||||||
|
},
|
||||||
],
|
],
|
||||||
interfaces: [
|
interfaces: [
|
||||||
{
|
{
|
||||||
@ -3133,6 +3152,75 @@ export default {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
kind: "OBJECT",
|
||||||
|
name: "UserAgent",
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: "deviceType",
|
||||||
|
type: {
|
||||||
|
kind: "NON_NULL",
|
||||||
|
ofType: {
|
||||||
|
kind: "SCALAR",
|
||||||
|
name: "Any",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
args: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "model",
|
||||||
|
type: {
|
||||||
|
kind: "SCALAR",
|
||||||
|
name: "Any",
|
||||||
|
},
|
||||||
|
args: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "name",
|
||||||
|
type: {
|
||||||
|
kind: "SCALAR",
|
||||||
|
name: "Any",
|
||||||
|
},
|
||||||
|
args: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "os",
|
||||||
|
type: {
|
||||||
|
kind: "SCALAR",
|
||||||
|
name: "Any",
|
||||||
|
},
|
||||||
|
args: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "osVersion",
|
||||||
|
type: {
|
||||||
|
kind: "SCALAR",
|
||||||
|
name: "Any",
|
||||||
|
},
|
||||||
|
args: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "raw",
|
||||||
|
type: {
|
||||||
|
kind: "NON_NULL",
|
||||||
|
ofType: {
|
||||||
|
kind: "SCALAR",
|
||||||
|
name: "Any",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
args: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "version",
|
||||||
|
type: {
|
||||||
|
kind: "SCALAR",
|
||||||
|
name: "Any",
|
||||||
|
},
|
||||||
|
args: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
interfaces: [],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
kind: "OBJECT",
|
kind: "OBJECT",
|
||||||
name: "UserEmail",
|
name: "UserEmail",
|
||||||
|
@ -90,10 +90,21 @@
|
|||||||
width: var(--cpd-space-10x);
|
width: var(--cpd-space-10x);
|
||||||
}
|
}
|
||||||
|
|
||||||
& .name {
|
& .lines {
|
||||||
font: var(--cpd-font-body-md-semibold);
|
display: flex;
|
||||||
letter-spacing: var(--cpd-font-letter-spacing-body-md);
|
flex-direction: column;
|
||||||
color: var(--cpd-color-text-primary);
|
|
||||||
|
& div:first-child {
|
||||||
|
font: var(--cpd-font-body-md-semibold);
|
||||||
|
letter-spacing: var(--cpd-font-letter-spacing-body-md);
|
||||||
|
color: var(--cpd-color-text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
& div {
|
||||||
|
font: var(--cpd-font-body-sm-regular);
|
||||||
|
letter-spacing: var(--cpd-font-letter-spacing-body-sm);
|
||||||
|
color: var(--cpd-color-text-secondary);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,390 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2023 The Matrix.org Foundation C.I.C.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { 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.Mobile,
|
|
||||||
"Apple",
|
|
||||||
"iPhone",
|
|
||||||
"iOS",
|
|
||||||
"8.4.1",
|
|
||||||
"Mobile Safari",
|
|
||||||
"8.0",
|
|
||||||
),
|
|
||||||
makeDeviceExtendedInfo(
|
|
||||||
DeviceType.Mobile,
|
|
||||||
"Samsung",
|
|
||||||
"SM-G973U",
|
|
||||||
"Android",
|
|
||||||
"9",
|
|
||||||
"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("");
|
|
||||||
});
|
|
||||||
});
|
|
@ -1,145 +0,0 @@
|
|||||||
/*
|
|
||||||
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 (
|
|
||||||
device.type === "mobile" ||
|
|
||||||
operatingSystem.name?.includes("Android") ||
|
|
||||||
userAgent.indexOf(IOS_KEYWORD) > -1
|
|
||||||
) {
|
|
||||||
return DeviceType.Mobile;
|
|
||||||
}
|
|
||||||
if (browser.name) {
|
|
||||||
return DeviceType.Web;
|
|
||||||
}
|
|
||||||
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;
|
|
||||||
};
|
|
@ -1,42 +0,0 @@
|
|||||||
// Copyright 2023 The Matrix.org Foundation C.I.C.
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
import { useQuery } from "urql";
|
|
||||||
|
|
||||||
import { graphql } from "../../gql";
|
|
||||||
|
|
||||||
const CURRENT_VIEWER_SESSION_QUERY = graphql(/* GraphQL */ `
|
|
||||||
query CurrentViewerSessionQuery {
|
|
||||||
viewerSession {
|
|
||||||
__typename
|
|
||||||
... on BrowserSession {
|
|
||||||
id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Query the current browser session id
|
|
||||||
* and unwrap the result
|
|
||||||
* throws error when error result
|
|
||||||
*/
|
|
||||||
export const useCurrentBrowserSessionId = (): string | null => {
|
|
||||||
const [result] = useQuery({ query: CURRENT_VIEWER_SESSION_QUERY });
|
|
||||||
if (result.error) throw result.error;
|
|
||||||
if (!result.data) throw new Error(); // Suspense mode is enabled
|
|
||||||
return result.data.viewerSession.__typename === "BrowserSession"
|
|
||||||
? result.data.viewerSession.id
|
|
||||||
: null;
|
|
||||||
};
|
|
@ -33,10 +33,46 @@ limitations under the License.
|
|||||||
<h1 class="title">Allow access to your account?</h1>
|
<h1 class="title">Allow access to your account?</h1>
|
||||||
|
|
||||||
<div class="consent-device-card">
|
<div class="consent-device-card">
|
||||||
<div class="device" {%- if grant.user_agent %} title="{{ grant.user_agent }}"{% endif %}>
|
<div class="device" {%- if grant.user_agent %} title="{{ grant.user_agent.raw }}"{% endif %}>
|
||||||
{{ icon.web_browser() }}
|
{% if grant.user_agent.device_type == "mobile" %}
|
||||||
{# TODO: Infer from the user agent #}
|
{{ icon.mobile() }}
|
||||||
<div class="name">Device</div>
|
{% elif grant.user_agent.device_type == "tablet" %}
|
||||||
|
{{ icon.web_browser() }}
|
||||||
|
{% elif grant.user_agent.device_type == "pc" %}
|
||||||
|
{{ icon.computer() }}
|
||||||
|
{% else %}
|
||||||
|
{{ icon.unknown_solid() }}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="lines">
|
||||||
|
{% if grant.user_agent.model %}
|
||||||
|
<div>{{ grant.user_agent.model }}</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if grant.user_agent.os %}
|
||||||
|
<div>
|
||||||
|
{{ grant.user_agent.os }}
|
||||||
|
{% if grant.user_agent.os_version %}
|
||||||
|
{{ grant.user_agent.os_version }}
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{# If we haven't detected a model, it's probably a browser, so show the name #}
|
||||||
|
{% if not grant.user_agent.model and grant.user_agent.name %}
|
||||||
|
<div>
|
||||||
|
{{ grant.user_agent.name }}
|
||||||
|
{% if grant.user_agent.version %}
|
||||||
|
{{ grant.user_agent.version }}
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{# If we couldn't detect anything, show a generic "Device" #}
|
||||||
|
{% if not grant.user_agent.model and not grant.user_agent.name and not grant.user_agent.os %}
|
||||||
|
<div>Device</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="meta">
|
<div class="meta">
|
||||||
{% if grant.ip_address %}
|
{% if grant.ip_address %}
|
||||||
|
@ -2,11 +2,11 @@
|
|||||||
"action": {
|
"action": {
|
||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
"@cancel": {
|
"@cancel": {
|
||||||
"context": "pages/consent.html:72:11-29, pages/device_consent.html:94:13-31, pages/login.html:100:13-31, pages/policy_violation.html:52:13-31, pages/register.html:77:13-31"
|
"context": "pages/consent.html:72:11-29, pages/device_consent.html:130:13-31, pages/login.html:100:13-31, pages/policy_violation.html:52:13-31, pages/register.html:77:13-31"
|
||||||
},
|
},
|
||||||
"continue": "Continue",
|
"continue": "Continue",
|
||||||
"@continue": {
|
"@continue": {
|
||||||
"context": "pages/account/emails/add.html:45:26-46, pages/account/emails/verify.html:60:26-46, pages/consent.html:60:28-48, pages/device_consent.html:91:13-33, pages/device_link.html:50:26-46, pages/login.html:62:30-50, pages/reauth.html:40:28-48, pages/register.html:72:28-48, pages/sso.html:45:28-48"
|
"context": "pages/account/emails/add.html:45:26-46, pages/account/emails/verify.html:60:26-46, pages/consent.html:60:28-48, pages/device_consent.html:127:13-33, pages/device_link.html:50:26-46, pages/login.html:62:30-50, pages/reauth.html:40:28-48, pages/register.html:72:28-48, pages/sso.html:45:28-48"
|
||||||
},
|
},
|
||||||
"create_account": "Create Account",
|
"create_account": "Create Account",
|
||||||
"@create_account": {
|
"@create_account": {
|
||||||
@ -18,7 +18,7 @@
|
|||||||
},
|
},
|
||||||
"sign_out": "Sign out",
|
"sign_out": "Sign out",
|
||||||
"@sign_out": {
|
"@sign_out": {
|
||||||
"context": "pages/consent.html:68:28-48, pages/device_consent.html:103:30-50, pages/index.html:36:28-48, pages/policy_violation.html:46:28-48, pages/sso.html:53:28-48, pages/upstream_oauth2/link_mismatch.html:32:24-44, pages/upstream_oauth2/suggest_link.html:40:26-46"
|
"context": "pages/consent.html:68:28-48, pages/device_consent.html:139:30-50, pages/index.html:36:28-48, pages/policy_violation.html:46:28-48, pages/sso.html:53:28-48, pages/upstream_oauth2/link_mismatch.html:32:24-44, pages/upstream_oauth2/suggest_link.html:40:26-46"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"app": {
|
"app": {
|
||||||
@ -246,7 +246,7 @@
|
|||||||
},
|
},
|
||||||
"not_you": "Not %(username)s?",
|
"not_you": "Not %(username)s?",
|
||||||
"@not_you": {
|
"@not_you": {
|
||||||
"context": "pages/consent.html:65:11-67, pages/device_consent.html:100:13-69, pages/sso.html:50:11-67",
|
"context": "pages/consent.html:65:11-67, pages/device_consent.html:136:13-69, pages/sso.html:50:11-67",
|
||||||
"description": "Suggestions for the user to log in as a different user"
|
"description": "Suggestions for the user to log in as a different user"
|
||||||
},
|
},
|
||||||
"or_separator": "Or",
|
"or_separator": "Or",
|
||||||
|
Reference in New Issue
Block a user