Allow setting custom names on sessions (#4459)
This commit is contained in:
@@ -305,7 +305,7 @@ impl Options {
|
||||
|
||||
let compat_session = repo
|
||||
.compat_session()
|
||||
.add(&mut rng, &clock, &user, device, None, admin)
|
||||
.add(&mut rng, &clock, &user, device, None, admin, None)
|
||||
.await?;
|
||||
|
||||
let token = TokenType::CompatAccessToken.generate(&mut rng);
|
||||
|
||||
@@ -160,6 +160,7 @@ pub struct AuthorizationGrant {
|
||||
pub response_type_id_token: bool,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub login_hint: Option<String>,
|
||||
pub locale: Option<String>,
|
||||
}
|
||||
|
||||
impl std::ops::Deref for AuthorizationGrant {
|
||||
@@ -263,6 +264,7 @@ impl AuthorizationGrant {
|
||||
response_type_id_token: false,
|
||||
created_at: now,
|
||||
login_hint: Some(String::from("mxid:@example-user:example.com")),
|
||||
locale: Some(String::from("fr")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,6 +83,7 @@ pub struct Session {
|
||||
pub user_agent: Option<String>,
|
||||
pub last_active_at: Option<DateTime<Utc>>,
|
||||
pub last_active_ip: Option<IpAddr>,
|
||||
pub human_name: Option<String>,
|
||||
}
|
||||
|
||||
impl std::ops::Deref for Session {
|
||||
|
||||
@@ -184,6 +184,9 @@ pub struct CompatSession {
|
||||
|
||||
/// The time this session was finished
|
||||
pub finished_at: Option<DateTime<Utc>>,
|
||||
|
||||
/// The user-provided name, if any
|
||||
pub human_name: Option<String>,
|
||||
}
|
||||
|
||||
impl
|
||||
@@ -210,6 +213,7 @@ impl
|
||||
last_active_at: session.last_active_at,
|
||||
last_active_ip: session.last_active_ip,
|
||||
finished_at,
|
||||
human_name: session.human_name,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -237,6 +241,7 @@ impl CompatSession {
|
||||
last_active_at: Some(DateTime::default()),
|
||||
last_active_ip: Some([1, 2, 3, 4].into()),
|
||||
finished_at: None,
|
||||
human_name: Some("Laptop".to_owned()),
|
||||
},
|
||||
Self {
|
||||
id: Ulid::from_bytes([0x02; 16]),
|
||||
@@ -249,6 +254,7 @@ impl CompatSession {
|
||||
last_active_at: Some(DateTime::default()),
|
||||
last_active_ip: Some([1, 2, 3, 4].into()),
|
||||
finished_at: Some(DateTime::default()),
|
||||
human_name: None,
|
||||
},
|
||||
Self {
|
||||
id: Ulid::from_bytes([0x03; 16]),
|
||||
@@ -261,6 +267,7 @@ impl CompatSession {
|
||||
last_active_at: None,
|
||||
last_active_ip: None,
|
||||
finished_at: None,
|
||||
human_name: None,
|
||||
},
|
||||
]
|
||||
}
|
||||
@@ -301,6 +308,9 @@ pub struct OAuth2Session {
|
||||
|
||||
/// The last IP address used by the session
|
||||
last_active_ip: Option<IpAddr>,
|
||||
|
||||
/// The user-provided name, if any
|
||||
human_name: Option<String>,
|
||||
}
|
||||
|
||||
impl From<mas_data_model::Session> for OAuth2Session {
|
||||
@@ -316,6 +326,7 @@ impl From<mas_data_model::Session> for OAuth2Session {
|
||||
user_agent: session.user_agent,
|
||||
last_active_at: session.last_active_at,
|
||||
last_active_ip: session.last_active_ip,
|
||||
human_name: session.human_name,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -335,6 +346,7 @@ impl OAuth2Session {
|
||||
user_agent: Some("Mozilla/5.0".to_owned()),
|
||||
last_active_at: Some(DateTime::default()),
|
||||
last_active_ip: Some("127.0.0.1".parse().unwrap()),
|
||||
human_name: Some("Laptop".to_owned()),
|
||||
},
|
||||
Self {
|
||||
id: Ulid::from_bytes([0x02; 16]),
|
||||
@@ -347,6 +359,7 @@ impl OAuth2Session {
|
||||
user_agent: None,
|
||||
last_active_at: None,
|
||||
last_active_ip: None,
|
||||
human_name: None,
|
||||
},
|
||||
Self {
|
||||
id: Ulid::from_bytes([0x03; 16]),
|
||||
@@ -359,6 +372,7 @@ impl OAuth2Session {
|
||||
user_agent: Some("Mozilla/5.0".to_owned()),
|
||||
last_active_at: Some(DateTime::default()),
|
||||
last_active_ip: Some("127.0.0.1".parse().unwrap()),
|
||||
human_name: None,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
@@ -107,7 +107,7 @@ mod tests {
|
||||
let device = Device::generate(&mut rng);
|
||||
let session = repo
|
||||
.compat_session()
|
||||
.add(&mut rng, &state.clock, &user, device, None, false)
|
||||
.add(&mut rng, &state.clock, &user, device, None, false, None)
|
||||
.await
|
||||
.unwrap();
|
||||
repo.save().await.unwrap();
|
||||
@@ -119,7 +119,7 @@ mod tests {
|
||||
let response = state.request(request).await;
|
||||
response.assert_status(StatusCode::OK);
|
||||
let body: serde_json::Value = response.json();
|
||||
assert_json_snapshot!(body, @r###"
|
||||
assert_json_snapshot!(body, @r#"
|
||||
{
|
||||
"data": {
|
||||
"type": "compat-session",
|
||||
@@ -133,7 +133,8 @@ mod tests {
|
||||
"user_agent": null,
|
||||
"last_active_at": null,
|
||||
"last_active_ip": null,
|
||||
"finished_at": null
|
||||
"finished_at": null,
|
||||
"human_name": null
|
||||
},
|
||||
"links": {
|
||||
"self": "/api/admin/v1/compat-sessions/01FSHN9AG0QHEHKX2JNQ2A2D07"
|
||||
@@ -143,7 +144,7 @@ mod tests {
|
||||
"self": "/api/admin/v1/compat-sessions/01FSHN9AG0QHEHKX2JNQ2A2D07"
|
||||
}
|
||||
}
|
||||
"###);
|
||||
"#);
|
||||
}
|
||||
|
||||
#[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")]
|
||||
|
||||
@@ -251,7 +251,7 @@ mod tests {
|
||||
|
||||
let device = Device::generate(&mut rng);
|
||||
repo.compat_session()
|
||||
.add(&mut rng, &state.clock, &alice, device, None, false)
|
||||
.add(&mut rng, &state.clock, &alice, device, None, false, None)
|
||||
.await
|
||||
.unwrap();
|
||||
let device = Device::generate(&mut rng);
|
||||
@@ -260,7 +260,7 @@ mod tests {
|
||||
|
||||
let session = repo
|
||||
.compat_session()
|
||||
.add(&mut rng, &state.clock, &bob, device, None, false)
|
||||
.add(&mut rng, &state.clock, &bob, device, None, false, None)
|
||||
.await
|
||||
.unwrap();
|
||||
state.clock.advance(Duration::minutes(1));
|
||||
@@ -276,7 +276,7 @@ mod tests {
|
||||
let response = state.request(request).await;
|
||||
response.assert_status(StatusCode::OK);
|
||||
let body: serde_json::Value = response.json();
|
||||
assert_json_snapshot!(body, @r###"
|
||||
assert_json_snapshot!(body, @r#"
|
||||
{
|
||||
"meta": {
|
||||
"count": 2
|
||||
@@ -294,7 +294,8 @@ mod tests {
|
||||
"user_agent": null,
|
||||
"last_active_at": null,
|
||||
"last_active_ip": null,
|
||||
"finished_at": null
|
||||
"finished_at": null,
|
||||
"human_name": null
|
||||
},
|
||||
"links": {
|
||||
"self": "/api/admin/v1/compat-sessions/01FSHNB530AAPR7PEV8KNBZD5Y"
|
||||
@@ -312,7 +313,8 @@ mod tests {
|
||||
"user_agent": null,
|
||||
"last_active_at": null,
|
||||
"last_active_ip": null,
|
||||
"finished_at": "2022-01-16T14:43:00Z"
|
||||
"finished_at": "2022-01-16T14:43:00Z",
|
||||
"human_name": null
|
||||
},
|
||||
"links": {
|
||||
"self": "/api/admin/v1/compat-sessions/01FSHNCZP0PPF7X0EVMJNECPZW"
|
||||
@@ -325,7 +327,7 @@ mod tests {
|
||||
"last": "/api/admin/v1/compat-sessions?page[last]=10"
|
||||
}
|
||||
}
|
||||
"###);
|
||||
"#);
|
||||
|
||||
// Filter by user
|
||||
let request = Request::get(format!(
|
||||
@@ -337,7 +339,7 @@ mod tests {
|
||||
let response = state.request(request).await;
|
||||
response.assert_status(StatusCode::OK);
|
||||
let body: serde_json::Value = response.json();
|
||||
assert_json_snapshot!(body, @r###"
|
||||
assert_json_snapshot!(body, @r#"
|
||||
{
|
||||
"meta": {
|
||||
"count": 1
|
||||
@@ -355,7 +357,8 @@ mod tests {
|
||||
"user_agent": null,
|
||||
"last_active_at": null,
|
||||
"last_active_ip": null,
|
||||
"finished_at": null
|
||||
"finished_at": null,
|
||||
"human_name": null
|
||||
},
|
||||
"links": {
|
||||
"self": "/api/admin/v1/compat-sessions/01FSHNB530AAPR7PEV8KNBZD5Y"
|
||||
@@ -368,7 +371,7 @@ mod tests {
|
||||
"last": "/api/admin/v1/compat-sessions?filter[user]=01FSHN9AG0MZAA6S4AF7CTV32E&page[last]=10"
|
||||
}
|
||||
}
|
||||
"###);
|
||||
"#);
|
||||
|
||||
// Filter by status (active)
|
||||
let request = Request::get("/api/admin/v1/compat-sessions?filter[status]=active")
|
||||
@@ -377,7 +380,7 @@ mod tests {
|
||||
let response = state.request(request).await;
|
||||
response.assert_status(StatusCode::OK);
|
||||
let body: serde_json::Value = response.json();
|
||||
assert_json_snapshot!(body, @r###"
|
||||
assert_json_snapshot!(body, @r#"
|
||||
{
|
||||
"meta": {
|
||||
"count": 1
|
||||
@@ -395,7 +398,8 @@ mod tests {
|
||||
"user_agent": null,
|
||||
"last_active_at": null,
|
||||
"last_active_ip": null,
|
||||
"finished_at": null
|
||||
"finished_at": null,
|
||||
"human_name": null
|
||||
},
|
||||
"links": {
|
||||
"self": "/api/admin/v1/compat-sessions/01FSHNB530AAPR7PEV8KNBZD5Y"
|
||||
@@ -408,7 +412,7 @@ mod tests {
|
||||
"last": "/api/admin/v1/compat-sessions?filter[status]=active&page[last]=10"
|
||||
}
|
||||
}
|
||||
"###);
|
||||
"#);
|
||||
|
||||
// Filter by status (finished)
|
||||
let request = Request::get("/api/admin/v1/compat-sessions?filter[status]=finished")
|
||||
@@ -417,7 +421,7 @@ mod tests {
|
||||
let response = state.request(request).await;
|
||||
response.assert_status(StatusCode::OK);
|
||||
let body: serde_json::Value = response.json();
|
||||
assert_json_snapshot!(body, @r###"
|
||||
assert_json_snapshot!(body, @r#"
|
||||
{
|
||||
"meta": {
|
||||
"count": 1
|
||||
@@ -435,7 +439,8 @@ mod tests {
|
||||
"user_agent": null,
|
||||
"last_active_at": null,
|
||||
"last_active_ip": null,
|
||||
"finished_at": "2022-01-16T14:43:00Z"
|
||||
"finished_at": "2022-01-16T14:43:00Z",
|
||||
"human_name": null
|
||||
},
|
||||
"links": {
|
||||
"self": "/api/admin/v1/compat-sessions/01FSHNCZP0PPF7X0EVMJNECPZW"
|
||||
@@ -448,6 +453,6 @@ mod tests {
|
||||
"last": "/api/admin/v1/compat-sessions?filter[status]=finished&page[last]=10"
|
||||
}
|
||||
}
|
||||
"###);
|
||||
"#);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,7 +110,7 @@ mod tests {
|
||||
response.assert_status(StatusCode::OK);
|
||||
let body: serde_json::Value = response.json();
|
||||
assert_eq!(body["data"]["type"], "oauth2-session");
|
||||
insta::assert_json_snapshot!(body, @r###"
|
||||
insta::assert_json_snapshot!(body, @r#"
|
||||
{
|
||||
"data": {
|
||||
"type": "oauth2-session",
|
||||
@@ -124,7 +124,8 @@ mod tests {
|
||||
"scope": "urn:mas:admin",
|
||||
"user_agent": null,
|
||||
"last_active_at": null,
|
||||
"last_active_ip": null
|
||||
"last_active_ip": null,
|
||||
"human_name": null
|
||||
},
|
||||
"links": {
|
||||
"self": "/api/admin/v1/oauth2-sessions/01FSHN9AG0MKGTBNZ16RDR3PVY"
|
||||
@@ -134,7 +135,7 @@ mod tests {
|
||||
"self": "/api/admin/v1/oauth2-sessions/01FSHN9AG0MKGTBNZ16RDR3PVY"
|
||||
}
|
||||
}
|
||||
"###);
|
||||
"#);
|
||||
}
|
||||
|
||||
#[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")]
|
||||
|
||||
@@ -331,7 +331,7 @@ mod tests {
|
||||
let response = state.request(request).await;
|
||||
response.assert_status(StatusCode::OK);
|
||||
let body: serde_json::Value = response.json();
|
||||
insta::assert_json_snapshot!(body, @r###"
|
||||
insta::assert_json_snapshot!(body, @r#"
|
||||
{
|
||||
"meta": {
|
||||
"count": 1
|
||||
@@ -349,7 +349,8 @@ mod tests {
|
||||
"scope": "urn:mas:admin",
|
||||
"user_agent": null,
|
||||
"last_active_at": null,
|
||||
"last_active_ip": null
|
||||
"last_active_ip": null,
|
||||
"human_name": null
|
||||
},
|
||||
"links": {
|
||||
"self": "/api/admin/v1/oauth2-sessions/01FSHN9AG0MKGTBNZ16RDR3PVY"
|
||||
@@ -362,6 +363,6 @@ mod tests {
|
||||
"last": "/api/admin/v1/oauth2-sessions?page[last]=10"
|
||||
}
|
||||
}
|
||||
"###);
|
||||
"#);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,6 +116,9 @@ pub struct RequestBody {
|
||||
/// this is not specified.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
device_id: Option<String>,
|
||||
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
initial_device_display_name: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
@@ -309,18 +312,20 @@ pub(crate) async fn post(
|
||||
user,
|
||||
password,
|
||||
input.device_id, // TODO check for validity
|
||||
input.initial_device_display_name,
|
||||
)
|
||||
.await?
|
||||
}
|
||||
|
||||
(_, Credentials::Token { token }) => {
|
||||
token_login(
|
||||
&mut repo,
|
||||
&mut rng,
|
||||
&clock,
|
||||
&mut repo,
|
||||
&homeserver,
|
||||
&token,
|
||||
input.device_id,
|
||||
&homeserver,
|
||||
&mut rng,
|
||||
input.initial_device_display_name,
|
||||
)
|
||||
.await?
|
||||
}
|
||||
@@ -387,12 +392,13 @@ pub(crate) async fn post(
|
||||
}
|
||||
|
||||
async fn token_login(
|
||||
repo: &mut BoxRepository,
|
||||
rng: &mut (dyn RngCore + Send),
|
||||
clock: &dyn Clock,
|
||||
repo: &mut BoxRepository,
|
||||
homeserver: &dyn HomeserverConnection,
|
||||
token: &str,
|
||||
requested_device_id: Option<String>,
|
||||
homeserver: &dyn HomeserverConnection,
|
||||
rng: &mut (dyn RngCore + Send),
|
||||
initial_device_display_name: Option<String>,
|
||||
) -> Result<(CompatSession, User), RouteError> {
|
||||
let login = repo
|
||||
.compat_sso_login()
|
||||
@@ -467,7 +473,11 @@ async fn token_login(
|
||||
};
|
||||
let mxid = homeserver.mxid(&browser_session.user.username);
|
||||
homeserver
|
||||
.create_device(&mxid, device.as_str())
|
||||
.create_device(
|
||||
&mxid,
|
||||
device.as_str(),
|
||||
initial_device_display_name.as_deref(),
|
||||
)
|
||||
.await
|
||||
.map_err(RouteError::ProvisionDeviceFailed)?;
|
||||
|
||||
@@ -484,6 +494,7 @@ async fn token_login(
|
||||
device,
|
||||
Some(&browser_session),
|
||||
false,
|
||||
initial_device_display_name,
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -505,6 +516,7 @@ async fn user_password_login(
|
||||
username: String,
|
||||
password: String,
|
||||
requested_device_id: Option<String>,
|
||||
initial_device_display_name: Option<String>,
|
||||
) -> Result<(CompatSession, User), RouteError> {
|
||||
// Try getting the localpart out of the MXID
|
||||
let username = homeserver.localpart(&username).unwrap_or(&username);
|
||||
@@ -566,7 +578,11 @@ async fn user_password_login(
|
||||
Device::generate(&mut rng)
|
||||
};
|
||||
homeserver
|
||||
.create_device(&mxid, device.as_str())
|
||||
.create_device(
|
||||
&mxid,
|
||||
device.as_str(),
|
||||
initial_device_display_name.as_deref(),
|
||||
)
|
||||
.await
|
||||
.map_err(RouteError::ProvisionDeviceFailed)?;
|
||||
|
||||
@@ -576,7 +592,15 @@ async fn user_password_login(
|
||||
|
||||
let session = repo
|
||||
.compat_session()
|
||||
.add(&mut rng, clock, &user, device, None, false)
|
||||
.add(
|
||||
&mut rng,
|
||||
clock,
|
||||
&user,
|
||||
device,
|
||||
None,
|
||||
false,
|
||||
initial_device_display_name,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok((session, user))
|
||||
|
||||
@@ -165,6 +165,11 @@ impl CompatSession {
|
||||
pub async fn last_active_at(&self) -> Option<DateTime<Utc>> {
|
||||
self.session.last_active_at
|
||||
}
|
||||
|
||||
/// A human-provided name for the session.
|
||||
pub async fn human_name(&self) -> Option<&str> {
|
||||
self.session.human_name.as_deref()
|
||||
}
|
||||
}
|
||||
|
||||
/// A compat SSO login represents a login done through the legacy Matrix login
|
||||
|
||||
@@ -128,6 +128,11 @@ impl OAuth2Session {
|
||||
pub async fn last_active_at(&self) -> Option<DateTime<Utc>> {
|
||||
self.0.last_active_at
|
||||
}
|
||||
|
||||
/// The user-provided name for this session.
|
||||
pub async fn human_name(&self) -> Option<&str> {
|
||||
self.0.human_name.as_deref()
|
||||
}
|
||||
}
|
||||
|
||||
/// The application type advertised by the client.
|
||||
|
||||
@@ -64,6 +64,54 @@ impl EndCompatSessionPayload {
|
||||
}
|
||||
}
|
||||
|
||||
/// The input of the `setCompatSessionName` mutation.
|
||||
#[derive(InputObject)]
|
||||
pub struct SetCompatSessionNameInput {
|
||||
/// The ID of the session to set the name of.
|
||||
compat_session_id: ID,
|
||||
|
||||
/// The new name of the session.
|
||||
human_name: String,
|
||||
}
|
||||
|
||||
/// The payload of the `setCompatSessionName` mutation.
|
||||
pub enum SetCompatSessionNamePayload {
|
||||
/// The session was not found.
|
||||
NotFound,
|
||||
|
||||
/// The session was updated.
|
||||
Updated(mas_data_model::CompatSession),
|
||||
}
|
||||
|
||||
/// The status of the `setCompatSessionName` mutation.
|
||||
#[derive(Enum, Copy, Clone, PartialEq, Eq, Debug)]
|
||||
enum SetCompatSessionNameStatus {
|
||||
/// The session was updated.
|
||||
Updated,
|
||||
|
||||
/// The session was not found.
|
||||
NotFound,
|
||||
}
|
||||
|
||||
#[Object]
|
||||
impl SetCompatSessionNamePayload {
|
||||
/// The status of the mutation.
|
||||
async fn status(&self) -> SetCompatSessionNameStatus {
|
||||
match self {
|
||||
Self::Updated(_) => SetCompatSessionNameStatus::Updated,
|
||||
Self::NotFound => SetCompatSessionNameStatus::NotFound,
|
||||
}
|
||||
}
|
||||
|
||||
/// The session that was updated.
|
||||
async fn oauth2_session(&self) -> Option<CompatSession> {
|
||||
match self {
|
||||
Self::Updated(session) => Some(CompatSession::new(session.clone())),
|
||||
Self::NotFound => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[Object]
|
||||
impl CompatSessionMutations {
|
||||
async fn end_compat_session(
|
||||
@@ -105,4 +153,50 @@ impl CompatSessionMutations {
|
||||
|
||||
Ok(EndCompatSessionPayload::Ended(Box::new(session)))
|
||||
}
|
||||
|
||||
async fn set_compat_session_name(
|
||||
&self,
|
||||
ctx: &Context<'_>,
|
||||
input: SetCompatSessionNameInput,
|
||||
) -> Result<SetCompatSessionNamePayload, async_graphql::Error> {
|
||||
let state = ctx.state();
|
||||
let compat_session_id = NodeType::CompatSession.extract_ulid(&input.compat_session_id)?;
|
||||
let requester = ctx.requester();
|
||||
|
||||
let mut repo = state.repository().await?;
|
||||
let homeserver = state.homeserver_connection();
|
||||
|
||||
let session = repo.compat_session().lookup(compat_session_id).await?;
|
||||
let Some(session) = session else {
|
||||
return Ok(SetCompatSessionNamePayload::NotFound);
|
||||
};
|
||||
|
||||
if !requester.is_owner_or_admin(&session) {
|
||||
return Ok(SetCompatSessionNamePayload::NotFound);
|
||||
}
|
||||
|
||||
let user = repo
|
||||
.user()
|
||||
.lookup(session.user_id)
|
||||
.await?
|
||||
.context("User not found")?;
|
||||
|
||||
let session = repo
|
||||
.compat_session()
|
||||
.set_human_name(session, Some(input.human_name.clone()))
|
||||
.await?;
|
||||
|
||||
// Update the device on the homeserver side
|
||||
let mxid = homeserver.mxid(&user.username);
|
||||
if let Some(device) = session.device.as_ref() {
|
||||
homeserver
|
||||
.update_device_display_name(&mxid, device.as_str(), &input.human_name)
|
||||
.await
|
||||
.context("Failed to provision device")?;
|
||||
}
|
||||
|
||||
repo.save().await?;
|
||||
|
||||
Ok(SetCompatSessionNamePayload::Updated(session))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,6 +110,54 @@ impl EndOAuth2SessionPayload {
|
||||
}
|
||||
}
|
||||
|
||||
/// The input of the `setOauth2SessionName` mutation.
|
||||
#[derive(InputObject)]
|
||||
pub struct SetOAuth2SessionNameInput {
|
||||
/// The ID of the session to set the name of.
|
||||
oauth2_session_id: ID,
|
||||
|
||||
/// The new name of the session.
|
||||
human_name: String,
|
||||
}
|
||||
|
||||
/// The payload of the `setOauth2SessionName` mutation.
|
||||
pub enum SetOAuth2SessionNamePayload {
|
||||
/// The session was not found.
|
||||
NotFound,
|
||||
|
||||
/// The session was updated.
|
||||
Updated(mas_data_model::Session),
|
||||
}
|
||||
|
||||
/// The status of the `setOauth2SessionName` mutation.
|
||||
#[derive(Enum, Copy, Clone, PartialEq, Eq, Debug)]
|
||||
enum SetOAuth2SessionNameStatus {
|
||||
/// The session was updated.
|
||||
Updated,
|
||||
|
||||
/// The session was not found.
|
||||
NotFound,
|
||||
}
|
||||
|
||||
#[Object]
|
||||
impl SetOAuth2SessionNamePayload {
|
||||
/// The status of the mutation.
|
||||
async fn status(&self) -> SetOAuth2SessionNameStatus {
|
||||
match self {
|
||||
Self::Updated(_) => SetOAuth2SessionNameStatus::Updated,
|
||||
Self::NotFound => SetOAuth2SessionNameStatus::NotFound,
|
||||
}
|
||||
}
|
||||
|
||||
/// The session that was updated.
|
||||
async fn oauth2_session(&self) -> Option<OAuth2Session> {
|
||||
match self {
|
||||
Self::Updated(session) => Some(OAuth2Session(session.clone())),
|
||||
Self::NotFound => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[Object]
|
||||
impl OAuth2SessionMutations {
|
||||
/// Create a new arbitrary OAuth 2.0 Session.
|
||||
@@ -168,7 +216,7 @@ impl OAuth2SessionMutations {
|
||||
for scope in &*session.scope {
|
||||
if let Some(device) = Device::from_scope_token(scope) {
|
||||
homeserver
|
||||
.create_device(&mxid, device.as_str())
|
||||
.create_device(&mxid, device.as_str(), None)
|
||||
.await
|
||||
.context("Failed to provision device")?;
|
||||
}
|
||||
@@ -247,4 +295,54 @@ impl OAuth2SessionMutations {
|
||||
|
||||
Ok(EndOAuth2SessionPayload::Ended(session))
|
||||
}
|
||||
|
||||
async fn set_oauth2_session_name(
|
||||
&self,
|
||||
ctx: &Context<'_>,
|
||||
input: SetOAuth2SessionNameInput,
|
||||
) -> Result<SetOAuth2SessionNamePayload, async_graphql::Error> {
|
||||
let state = ctx.state();
|
||||
let oauth2_session_id = NodeType::OAuth2Session.extract_ulid(&input.oauth2_session_id)?;
|
||||
let requester = ctx.requester();
|
||||
|
||||
let mut repo = state.repository().await?;
|
||||
let homeserver = state.homeserver_connection();
|
||||
|
||||
let session = repo.oauth2_session().lookup(oauth2_session_id).await?;
|
||||
let Some(session) = session else {
|
||||
return Ok(SetOAuth2SessionNamePayload::NotFound);
|
||||
};
|
||||
|
||||
if !requester.is_owner_or_admin(&session) {
|
||||
return Ok(SetOAuth2SessionNamePayload::NotFound);
|
||||
}
|
||||
|
||||
let user_id = session.user_id.context("Session has no user")?;
|
||||
|
||||
let user = repo
|
||||
.user()
|
||||
.lookup(user_id)
|
||||
.await?
|
||||
.context("User not found")?;
|
||||
|
||||
let session = repo
|
||||
.oauth2_session()
|
||||
.set_human_name(session, Some(input.human_name.clone()))
|
||||
.await?;
|
||||
|
||||
// Update the device on the homeserver side
|
||||
let mxid = homeserver.mxid(&user.username);
|
||||
for scope in &*session.scope {
|
||||
if let Some(device) = Device::from_scope_token(scope) {
|
||||
homeserver
|
||||
.update_device_display_name(&mxid, device.as_str(), &input.human_name)
|
||||
.await
|
||||
.context("Failed to provision device")?;
|
||||
}
|
||||
}
|
||||
|
||||
repo.save().await?;
|
||||
|
||||
Ok(SetOAuth2SessionNamePayload::Updated(session))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -203,6 +203,7 @@ where
|
||||
Encrypter: FromRef<S>,
|
||||
reqwest::Client: FromRef<S>,
|
||||
SiteConfig: FromRef<S>,
|
||||
Templates: FromRef<S>,
|
||||
Arc<dyn HomeserverConnection>: FromRef<S>,
|
||||
BoxClock: FromRequestParts<S>,
|
||||
BoxRng: FromRequestParts<S>,
|
||||
|
||||
@@ -274,6 +274,7 @@ pub(crate) async fn get(
|
||||
response_mode,
|
||||
response_type.has_id_token(),
|
||||
params.auth.login_hint,
|
||||
Some(locale.to_string()),
|
||||
)
|
||||
.await?;
|
||||
let continue_grant = PostAuthAction::continue_grant(grant.id);
|
||||
|
||||
@@ -18,6 +18,7 @@ use mas_axum_utils::{
|
||||
use mas_data_model::{
|
||||
AuthorizationGrantStage, Client, Device, DeviceCodeGrantState, SiteConfig, TokenType,
|
||||
};
|
||||
use mas_i18n::DataLocale;
|
||||
use mas_keystore::{Encrypter, Keystore};
|
||||
use mas_matrix::HomeserverConnection;
|
||||
use mas_oidc_client::types::scope::ScopeToken;
|
||||
@@ -31,6 +32,7 @@ use mas_storage::{
|
||||
},
|
||||
user::BrowserSessionRepository,
|
||||
};
|
||||
use mas_templates::{DeviceNameContext, TemplateContext, Templates};
|
||||
use oauth2_types::{
|
||||
errors::{ClientError, ClientErrorCode},
|
||||
pkce::CodeChallengeError,
|
||||
@@ -261,6 +263,8 @@ impl IntoResponse for RouteError {
|
||||
}
|
||||
}
|
||||
|
||||
impl_from_error_for_route!(mas_i18n::DataError);
|
||||
impl_from_error_for_route!(mas_templates::TemplateError);
|
||||
impl_from_error_for_route!(mas_storage::RepositoryError);
|
||||
impl_from_error_for_route!(mas_policy::EvaluationError);
|
||||
impl_from_error_for_route!(super::IdTokenSignatureError);
|
||||
@@ -281,6 +285,7 @@ pub(crate) async fn post(
|
||||
State(homeserver): State<Arc<dyn HomeserverConnection>>,
|
||||
State(site_config): State<SiteConfig>,
|
||||
State(encrypter): State<Encrypter>,
|
||||
State(templates): State<Templates>,
|
||||
policy: Policy,
|
||||
user_agent: Option<TypedHeader<headers::UserAgent>>,
|
||||
client_authorization: ClientAuthorization<AccessTokenRequest>,
|
||||
@@ -334,6 +339,7 @@ pub(crate) async fn post(
|
||||
&site_config,
|
||||
repo,
|
||||
&homeserver,
|
||||
&templates,
|
||||
user_agent,
|
||||
)
|
||||
.await?
|
||||
@@ -415,6 +421,7 @@ async fn authorization_code_grant(
|
||||
site_config: &SiteConfig,
|
||||
mut repo: BoxRepository,
|
||||
homeserver: &Arc<dyn HomeserverConnection>,
|
||||
templates: &Templates,
|
||||
user_agent: Option<String>,
|
||||
) -> Result<(AccessTokenResponse, BoxRepository), RouteError> {
|
||||
// Check that the client is allowed to use this grant type
|
||||
@@ -482,6 +489,11 @@ async fn authorization_code_grant(
|
||||
.await?
|
||||
.ok_or(RouteError::NoSuchOAuthSession(session_id))?;
|
||||
|
||||
// Generate a device name
|
||||
let lang: DataLocale = authz_grant.locale.as_deref().unwrap_or("en").parse()?;
|
||||
let ctx = DeviceNameContext::new(client.clone(), user_agent.clone()).with_language(lang);
|
||||
let device_name = templates.render_device_name(&ctx)?;
|
||||
|
||||
if let Some(user_agent) = user_agent {
|
||||
session = repo
|
||||
.oauth2_session()
|
||||
@@ -567,7 +579,7 @@ async fn authorization_code_grant(
|
||||
for scope in &*session.scope {
|
||||
if let Some(device) = Device::from_scope_token(scope) {
|
||||
homeserver
|
||||
.create_device(&mxid, device.as_str())
|
||||
.create_device(&mxid, device.as_str(), Some(&device_name))
|
||||
.await
|
||||
.map_err(RouteError::ProvisionDeviceFailed)?;
|
||||
}
|
||||
@@ -943,7 +955,7 @@ async fn device_code_grant(
|
||||
for scope in &*session.scope {
|
||||
if let Some(device) = Device::from_scope_token(scope) {
|
||||
homeserver
|
||||
.create_device(&mxid, device.as_str())
|
||||
.create_device(&mxid, device.as_str(), None)
|
||||
.await
|
||||
.map_err(RouteError::ProvisionDeviceFailed)?;
|
||||
}
|
||||
@@ -1042,6 +1054,7 @@ mod tests {
|
||||
ResponseMode::Query,
|
||||
false,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -1141,6 +1154,7 @@ mod tests {
|
||||
ResponseMode::Query,
|
||||
false,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -11,7 +11,7 @@ mod translator;
|
||||
pub use icu_calendar;
|
||||
pub use icu_datetime;
|
||||
pub use icu_locid::locale;
|
||||
pub use icu_provider::DataLocale;
|
||||
pub use icu_provider::{DataError, DataLocale};
|
||||
|
||||
pub use self::{
|
||||
sprintf::{Argument, ArgumentList, Message},
|
||||
|
||||
@@ -133,6 +133,11 @@ struct SynapseDevice {
|
||||
dehydrated: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct SynapseUpdateDeviceRequest<'a> {
|
||||
display_name: Option<&'a str>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct SynapseDeleteDevicesRequest {
|
||||
devices: Vec<String>,
|
||||
@@ -318,11 +323,16 @@ impl HomeserverConnection for SynapseConnection {
|
||||
),
|
||||
err(Debug),
|
||||
)]
|
||||
async fn create_device(&self, mxid: &str, device_id: &str) -> Result<(), anyhow::Error> {
|
||||
let mxid = urlencoding::encode(mxid);
|
||||
async fn create_device(
|
||||
&self,
|
||||
mxid: &str,
|
||||
device_id: &str,
|
||||
initial_display_name: Option<&str>,
|
||||
) -> Result<(), anyhow::Error> {
|
||||
let encoded_mxid = urlencoding::encode(mxid);
|
||||
|
||||
let response = self
|
||||
.post(&format!("_synapse/admin/v2/users/{mxid}/devices"))
|
||||
.post(&format!("_synapse/admin/v2/users/{encoded_mxid}/devices"))
|
||||
.json(&SynapseDevice {
|
||||
device_id: device_id.to_owned(),
|
||||
dehydrated: None,
|
||||
@@ -343,6 +353,56 @@ impl HomeserverConnection for SynapseConnection {
|
||||
);
|
||||
}
|
||||
|
||||
// It's annoying, but the POST endpoint doesn't let us set the display name
|
||||
// of the device, so we have to do it manually.
|
||||
if let Some(display_name) = initial_display_name {
|
||||
self.update_device_display_name(mxid, device_id, display_name)
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tracing::instrument(
|
||||
name = "homeserver.update_device_display_name",
|
||||
skip_all,
|
||||
fields(
|
||||
matrix.homeserver = self.homeserver,
|
||||
matrix.mxid = mxid,
|
||||
matrix.device_id = device_id,
|
||||
),
|
||||
err(Debug),
|
||||
)]
|
||||
async fn update_device_display_name(
|
||||
&self,
|
||||
mxid: &str,
|
||||
device_id: &str,
|
||||
display_name: &str,
|
||||
) -> Result<(), anyhow::Error> {
|
||||
let device_id = urlencoding::encode(device_id);
|
||||
let response = self
|
||||
.put(&format!(
|
||||
"_synapse/admin/v2/users/{mxid}/devices/{device_id}"
|
||||
))
|
||||
.json(&SynapseUpdateDeviceRequest {
|
||||
display_name: Some(display_name),
|
||||
})
|
||||
.send_traced()
|
||||
.await
|
||||
.context("Failed to update device display name in Synapse")?;
|
||||
|
||||
let response = response
|
||||
.error_for_synapse_error()
|
||||
.await
|
||||
.context("Unexpected HTTP response while updating device display name in Synapse")?;
|
||||
|
||||
if response.status() != StatusCode::OK {
|
||||
bail!(
|
||||
"Unexpected HTTP code while updating device display name in Synapse: {}",
|
||||
response.status()
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -454,7 +514,7 @@ impl HomeserverConnection for SynapseConnection {
|
||||
// Then, create the devices that are missing. There is no batching API to do
|
||||
// this, so we do this sequentially, which is fine as the API is idempotent.
|
||||
for device_id in devices.difference(&existing_devices) {
|
||||
self.create_device(mxid, device_id).await?;
|
||||
self.create_device(mxid, device_id, None).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -254,7 +254,31 @@ pub trait HomeserverConnection: Send + Sync {
|
||||
///
|
||||
/// Returns an error if the homeserver is unreachable or the device could
|
||||
/// not be created.
|
||||
async fn create_device(&self, mxid: &str, device_id: &str) -> Result<(), anyhow::Error>;
|
||||
async fn create_device(
|
||||
&self,
|
||||
mxid: &str,
|
||||
device_id: &str,
|
||||
initial_display_name: Option<&str>,
|
||||
) -> Result<(), anyhow::Error>;
|
||||
|
||||
/// Update the display name of a device for a user on the homeserver.
|
||||
///
|
||||
/// # Parameters
|
||||
///
|
||||
/// * `mxid` - The Matrix ID of the user to update a device for.
|
||||
/// * `device_id` - The device ID to update.
|
||||
/// * `display_name` - The new display name to set
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if the homeserver is unreachable or the device could
|
||||
/// not be updated.
|
||||
async fn update_device_display_name(
|
||||
&self,
|
||||
mxid: &str,
|
||||
device_id: &str,
|
||||
display_name: &str,
|
||||
) -> Result<(), anyhow::Error>;
|
||||
|
||||
/// Delete a device for a user on the homeserver.
|
||||
///
|
||||
@@ -364,8 +388,26 @@ impl<T: HomeserverConnection + Send + Sync + ?Sized> HomeserverConnection for &T
|
||||
(**self).is_localpart_available(localpart).await
|
||||
}
|
||||
|
||||
async fn create_device(&self, mxid: &str, device_id: &str) -> Result<(), anyhow::Error> {
|
||||
(**self).create_device(mxid, device_id).await
|
||||
async fn create_device(
|
||||
&self,
|
||||
mxid: &str,
|
||||
device_id: &str,
|
||||
initial_display_name: Option<&str>,
|
||||
) -> Result<(), anyhow::Error> {
|
||||
(**self)
|
||||
.create_device(mxid, device_id, initial_display_name)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn update_device_display_name(
|
||||
&self,
|
||||
mxid: &str,
|
||||
device_id: &str,
|
||||
display_name: &str,
|
||||
) -> Result<(), anyhow::Error> {
|
||||
(**self)
|
||||
.update_device_display_name(mxid, device_id, display_name)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn delete_device(&self, mxid: &str, device_id: &str) -> Result<(), anyhow::Error> {
|
||||
@@ -420,8 +462,26 @@ impl<T: HomeserverConnection + ?Sized> HomeserverConnection for Arc<T> {
|
||||
(**self).is_localpart_available(localpart).await
|
||||
}
|
||||
|
||||
async fn create_device(&self, mxid: &str, device_id: &str) -> Result<(), anyhow::Error> {
|
||||
(**self).create_device(mxid, device_id).await
|
||||
async fn create_device(
|
||||
&self,
|
||||
mxid: &str,
|
||||
device_id: &str,
|
||||
initial_display_name: Option<&str>,
|
||||
) -> Result<(), anyhow::Error> {
|
||||
(**self)
|
||||
.create_device(mxid, device_id, initial_display_name)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn update_device_display_name(
|
||||
&self,
|
||||
mxid: &str,
|
||||
device_id: &str,
|
||||
display_name: &str,
|
||||
) -> Result<(), anyhow::Error> {
|
||||
(**self)
|
||||
.update_device_display_name(mxid, device_id, display_name)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn delete_device(&self, mxid: &str, device_id: &str) -> Result<(), anyhow::Error> {
|
||||
|
||||
@@ -107,13 +107,30 @@ impl crate::HomeserverConnection for HomeserverConnection {
|
||||
Ok(!users.contains_key(&mxid))
|
||||
}
|
||||
|
||||
async fn create_device(&self, mxid: &str, device_id: &str) -> Result<(), anyhow::Error> {
|
||||
async fn create_device(
|
||||
&self,
|
||||
mxid: &str,
|
||||
device_id: &str,
|
||||
_initial_display_name: Option<&str>,
|
||||
) -> Result<(), anyhow::Error> {
|
||||
let mut users = self.users.write().await;
|
||||
let user = users.get_mut(mxid).context("User not found")?;
|
||||
user.devices.insert(device_id.to_owned());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn update_device_display_name(
|
||||
&self,
|
||||
mxid: &str,
|
||||
device_id: &str,
|
||||
_display_name: &str,
|
||||
) -> Result<(), anyhow::Error> {
|
||||
let mut users = self.users.write().await;
|
||||
let user = users.get_mut(mxid).context("User not found")?;
|
||||
user.devices.get(device_id).context("Device not found")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn delete_device(&self, mxid: &str, device_id: &str) -> Result<(), anyhow::Error> {
|
||||
let mut users = self.users.write().await;
|
||||
let user = users.get_mut(mxid).context("User not found")?;
|
||||
@@ -191,7 +208,7 @@ mod tests {
|
||||
assert_eq!(conn.mxid("test"), mxid);
|
||||
|
||||
assert!(conn.query_user(mxid).await.is_err());
|
||||
assert!(conn.create_device(mxid, device).await.is_err());
|
||||
assert!(conn.create_device(mxid, device, None).await.is_err());
|
||||
assert!(conn.delete_device(mxid, device).await.is_err());
|
||||
|
||||
let request = ProvisionRequest::new("@test:example.org", "test")
|
||||
@@ -222,9 +239,9 @@ mod tests {
|
||||
assert!(conn.delete_device(mxid, device).await.is_ok());
|
||||
|
||||
// Create the device
|
||||
assert!(conn.create_device(mxid, device).await.is_ok());
|
||||
assert!(conn.create_device(mxid, device, None).await.is_ok());
|
||||
// Create the same device again
|
||||
assert!(conn.create_device(mxid, device).await.is_ok());
|
||||
assert!(conn.create_device(mxid, device, None).await.is_ok());
|
||||
|
||||
// XXX: there is no API to query devices yet in the trait
|
||||
// Delete the device
|
||||
|
||||
@@ -40,10 +40,24 @@ impl<C: HomeserverConnection> HomeserverConnection for ReadOnlyHomeserverConnect
|
||||
self.inner.is_localpart_available(localpart).await
|
||||
}
|
||||
|
||||
async fn create_device(&self, _mxid: &str, _device_id: &str) -> Result<(), anyhow::Error> {
|
||||
async fn create_device(
|
||||
&self,
|
||||
_mxid: &str,
|
||||
_device_id: &str,
|
||||
_initial_display_name: Option<&str>,
|
||||
) -> Result<(), anyhow::Error> {
|
||||
anyhow::bail!("Device creation is not supported in read-only mode");
|
||||
}
|
||||
|
||||
async fn update_device_display_name(
|
||||
&self,
|
||||
_mxid: &str,
|
||||
_device_id: &str,
|
||||
_display_name: &str,
|
||||
) -> Result<(), anyhow::Error> {
|
||||
anyhow::bail!("Device display name update is not supported in read-only mode");
|
||||
}
|
||||
|
||||
async fn delete_device(&self, _mxid: &str, _device_id: &str) -> Result<(), anyhow::Error> {
|
||||
anyhow::bail!("Device deletion is not supported in read-only mode");
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT oauth2_session_id\n , user_id\n , user_session_id\n , oauth2_client_id\n , scope_list\n , created_at\n , finished_at\n , user_agent\n , last_active_at\n , last_active_ip as \"last_active_ip: IpAddr\"\n FROM oauth2_sessions\n\n WHERE oauth2_session_id = $1\n ",
|
||||
"query": "\n SELECT oauth2_session_id\n , user_id\n , user_session_id\n , oauth2_client_id\n , scope_list\n , created_at\n , finished_at\n , user_agent\n , last_active_at\n , last_active_ip as \"last_active_ip: IpAddr\"\n , human_name\n FROM oauth2_sessions\n\n WHERE oauth2_session_id = $1\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -52,6 +52,11 @@
|
||||
"ordinal": 9,
|
||||
"name": "last_active_ip: IpAddr",
|
||||
"type_info": "Inet"
|
||||
},
|
||||
{
|
||||
"ordinal": 10,
|
||||
"name": "human_name",
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
@@ -69,8 +74,9 @@
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "5a2e9b5002c1927c0035c22e393172b36ab46a4377b46618205151ea041886d5"
|
||||
"hash": "6b8d28b76d7ab33178b46dbb28c11e41d86f22b3fa899a952cad00129e59bee6"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n INSERT INTO oauth2_authorization_grants (\n oauth2_authorization_grant_id,\n oauth2_client_id,\n redirect_uri,\n scope,\n state,\n nonce,\n response_mode,\n code_challenge,\n code_challenge_method,\n response_type_code,\n response_type_id_token,\n authorization_code,\n login_hint,\n created_at\n )\n VALUES\n ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14)\n ",
|
||||
"query": "\n INSERT INTO oauth2_authorization_grants (\n oauth2_authorization_grant_id,\n oauth2_client_id,\n redirect_uri,\n scope,\n state,\n nonce,\n response_mode,\n code_challenge,\n code_challenge_method,\n response_type_code,\n response_type_id_token,\n authorization_code,\n login_hint,\n locale,\n created_at\n )\n VALUES\n ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15)\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
@@ -18,10 +18,11 @@
|
||||
"Bool",
|
||||
"Text",
|
||||
"Text",
|
||||
"Text",
|
||||
"Timestamptz"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "96208afd8b81e00f2700b0ded40c1eb529d321d0aa543be70f86ef96d0d8ff28"
|
||||
"hash": "7a0641df5058927c5cd67d4cdaa59fe609112afbabcbfcc0e7f96c1e531b6567"
|
||||
}
|
||||
15
crates/storage-pg/.sqlx/query-8afada5220fefb0d01ed6f87d3d0ee8fca86b5cdce9320e190e3d3b8fd9f63bc.json
generated
Normal file
15
crates/storage-pg/.sqlx/query-8afada5220fefb0d01ed6f87d3d0ee8fca86b5cdce9320e190e3d3b8fd9f63bc.json
generated
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n UPDATE oauth2_sessions\n SET human_name = $2\n WHERE oauth2_session_id = $1\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "8afada5220fefb0d01ed6f87d3d0ee8fca86b5cdce9320e190e3d3b8fd9f63bc"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT oauth2_authorization_grant_id\n , created_at\n , cancelled_at\n , fulfilled_at\n , exchanged_at\n , scope\n , state\n , redirect_uri\n , response_mode\n , nonce\n , oauth2_client_id\n , authorization_code\n , response_type_code\n , response_type_id_token\n , code_challenge\n , code_challenge_method\n , login_hint\n , oauth2_session_id\n FROM\n oauth2_authorization_grants\n\n WHERE authorization_code = $1\n ",
|
||||
"query": "\n SELECT oauth2_authorization_grant_id\n , created_at\n , cancelled_at\n , fulfilled_at\n , exchanged_at\n , scope\n , state\n , redirect_uri\n , response_mode\n , nonce\n , oauth2_client_id\n , authorization_code\n , response_type_code\n , response_type_id_token\n , code_challenge\n , code_challenge_method\n , login_hint\n , locale\n , oauth2_session_id\n FROM\n oauth2_authorization_grants\n\n WHERE authorization_code = $1\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -90,6 +90,11 @@
|
||||
},
|
||||
{
|
||||
"ordinal": 17,
|
||||
"name": "locale",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 18,
|
||||
"name": "oauth2_session_id",
|
||||
"type_info": "Uuid"
|
||||
}
|
||||
@@ -117,8 +122,9 @@
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "890516adeeaf2d6a4fe83a69e42e18b8817d1bb511e08ee761a031fa12d27251"
|
||||
"hash": "8ef27901b96b73826a431ad6c5fabecc18c36d8cdba8db3b47953855fa5c9035"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT oauth2_authorization_grant_id\n , created_at\n , cancelled_at\n , fulfilled_at\n , exchanged_at\n , scope\n , state\n , redirect_uri\n , response_mode\n , nonce\n , oauth2_client_id\n , authorization_code\n , response_type_code\n , response_type_id_token\n , code_challenge\n , code_challenge_method\n , login_hint\n , oauth2_session_id\n FROM\n oauth2_authorization_grants\n\n WHERE oauth2_authorization_grant_id = $1\n ",
|
||||
"query": "\n SELECT oauth2_authorization_grant_id\n , created_at\n , cancelled_at\n , fulfilled_at\n , exchanged_at\n , scope\n , state\n , redirect_uri\n , response_mode\n , nonce\n , oauth2_client_id\n , authorization_code\n , response_type_code\n , response_type_id_token\n , code_challenge\n , code_challenge_method\n , login_hint\n , locale\n , oauth2_session_id\n FROM\n oauth2_authorization_grants\n\n WHERE oauth2_authorization_grant_id = $1\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -90,6 +90,11 @@
|
||||
},
|
||||
{
|
||||
"ordinal": 17,
|
||||
"name": "locale",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 18,
|
||||
"name": "oauth2_session_id",
|
||||
"type_info": "Uuid"
|
||||
}
|
||||
@@ -117,8 +122,9 @@
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "bf6d1e3e3145438c988b1a47fc13f0168a63e278d8f8c947cb3ab65173f92ae4"
|
||||
"hash": "c960f4f5571ee68816c49898125979f3c78c2caca52cb4b8dc9880e669a1f23e"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n INSERT INTO compat_sessions\n (compat_session_id, user_id, device_id,\n user_session_id, created_at, is_synapse_admin)\n VALUES ($1, $2, $3, $4, $5, $6)\n ",
|
||||
"query": "\n INSERT INTO compat_sessions\n (compat_session_id, user_id, device_id,\n user_session_id, created_at, is_synapse_admin,\n human_name)\n VALUES ($1, $2, $3, $4, $5, $6, $7)\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
@@ -10,10 +10,11 @@
|
||||
"Text",
|
||||
"Uuid",
|
||||
"Timestamptz",
|
||||
"Bool"
|
||||
"Bool",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "cf1273b8aaaccedeb212a971d5e8e0dd23bfddab0ec08ee192783e103a1c4766"
|
||||
"hash": "e99ab37ab3e03ad9c48792772b09bac77b09f67e623d5371ab4dadbe2d41fa1c"
|
||||
}
|
||||
15
crates/storage-pg/.sqlx/query-eb095f64bec5ac885683a8c6708320760971317c4519fae7af9d44e2be50985d.json
generated
Normal file
15
crates/storage-pg/.sqlx/query-eb095f64bec5ac885683a8c6708320760971317c4519fae7af9d44e2be50985d.json
generated
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n UPDATE compat_sessions\n SET human_name = $2\n WHERE compat_session_id = $1\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "eb095f64bec5ac885683a8c6708320760971317c4519fae7af9d44e2be50985d"
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
-- Copyright 2025 New Vector Ltd.
|
||||
--
|
||||
-- SPDX-License-Identifier: AGPL-3.0-only
|
||||
-- Please see LICENSE in the repository root for full details.
|
||||
|
||||
-- Track the locale of the user which asked for the authorization grant
|
||||
ALTER TABLE oauth2_authorization_grants
|
||||
ADD COLUMN locale TEXT;
|
||||
@@ -0,0 +1,8 @@
|
||||
-- Copyright 2025 New Vector Ltd.
|
||||
--
|
||||
-- SPDX-License-Identifier: AGPL-3.0-only
|
||||
-- Please see LICENSE in the repository root for full details.
|
||||
|
||||
-- Add a user-provided human name to OAuth 2.0 sessions
|
||||
ALTER TABLE oauth2_sessions
|
||||
ADD COLUMN human_name TEXT;
|
||||
@@ -192,6 +192,7 @@ impl TryFrom<AppSessionLookup> for AppSession {
|
||||
user_agent,
|
||||
last_active_at,
|
||||
last_active_ip,
|
||||
human_name,
|
||||
};
|
||||
|
||||
Ok(AppSession::OAuth2(Box::new(session)))
|
||||
@@ -299,7 +300,10 @@ impl AppSessionRepository for PgAppSessionRepository<'_> {
|
||||
AppSessionLookupIden::ScopeList,
|
||||
)
|
||||
.expr_as(Expr::cust("NULL"), AppSessionLookupIden::DeviceId)
|
||||
.expr_as(Expr::cust("NULL"), AppSessionLookupIden::HumanName)
|
||||
.expr_as(
|
||||
Expr::col((OAuth2Sessions::Table, OAuth2Sessions::HumanName)),
|
||||
AppSessionLookupIden::HumanName,
|
||||
)
|
||||
.expr_as(
|
||||
Expr::col((OAuth2Sessions::Table, OAuth2Sessions::CreatedAt)),
|
||||
AppSessionLookupIden::CreatedAt,
|
||||
@@ -571,7 +575,7 @@ mod tests {
|
||||
let device = Device::generate(&mut rng);
|
||||
let compat_session = repo
|
||||
.compat_session()
|
||||
.add(&mut rng, &clock, &user, device.clone(), None, false)
|
||||
.add(&mut rng, &clock, &user, device.clone(), None, false, None)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
|
||||
@@ -79,7 +79,7 @@ mod tests {
|
||||
let device_str = device.as_str().to_owned();
|
||||
let session = repo
|
||||
.compat_session()
|
||||
.add(&mut rng, &clock, &user, device.clone(), None, false)
|
||||
.add(&mut rng, &clock, &user, device.clone(), None, false, None)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(session.user_id, user.id);
|
||||
@@ -227,6 +227,7 @@ mod tests {
|
||||
device,
|
||||
Some(&browser_session),
|
||||
false,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -331,7 +332,7 @@ mod tests {
|
||||
let device = Device::generate(&mut rng);
|
||||
let session = repo
|
||||
.compat_session()
|
||||
.add(&mut rng, &clock, &user, device, None, false)
|
||||
.add(&mut rng, &clock, &user, device, None, false, None)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -452,7 +453,7 @@ mod tests {
|
||||
let device = Device::generate(&mut rng);
|
||||
let session = repo
|
||||
.compat_session()
|
||||
.add(&mut rng, &clock, &user, device, None, false)
|
||||
.add(&mut rng, &clock, &user, device, None, false, None)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -618,7 +619,7 @@ mod tests {
|
||||
let device = Device::generate(&mut rng);
|
||||
let compat_session = repo
|
||||
.compat_session()
|
||||
.add(&mut rng, &clock, &user, device, None, false)
|
||||
.add(&mut rng, &clock, &user, device, None, false, None)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
|
||||
@@ -305,6 +305,7 @@ impl CompatSessionRepository for PgCompatSessionRepository<'_> {
|
||||
device: Device,
|
||||
browser_session: Option<&BrowserSession>,
|
||||
is_synapse_admin: bool,
|
||||
human_name: Option<String>,
|
||||
) -> Result<CompatSession, Self::Error> {
|
||||
let created_at = clock.now();
|
||||
let id = Ulid::from_datetime_with_source(created_at.into(), rng);
|
||||
@@ -314,8 +315,9 @@ impl CompatSessionRepository for PgCompatSessionRepository<'_> {
|
||||
r#"
|
||||
INSERT INTO compat_sessions
|
||||
(compat_session_id, user_id, device_id,
|
||||
user_session_id, created_at, is_synapse_admin)
|
||||
VALUES ($1, $2, $3, $4, $5, $6)
|
||||
user_session_id, created_at, is_synapse_admin,
|
||||
human_name)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||
"#,
|
||||
Uuid::from(id),
|
||||
Uuid::from(user.id),
|
||||
@@ -323,6 +325,7 @@ impl CompatSessionRepository for PgCompatSessionRepository<'_> {
|
||||
browser_session.map(|s| Uuid::from(s.id)),
|
||||
created_at,
|
||||
is_synapse_admin,
|
||||
human_name.as_deref(),
|
||||
)
|
||||
.traced()
|
||||
.execute(&mut *self.conn)
|
||||
@@ -333,7 +336,7 @@ impl CompatSessionRepository for PgCompatSessionRepository<'_> {
|
||||
state: CompatSessionState::default(),
|
||||
user_id: user.id,
|
||||
device: Some(device),
|
||||
human_name: None,
|
||||
human_name,
|
||||
user_session_id: browser_session.map(|s| s.id),
|
||||
created_at,
|
||||
is_synapse_admin,
|
||||
@@ -622,4 +625,38 @@ impl CompatSessionRepository for PgCompatSessionRepository<'_> {
|
||||
|
||||
Ok(compat_session)
|
||||
}
|
||||
|
||||
#[tracing::instrument(
|
||||
name = "repository.compat_session.set_human_name",
|
||||
skip(self),
|
||||
fields(
|
||||
compat_session.id = %compat_session.id,
|
||||
compat_session.human_name = ?human_name,
|
||||
),
|
||||
err,
|
||||
)]
|
||||
async fn set_human_name(
|
||||
&mut self,
|
||||
mut compat_session: CompatSession,
|
||||
human_name: Option<String>,
|
||||
) -> Result<CompatSession, Self::Error> {
|
||||
let res = sqlx::query!(
|
||||
r#"
|
||||
UPDATE compat_sessions
|
||||
SET human_name = $2
|
||||
WHERE compat_session_id = $1
|
||||
"#,
|
||||
Uuid::from(compat_session.id),
|
||||
human_name.as_deref(),
|
||||
)
|
||||
.traced()
|
||||
.execute(&mut *self.conn)
|
||||
.await?;
|
||||
|
||||
compat_session.human_name = human_name;
|
||||
|
||||
DatabaseError::ensure_affected_rows(&res, 1)?;
|
||||
|
||||
Ok(compat_session)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,6 +83,7 @@ pub enum OAuth2Sessions {
|
||||
UserAgent,
|
||||
LastActiveAt,
|
||||
LastActiveIp,
|
||||
HumanName,
|
||||
}
|
||||
|
||||
#[derive(sea_query::Iden)]
|
||||
|
||||
@@ -52,6 +52,7 @@ struct GrantLookup {
|
||||
code_challenge: Option<String>,
|
||||
code_challenge_method: Option<String>,
|
||||
login_hint: Option<String>,
|
||||
locale: Option<String>,
|
||||
oauth2_client_id: Uuid,
|
||||
oauth2_session_id: Option<Uuid>,
|
||||
}
|
||||
@@ -162,6 +163,7 @@ impl TryFrom<GrantLookup> for AuthorizationGrant {
|
||||
created_at: value.created_at,
|
||||
response_type_id_token: value.response_type_id_token,
|
||||
login_hint: value.login_hint,
|
||||
locale: value.locale,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -194,6 +196,7 @@ impl OAuth2AuthorizationGrantRepository for PgOAuth2AuthorizationGrantRepository
|
||||
response_mode: ResponseMode,
|
||||
response_type_id_token: bool,
|
||||
login_hint: Option<String>,
|
||||
locale: Option<String>,
|
||||
) -> Result<AuthorizationGrant, Self::Error> {
|
||||
let code_challenge = code
|
||||
.as_ref()
|
||||
@@ -225,10 +228,11 @@ impl OAuth2AuthorizationGrantRepository for PgOAuth2AuthorizationGrantRepository
|
||||
response_type_id_token,
|
||||
authorization_code,
|
||||
login_hint,
|
||||
locale,
|
||||
created_at
|
||||
)
|
||||
VALUES
|
||||
($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14)
|
||||
($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15)
|
||||
"#,
|
||||
Uuid::from(id),
|
||||
Uuid::from(client.id),
|
||||
@@ -243,6 +247,7 @@ impl OAuth2AuthorizationGrantRepository for PgOAuth2AuthorizationGrantRepository
|
||||
response_type_id_token,
|
||||
code_str,
|
||||
login_hint,
|
||||
locale,
|
||||
created_at,
|
||||
)
|
||||
.traced()
|
||||
@@ -262,6 +267,7 @@ impl OAuth2AuthorizationGrantRepository for PgOAuth2AuthorizationGrantRepository
|
||||
created_at,
|
||||
response_type_id_token,
|
||||
login_hint,
|
||||
locale,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -295,6 +301,7 @@ impl OAuth2AuthorizationGrantRepository for PgOAuth2AuthorizationGrantRepository
|
||||
, code_challenge
|
||||
, code_challenge_method
|
||||
, login_hint
|
||||
, locale
|
||||
, oauth2_session_id
|
||||
FROM
|
||||
oauth2_authorization_grants
|
||||
@@ -344,6 +351,7 @@ impl OAuth2AuthorizationGrantRepository for PgOAuth2AuthorizationGrantRepository
|
||||
, code_challenge
|
||||
, code_challenge_method
|
||||
, login_hint
|
||||
, locale
|
||||
, oauth2_session_id
|
||||
FROM
|
||||
oauth2_authorization_grants
|
||||
|
||||
@@ -138,6 +138,7 @@ mod tests {
|
||||
ResponseMode::Query,
|
||||
true,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -55,6 +55,7 @@ struct OAuthSessionLookup {
|
||||
user_agent: Option<String>,
|
||||
last_active_at: Option<DateTime<Utc>>,
|
||||
last_active_ip: Option<IpAddr>,
|
||||
human_name: Option<String>,
|
||||
}
|
||||
|
||||
impl TryFrom<OAuthSessionLookup> for Session {
|
||||
@@ -90,6 +91,7 @@ impl TryFrom<OAuthSessionLookup> for Session {
|
||||
user_agent: value.user_agent,
|
||||
last_active_at: value.last_active_at,
|
||||
last_active_ip: value.last_active_ip,
|
||||
human_name: value.human_name,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -195,6 +197,7 @@ impl OAuth2SessionRepository for PgOAuth2SessionRepository<'_> {
|
||||
, user_agent
|
||||
, last_active_at
|
||||
, last_active_ip as "last_active_ip: IpAddr"
|
||||
, human_name
|
||||
FROM oauth2_sessions
|
||||
|
||||
WHERE oauth2_session_id = $1
|
||||
@@ -270,6 +273,7 @@ impl OAuth2SessionRepository for PgOAuth2SessionRepository<'_> {
|
||||
user_agent: None,
|
||||
last_active_at: None,
|
||||
last_active_ip: None,
|
||||
human_name: None,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -392,6 +396,10 @@ impl OAuth2SessionRepository for PgOAuth2SessionRepository<'_> {
|
||||
Expr::col((OAuth2Sessions::Table, OAuth2Sessions::LastActiveIp)),
|
||||
OAuthSessionLookupIden::LastActiveIp,
|
||||
)
|
||||
.expr_as(
|
||||
Expr::col((OAuth2Sessions::Table, OAuth2Sessions::HumanName)),
|
||||
OAuthSessionLookupIden::HumanName,
|
||||
)
|
||||
.from(OAuth2Sessions::Table)
|
||||
.apply_filter(filter)
|
||||
.generate_pagination(
|
||||
@@ -521,4 +529,38 @@ impl OAuth2SessionRepository for PgOAuth2SessionRepository<'_> {
|
||||
|
||||
Ok(session)
|
||||
}
|
||||
|
||||
#[tracing::instrument(
|
||||
name = "repository.oauth2_session.set_human_name",
|
||||
skip(self),
|
||||
fields(
|
||||
client.id = %session.client_id,
|
||||
session.human_name = ?human_name,
|
||||
),
|
||||
err,
|
||||
)]
|
||||
async fn set_human_name(
|
||||
&mut self,
|
||||
mut session: Session,
|
||||
human_name: Option<String>,
|
||||
) -> Result<Session, Self::Error> {
|
||||
let res = sqlx::query!(
|
||||
r#"
|
||||
UPDATE oauth2_sessions
|
||||
SET human_name = $2
|
||||
WHERE oauth2_session_id = $1
|
||||
"#,
|
||||
Uuid::from(session.id),
|
||||
human_name.as_deref(),
|
||||
)
|
||||
.traced()
|
||||
.execute(&mut *self.conn)
|
||||
.await?;
|
||||
|
||||
session.human_name = human_name;
|
||||
|
||||
DatabaseError::ensure_affected_rows(&res, 1)?;
|
||||
|
||||
Ok(session)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -215,10 +215,13 @@ pub trait CompatSessionRepository: Send + Sync {
|
||||
/// * `device`: The device ID of this session
|
||||
/// * `browser_session`: The browser session which created this session
|
||||
/// * `is_synapse_admin`: Whether the session is a synapse admin session
|
||||
/// * `human_name`: The human-readable name of the session provided by the
|
||||
/// client or the user
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns [`Self::Error`] if the underlying repository fails
|
||||
#[expect(clippy::too_many_arguments)]
|
||||
async fn add(
|
||||
&mut self,
|
||||
rng: &mut (dyn RngCore + Send),
|
||||
@@ -227,6 +230,7 @@ pub trait CompatSessionRepository: Send + Sync {
|
||||
device: Device,
|
||||
browser_session: Option<&BrowserSession>,
|
||||
is_synapse_admin: bool,
|
||||
human_name: Option<String>,
|
||||
) -> Result<CompatSession, Self::Error>;
|
||||
|
||||
/// End a compat session
|
||||
@@ -324,6 +328,22 @@ pub trait CompatSessionRepository: Send + Sync {
|
||||
compat_session: CompatSession,
|
||||
user_agent: String,
|
||||
) -> Result<CompatSession, Self::Error>;
|
||||
|
||||
/// Set the human name of a compat session
|
||||
///
|
||||
/// # Parameters
|
||||
///
|
||||
/// * `compat_session`: The compat session to set the human name for
|
||||
/// * `human_name`: The human name to set
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns [`Self::Error`] if the underlying repository fails
|
||||
async fn set_human_name(
|
||||
&mut self,
|
||||
compat_session: CompatSession,
|
||||
human_name: Option<String>,
|
||||
) -> Result<CompatSession, Self::Error>;
|
||||
}
|
||||
|
||||
repository_impl!(CompatSessionRepository:
|
||||
@@ -337,6 +357,7 @@ repository_impl!(CompatSessionRepository:
|
||||
device: Device,
|
||||
browser_session: Option<&BrowserSession>,
|
||||
is_synapse_admin: bool,
|
||||
human_name: Option<String>,
|
||||
) -> Result<CompatSession, Self::Error>;
|
||||
|
||||
async fn finish(
|
||||
@@ -369,4 +390,10 @@ repository_impl!(CompatSessionRepository:
|
||||
compat_session: CompatSession,
|
||||
user_agent: String,
|
||||
) -> Result<CompatSession, Self::Error>;
|
||||
|
||||
async fn set_human_name(
|
||||
&mut self,
|
||||
compat_session: CompatSession,
|
||||
human_name: Option<String>,
|
||||
) -> Result<CompatSession, Self::Error>;
|
||||
);
|
||||
|
||||
@@ -39,6 +39,8 @@ pub trait OAuth2AuthorizationGrantRepository: Send + Sync {
|
||||
/// * `response_type_id_token`: Whether the `id_token` `response_type` was
|
||||
/// requested
|
||||
/// * `login_hint`: The login_hint the client sent, if set
|
||||
/// * `locale`: The locale the detected when the user asked for the
|
||||
/// authorization grant
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
@@ -57,6 +59,7 @@ pub trait OAuth2AuthorizationGrantRepository: Send + Sync {
|
||||
response_mode: ResponseMode,
|
||||
response_type_id_token: bool,
|
||||
login_hint: Option<String>,
|
||||
locale: Option<String>,
|
||||
) -> Result<AuthorizationGrant, Self::Error>;
|
||||
|
||||
/// Lookup an authorization grant by its ID
|
||||
@@ -140,6 +143,7 @@ repository_impl!(OAuth2AuthorizationGrantRepository:
|
||||
response_mode: ResponseMode,
|
||||
response_type_id_token: bool,
|
||||
login_hint: Option<String>,
|
||||
locale: Option<String>,
|
||||
) -> Result<AuthorizationGrant, Self::Error>;
|
||||
|
||||
async fn lookup(&mut self, id: Ulid) -> Result<Option<AuthorizationGrant>, Self::Error>;
|
||||
|
||||
@@ -430,6 +430,18 @@ pub trait OAuth2SessionRepository: Send + Sync {
|
||||
session: Session,
|
||||
user_agent: String,
|
||||
) -> Result<Session, Self::Error>;
|
||||
|
||||
/// Set the human name of a [`Session`]
|
||||
///
|
||||
/// # Parameters
|
||||
///
|
||||
/// * `session`: The [`Session`] to set the human name for
|
||||
/// * `human_name`: The human name to set
|
||||
async fn set_human_name(
|
||||
&mut self,
|
||||
session: Session,
|
||||
human_name: Option<String>,
|
||||
) -> Result<Session, Self::Error>;
|
||||
}
|
||||
|
||||
repository_impl!(OAuth2SessionRepository:
|
||||
@@ -489,4 +501,10 @@ repository_impl!(OAuth2SessionRepository:
|
||||
session: Session,
|
||||
user_agent: String,
|
||||
) -> Result<Session, Self::Error>;
|
||||
|
||||
async fn set_human_name(
|
||||
&mut self,
|
||||
session: Session,
|
||||
human_name: Option<String>,
|
||||
) -> Result<Session, Self::Error>;
|
||||
);
|
||||
|
||||
@@ -1564,6 +1564,39 @@ impl TemplateContext for AccountInactiveContext {
|
||||
}
|
||||
}
|
||||
|
||||
/// Context used by the `device_name.txt` template
|
||||
#[derive(Serialize)]
|
||||
pub struct DeviceNameContext {
|
||||
client: Client,
|
||||
raw_user_agent: String,
|
||||
}
|
||||
|
||||
impl DeviceNameContext {
|
||||
/// Constructs a new context with a client and user agent
|
||||
#[must_use]
|
||||
pub fn new(client: Client, user_agent: Option<String>) -> Self {
|
||||
Self {
|
||||
client,
|
||||
raw_user_agent: user_agent.unwrap_or_default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TemplateContext for DeviceNameContext {
|
||||
fn sample(now: chrono::DateTime<Utc>, rng: &mut impl Rng) -> Vec<Self>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
Client::samples(now, rng)
|
||||
.into_iter()
|
||||
.map(|client| DeviceNameContext {
|
||||
client,
|
||||
raw_user_agent: "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(),
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
/// Context used by the `form_post.html` template
|
||||
#[derive(Serialize)]
|
||||
pub struct FormPostContext<T> {
|
||||
|
||||
@@ -40,6 +40,7 @@ pub fn register(
|
||||
env.add_filter("to_params", filter_to_params);
|
||||
env.add_filter("simplify_url", filter_simplify_url);
|
||||
env.add_filter("add_slashes", filter_add_slashes);
|
||||
env.add_filter("parse_user_agent", filter_parse_user_agent);
|
||||
env.add_function("add_params_to_url", function_add_params_to_url);
|
||||
env.add_function("counter", || Ok(Value::from_object(Counter::default())));
|
||||
env.add_global(
|
||||
@@ -133,6 +134,12 @@ fn filter_simplify_url(url: &str, kwargs: Kwargs) -> Result<String, minijinja::E
|
||||
}
|
||||
}
|
||||
|
||||
/// Filter which parses a user-agent string
|
||||
fn filter_parse_user_agent(user_agent: String) -> Value {
|
||||
let user_agent = mas_data_model::UserAgent::parse(user_agent);
|
||||
Value::from_serialize(user_agent)
|
||||
}
|
||||
|
||||
enum ParamsWhere {
|
||||
Fragment,
|
||||
Query,
|
||||
|
||||
@@ -35,13 +35,13 @@ mod macros;
|
||||
pub use self::{
|
||||
context::{
|
||||
AccountInactiveContext, ApiDocContext, AppContext, CompatSsoContext, ConsentContext,
|
||||
DeviceConsentContext, DeviceLinkContext, DeviceLinkFormField, EmailRecoveryContext,
|
||||
EmailVerificationContext, EmptyContext, ErrorContext, FormPostContext, IndexContext,
|
||||
LoginContext, LoginFormField, NotFoundContext, PasswordRegisterContext,
|
||||
PolicyViolationContext, PostAuthContext, PostAuthContextInner, RecoveryExpiredContext,
|
||||
RecoveryFinishContext, RecoveryFinishFormField, RecoveryProgressContext,
|
||||
RecoveryStartContext, RecoveryStartFormField, RegisterContext, RegisterFormField,
|
||||
RegisterStepsDisplayNameContext, RegisterStepsDisplayNameFormField,
|
||||
DeviceConsentContext, DeviceLinkContext, DeviceLinkFormField, DeviceNameContext,
|
||||
EmailRecoveryContext, EmailVerificationContext, EmptyContext, ErrorContext,
|
||||
FormPostContext, IndexContext, LoginContext, LoginFormField, NotFoundContext,
|
||||
PasswordRegisterContext, PolicyViolationContext, PostAuthContext, PostAuthContextInner,
|
||||
RecoveryExpiredContext, RecoveryFinishContext, RecoveryFinishFormField,
|
||||
RecoveryProgressContext, RecoveryStartContext, RecoveryStartFormField, RegisterContext,
|
||||
RegisterFormField, RegisterStepsDisplayNameContext, RegisterStepsDisplayNameFormField,
|
||||
RegisterStepsEmailInUseContext, RegisterStepsVerifyEmailContext,
|
||||
RegisterStepsVerifyEmailFormField, SiteBranding, SiteConfigExt, SiteFeatures,
|
||||
TemplateContext, UpstreamExistingLinkContext, UpstreamRegister, UpstreamRegisterFormField,
|
||||
@@ -417,6 +417,9 @@ register_templates! {
|
||||
|
||||
/// Render the 'account logged out' page
|
||||
pub fn render_account_logged_out(WithLanguage<WithCsrf<AccountInactiveContext>>) { "pages/account/logged_out.html" }
|
||||
|
||||
/// Render the automatic device name for OAuth 2.0 client
|
||||
pub fn render_device_name(WithLanguage<DeviceNameContext>) { "device_name.txt" }
|
||||
}
|
||||
|
||||
impl Templates {
|
||||
@@ -459,6 +462,7 @@ impl Templates {
|
||||
check::render_upstream_oauth2_link_mismatch(self, now, rng)?;
|
||||
check::render_upstream_oauth2_suggest_link(self, now, rng)?;
|
||||
check::render_upstream_oauth2_do_register(self, now, rng)?;
|
||||
check::render_device_name(self, now, rng)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -132,7 +132,8 @@
|
||||
"user_agent": "Mozilla/5.0",
|
||||
"last_active_at": "1970-01-01T00:00:00Z",
|
||||
"last_active_ip": "1.2.3.4",
|
||||
"finished_at": null
|
||||
"finished_at": null,
|
||||
"human_name": "Laptop"
|
||||
},
|
||||
"links": {
|
||||
"self": "/api/admin/v1/compat-sessions/01040G2081040G2081040G2081"
|
||||
@@ -150,7 +151,8 @@
|
||||
"user_agent": "Mozilla/5.0",
|
||||
"last_active_at": "1970-01-01T00:00:00Z",
|
||||
"last_active_ip": "1.2.3.4",
|
||||
"finished_at": "1970-01-01T00:00:00Z"
|
||||
"finished_at": "1970-01-01T00:00:00Z",
|
||||
"human_name": null
|
||||
},
|
||||
"links": {
|
||||
"self": "/api/admin/v1/compat-sessions/02081040G2081040G2081040G2"
|
||||
@@ -168,7 +170,8 @@
|
||||
"user_agent": null,
|
||||
"last_active_at": null,
|
||||
"last_active_ip": null,
|
||||
"finished_at": null
|
||||
"finished_at": null,
|
||||
"human_name": null
|
||||
},
|
||||
"links": {
|
||||
"self": "/api/admin/v1/compat-sessions/030C1G60R30C1G60R30C1G60R3"
|
||||
@@ -245,7 +248,8 @@
|
||||
"user_agent": "Mozilla/5.0",
|
||||
"last_active_at": "1970-01-01T00:00:00Z",
|
||||
"last_active_ip": "1.2.3.4",
|
||||
"finished_at": null
|
||||
"finished_at": null,
|
||||
"human_name": "Laptop"
|
||||
},
|
||||
"links": {
|
||||
"self": "/api/admin/v1/compat-sessions/01040G2081040G2081040G2081"
|
||||
@@ -430,7 +434,8 @@
|
||||
"scope": "openid",
|
||||
"user_agent": "Mozilla/5.0",
|
||||
"last_active_at": "1970-01-01T00:00:00Z",
|
||||
"last_active_ip": "127.0.0.1"
|
||||
"last_active_ip": "127.0.0.1",
|
||||
"human_name": "Laptop"
|
||||
},
|
||||
"links": {
|
||||
"self": "/api/admin/v1/oauth2-sessions/01040G2081040G2081040G2081"
|
||||
@@ -448,7 +453,8 @@
|
||||
"scope": "urn:mas:admin",
|
||||
"user_agent": null,
|
||||
"last_active_at": null,
|
||||
"last_active_ip": null
|
||||
"last_active_ip": null,
|
||||
"human_name": null
|
||||
},
|
||||
"links": {
|
||||
"self": "/api/admin/v1/oauth2-sessions/02081040G2081040G2081040G2"
|
||||
@@ -466,7 +472,8 @@
|
||||
"scope": "urn:matrix:org.matrix.msc2967.client:api:*",
|
||||
"user_agent": "Mozilla/5.0",
|
||||
"last_active_at": "1970-01-01T00:00:00Z",
|
||||
"last_active_ip": "127.0.0.1"
|
||||
"last_active_ip": "127.0.0.1",
|
||||
"human_name": null
|
||||
},
|
||||
"links": {
|
||||
"self": "/api/admin/v1/oauth2-sessions/030C1G60R30C1G60R30C1G60R3"
|
||||
@@ -560,7 +567,8 @@
|
||||
"scope": "openid",
|
||||
"user_agent": "Mozilla/5.0",
|
||||
"last_active_at": "1970-01-01T00:00:00Z",
|
||||
"last_active_ip": "127.0.0.1"
|
||||
"last_active_ip": "127.0.0.1",
|
||||
"human_name": "Laptop"
|
||||
},
|
||||
"links": {
|
||||
"self": "/api/admin/v1/oauth2-sessions/01040G2081040G2081040G2081"
|
||||
@@ -2726,6 +2734,11 @@
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"nullable": true
|
||||
},
|
||||
"human_name": {
|
||||
"description": "The user-provided name, if any",
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -3001,6 +3014,11 @@
|
||||
"type": "string",
|
||||
"format": "ip",
|
||||
"nullable": true
|
||||
},
|
||||
"human_name": {
|
||||
"description": "The user-provided name, if any",
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -258,6 +258,11 @@
|
||||
"last_active_label": "Last Active",
|
||||
"name_for_platform": "{{name}} for {{platform}}",
|
||||
"scopes_label": "Scopes",
|
||||
"set_device_name": {
|
||||
"help": "Set a name that will help you identify this device.",
|
||||
"label": "Device name",
|
||||
"title": "Edit device name"
|
||||
},
|
||||
"signed_in_label": "Signed in",
|
||||
"title": "Device details",
|
||||
"unknown_browser": "Unknown browser",
|
||||
|
||||
@@ -370,6 +370,10 @@ type CompatSession implements Node & CreationEvent {
|
||||
The last time the session was active.
|
||||
"""
|
||||
lastActiveAt: DateTime
|
||||
"""
|
||||
A human-provided name for the session.
|
||||
"""
|
||||
humanName: String
|
||||
}
|
||||
|
||||
type CompatSessionConnection {
|
||||
@@ -937,7 +941,13 @@ type Mutation {
|
||||
input: CreateOAuth2SessionInput!
|
||||
): CreateOAuth2SessionPayload!
|
||||
endOauth2Session(input: EndOAuth2SessionInput!): EndOAuth2SessionPayload!
|
||||
setOauth2SessionName(
|
||||
input: SetOAuth2SessionNameInput!
|
||||
): SetOAuth2SessionNamePayload!
|
||||
endCompatSession(input: EndCompatSessionInput!): EndCompatSessionPayload!
|
||||
setCompatSessionName(
|
||||
input: SetCompatSessionNameInput!
|
||||
): SetCompatSessionNamePayload!
|
||||
endBrowserSession(input: EndBrowserSessionInput!): EndBrowserSessionPayload!
|
||||
"""
|
||||
Set the display name of a user
|
||||
@@ -1060,6 +1070,10 @@ type Oauth2Session implements Node & CreationEvent {
|
||||
The last time the session was active.
|
||||
"""
|
||||
lastActiveAt: DateTime
|
||||
"""
|
||||
The user-provided name for this session.
|
||||
"""
|
||||
humanName: String
|
||||
}
|
||||
|
||||
type Oauth2SessionConnection {
|
||||
@@ -1426,6 +1440,45 @@ type SetCanRequestAdminPayload {
|
||||
user: User
|
||||
}
|
||||
|
||||
"""
|
||||
The input of the `setCompatSessionName` mutation.
|
||||
"""
|
||||
input SetCompatSessionNameInput {
|
||||
"""
|
||||
The ID of the session to set the name of.
|
||||
"""
|
||||
compatSessionId: ID!
|
||||
"""
|
||||
The new name of the session.
|
||||
"""
|
||||
humanName: String!
|
||||
}
|
||||
|
||||
type SetCompatSessionNamePayload {
|
||||
"""
|
||||
The status of the mutation.
|
||||
"""
|
||||
status: SetCompatSessionNameStatus!
|
||||
"""
|
||||
The session that was updated.
|
||||
"""
|
||||
oauth2Session: CompatSession
|
||||
}
|
||||
|
||||
"""
|
||||
The status of the `setCompatSessionName` mutation.
|
||||
"""
|
||||
enum SetCompatSessionNameStatus {
|
||||
"""
|
||||
The session was updated.
|
||||
"""
|
||||
UPDATED
|
||||
"""
|
||||
The session was not found.
|
||||
"""
|
||||
NOT_FOUND
|
||||
}
|
||||
|
||||
"""
|
||||
The input for the `addEmail` mutation
|
||||
"""
|
||||
@@ -1468,6 +1521,45 @@ enum SetDisplayNameStatus {
|
||||
INVALID
|
||||
}
|
||||
|
||||
"""
|
||||
The input of the `setOauth2SessionName` mutation.
|
||||
"""
|
||||
input SetOAuth2SessionNameInput {
|
||||
"""
|
||||
The ID of the session to set the name of.
|
||||
"""
|
||||
oauth2SessionId: ID!
|
||||
"""
|
||||
The new name of the session.
|
||||
"""
|
||||
humanName: String!
|
||||
}
|
||||
|
||||
type SetOAuth2SessionNamePayload {
|
||||
"""
|
||||
The status of the mutation.
|
||||
"""
|
||||
status: SetOAuth2SessionNameStatus!
|
||||
"""
|
||||
The session that was updated.
|
||||
"""
|
||||
oauth2Session: Oauth2Session
|
||||
}
|
||||
|
||||
"""
|
||||
The status of the `setOauth2SessionName` mutation.
|
||||
"""
|
||||
enum SetOAuth2SessionNameStatus {
|
||||
"""
|
||||
The session was updated.
|
||||
"""
|
||||
UPDATED
|
||||
"""
|
||||
The session was not found.
|
||||
"""
|
||||
NOT_FOUND
|
||||
}
|
||||
|
||||
"""
|
||||
The input for the `setPasswordByRecovery` mutation.
|
||||
"""
|
||||
|
||||
@@ -22,6 +22,7 @@ export const FRAGMENT = graphql(/* GraphQL */ `
|
||||
finishedAt
|
||||
lastActiveIp
|
||||
lastActiveAt
|
||||
humanName
|
||||
...EndCompatSessionButton_session
|
||||
userAgent {
|
||||
name
|
||||
@@ -42,9 +43,11 @@ const CompatSession: React.FC<{
|
||||
const { t } = useTranslation();
|
||||
const data = useFragment(FRAGMENT, session);
|
||||
|
||||
const clientName = data.ssoLogin?.redirectUri
|
||||
? simplifyUrl(data.ssoLogin.redirectUri)
|
||||
: undefined;
|
||||
const clientName =
|
||||
data.humanName ??
|
||||
(data.ssoLogin?.redirectUri
|
||||
? simplifyUrl(data.ssoLogin.redirectUri)
|
||||
: undefined);
|
||||
|
||||
const deviceType = data.userAgent?.deviceType ?? "UNKNOWN";
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ export const FRAGMENT = graphql(/* GraphQL */ `
|
||||
finishedAt
|
||||
lastActiveIp
|
||||
lastActiveAt
|
||||
humanName
|
||||
|
||||
...EndOAuth2SessionButton_session
|
||||
|
||||
@@ -72,6 +73,7 @@ const OAuth2Session: React.FC<Props> = ({ session }) => {
|
||||
const clientName = data.client.clientName || data.client.clientId;
|
||||
|
||||
const deviceName =
|
||||
data.humanName ??
|
||||
data.userAgent?.model ??
|
||||
(data.userAgent?.name
|
||||
? data.userAgent?.os
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import { TooltipProvider } from "@vector-im/compound-web";
|
||||
import { beforeAll, describe, expect, it } from "vitest";
|
||||
import { makeFragmentData } from "../../gql";
|
||||
import { mockLocale } from "../../test-utils/mockLocale";
|
||||
@@ -33,7 +34,9 @@ describe("<CompatSessionDetail>", () => {
|
||||
const data = makeFragmentData({ ...baseSession }, FRAGMENT);
|
||||
|
||||
const { container, getByText, queryByText } = render(
|
||||
<CompatSessionDetail session={data} />,
|
||||
<TooltipProvider>
|
||||
<CompatSessionDetail session={data} />
|
||||
</TooltipProvider>,
|
||||
);
|
||||
|
||||
expect(container).toMatchSnapshot();
|
||||
@@ -51,7 +54,9 @@ describe("<CompatSessionDetail>", () => {
|
||||
);
|
||||
|
||||
const { container, getByText, queryByText } = render(
|
||||
<CompatSessionDetail session={data} />,
|
||||
<TooltipProvider>
|
||||
<CompatSessionDetail session={data} />
|
||||
</TooltipProvider>,
|
||||
);
|
||||
|
||||
expect(container).toMatchSnapshot();
|
||||
@@ -69,7 +74,9 @@ describe("<CompatSessionDetail>", () => {
|
||||
);
|
||||
|
||||
const { container, getByText, queryByText } = render(
|
||||
<CompatSessionDetail session={data} />,
|
||||
<TooltipProvider>
|
||||
<CompatSessionDetail session={data} />
|
||||
</TooltipProvider>,
|
||||
);
|
||||
|
||||
expect(container).toMatchSnapshot();
|
||||
|
||||
@@ -4,17 +4,28 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
// Please see LICENSE in the repository root for full details.
|
||||
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { VisualList } from "@vector-im/compound-web";
|
||||
import { parseISO } from "date-fns";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { type FragmentType, graphql, useFragment } from "../../gql";
|
||||
import { graphqlRequest } from "../../graphql";
|
||||
import simplifyUrl from "../../utils/simplifyUrl";
|
||||
import DateTime from "../DateTime";
|
||||
import EndCompatSessionButton from "../Session/EndCompatSessionButton";
|
||||
import LastActive from "../Session/LastActive";
|
||||
import EditSessionName from "./EditSessionName";
|
||||
import SessionHeader from "./SessionHeader";
|
||||
import * as Info from "./SessionInfo";
|
||||
|
||||
const SET_SESSION_NAME_MUTATION = graphql(/* GraphQL */ `
|
||||
mutation SetCompatSessionName($sessionId: ID!, $displayName: String!) {
|
||||
setCompatSessionName(input: { compatSessionId: $sessionId, humanName: $displayName }) {
|
||||
status
|
||||
}
|
||||
}
|
||||
`);
|
||||
|
||||
export const FRAGMENT = graphql(/* GraphQL */ `
|
||||
fragment CompatSession_detail on CompatSession {
|
||||
id
|
||||
@@ -23,6 +34,7 @@ export const FRAGMENT = graphql(/* GraphQL */ `
|
||||
finishedAt
|
||||
lastActiveIp
|
||||
lastActiveAt
|
||||
humanName
|
||||
|
||||
...EndCompatSessionButton_session
|
||||
|
||||
@@ -46,6 +58,19 @@ type Props = {
|
||||
const CompatSessionDetail: React.FC<Props> = ({ session }) => {
|
||||
const data = useFragment(FRAGMENT, session);
|
||||
const { t } = useTranslation();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const setDisplayName = useMutation({
|
||||
mutationFn: (displayName: string) =>
|
||||
graphqlRequest({
|
||||
query: SET_SESSION_NAME_MUTATION,
|
||||
variables: { sessionId: data.id, displayName },
|
||||
}),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["sessionDetail", data.id] });
|
||||
queryClient.invalidateQueries({ queryKey: ["sessionsOverview"] });
|
||||
},
|
||||
});
|
||||
|
||||
const deviceName =
|
||||
data.userAgent?.model ??
|
||||
@@ -62,10 +87,13 @@ const CompatSessionDetail: React.FC<Props> = ({ session }) => {
|
||||
? simplifyUrl(data.ssoLogin.redirectUri)
|
||||
: data.deviceId || data.id;
|
||||
|
||||
const sessionName = data.humanName ?? `${clientName}: ${deviceName}`;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-10">
|
||||
<SessionHeader to="/sessions">
|
||||
{clientName}: {deviceName}
|
||||
{sessionName}
|
||||
<EditSessionName mutation={setDisplayName} deviceName={sessionName} />
|
||||
</SessionHeader>
|
||||
<Info.DataSection>
|
||||
<Info.DataSectionHeader>
|
||||
@@ -141,10 +169,12 @@ const CompatSessionDetail: React.FC<Props> = ({ session }) => {
|
||||
</Info.DataLabel>
|
||||
<Info.DataValue>{deviceName}</Info.DataValue>
|
||||
</Info.Data>
|
||||
<Info.Data>
|
||||
<Info.DataLabel>{t("frontend.session.uri_label")}</Info.DataLabel>
|
||||
<Info.DataValue>{data.ssoLogin?.redirectUri}</Info.DataValue>
|
||||
</Info.Data>
|
||||
{data.ssoLogin && (
|
||||
<Info.Data>
|
||||
<Info.DataLabel>{t("frontend.session.uri_label")}</Info.DataLabel>
|
||||
<Info.DataValue>{data.ssoLogin?.redirectUri}</Info.DataValue>
|
||||
</Info.Data>
|
||||
)}
|
||||
</Info.DataList>
|
||||
</Info.DataSection>
|
||||
|
||||
|
||||
100
frontend/src/components/SessionDetail/EditSessionName.tsx
Normal file
100
frontend/src/components/SessionDetail/EditSessionName.tsx
Normal file
@@ -0,0 +1,100 @@
|
||||
// Copyright 2025 New Vector Ltd.
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
// Please see LICENSE in the repository root for full details.
|
||||
|
||||
import IconEdit from "@vector-im/compound-design-tokens/assets/web/icons/edit";
|
||||
import { Button, Form, IconButton, Tooltip } from "@vector-im/compound-web";
|
||||
import {
|
||||
type ComponentPropsWithoutRef,
|
||||
forwardRef,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import * as Dialog from "../Dialog";
|
||||
import LoadingSpinner from "../LoadingSpinner";
|
||||
|
||||
import type { UseMutationResult } from "@tanstack/react-query";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
// This needs to be its own component because else props and refs aren't passed properly in the trigger
|
||||
const EditButton = forwardRef<
|
||||
HTMLButtonElement,
|
||||
{ label: string } & ComponentPropsWithoutRef<"button">
|
||||
>(({ label, ...props }, ref) => (
|
||||
<Tooltip label={label}>
|
||||
<IconButton
|
||||
ref={ref}
|
||||
type="button"
|
||||
size="var(--cpd-space-6x)"
|
||||
style={{ marginInline: "var(--cpd-space-2x)" }}
|
||||
{...props}
|
||||
>
|
||||
<IconEdit />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
));
|
||||
|
||||
type Props = {
|
||||
mutation: UseMutationResult<unknown, unknown, string, unknown>;
|
||||
deviceName: string;
|
||||
};
|
||||
|
||||
const EditSessionName: React.FC<Props> = ({ mutation, deviceName }) => {
|
||||
const { t } = useTranslation();
|
||||
const fieldRef = useRef<HTMLInputElement>(null);
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const onSubmit = async (
|
||||
event: React.FormEvent<HTMLFormElement>,
|
||||
): Promise<void> => {
|
||||
event.preventDefault();
|
||||
|
||||
const form = event.currentTarget;
|
||||
const formData = new FormData(form);
|
||||
const displayName = formData.get("name") as string;
|
||||
await mutation.mutateAsync(displayName);
|
||||
setOpen(false);
|
||||
};
|
||||
return (
|
||||
<Dialog.Dialog
|
||||
trigger={<EditButton label={t("action.edit")} />}
|
||||
open={open}
|
||||
onOpenChange={(open) => {
|
||||
// Reset the form when the dialog is opened or closed
|
||||
fieldRef.current?.form?.reset();
|
||||
setOpen(open);
|
||||
}}
|
||||
>
|
||||
<Dialog.Title>{t("frontend.session.set_device_name.title")}</Dialog.Title>
|
||||
|
||||
<Form.Root onSubmit={onSubmit}>
|
||||
<Form.Field name="name">
|
||||
<Form.Label>{t("frontend.session.set_device_name.label")}</Form.Label>
|
||||
|
||||
<Form.TextControl
|
||||
type="text"
|
||||
required
|
||||
defaultValue={deviceName}
|
||||
ref={fieldRef}
|
||||
/>
|
||||
|
||||
<Form.HelpMessage>
|
||||
{t("frontend.session.set_device_name.help")}
|
||||
</Form.HelpMessage>
|
||||
</Form.Field>
|
||||
|
||||
<Form.Submit disabled={mutation.isPending}>
|
||||
{mutation.isPending && <LoadingSpinner inline />}
|
||||
{t("action.save")}
|
||||
</Form.Submit>
|
||||
</Form.Root>
|
||||
|
||||
<Dialog.Close asChild>
|
||||
<Button kind="tertiary">{t("action.cancel")}</Button>
|
||||
</Dialog.Close>
|
||||
</Dialog.Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default EditSessionName;
|
||||
@@ -11,6 +11,7 @@ import { beforeAll, describe, expect, it } from "vitest";
|
||||
import { makeFragmentData } from "../../gql";
|
||||
import { mockLocale } from "../../test-utils/mockLocale";
|
||||
|
||||
import { TooltipProvider } from "@vector-im/compound-web";
|
||||
import render from "../../test-utils/render";
|
||||
import OAuth2SessionDetail, { FRAGMENT } from "./OAuth2SessionDetail";
|
||||
|
||||
@@ -39,7 +40,9 @@ describe("<OAuth2SessionDetail>", () => {
|
||||
const data = makeFragmentData(baseSession, FRAGMENT);
|
||||
|
||||
const { asFragment, getByText, queryByText } = render(
|
||||
<OAuth2SessionDetail session={data} />,
|
||||
<TooltipProvider>
|
||||
<OAuth2SessionDetail session={data} />
|
||||
</TooltipProvider>,
|
||||
);
|
||||
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
@@ -57,7 +60,9 @@ describe("<OAuth2SessionDetail>", () => {
|
||||
);
|
||||
|
||||
const { asFragment, getByText, queryByText } = render(
|
||||
<OAuth2SessionDetail session={data} />,
|
||||
<TooltipProvider>
|
||||
<OAuth2SessionDetail session={data} />
|
||||
</TooltipProvider>,
|
||||
);
|
||||
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
|
||||
@@ -4,17 +4,28 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
// Please see LICENSE in the repository root for full details.
|
||||
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { parseISO } from "date-fns";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { type FragmentType, graphql, useFragment } from "../../gql";
|
||||
import { graphqlRequest } from "../../graphql";
|
||||
import { getDeviceIdFromScope } from "../../utils/deviceIdFromScope";
|
||||
import DateTime from "../DateTime";
|
||||
import ClientAvatar from "../Session/ClientAvatar";
|
||||
import EndOAuth2SessionButton from "../Session/EndOAuth2SessionButton";
|
||||
import LastActive from "../Session/LastActive";
|
||||
import EditSessionName from "./EditSessionName";
|
||||
import SessionHeader from "./SessionHeader";
|
||||
import * as Info from "./SessionInfo";
|
||||
|
||||
const SET_SESSION_NAME_MUTATION = graphql(/* GraphQL */ `
|
||||
mutation SetOAuth2SessionName($sessionId: ID!, $displayName: String!) {
|
||||
setOauth2SessionName(input: { oauth2SessionId: $sessionId, humanName: $displayName }) {
|
||||
status
|
||||
}
|
||||
}
|
||||
`);
|
||||
|
||||
export const FRAGMENT = graphql(/* GraphQL */ `
|
||||
fragment OAuth2Session_detail on Oauth2Session {
|
||||
id
|
||||
@@ -23,6 +34,7 @@ export const FRAGMENT = graphql(/* GraphQL */ `
|
||||
finishedAt
|
||||
lastActiveIp
|
||||
lastActiveAt
|
||||
humanName
|
||||
|
||||
...EndOAuth2SessionButton_session
|
||||
|
||||
@@ -49,11 +61,25 @@ type Props = {
|
||||
const OAuth2SessionDetail: React.FC<Props> = ({ session }) => {
|
||||
const data = useFragment(FRAGMENT, session);
|
||||
const { t } = useTranslation();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const setDisplayName = useMutation({
|
||||
mutationFn: (displayName: string) =>
|
||||
graphqlRequest({
|
||||
query: SET_SESSION_NAME_MUTATION,
|
||||
variables: { sessionId: data.id, displayName },
|
||||
}),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["sessionDetail", data.id] });
|
||||
queryClient.invalidateQueries({ queryKey: ["sessionsOverview"] });
|
||||
},
|
||||
});
|
||||
|
||||
const deviceId = getDeviceIdFromScope(data.scope);
|
||||
const clientName = data.client.clientName || data.client.clientId;
|
||||
|
||||
const deviceName =
|
||||
data.humanName ??
|
||||
data.userAgent?.model ??
|
||||
(data.userAgent?.name
|
||||
? data.userAgent?.os
|
||||
@@ -68,7 +94,9 @@ const OAuth2SessionDetail: React.FC<Props> = ({ session }) => {
|
||||
<div className="flex flex-col gap-10">
|
||||
<SessionHeader to="/sessions">
|
||||
{clientName}: {deviceName}
|
||||
<EditSessionName mutation={setDisplayName} deviceName={deviceName} />
|
||||
</SessionHeader>
|
||||
|
||||
<Info.DataSection>
|
||||
<Info.DataSectionHeader>
|
||||
{t("frontend.session.title")}
|
||||
|
||||
@@ -27,9 +27,38 @@ exports[`<CompatSessionDetail> > renders a compatability session details 1`] = `
|
||||
<h3
|
||||
class="_typography_6v6n8_153 _font-heading-md-semibold_6v6n8_112"
|
||||
>
|
||||
element.io
|
||||
:
|
||||
Unknown device
|
||||
element.io: Unknown device
|
||||
<button
|
||||
aria-controls="radix-«r0»"
|
||||
aria-expanded="false"
|
||||
aria-haspopup="dialog"
|
||||
aria-labelledby="«r3»"
|
||||
class="_icon-button_m2erp_8"
|
||||
data-state="closed"
|
||||
role="button"
|
||||
style="--cpd-icon-button-size: var(--cpd-space-6x);"
|
||||
tabindex="0"
|
||||
type="button"
|
||||
>
|
||||
<div
|
||||
class="_indicator-icon_zr2a0_17"
|
||||
style="--cpd-icon-button-size: 100%;"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
clip-rule="evenodd"
|
||||
d="M15.706 2.637a2 2 0 0 1 2.829 0l2.828 2.828a2 2 0 0 1 0 2.829L9.605 20.052a1 1 0 0 1-.465.263L3.483 21.73a1 1 0 0 1-1.212-1.213l1.414-5.657a1 1 0 0 1 .263-.465zm1.224 7.262L14.102 7.07l-8.544 8.544-.943 3.771 3.771-.943z"
|
||||
fill-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
</h3>
|
||||
</header>
|
||||
<section
|
||||
@@ -238,7 +267,7 @@ exports[`<CompatSessionDetail> > renders a compatability session details 1`] = `
|
||||
</ul>
|
||||
</section>
|
||||
<button
|
||||
aria-controls="radix-«r0»"
|
||||
aria-controls="radix-«r8»"
|
||||
aria-expanded="false"
|
||||
aria-haspopup="dialog"
|
||||
class="_button_vczzf_8 _has-icon_vczzf_57 _destructive_vczzf_107"
|
||||
@@ -294,9 +323,38 @@ exports[`<CompatSessionDetail> > renders a compatability session without an ssoL
|
||||
<h3
|
||||
class="_typography_6v6n8_153 _font-heading-md-semibold_6v6n8_112"
|
||||
>
|
||||
abcd1234
|
||||
:
|
||||
Unknown device
|
||||
abcd1234: Unknown device
|
||||
<button
|
||||
aria-controls="radix-«rc»"
|
||||
aria-expanded="false"
|
||||
aria-haspopup="dialog"
|
||||
aria-labelledby="«rf»"
|
||||
class="_icon-button_m2erp_8"
|
||||
data-state="closed"
|
||||
role="button"
|
||||
style="--cpd-icon-button-size: var(--cpd-space-6x);"
|
||||
tabindex="0"
|
||||
type="button"
|
||||
>
|
||||
<div
|
||||
class="_indicator-icon_zr2a0_17"
|
||||
style="--cpd-icon-button-size: 100%;"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
clip-rule="evenodd"
|
||||
d="M15.706 2.637a2 2 0 0 1 2.829 0l2.828 2.828a2 2 0 0 1 0 2.829L9.605 20.052a1 1 0 0 1-.465.263L3.483 21.73a1 1 0 0 1-1.212-1.213l1.414-5.657a1 1 0 0 1 .263-.465zm1.224 7.262L14.102 7.07l-8.544 8.544-.943 3.771 3.771-.943z"
|
||||
fill-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
</h3>
|
||||
</header>
|
||||
<section
|
||||
@@ -488,22 +546,10 @@ exports[`<CompatSessionDetail> > renders a compatability session without an ssoL
|
||||
Unknown device
|
||||
</p>
|
||||
</li>
|
||||
<li
|
||||
class="flex flex-col min-w-0"
|
||||
>
|
||||
<h5
|
||||
class="_typography_6v6n8_153 _font-body-sm-regular_6v6n8_31 text-secondary"
|
||||
>
|
||||
Uri
|
||||
</h5>
|
||||
<p
|
||||
class="_typography_6v6n8_153 _font-body-md-regular_6v6n8_50 text-ellipsis overflow-hidden"
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
<button
|
||||
aria-controls="radix-«r3»"
|
||||
aria-controls="radix-«rk»"
|
||||
aria-expanded="false"
|
||||
aria-haspopup="dialog"
|
||||
class="_button_vczzf_8 _has-icon_vczzf_57 _destructive_vczzf_107"
|
||||
@@ -559,9 +605,38 @@ exports[`<CompatSessionDetail> > renders a finished compatability session detail
|
||||
<h3
|
||||
class="_typography_6v6n8_153 _font-heading-md-semibold_6v6n8_112"
|
||||
>
|
||||
element.io
|
||||
:
|
||||
Unknown device
|
||||
element.io: Unknown device
|
||||
<button
|
||||
aria-controls="radix-«ro»"
|
||||
aria-expanded="false"
|
||||
aria-haspopup="dialog"
|
||||
aria-labelledby="«rr»"
|
||||
class="_icon-button_m2erp_8"
|
||||
data-state="closed"
|
||||
role="button"
|
||||
style="--cpd-icon-button-size: var(--cpd-space-6x);"
|
||||
tabindex="0"
|
||||
type="button"
|
||||
>
|
||||
<div
|
||||
class="_indicator-icon_zr2a0_17"
|
||||
style="--cpd-icon-button-size: 100%;"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
clip-rule="evenodd"
|
||||
d="M15.706 2.637a2 2 0 0 1 2.829 0l2.828 2.828a2 2 0 0 1 0 2.829L9.605 20.052a1 1 0 0 1-.465.263L3.483 21.73a1 1 0 0 1-1.212-1.213l1.414-5.657a1 1 0 0 1 .263-.465zm1.224 7.262L14.102 7.07l-8.544 8.544-.943 3.771 3.771-.943z"
|
||||
fill-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
</h3>
|
||||
</header>
|
||||
<section
|
||||
|
||||
@@ -28,6 +28,37 @@ exports[`<OAuth2SessionDetail> > renders a finished session details 1`] = `
|
||||
class="_typography_6v6n8_153 _font-heading-md-semibold_6v6n8_112"
|
||||
>
|
||||
Element: Unknown device
|
||||
<button
|
||||
aria-controls="radix-«rc»"
|
||||
aria-expanded="false"
|
||||
aria-haspopup="dialog"
|
||||
aria-labelledby="«rf»"
|
||||
class="_icon-button_m2erp_8"
|
||||
data-state="closed"
|
||||
role="button"
|
||||
style="--cpd-icon-button-size: var(--cpd-space-6x);"
|
||||
tabindex="0"
|
||||
type="button"
|
||||
>
|
||||
<div
|
||||
class="_indicator-icon_zr2a0_17"
|
||||
style="--cpd-icon-button-size: 100%;"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
clip-rule="evenodd"
|
||||
d="M15.706 2.637a2 2 0 0 1 2.829 0l2.828 2.828a2 2 0 0 1 0 2.829L9.605 20.052a1 1 0 0 1-.465.263L3.483 21.73a1 1 0 0 1-1.212-1.213l1.414-5.657a1 1 0 0 1 .263-.465zm1.224 7.262L14.102 7.07l-8.544 8.544-.943 3.771 3.771-.943z"
|
||||
fill-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
</h3>
|
||||
</header>
|
||||
<section
|
||||
@@ -307,6 +338,37 @@ exports[`<OAuth2SessionDetail> > renders session details 1`] = `
|
||||
class="_typography_6v6n8_153 _font-heading-md-semibold_6v6n8_112"
|
||||
>
|
||||
Element: Unknown device
|
||||
<button
|
||||
aria-controls="radix-«r0»"
|
||||
aria-expanded="false"
|
||||
aria-haspopup="dialog"
|
||||
aria-labelledby="«r3»"
|
||||
class="_icon-button_m2erp_8"
|
||||
data-state="closed"
|
||||
role="button"
|
||||
style="--cpd-icon-button-size: var(--cpd-space-6x);"
|
||||
tabindex="0"
|
||||
type="button"
|
||||
>
|
||||
<div
|
||||
class="_indicator-icon_zr2a0_17"
|
||||
style="--cpd-icon-button-size: 100%;"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
clip-rule="evenodd"
|
||||
d="M15.706 2.637a2 2 0 0 1 2.829 0l2.828 2.828a2 2 0 0 1 0 2.829L9.605 20.052a1 1 0 0 1-.465.263L3.483 21.73a1 1 0 0 1-1.212-1.213l1.414-5.657a1 1 0 0 1 .263-.465zm1.224 7.262L14.102 7.07l-8.544 8.544-.943 3.771 3.771-.943z"
|
||||
fill-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
</h3>
|
||||
</header>
|
||||
<section
|
||||
@@ -537,7 +599,7 @@ exports[`<OAuth2SessionDetail> > renders session details 1`] = `
|
||||
</ul>
|
||||
</section>
|
||||
<button
|
||||
aria-controls="radix-«r0»"
|
||||
aria-controls="radix-«r8»"
|
||||
aria-expanded="false"
|
||||
aria-haspopup="dialog"
|
||||
class="_button_vczzf_8 _has-icon_vczzf_57 _destructive_vczzf_107"
|
||||
|
||||
@@ -21,10 +21,10 @@ type Documents = {
|
||||
"\n fragment PasswordChange_siteConfig on SiteConfig {\n passwordChangeAllowed\n }\n": typeof types.PasswordChange_SiteConfigFragmentDoc,
|
||||
"\n fragment BrowserSession_session on BrowserSession {\n id\n createdAt\n finishedAt\n ...EndBrowserSessionButton_session\n userAgent {\n deviceType\n name\n os\n model\n }\n lastActiveAt\n }\n": typeof types.BrowserSession_SessionFragmentDoc,
|
||||
"\n fragment OAuth2Client_detail on Oauth2Client {\n id\n clientId\n clientName\n clientUri\n logoUri\n tosUri\n policyUri\n redirectUris\n }\n": typeof types.OAuth2Client_DetailFragmentDoc,
|
||||
"\n fragment CompatSession_session on CompatSession {\n id\n createdAt\n deviceId\n finishedAt\n lastActiveIp\n lastActiveAt\n ...EndCompatSessionButton_session\n userAgent {\n name\n os\n model\n deviceType\n }\n ssoLogin {\n id\n redirectUri\n }\n }\n": typeof types.CompatSession_SessionFragmentDoc,
|
||||
"\n fragment CompatSession_session on CompatSession {\n id\n createdAt\n deviceId\n finishedAt\n lastActiveIp\n lastActiveAt\n humanName\n ...EndCompatSessionButton_session\n userAgent {\n name\n os\n model\n deviceType\n }\n ssoLogin {\n id\n redirectUri\n }\n }\n": typeof types.CompatSession_SessionFragmentDoc,
|
||||
"\n fragment Footer_siteConfig on SiteConfig {\n id\n imprint\n tosUri\n policyUri\n }\n": typeof types.Footer_SiteConfigFragmentDoc,
|
||||
"\n query Footer {\n siteConfig {\n id\n ...Footer_siteConfig\n }\n }\n": typeof types.FooterDocument,
|
||||
"\n fragment OAuth2Session_session on Oauth2Session {\n id\n scope\n createdAt\n finishedAt\n lastActiveIp\n lastActiveAt\n\n ...EndOAuth2SessionButton_session\n\n userAgent {\n name\n model\n os\n deviceType\n }\n\n client {\n id\n clientId\n clientName\n applicationType\n logoUri\n }\n }\n": typeof types.OAuth2Session_SessionFragmentDoc,
|
||||
"\n fragment OAuth2Session_session on Oauth2Session {\n id\n scope\n createdAt\n finishedAt\n lastActiveIp\n lastActiveAt\n humanName\n\n ...EndOAuth2SessionButton_session\n\n userAgent {\n name\n model\n os\n deviceType\n }\n\n client {\n id\n clientId\n clientName\n applicationType\n logoUri\n }\n }\n": typeof types.OAuth2Session_SessionFragmentDoc,
|
||||
"\n fragment PasswordCreationDoubleInput_siteConfig on SiteConfig {\n id\n minimumPasswordComplexity\n }\n": typeof types.PasswordCreationDoubleInput_SiteConfigFragmentDoc,
|
||||
"\n fragment EndBrowserSessionButton_session on BrowserSession {\n id\n userAgent {\n name\n os\n model\n deviceType\n }\n }\n": typeof types.EndBrowserSessionButton_SessionFragmentDoc,
|
||||
"\n mutation EndBrowserSession($id: ID!) {\n endBrowserSession(input: { browserSessionId: $id }) {\n status\n browserSession {\n id\n }\n }\n }\n": typeof types.EndBrowserSessionDocument,
|
||||
@@ -33,8 +33,10 @@ type Documents = {
|
||||
"\n fragment EndOAuth2SessionButton_session on Oauth2Session {\n id\n\n userAgent {\n name\n model\n os\n deviceType\n }\n\n client {\n clientId\n clientName\n applicationType\n logoUri\n }\n }\n": typeof types.EndOAuth2SessionButton_SessionFragmentDoc,
|
||||
"\n mutation EndOAuth2Session($id: ID!) {\n endOauth2Session(input: { oauth2SessionId: $id }) {\n status\n oauth2Session {\n id\n }\n }\n }\n": typeof types.EndOAuth2SessionDocument,
|
||||
"\n fragment BrowserSession_detail on BrowserSession {\n id\n createdAt\n finishedAt\n ...EndBrowserSessionButton_session\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 types.BrowserSession_DetailFragmentDoc,
|
||||
"\n fragment CompatSession_detail on CompatSession {\n id\n createdAt\n deviceId\n finishedAt\n lastActiveIp\n lastActiveAt\n\n ...EndCompatSessionButton_session\n\n userAgent {\n name\n os\n model\n }\n\n ssoLogin {\n id\n redirectUri\n }\n }\n": typeof types.CompatSession_DetailFragmentDoc,
|
||||
"\n fragment OAuth2Session_detail on Oauth2Session {\n id\n scope\n createdAt\n finishedAt\n lastActiveIp\n lastActiveAt\n\n ...EndOAuth2SessionButton_session\n\n userAgent {\n name\n model\n os\n }\n\n client {\n id\n clientId\n clientName\n clientUri\n logoUri\n }\n }\n": typeof types.OAuth2Session_DetailFragmentDoc,
|
||||
"\n mutation SetCompatSessionName($sessionId: ID!, $displayName: String!) {\n setCompatSessionName(input: { compatSessionId: $sessionId, humanName: $displayName }) {\n status\n }\n }\n": typeof types.SetCompatSessionNameDocument,
|
||||
"\n fragment CompatSession_detail on CompatSession {\n id\n createdAt\n deviceId\n finishedAt\n lastActiveIp\n lastActiveAt\n humanName\n\n ...EndCompatSessionButton_session\n\n userAgent {\n name\n os\n model\n }\n\n ssoLogin {\n id\n redirectUri\n }\n }\n": typeof types.CompatSession_DetailFragmentDoc,
|
||||
"\n mutation SetOAuth2SessionName($sessionId: ID!, $displayName: String!) {\n setOauth2SessionName(input: { oauth2SessionId: $sessionId, humanName: $displayName }) {\n status\n }\n }\n": typeof types.SetOAuth2SessionNameDocument,
|
||||
"\n fragment OAuth2Session_detail on Oauth2Session {\n id\n scope\n createdAt\n finishedAt\n lastActiveIp\n lastActiveAt\n humanName\n\n ...EndOAuth2SessionButton_session\n\n userAgent {\n name\n model\n os\n }\n\n client {\n id\n clientId\n clientName\n clientUri\n logoUri\n }\n }\n": typeof types.OAuth2Session_DetailFragmentDoc,
|
||||
"\n fragment UserEmail_email on UserEmail {\n id\n email\n }\n": typeof types.UserEmail_EmailFragmentDoc,
|
||||
"\n mutation RemoveEmail($id: ID!, $password: String) {\n removeEmail(input: { userEmailId: $id, password: $password }) {\n status\n\n user {\n id\n }\n }\n }\n": typeof types.RemoveEmailDocument,
|
||||
"\n fragment UserGreeting_user on User {\n id\n matrix {\n mxid\n displayName\n }\n }\n": typeof types.UserGreeting_UserFragmentDoc,
|
||||
@@ -75,10 +77,10 @@ const documents: Documents = {
|
||||
"\n fragment PasswordChange_siteConfig on SiteConfig {\n passwordChangeAllowed\n }\n": types.PasswordChange_SiteConfigFragmentDoc,
|
||||
"\n fragment BrowserSession_session on BrowserSession {\n id\n createdAt\n finishedAt\n ...EndBrowserSessionButton_session\n userAgent {\n deviceType\n name\n os\n model\n }\n lastActiveAt\n }\n": types.BrowserSession_SessionFragmentDoc,
|
||||
"\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,
|
||||
"\n fragment CompatSession_session on CompatSession {\n id\n createdAt\n deviceId\n finishedAt\n lastActiveIp\n lastActiveAt\n ...EndCompatSessionButton_session\n userAgent {\n name\n os\n model\n deviceType\n }\n ssoLogin {\n id\n redirectUri\n }\n }\n": types.CompatSession_SessionFragmentDoc,
|
||||
"\n fragment CompatSession_session on CompatSession {\n id\n createdAt\n deviceId\n finishedAt\n lastActiveIp\n lastActiveAt\n humanName\n ...EndCompatSessionButton_session\n userAgent {\n name\n os\n model\n deviceType\n }\n ssoLogin {\n id\n redirectUri\n }\n }\n": types.CompatSession_SessionFragmentDoc,
|
||||
"\n fragment Footer_siteConfig on SiteConfig {\n id\n imprint\n tosUri\n policyUri\n }\n": types.Footer_SiteConfigFragmentDoc,
|
||||
"\n query Footer {\n siteConfig {\n id\n ...Footer_siteConfig\n }\n }\n": types.FooterDocument,
|
||||
"\n fragment OAuth2Session_session on Oauth2Session {\n id\n scope\n createdAt\n finishedAt\n lastActiveIp\n lastActiveAt\n\n ...EndOAuth2SessionButton_session\n\n userAgent {\n name\n model\n os\n deviceType\n }\n\n client {\n id\n clientId\n clientName\n applicationType\n logoUri\n }\n }\n": types.OAuth2Session_SessionFragmentDoc,
|
||||
"\n fragment OAuth2Session_session on Oauth2Session {\n id\n scope\n createdAt\n finishedAt\n lastActiveIp\n lastActiveAt\n humanName\n\n ...EndOAuth2SessionButton_session\n\n userAgent {\n name\n model\n os\n deviceType\n }\n\n client {\n id\n clientId\n clientName\n applicationType\n logoUri\n }\n }\n": types.OAuth2Session_SessionFragmentDoc,
|
||||
"\n fragment PasswordCreationDoubleInput_siteConfig on SiteConfig {\n id\n minimumPasswordComplexity\n }\n": types.PasswordCreationDoubleInput_SiteConfigFragmentDoc,
|
||||
"\n fragment EndBrowserSessionButton_session on BrowserSession {\n id\n userAgent {\n name\n os\n model\n deviceType\n }\n }\n": types.EndBrowserSessionButton_SessionFragmentDoc,
|
||||
"\n mutation EndBrowserSession($id: ID!) {\n endBrowserSession(input: { browserSessionId: $id }) {\n status\n browserSession {\n id\n }\n }\n }\n": types.EndBrowserSessionDocument,
|
||||
@@ -87,8 +89,10 @@ const documents: Documents = {
|
||||
"\n fragment EndOAuth2SessionButton_session on Oauth2Session {\n id\n\n userAgent {\n name\n model\n os\n deviceType\n }\n\n client {\n clientId\n clientName\n applicationType\n logoUri\n }\n }\n": types.EndOAuth2SessionButton_SessionFragmentDoc,
|
||||
"\n mutation EndOAuth2Session($id: ID!) {\n endOauth2Session(input: { oauth2SessionId: $id }) {\n status\n oauth2Session {\n id\n }\n }\n }\n": types.EndOAuth2SessionDocument,
|
||||
"\n fragment BrowserSession_detail on BrowserSession {\n id\n createdAt\n finishedAt\n ...EndBrowserSessionButton_session\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,
|
||||
"\n fragment CompatSession_detail on CompatSession {\n id\n createdAt\n deviceId\n finishedAt\n lastActiveIp\n lastActiveAt\n\n ...EndCompatSessionButton_session\n\n userAgent {\n name\n os\n model\n }\n\n ssoLogin {\n id\n redirectUri\n }\n }\n": types.CompatSession_DetailFragmentDoc,
|
||||
"\n fragment OAuth2Session_detail on Oauth2Session {\n id\n scope\n createdAt\n finishedAt\n lastActiveIp\n lastActiveAt\n\n ...EndOAuth2SessionButton_session\n\n userAgent {\n name\n model\n os\n }\n\n client {\n id\n clientId\n clientName\n clientUri\n logoUri\n }\n }\n": types.OAuth2Session_DetailFragmentDoc,
|
||||
"\n mutation SetCompatSessionName($sessionId: ID!, $displayName: String!) {\n setCompatSessionName(input: { compatSessionId: $sessionId, humanName: $displayName }) {\n status\n }\n }\n": types.SetCompatSessionNameDocument,
|
||||
"\n fragment CompatSession_detail on CompatSession {\n id\n createdAt\n deviceId\n finishedAt\n lastActiveIp\n lastActiveAt\n humanName\n\n ...EndCompatSessionButton_session\n\n userAgent {\n name\n os\n model\n }\n\n ssoLogin {\n id\n redirectUri\n }\n }\n": types.CompatSession_DetailFragmentDoc,
|
||||
"\n mutation SetOAuth2SessionName($sessionId: ID!, $displayName: String!) {\n setOauth2SessionName(input: { oauth2SessionId: $sessionId, humanName: $displayName }) {\n status\n }\n }\n": types.SetOAuth2SessionNameDocument,
|
||||
"\n fragment OAuth2Session_detail on Oauth2Session {\n id\n scope\n createdAt\n finishedAt\n lastActiveIp\n lastActiveAt\n humanName\n\n ...EndOAuth2SessionButton_session\n\n userAgent {\n name\n model\n os\n }\n\n client {\n id\n clientId\n clientName\n clientUri\n logoUri\n }\n }\n": types.OAuth2Session_DetailFragmentDoc,
|
||||
"\n fragment UserEmail_email on UserEmail {\n id\n email\n }\n": types.UserEmail_EmailFragmentDoc,
|
||||
"\n mutation RemoveEmail($id: ID!, $password: String) {\n removeEmail(input: { userEmailId: $id, password: $password }) {\n status\n\n user {\n id\n }\n }\n }\n": types.RemoveEmailDocument,
|
||||
"\n fragment UserGreeting_user on User {\n id\n matrix {\n mxid\n displayName\n }\n }\n": types.UserGreeting_UserFragmentDoc,
|
||||
@@ -150,7 +154,7 @@ export function graphql(source: "\n fragment OAuth2Client_detail on Oauth2Clien
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(source: "\n fragment CompatSession_session on CompatSession {\n id\n createdAt\n deviceId\n finishedAt\n lastActiveIp\n lastActiveAt\n ...EndCompatSessionButton_session\n userAgent {\n name\n os\n model\n deviceType\n }\n ssoLogin {\n id\n redirectUri\n }\n }\n"): typeof import('./graphql').CompatSession_SessionFragmentDoc;
|
||||
export function graphql(source: "\n fragment CompatSession_session on CompatSession {\n id\n createdAt\n deviceId\n finishedAt\n lastActiveIp\n lastActiveAt\n humanName\n ...EndCompatSessionButton_session\n userAgent {\n name\n os\n model\n deviceType\n }\n ssoLogin {\n id\n redirectUri\n }\n }\n"): typeof import('./graphql').CompatSession_SessionFragmentDoc;
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
@@ -162,7 +166,7 @@ export function graphql(source: "\n query Footer {\n siteConfig {\n id\
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(source: "\n fragment OAuth2Session_session on Oauth2Session {\n id\n scope\n createdAt\n finishedAt\n lastActiveIp\n lastActiveAt\n\n ...EndOAuth2SessionButton_session\n\n userAgent {\n name\n model\n os\n deviceType\n }\n\n client {\n id\n clientId\n clientName\n applicationType\n logoUri\n }\n }\n"): typeof import('./graphql').OAuth2Session_SessionFragmentDoc;
|
||||
export function graphql(source: "\n fragment OAuth2Session_session on Oauth2Session {\n id\n scope\n createdAt\n finishedAt\n lastActiveIp\n lastActiveAt\n humanName\n\n ...EndOAuth2SessionButton_session\n\n userAgent {\n name\n model\n os\n deviceType\n }\n\n client {\n id\n clientId\n clientName\n applicationType\n logoUri\n }\n }\n"): typeof import('./graphql').OAuth2Session_SessionFragmentDoc;
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
@@ -198,11 +202,19 @@ export function graphql(source: "\n fragment BrowserSession_detail on BrowserSe
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(source: "\n fragment CompatSession_detail on CompatSession {\n id\n createdAt\n deviceId\n finishedAt\n lastActiveIp\n lastActiveAt\n\n ...EndCompatSessionButton_session\n\n userAgent {\n name\n os\n model\n }\n\n ssoLogin {\n id\n redirectUri\n }\n }\n"): typeof import('./graphql').CompatSession_DetailFragmentDoc;
|
||||
export function graphql(source: "\n mutation SetCompatSessionName($sessionId: ID!, $displayName: String!) {\n setCompatSessionName(input: { compatSessionId: $sessionId, humanName: $displayName }) {\n status\n }\n }\n"): typeof import('./graphql').SetCompatSessionNameDocument;
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(source: "\n fragment OAuth2Session_detail on Oauth2Session {\n id\n scope\n createdAt\n finishedAt\n lastActiveIp\n lastActiveAt\n\n ...EndOAuth2SessionButton_session\n\n userAgent {\n name\n model\n os\n }\n\n client {\n id\n clientId\n clientName\n clientUri\n logoUri\n }\n }\n"): typeof import('./graphql').OAuth2Session_DetailFragmentDoc;
|
||||
export function graphql(source: "\n fragment CompatSession_detail on CompatSession {\n id\n createdAt\n deviceId\n finishedAt\n lastActiveIp\n lastActiveAt\n humanName\n\n ...EndCompatSessionButton_session\n\n userAgent {\n name\n os\n model\n }\n\n ssoLogin {\n id\n redirectUri\n }\n }\n"): typeof import('./graphql').CompatSession_DetailFragmentDoc;
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(source: "\n mutation SetOAuth2SessionName($sessionId: ID!, $displayName: String!) {\n setOauth2SessionName(input: { oauth2SessionId: $sessionId, humanName: $displayName }) {\n status\n }\n }\n"): typeof import('./graphql').SetOAuth2SessionNameDocument;
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(source: "\n fragment OAuth2Session_detail on Oauth2Session {\n id\n scope\n createdAt\n finishedAt\n lastActiveIp\n lastActiveAt\n humanName\n\n ...EndOAuth2SessionButton_session\n\n userAgent {\n name\n model\n os\n }\n\n client {\n id\n clientId\n clientName\n clientUri\n logoUri\n }\n }\n"): typeof import('./graphql').OAuth2Session_DetailFragmentDoc;
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
|
||||
@@ -238,6 +238,8 @@ export type CompatSession = CreationEvent & Node & {
|
||||
deviceId?: Maybe<Scalars['String']['output']>;
|
||||
/** When the session ended. */
|
||||
finishedAt?: Maybe<Scalars['DateTime']['output']>;
|
||||
/** A human-provided name for the session. */
|
||||
humanName?: Maybe<Scalars['String']['output']>;
|
||||
/** ID of the object. */
|
||||
id: Scalars['ID']['output'];
|
||||
/** The last time the session was active. */
|
||||
@@ -580,8 +582,10 @@ export type Mutation = {
|
||||
* administrators.
|
||||
*/
|
||||
setCanRequestAdmin: SetCanRequestAdminPayload;
|
||||
setCompatSessionName: SetCompatSessionNamePayload;
|
||||
/** Set the display name of a user */
|
||||
setDisplayName: SetDisplayNamePayload;
|
||||
setOauth2SessionName: SetOAuth2SessionNamePayload;
|
||||
/**
|
||||
* Set the password for a user.
|
||||
*
|
||||
@@ -689,12 +693,24 @@ export type MutationSetCanRequestAdminArgs = {
|
||||
};
|
||||
|
||||
|
||||
/** The mutations root of the GraphQL interface. */
|
||||
export type MutationSetCompatSessionNameArgs = {
|
||||
input: SetCompatSessionNameInput;
|
||||
};
|
||||
|
||||
|
||||
/** The mutations root of the GraphQL interface. */
|
||||
export type MutationSetDisplayNameArgs = {
|
||||
input: SetDisplayNameInput;
|
||||
};
|
||||
|
||||
|
||||
/** The mutations root of the GraphQL interface. */
|
||||
export type MutationSetOauth2SessionNameArgs = {
|
||||
input: SetOAuth2SessionNameInput;
|
||||
};
|
||||
|
||||
|
||||
/** The mutations root of the GraphQL interface. */
|
||||
export type MutationSetPasswordArgs = {
|
||||
input: SetPasswordInput;
|
||||
@@ -774,6 +790,8 @@ export type Oauth2Session = CreationEvent & Node & {
|
||||
createdAt: Scalars['DateTime']['output'];
|
||||
/** When the session ended. */
|
||||
finishedAt?: Maybe<Scalars['DateTime']['output']>;
|
||||
/** The user-provided name for this session. */
|
||||
humanName?: Maybe<Scalars['String']['output']>;
|
||||
/** ID of the object. */
|
||||
id: Scalars['ID']['output'];
|
||||
/** The last time the session was active. */
|
||||
@@ -1082,6 +1100,29 @@ export type SetCanRequestAdminPayload = {
|
||||
user?: Maybe<User>;
|
||||
};
|
||||
|
||||
/** The input of the `setCompatSessionName` mutation. */
|
||||
export type SetCompatSessionNameInput = {
|
||||
/** The ID of the session to set the name of. */
|
||||
compatSessionId: Scalars['ID']['input'];
|
||||
/** The new name of the session. */
|
||||
humanName: Scalars['String']['input'];
|
||||
};
|
||||
|
||||
export type SetCompatSessionNamePayload = {
|
||||
__typename?: 'SetCompatSessionNamePayload';
|
||||
/** The session that was updated. */
|
||||
oauth2Session?: Maybe<CompatSession>;
|
||||
/** The status of the mutation. */
|
||||
status: SetCompatSessionNameStatus;
|
||||
};
|
||||
|
||||
/** The status of the `setCompatSessionName` mutation. */
|
||||
export type SetCompatSessionNameStatus =
|
||||
/** The session was not found. */
|
||||
| 'NOT_FOUND'
|
||||
/** The session was updated. */
|
||||
| 'UPDATED';
|
||||
|
||||
/** The input for the `addEmail` mutation */
|
||||
export type SetDisplayNameInput = {
|
||||
/** The display name to set. If `None`, the display name will be removed. */
|
||||
@@ -1106,6 +1147,29 @@ export type SetDisplayNameStatus =
|
||||
/** The display name was set */
|
||||
| 'SET';
|
||||
|
||||
/** The input of the `setOauth2SessionName` mutation. */
|
||||
export type SetOAuth2SessionNameInput = {
|
||||
/** The new name of the session. */
|
||||
humanName: Scalars['String']['input'];
|
||||
/** The ID of the session to set the name of. */
|
||||
oauth2SessionId: Scalars['ID']['input'];
|
||||
};
|
||||
|
||||
export type SetOAuth2SessionNamePayload = {
|
||||
__typename?: 'SetOAuth2SessionNamePayload';
|
||||
/** The session that was updated. */
|
||||
oauth2Session?: Maybe<Oauth2Session>;
|
||||
/** The status of the mutation. */
|
||||
status: SetOAuth2SessionNameStatus;
|
||||
};
|
||||
|
||||
/** The status of the `setOauth2SessionName` mutation. */
|
||||
export type SetOAuth2SessionNameStatus =
|
||||
/** The session was not found. */
|
||||
| 'NOT_FOUND'
|
||||
/** The session was updated. */
|
||||
| 'UPDATED';
|
||||
|
||||
/** The input for the `setPasswordByRecovery` mutation. */
|
||||
export type SetPasswordByRecoveryInput = {
|
||||
/** The new password for the user. */
|
||||
@@ -1650,7 +1714,7 @@ export type BrowserSession_SessionFragment = (
|
||||
export type OAuth2Client_DetailFragment = { __typename?: 'Oauth2Client', id: string, clientId: string, clientName?: string | null, clientUri?: string | null, logoUri?: string | null, tosUri?: string | null, policyUri?: string | null, redirectUris: Array<string> } & { ' $fragmentName'?: 'OAuth2Client_DetailFragment' };
|
||||
|
||||
export type CompatSession_SessionFragment = (
|
||||
{ __typename?: 'CompatSession', id: string, createdAt: string, deviceId?: string | null, finishedAt?: string | null, lastActiveIp?: string | null, lastActiveAt?: string | null, userAgent?: { __typename?: 'UserAgent', name?: string | null, os?: string | null, model?: string | null, deviceType: DeviceType } | null, ssoLogin?: { __typename?: 'CompatSsoLogin', id: string, redirectUri: string } | null }
|
||||
{ __typename?: 'CompatSession', id: string, createdAt: string, deviceId?: string | null, finishedAt?: string | null, lastActiveIp?: string | null, lastActiveAt?: string | null, humanName?: string | null, userAgent?: { __typename?: 'UserAgent', name?: string | null, os?: string | null, model?: string | null, deviceType: DeviceType } | null, ssoLogin?: { __typename?: 'CompatSsoLogin', id: string, redirectUri: string } | null }
|
||||
& { ' $fragmentRefs'?: { 'EndCompatSessionButton_SessionFragment': EndCompatSessionButton_SessionFragment } }
|
||||
) & { ' $fragmentName'?: 'CompatSession_SessionFragment' };
|
||||
|
||||
@@ -1665,7 +1729,7 @@ export type FooterQuery = { __typename?: 'Query', siteConfig: (
|
||||
) };
|
||||
|
||||
export type OAuth2Session_SessionFragment = (
|
||||
{ __typename?: 'Oauth2Session', id: string, scope: string, createdAt: string, finishedAt?: string | null, lastActiveIp?: string | null, lastActiveAt?: string | null, userAgent?: { __typename?: 'UserAgent', name?: string | null, model?: string | null, os?: string | null, deviceType: DeviceType } | null, client: { __typename?: 'Oauth2Client', id: string, clientId: string, clientName?: string | null, applicationType?: Oauth2ApplicationType | null, logoUri?: string | null } }
|
||||
{ __typename?: 'Oauth2Session', id: string, scope: string, createdAt: string, finishedAt?: string | null, lastActiveIp?: string | null, lastActiveAt?: string | null, humanName?: string | null, userAgent?: { __typename?: 'UserAgent', name?: string | null, model?: string | null, os?: string | null, deviceType: DeviceType } | null, client: { __typename?: 'Oauth2Client', id: string, clientId: string, clientName?: string | null, applicationType?: Oauth2ApplicationType | null, logoUri?: string | null } }
|
||||
& { ' $fragmentRefs'?: { 'EndOAuth2SessionButton_SessionFragment': EndOAuth2SessionButton_SessionFragment } }
|
||||
) & { ' $fragmentName'?: 'OAuth2Session_SessionFragment' };
|
||||
|
||||
@@ -1703,13 +1767,29 @@ export type BrowserSession_DetailFragment = (
|
||||
& { ' $fragmentRefs'?: { 'EndBrowserSessionButton_SessionFragment': EndBrowserSessionButton_SessionFragment } }
|
||||
) & { ' $fragmentName'?: 'BrowserSession_DetailFragment' };
|
||||
|
||||
export type SetCompatSessionNameMutationVariables = Exact<{
|
||||
sessionId: Scalars['ID']['input'];
|
||||
displayName: Scalars['String']['input'];
|
||||
}>;
|
||||
|
||||
|
||||
export type SetCompatSessionNameMutation = { __typename?: 'Mutation', setCompatSessionName: { __typename?: 'SetCompatSessionNamePayload', status: SetCompatSessionNameStatus } };
|
||||
|
||||
export type CompatSession_DetailFragment = (
|
||||
{ __typename?: 'CompatSession', id: string, createdAt: string, deviceId?: string | null, finishedAt?: string | null, lastActiveIp?: string | null, lastActiveAt?: string | null, userAgent?: { __typename?: 'UserAgent', name?: string | null, os?: string | null, model?: string | null } | null, ssoLogin?: { __typename?: 'CompatSsoLogin', id: string, redirectUri: string } | null }
|
||||
{ __typename?: 'CompatSession', id: string, createdAt: string, deviceId?: string | null, finishedAt?: string | null, lastActiveIp?: string | null, lastActiveAt?: string | null, humanName?: string | null, userAgent?: { __typename?: 'UserAgent', name?: string | null, os?: string | null, model?: string | null } | null, ssoLogin?: { __typename?: 'CompatSsoLogin', id: string, redirectUri: string } | null }
|
||||
& { ' $fragmentRefs'?: { 'EndCompatSessionButton_SessionFragment': EndCompatSessionButton_SessionFragment } }
|
||||
) & { ' $fragmentName'?: 'CompatSession_DetailFragment' };
|
||||
|
||||
export type SetOAuth2SessionNameMutationVariables = Exact<{
|
||||
sessionId: Scalars['ID']['input'];
|
||||
displayName: Scalars['String']['input'];
|
||||
}>;
|
||||
|
||||
|
||||
export type SetOAuth2SessionNameMutation = { __typename?: 'Mutation', setOauth2SessionName: { __typename?: 'SetOAuth2SessionNamePayload', status: SetOAuth2SessionNameStatus } };
|
||||
|
||||
export type OAuth2Session_DetailFragment = (
|
||||
{ __typename?: 'Oauth2Session', id: string, scope: string, createdAt: string, finishedAt?: string | null, lastActiveIp?: string | null, lastActiveAt?: string | null, userAgent?: { __typename?: 'UserAgent', name?: string | null, model?: string | null, os?: string | null } | null, client: { __typename?: 'Oauth2Client', id: string, clientId: string, clientName?: string | null, clientUri?: string | null, logoUri?: string | null } }
|
||||
{ __typename?: 'Oauth2Session', id: string, scope: string, createdAt: string, finishedAt?: string | null, lastActiveIp?: string | null, lastActiveAt?: string | null, humanName?: string | null, userAgent?: { __typename?: 'UserAgent', name?: string | null, model?: string | null, os?: string | null } | null, client: { __typename?: 'Oauth2Client', id: string, clientId: string, clientName?: string | null, clientUri?: string | null, logoUri?: string | null } }
|
||||
& { ' $fragmentRefs'?: { 'EndOAuth2SessionButton_SessionFragment': EndOAuth2SessionButton_SessionFragment } }
|
||||
) & { ' $fragmentName'?: 'OAuth2Session_DetailFragment' };
|
||||
|
||||
@@ -2056,6 +2136,7 @@ export const CompatSession_SessionFragmentDoc = new TypedDocumentString(`
|
||||
finishedAt
|
||||
lastActiveIp
|
||||
lastActiveAt
|
||||
humanName
|
||||
...EndCompatSessionButton_session
|
||||
userAgent {
|
||||
name
|
||||
@@ -2114,6 +2195,7 @@ export const OAuth2Session_SessionFragmentDoc = new TypedDocumentString(`
|
||||
finishedAt
|
||||
lastActiveIp
|
||||
lastActiveAt
|
||||
humanName
|
||||
...EndOAuth2SessionButton_session
|
||||
userAgent {
|
||||
name
|
||||
@@ -2183,6 +2265,7 @@ export const CompatSession_DetailFragmentDoc = new TypedDocumentString(`
|
||||
finishedAt
|
||||
lastActiveIp
|
||||
lastActiveAt
|
||||
humanName
|
||||
...EndCompatSessionButton_session
|
||||
userAgent {
|
||||
name
|
||||
@@ -2215,6 +2298,7 @@ export const OAuth2Session_DetailFragmentDoc = new TypedDocumentString(`
|
||||
finishedAt
|
||||
lastActiveIp
|
||||
lastActiveAt
|
||||
humanName
|
||||
...EndOAuth2SessionButton_session
|
||||
userAgent {
|
||||
name
|
||||
@@ -2363,6 +2447,24 @@ export const EndOAuth2SessionDocument = new TypedDocumentString(`
|
||||
}
|
||||
}
|
||||
`) as unknown as TypedDocumentString<EndOAuth2SessionMutation, EndOAuth2SessionMutationVariables>;
|
||||
export const SetCompatSessionNameDocument = new TypedDocumentString(`
|
||||
mutation SetCompatSessionName($sessionId: ID!, $displayName: String!) {
|
||||
setCompatSessionName(
|
||||
input: {compatSessionId: $sessionId, humanName: $displayName}
|
||||
) {
|
||||
status
|
||||
}
|
||||
}
|
||||
`) as unknown as TypedDocumentString<SetCompatSessionNameMutation, SetCompatSessionNameMutationVariables>;
|
||||
export const SetOAuth2SessionNameDocument = new TypedDocumentString(`
|
||||
mutation SetOAuth2SessionName($sessionId: ID!, $displayName: String!) {
|
||||
setOauth2SessionName(
|
||||
input: {oauth2SessionId: $sessionId, humanName: $displayName}
|
||||
) {
|
||||
status
|
||||
}
|
||||
}
|
||||
`) as unknown as TypedDocumentString<SetOAuth2SessionNameMutation, SetOAuth2SessionNameMutationVariables>;
|
||||
export const RemoveEmailDocument = new TypedDocumentString(`
|
||||
mutation RemoveEmail($id: ID!, $password: String) {
|
||||
removeEmail(input: {userEmailId: $id, password: $password}) {
|
||||
@@ -2587,6 +2689,7 @@ export const AppSessionsListDocument = new TypedDocumentString(`
|
||||
finishedAt
|
||||
lastActiveIp
|
||||
lastActiveAt
|
||||
humanName
|
||||
...EndCompatSessionButton_session
|
||||
userAgent {
|
||||
name
|
||||
@@ -2606,6 +2709,7 @@ fragment OAuth2Session_session on Oauth2Session {
|
||||
finishedAt
|
||||
lastActiveIp
|
||||
lastActiveAt
|
||||
humanName
|
||||
...EndOAuth2SessionButton_session
|
||||
userAgent {
|
||||
name
|
||||
@@ -2880,6 +2984,7 @@ fragment CompatSession_detail on CompatSession {
|
||||
finishedAt
|
||||
lastActiveIp
|
||||
lastActiveAt
|
||||
humanName
|
||||
...EndCompatSessionButton_session
|
||||
userAgent {
|
||||
name
|
||||
@@ -2898,6 +3003,7 @@ fragment OAuth2Session_detail on Oauth2Session {
|
||||
finishedAt
|
||||
lastActiveIp
|
||||
lastActiveAt
|
||||
humanName
|
||||
...EndOAuth2SessionButton_session
|
||||
userAgent {
|
||||
name
|
||||
@@ -3022,6 +3128,50 @@ export const mockEndOAuth2SessionMutation = (resolver: GraphQLResponseResolver<E
|
||||
options
|
||||
)
|
||||
|
||||
/**
|
||||
* @param resolver A function that accepts [resolver arguments](https://mswjs.io/docs/api/graphql#resolver-argument) and must always return the instruction on what to do with the intercepted request. ([see more](https://mswjs.io/docs/concepts/response-resolver#resolver-instructions))
|
||||
* @param options Options object to customize the behavior of the mock. ([see more](https://mswjs.io/docs/api/graphql#handler-options))
|
||||
* @see https://mswjs.io/docs/basics/response-resolver
|
||||
* @example
|
||||
* mockSetCompatSessionNameMutation(
|
||||
* ({ query, variables }) => {
|
||||
* const { sessionId, displayName } = variables;
|
||||
* return HttpResponse.json({
|
||||
* data: { setCompatSessionName }
|
||||
* })
|
||||
* },
|
||||
* requestOptions
|
||||
* )
|
||||
*/
|
||||
export const mockSetCompatSessionNameMutation = (resolver: GraphQLResponseResolver<SetCompatSessionNameMutation, SetCompatSessionNameMutationVariables>, options?: RequestHandlerOptions) =>
|
||||
graphql.mutation<SetCompatSessionNameMutation, SetCompatSessionNameMutationVariables>(
|
||||
'SetCompatSessionName',
|
||||
resolver,
|
||||
options
|
||||
)
|
||||
|
||||
/**
|
||||
* @param resolver A function that accepts [resolver arguments](https://mswjs.io/docs/api/graphql#resolver-argument) and must always return the instruction on what to do with the intercepted request. ([see more](https://mswjs.io/docs/concepts/response-resolver#resolver-instructions))
|
||||
* @param options Options object to customize the behavior of the mock. ([see more](https://mswjs.io/docs/api/graphql#handler-options))
|
||||
* @see https://mswjs.io/docs/basics/response-resolver
|
||||
* @example
|
||||
* mockSetOAuth2SessionNameMutation(
|
||||
* ({ query, variables }) => {
|
||||
* const { sessionId, displayName } = variables;
|
||||
* return HttpResponse.json({
|
||||
* data: { setOauth2SessionName }
|
||||
* })
|
||||
* },
|
||||
* requestOptions
|
||||
* )
|
||||
*/
|
||||
export const mockSetOAuth2SessionNameMutation = (resolver: GraphQLResponseResolver<SetOAuth2SessionNameMutation, SetOAuth2SessionNameMutationVariables>, options?: RequestHandlerOptions) =>
|
||||
graphql.mutation<SetOAuth2SessionNameMutation, SetOAuth2SessionNameMutationVariables>(
|
||||
'SetOAuth2SessionName',
|
||||
resolver,
|
||||
options
|
||||
)
|
||||
|
||||
/**
|
||||
* @param resolver A function that accepts [resolver arguments](https://mswjs.io/docs/api/graphql#resolver-argument) and must always return the instruction on what to do with the intercepted request. ([see more](https://mswjs.io/docs/concepts/response-resolver#resolver-instructions))
|
||||
* @param options Options object to customize the behavior of the mock. ([see more](https://mswjs.io/docs/api/graphql#handler-options))
|
||||
|
||||
28
templates/device_name.txt
Normal file
28
templates/device_name.txt
Normal file
@@ -0,0 +1,28 @@
|
||||
{#
|
||||
Copyright 2024, 2025 New Vector Ltd.
|
||||
Copyright 2021-2024 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
Please see LICENSE in the repository root for full details.
|
||||
-#}
|
||||
|
||||
{%- set _ = translator(lang) -%}
|
||||
|
||||
{%- set client_name = client.client_name or client.client_id -%}
|
||||
{%- set user_agent = raw_user_agent | parse_user_agent() -%}
|
||||
|
||||
{%- set device_name -%}
|
||||
{%- if user_agent.model -%}
|
||||
{{- user_agent.model -}}
|
||||
{%- elif user_agent.name -%}
|
||||
{%- if user_agent.os -%}
|
||||
{{- _("mas.device_display_name.name_for_platform", name=user_agent.name, platform=user_agent.os) -}}
|
||||
{%- else -%}
|
||||
{{- user_agent.name -}}
|
||||
{%- endif -%}
|
||||
{%- else -%}
|
||||
{{- _("mas.device_display_name.unknown_device") -}}
|
||||
{%- endif -%}
|
||||
{%- endset -%}
|
||||
|
||||
{{- _("mas.device_display_name.client_on_device", client_name=client_name, device_name=device_name) -}}
|
||||
@@ -12,6 +12,7 @@ Please see LICENSE in the repository root for full details.
|
||||
|
||||
{% block content %}
|
||||
{% set client_name = client.client_name or client.client_id %}
|
||||
{% set user_agent = grant.user_agent | parse_user_agent() %}
|
||||
|
||||
{% if grant.state == "pending" %}
|
||||
<header class="page-heading">
|
||||
@@ -27,13 +28,13 @@ Please see LICENSE in the repository root for full details.
|
||||
<h1 class="title">{{ _("mas.consent.heading") }}</h1>
|
||||
|
||||
<div class="session-card my-4">
|
||||
<div class="card-header" {%- if grant.user_agent %} title="{{ grant.user_agent.raw }}"{% endif %}>
|
||||
<div class="card-header" {%- if user_agent %} title="{{ user_agent.raw }}"{% endif %}>
|
||||
<div class="device-type-icon">
|
||||
{% if grant.user_agent.device_type == "mobile" %}
|
||||
{% if user_agent.device_type == "mobile" %}
|
||||
{{ icon.mobile() }}
|
||||
{% elif grant.user_agent.device_type == "tablet" %}
|
||||
{% elif user_agent.device_type == "tablet" %}
|
||||
{{ icon.web_browser() }}
|
||||
{% elif grant.user_agent.device_type == "pc" %}
|
||||
{% elif user_agent.device_type == "pc" %}
|
||||
{{ icon.computer() }}
|
||||
{% else %}
|
||||
{{ icon.unknown_solid() }}
|
||||
@@ -41,31 +42,31 @@ Please see LICENSE in the repository root for full details.
|
||||
</div>
|
||||
|
||||
<div class="content auto">
|
||||
{% if grant.user_agent.model %}
|
||||
<div>{{ grant.user_agent.model }}</div>
|
||||
{% if user_agent.model %}
|
||||
<div>{{ user_agent.model }}</div>
|
||||
{% endif %}
|
||||
|
||||
{% if grant.user_agent.os %}
|
||||
{% if user_agent.os %}
|
||||
<div>
|
||||
{{ grant.user_agent.os }}
|
||||
{% if grant.user_agent.os_version %}
|
||||
{{ grant.user_agent.os_version }}
|
||||
{{ user_agent.os }}
|
||||
{% if user_agent.os_version %}
|
||||
{{ 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 %}
|
||||
{% if not user_agent.model and user_agent.name %}
|
||||
<div>
|
||||
{{ grant.user_agent.name }}
|
||||
{% if grant.user_agent.version %}
|
||||
{{ grant.user_agent.version }}
|
||||
{{ user_agent.name }}
|
||||
{% if user_agent.version %}
|
||||
{{ 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 %}
|
||||
{% if not user_agent.model and not user_agent.name and not user_agent.os %}
|
||||
<div>{{ _("mas.device_card.generic_device") }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
@@ -6,11 +6,11 @@
|
||||
},
|
||||
"cancel": "Cancel",
|
||||
"@cancel": {
|
||||
"context": "pages/consent.html:69:11-29, pages/device_consent.html:126:13-31, pages/policy_violation.html:44:13-31"
|
||||
"context": "pages/consent.html:69:11-29, pages/device_consent.html:127:13-31, pages/policy_violation.html:44:13-31"
|
||||
},
|
||||
"continue": "Continue",
|
||||
"@continue": {
|
||||
"context": "form_post.html:25:28-48, pages/consent.html:57:28-48, pages/device_consent.html:123:13-33, pages/device_link.html:40:26-46, pages/login.html:68:30-50, pages/reauth.html:32:28-48, pages/recovery/start.html:38:26-46, pages/register/password.html:74:26-46, pages/register/steps/display_name.html:43:28-48, pages/register/steps/verify_email.html:51:26-46, pages/sso.html:37:28-48"
|
||||
"context": "form_post.html:25:28-48, pages/consent.html:57:28-48, pages/device_consent.html:124:13-33, pages/device_link.html:40:26-46, pages/login.html:68:30-50, pages/reauth.html:32:28-48, pages/recovery/start.html:38:26-46, pages/register/password.html:74:26-46, pages/register/steps/display_name.html:43:28-48, pages/register/steps/verify_email.html:51:26-46, pages/sso.html:37:28-48"
|
||||
},
|
||||
"create_account": "Create Account",
|
||||
"@create_account": {
|
||||
@@ -22,7 +22,7 @@
|
||||
},
|
||||
"sign_out": "Sign out",
|
||||
"@sign_out": {
|
||||
"context": "pages/account/logged_out.html:22:28-48, pages/consent.html:65:28-48, pages/device_consent.html:135:30-50, pages/index.html:28:28-48, pages/policy_violation.html:38:28-48, pages/sso.html:45:28-48, pages/upstream_oauth2/link_mismatch.html:24:24-44, pages/upstream_oauth2/suggest_link.html:32:26-46"
|
||||
"context": "pages/account/logged_out.html:22:28-48, pages/consent.html:65:28-48, pages/device_consent.html:136:30-50, pages/index.html:28:28-48, pages/policy_violation.html:38:28-48, pages/sso.html:45:28-48, pages/upstream_oauth2/link_mismatch.html:24:24-44, pages/upstream_oauth2/suggest_link.html:32:26-46"
|
||||
},
|
||||
"skip": "Skip",
|
||||
"@skip": {
|
||||
@@ -195,37 +195,37 @@
|
||||
},
|
||||
"heading": "Allow access to your account?",
|
||||
"@heading": {
|
||||
"context": "pages/consent.html:25:27-51, pages/device_consent.html:27:29-53"
|
||||
"context": "pages/consent.html:25:27-51, pages/device_consent.html:28:29-53"
|
||||
},
|
||||
"make_sure_you_trust": "Make sure that you trust <span>%(client_name)s</span>.",
|
||||
"@make_sure_you_trust": {
|
||||
"context": "pages/consent.html:38:81-142, pages/device_consent.html:103:83-144"
|
||||
"context": "pages/consent.html:38:81-142, pages/device_consent.html:104:83-144"
|
||||
},
|
||||
"this_will_allow": "This will allow <span>%(client_name)s</span> to:",
|
||||
"@this_will_allow": {
|
||||
"context": "pages/consent.html:28:11-68, pages/device_consent.html:93:13-70"
|
||||
"context": "pages/consent.html:28:11-68, pages/device_consent.html:94:13-70"
|
||||
},
|
||||
"you_may_be_sharing": "You may be sharing sensitive information with this site or app.",
|
||||
"@you_may_be_sharing": {
|
||||
"context": "pages/consent.html:39:7-42, pages/device_consent.html:104:9-44"
|
||||
"context": "pages/consent.html:39:7-42, pages/device_consent.html:105:9-44"
|
||||
}
|
||||
},
|
||||
"device_card": {
|
||||
"access_requested": "Access requested",
|
||||
"@access_requested": {
|
||||
"context": "pages/device_consent.html:81:34-71"
|
||||
"context": "pages/device_consent.html:82:34-71"
|
||||
},
|
||||
"device_code": "Code",
|
||||
"@device_code": {
|
||||
"context": "pages/device_consent.html:85:34-66"
|
||||
"context": "pages/device_consent.html:86:34-66"
|
||||
},
|
||||
"generic_device": "Device",
|
||||
"@generic_device": {
|
||||
"context": "pages/device_consent.html:69:22-57"
|
||||
"context": "pages/device_consent.html:70:22-57"
|
||||
},
|
||||
"ip_address": "IP address",
|
||||
"@ip_address": {
|
||||
"context": "pages/device_consent.html:76:36-67"
|
||||
"context": "pages/device_consent.html:77:36-67"
|
||||
}
|
||||
},
|
||||
"device_code_link": {
|
||||
@@ -241,29 +241,45 @@
|
||||
"device_consent": {
|
||||
"another_device_access": "Another device wants to access your account.",
|
||||
"@another_device_access": {
|
||||
"context": "pages/device_consent.html:92:13-58"
|
||||
"context": "pages/device_consent.html:93:13-58"
|
||||
},
|
||||
"denied": {
|
||||
"description": "You denied access to %(client_name)s. You can close this window.",
|
||||
"@description": {
|
||||
"context": "pages/device_consent.html:146:27-94"
|
||||
"context": "pages/device_consent.html:147:27-94"
|
||||
},
|
||||
"heading": "Access denied",
|
||||
"@heading": {
|
||||
"context": "pages/device_consent.html:145:29-67"
|
||||
"context": "pages/device_consent.html:146:29-67"
|
||||
}
|
||||
},
|
||||
"granted": {
|
||||
"description": "You granted access to %(client_name)s. You can close this window.",
|
||||
"@description": {
|
||||
"context": "pages/device_consent.html:157:27-95"
|
||||
"context": "pages/device_consent.html:158:27-95"
|
||||
},
|
||||
"heading": "Access granted",
|
||||
"@heading": {
|
||||
"context": "pages/device_consent.html:156:29-68"
|
||||
"context": "pages/device_consent.html:157:29-68"
|
||||
}
|
||||
}
|
||||
},
|
||||
"device_display_name": {
|
||||
"client_on_device": "%(client_name)s on %(device_name)s",
|
||||
"@client_on_device": {
|
||||
"context": "device_name.txt:28:4-99",
|
||||
"description": "The automatic device name generated for a client, e.g. 'Element on iPhone'"
|
||||
},
|
||||
"name_for_platform": "%(name)s for %(platform)s",
|
||||
"@name_for_platform": {
|
||||
"context": "device_name.txt:19:10-102",
|
||||
"description": "Part of the automatic device name for the platfom, e.g. 'Safari for macOS'"
|
||||
},
|
||||
"unknown_device": "Unknown device",
|
||||
"@unknown_device": {
|
||||
"context": "device_name.txt:24:8-51"
|
||||
}
|
||||
},
|
||||
"email_in_use": {
|
||||
"description": "If you have forgotten your account credentials, you can recover your account. You can also start over and use a different email address.",
|
||||
"@description": {
|
||||
@@ -469,7 +485,7 @@
|
||||
},
|
||||
"not_you": "Not %(username)s?",
|
||||
"@not_you": {
|
||||
"context": "pages/consent.html:62:11-67, pages/device_consent.html:132:13-69, pages/sso.html:42:11-67",
|
||||
"context": "pages/consent.html:62:11-67, pages/device_consent.html:133:13-69, pages/sso.html:42:11-67",
|
||||
"description": "Suggestions for the user to log in as a different user"
|
||||
},
|
||||
"or_separator": "Or",
|
||||
|
||||
Reference in New Issue
Block a user