You've already forked authentication-service
mirror of
https://github.com/matrix-org/matrix-authentication-service.git
synced 2025-07-31 09:24:31 +03:00
graphql: API to set the user displayname (#1412)
This commit is contained in:
115
crates/graphql/src/mutations/matrix.rs
Normal file
115
crates/graphql/src/mutations/matrix.rs
Normal file
@ -0,0 +1,115 @@
|
||||
// Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use anyhow::Context as _;
|
||||
use async_graphql::{Context, Description, Enum, InputObject, Object, ID};
|
||||
|
||||
use crate::{
|
||||
model::{NodeType, User},
|
||||
state::ContextExt,
|
||||
};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct MatrixMutations {
|
||||
_private: (),
|
||||
}
|
||||
|
||||
/// The input for the `addEmail` mutation
|
||||
#[derive(InputObject)]
|
||||
struct SetDisplayNameInput {
|
||||
/// The ID of the user to add the email address to
|
||||
user_id: ID,
|
||||
|
||||
/// The display name to set. If `None`, the display name will be removed.
|
||||
display_name: Option<String>,
|
||||
}
|
||||
|
||||
/// The status of the `setDisplayName` mutation
|
||||
#[derive(Enum, Copy, Clone, Eq, PartialEq)]
|
||||
pub enum SetDisplayNameStatus {
|
||||
/// The display name was set
|
||||
Set,
|
||||
/// The display name is invalid
|
||||
Invalid,
|
||||
}
|
||||
|
||||
/// The payload of the `setDisplayName` mutation
|
||||
#[derive(Description)]
|
||||
enum SetDisplayNamePayload {
|
||||
Set(User),
|
||||
Invalid,
|
||||
}
|
||||
|
||||
#[Object(use_type_description)]
|
||||
impl SetDisplayNamePayload {
|
||||
/// Status of the operation
|
||||
async fn status(&self) -> SetDisplayNameStatus {
|
||||
match self {
|
||||
SetDisplayNamePayload::Set(_) => SetDisplayNameStatus::Set,
|
||||
SetDisplayNamePayload::Invalid => SetDisplayNameStatus::Invalid,
|
||||
}
|
||||
}
|
||||
|
||||
/// The user that was updated
|
||||
async fn user(&self) -> Option<&User> {
|
||||
match self {
|
||||
SetDisplayNamePayload::Set(user) => Some(user),
|
||||
SetDisplayNamePayload::Invalid => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[Object]
|
||||
impl MatrixMutations {
|
||||
/// Set the display name of a user
|
||||
async fn set_display_name(
|
||||
&self,
|
||||
ctx: &Context<'_>,
|
||||
input: SetDisplayNameInput,
|
||||
) -> Result<SetDisplayNamePayload, async_graphql::Error> {
|
||||
let state = ctx.state();
|
||||
let id = NodeType::User.extract_ulid(&input.user_id)?;
|
||||
let requester = ctx.requester();
|
||||
|
||||
let user = requester.user().context("Unauthorized")?;
|
||||
|
||||
if user.id != id {
|
||||
return Err(async_graphql::Error::new("Unauthorized"));
|
||||
}
|
||||
|
||||
let conn = state.homeserver_connection();
|
||||
let mxid = conn.mxid(&user.username);
|
||||
|
||||
if let Some(display_name) = &input.display_name {
|
||||
// Let's do some basic validation on the display name
|
||||
if display_name.len() > 256 {
|
||||
return Ok(SetDisplayNamePayload::Invalid);
|
||||
}
|
||||
|
||||
if display_name.is_empty() {
|
||||
return Ok(SetDisplayNamePayload::Invalid);
|
||||
}
|
||||
|
||||
conn.set_displayname(&mxid, display_name)
|
||||
.await
|
||||
.context("Failed to set display name")?;
|
||||
} else {
|
||||
conn.unset_displayname(&mxid)
|
||||
.await
|
||||
.context("Failed to unset display name")?;
|
||||
}
|
||||
|
||||
Ok(SetDisplayNamePayload::Set(User(user.clone())))
|
||||
}
|
||||
}
|
@ -14,6 +14,7 @@
|
||||
|
||||
mod browser_session;
|
||||
mod compat_session;
|
||||
mod matrix;
|
||||
mod oauth2_session;
|
||||
mod user_email;
|
||||
|
||||
@ -26,6 +27,7 @@ pub struct Mutation(
|
||||
oauth2_session::OAuth2SessionMutations,
|
||||
compat_session::CompatSessionMutations,
|
||||
browser_session::BrowserSessionMutations,
|
||||
matrix::MatrixMutations,
|
||||
);
|
||||
|
||||
impl Mutation {
|
||||
|
@ -119,9 +119,14 @@ struct SynapseUser {
|
||||
external_ids: Option<Vec<ExternalID>>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct SynapseDevice {
|
||||
device_id: String,
|
||||
#[derive(Serialize)]
|
||||
struct SynapseDevice<'a> {
|
||||
device_id: &'a str,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct SetDisplayNameRequest<'a> {
|
||||
displayname: &'a str,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
@ -257,9 +262,7 @@ impl HomeserverConnection for SynapseConnection {
|
||||
|
||||
let request = self
|
||||
.post(&format!("_synapse/admin/v2/users/{mxid}/devices"))
|
||||
.body(SynapseDevice {
|
||||
device_id: device_id.to_owned(),
|
||||
})?;
|
||||
.body(SynapseDevice { device_id })?;
|
||||
|
||||
let response = client.ready().await?.call(request).await?;
|
||||
|
||||
@ -328,4 +331,48 @@ impl HomeserverConnection for SynapseConnection {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tracing::instrument(
|
||||
name = "homeserver.set_displayname",
|
||||
skip_all,
|
||||
fields(
|
||||
matrix.homeserver = self.homeserver,
|
||||
matrix.mxid = mxid,
|
||||
matrix.displayname = displayname,
|
||||
),
|
||||
err(Display),
|
||||
)]
|
||||
async fn set_displayname(&self, mxid: &str, displayname: &str) -> Result<(), Self::Error> {
|
||||
let mut client = self
|
||||
.http_client_factory
|
||||
.client()
|
||||
.await?
|
||||
.request_bytes_to_body()
|
||||
.json_request();
|
||||
|
||||
let request = self
|
||||
.put(&format!("_matrix/client/v3/profile/{mxid}/displayname"))
|
||||
.body(SetDisplayNameRequest { displayname })?;
|
||||
|
||||
let response = client.ready().await?.call(request).await?;
|
||||
|
||||
if response.status() != StatusCode::OK {
|
||||
return Err(anyhow::anyhow!("Failed to set displayname in Synapse"));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tracing::instrument(
|
||||
name = "homeserver.unset_displayname",
|
||||
skip_all,
|
||||
fields(
|
||||
matrix.homeserver = self.homeserver,
|
||||
matrix.mxid = mxid,
|
||||
),
|
||||
err(Display),
|
||||
)]
|
||||
async fn unset_displayname(&self, mxid: &str) -> Result<(), Self::Error> {
|
||||
self.set_displayname(mxid, "").await
|
||||
}
|
||||
}
|
||||
|
@ -95,7 +95,10 @@ impl ProvisionRequest {
|
||||
/// # Parameters
|
||||
///
|
||||
/// * `callback` - The callback to call.
|
||||
pub fn on_displayname(&self, callback: impl FnOnce(Option<&str>)) -> &Self {
|
||||
pub fn on_displayname<F>(&self, callback: F) -> &Self
|
||||
where
|
||||
F: FnOnce(Option<&str>),
|
||||
{
|
||||
match &self.displayname {
|
||||
FieldAction::Unset => callback(None),
|
||||
FieldAction::Set(displayname) => callback(Some(displayname)),
|
||||
@ -128,7 +131,10 @@ impl ProvisionRequest {
|
||||
/// # Parameters
|
||||
///
|
||||
/// * `callback` - The callback to call.
|
||||
pub fn on_avatar_url(&self, callback: impl FnOnce(Option<&str>)) -> &Self {
|
||||
pub fn on_avatar_url<F>(&self, callback: F) -> &Self
|
||||
where
|
||||
F: FnOnce(Option<&str>),
|
||||
{
|
||||
match &self.avatar_url {
|
||||
FieldAction::Unset => callback(None),
|
||||
FieldAction::Set(avatar_url) => callback(Some(avatar_url)),
|
||||
@ -161,7 +167,10 @@ impl ProvisionRequest {
|
||||
/// # Parameters
|
||||
///
|
||||
/// * `callback` - The callback to call.
|
||||
pub fn on_emails(&self, callback: impl FnOnce(Option<&[String]>)) -> &Self {
|
||||
pub fn on_emails<F>(&self, callback: F) -> &Self
|
||||
where
|
||||
F: FnOnce(Option<&[String]>),
|
||||
{
|
||||
match &self.emails {
|
||||
FieldAction::Unset => callback(None),
|
||||
FieldAction::Set(emails) => callback(Some(emails)),
|
||||
@ -252,6 +261,31 @@ pub trait HomeserverConnection: Send + Sync {
|
||||
/// Returns an error if the homeserver is unreachable or the user could not
|
||||
/// be deleted.
|
||||
async fn delete_user(&self, mxid: &str, erase: bool) -> Result<(), Self::Error>;
|
||||
|
||||
/// Set the displayname of a user on the homeserver.
|
||||
///
|
||||
/// # Parameters
|
||||
///
|
||||
/// * `mxid` - The Matrix ID of the user to set the displayname for.
|
||||
/// * `displayname` - The displayname to set.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if the homeserver is unreachable or the displayname
|
||||
/// could not be set.
|
||||
async fn set_displayname(&self, mxid: &str, displayname: &str) -> Result<(), Self::Error>;
|
||||
|
||||
/// Unset the displayname of a user on the homeserver.
|
||||
///
|
||||
/// # Parameters
|
||||
///
|
||||
/// * `mxid` - The Matrix ID of the user to unset the displayname for.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if the homeserver is unreachable or the displayname
|
||||
/// could not be unset.
|
||||
async fn unset_displayname(&self, mxid: &str) -> Result<(), Self::Error>;
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
@ -281,4 +315,12 @@ impl<T: HomeserverConnection + Send + Sync + ?Sized> HomeserverConnection for &T
|
||||
async fn delete_user(&self, mxid: &str, erase: bool) -> Result<(), Self::Error> {
|
||||
(**self).delete_user(mxid, erase).await
|
||||
}
|
||||
|
||||
async fn set_displayname(&self, mxid: &str, displayname: &str) -> Result<(), Self::Error> {
|
||||
(**self).set_displayname(mxid, displayname).await
|
||||
}
|
||||
|
||||
async fn unset_displayname(&self, mxid: &str) -> Result<(), Self::Error> {
|
||||
(**self).unset_displayname(mxid).await
|
||||
}
|
||||
}
|
||||
|
@ -122,6 +122,20 @@ impl crate::HomeserverConnection for HomeserverConnection {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn set_displayname(&self, mxid: &str, displayname: &str) -> Result<(), Self::Error> {
|
||||
let mut users = self.users.write().await;
|
||||
let user = users.get_mut(mxid).context("User not found")?;
|
||||
user.displayname = Some(displayname.to_owned());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn unset_displayname(&self, mxid: &str) -> Result<(), Self::Error> {
|
||||
let mut users = self.users.write().await;
|
||||
let user = users.get_mut(mxid).context("User not found")?;
|
||||
user.displayname = None;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@ -150,10 +164,22 @@ mod tests {
|
||||
let inserted = conn.provision_user(&request).await.unwrap();
|
||||
assert!(inserted);
|
||||
|
||||
let user = conn.query_user("@test:example.org").await.unwrap();
|
||||
let user = conn.query_user(mxid).await.unwrap();
|
||||
assert_eq!(user.displayname, Some("Test User".into()));
|
||||
assert_eq!(user.avatar_url, Some("mxc://example.org/1234567890".into()));
|
||||
|
||||
// Set the displayname again
|
||||
assert!(conn.set_displayname(mxid, "John").await.is_ok());
|
||||
|
||||
let user = conn.query_user(mxid).await.unwrap();
|
||||
assert_eq!(user.displayname, Some("John".into()));
|
||||
|
||||
// Unset the displayname
|
||||
assert!(conn.unset_displayname(mxid).await.is_ok());
|
||||
|
||||
let user = conn.query_user(mxid).await.unwrap();
|
||||
assert_eq!(user.displayname, None);
|
||||
|
||||
// Deleting a non-existent device should not fail
|
||||
assert!(conn.delete_device(mxid, device).await.is_ok());
|
||||
|
||||
|
@ -459,6 +459,10 @@ type Mutation {
|
||||
endOauth2Session(input: EndOAuth2SessionInput!): EndOAuth2SessionPayload!
|
||||
endCompatSession(input: EndCompatSessionInput!): EndCompatSessionPayload!
|
||||
endBrowserSession(input: EndBrowserSessionInput!): EndBrowserSessionPayload!
|
||||
"""
|
||||
Set the display name of a user
|
||||
"""
|
||||
setDisplayName(input: SetDisplayNameInput!): SetDisplayNamePayload!
|
||||
}
|
||||
|
||||
"""
|
||||
@ -757,6 +761,48 @@ enum SendVerificationEmailStatus {
|
||||
ALREADY_VERIFIED
|
||||
}
|
||||
|
||||
"""
|
||||
The input for the `addEmail` mutation
|
||||
"""
|
||||
input SetDisplayNameInput {
|
||||
"""
|
||||
The ID of the user to add the email address to
|
||||
"""
|
||||
userId: ID!
|
||||
"""
|
||||
The display name to set. If `None`, the display name will be removed.
|
||||
"""
|
||||
displayName: String
|
||||
}
|
||||
|
||||
"""
|
||||
The payload of the `setDisplayName` mutation
|
||||
"""
|
||||
type SetDisplayNamePayload {
|
||||
"""
|
||||
Status of the operation
|
||||
"""
|
||||
status: SetDisplayNameStatus!
|
||||
"""
|
||||
The user that was updated
|
||||
"""
|
||||
user: User
|
||||
}
|
||||
|
||||
"""
|
||||
The status of the `setDisplayName` mutation
|
||||
"""
|
||||
enum SetDisplayNameStatus {
|
||||
"""
|
||||
The display name was set
|
||||
"""
|
||||
SET
|
||||
"""
|
||||
The display name is invalid
|
||||
"""
|
||||
INVALID
|
||||
}
|
||||
|
||||
"""
|
||||
The input for the `setPrimaryEmail` mutation
|
||||
"""
|
||||
|
@ -323,6 +323,8 @@ export type Mutation = {
|
||||
removeEmail: RemoveEmailPayload;
|
||||
/** Send a verification code for an email address */
|
||||
sendVerificationEmail: SendVerificationEmailPayload;
|
||||
/** Set the display name of a user */
|
||||
setDisplayName: SetDisplayNamePayload;
|
||||
/** Set an email address as primary */
|
||||
setPrimaryEmail: SetPrimaryEmailPayload;
|
||||
/** Submit a verification code for an email address */
|
||||
@ -359,6 +361,11 @@ export type MutationSendVerificationEmailArgs = {
|
||||
input: SendVerificationEmailInput;
|
||||
};
|
||||
|
||||
/** The mutations root of the GraphQL interface. */
|
||||
export type MutationSetDisplayNameArgs = {
|
||||
input: SetDisplayNameInput;
|
||||
};
|
||||
|
||||
/** The mutations root of the GraphQL interface. */
|
||||
export type MutationSetPrimaryEmailArgs = {
|
||||
input: SetPrimaryEmailInput;
|
||||
@ -589,6 +596,31 @@ export enum SendVerificationEmailStatus {
|
||||
Sent = "SENT",
|
||||
}
|
||||
|
||||
/** The input for the `addEmail` mutation */
|
||||
export type SetDisplayNameInput = {
|
||||
/** The display name to set. If `None`, the display name will be removed. */
|
||||
displayName?: InputMaybe<Scalars["String"]["input"]>;
|
||||
/** The ID of the user to add the email address to */
|
||||
userId: Scalars["ID"]["input"];
|
||||
};
|
||||
|
||||
/** The payload of the `setDisplayName` mutation */
|
||||
export type SetDisplayNamePayload = {
|
||||
__typename?: "SetDisplayNamePayload";
|
||||
/** Status of the operation */
|
||||
status: SetDisplayNameStatus;
|
||||
/** The user that was updated */
|
||||
user?: Maybe<User>;
|
||||
};
|
||||
|
||||
/** The status of the `setDisplayName` mutation */
|
||||
export enum SetDisplayNameStatus {
|
||||
/** The display name is invalid */
|
||||
Invalid = "INVALID",
|
||||
/** The display name was set */
|
||||
Set = "SET",
|
||||
}
|
||||
|
||||
/** The input for the `setPrimaryEmail` mutation */
|
||||
export type SetPrimaryEmailInput = {
|
||||
/** The ID of the email address to set as primary */
|
||||
|
@ -906,6 +906,29 @@ export default {
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "setDisplayName",
|
||||
type: {
|
||||
kind: "NON_NULL",
|
||||
ofType: {
|
||||
kind: "OBJECT",
|
||||
name: "SetDisplayNamePayload",
|
||||
ofType: null,
|
||||
},
|
||||
},
|
||||
args: [
|
||||
{
|
||||
name: "input",
|
||||
type: {
|
||||
kind: "NON_NULL",
|
||||
ofType: {
|
||||
kind: "SCALAR",
|
||||
name: "Any",
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "setPrimaryEmail",
|
||||
type: {
|
||||
@ -1644,6 +1667,33 @@ export default {
|
||||
],
|
||||
interfaces: [],
|
||||
},
|
||||
{
|
||||
kind: "OBJECT",
|
||||
name: "SetDisplayNamePayload",
|
||||
fields: [
|
||||
{
|
||||
name: "status",
|
||||
type: {
|
||||
kind: "NON_NULL",
|
||||
ofType: {
|
||||
kind: "SCALAR",
|
||||
name: "Any",
|
||||
},
|
||||
},
|
||||
args: [],
|
||||
},
|
||||
{
|
||||
name: "user",
|
||||
type: {
|
||||
kind: "OBJECT",
|
||||
name: "User",
|
||||
ofType: null,
|
||||
},
|
||||
args: [],
|
||||
},
|
||||
],
|
||||
interfaces: [],
|
||||
},
|
||||
{
|
||||
kind: "OBJECT",
|
||||
name: "SetPrimaryEmailPayload",
|
||||
|
Reference in New Issue
Block a user