pub mod v3 {
use std::borrow::Cow;
use ruma_common::{
api::ruma_api,
serde::{JsonObject, StringEnum},
OwnedMxcUri,
};
use serde::{de::DeserializeOwned, Deserialize, Serialize};
use serde_json::Value as JsonValue;
use crate::PrivOwnedStr;
ruma_api! {
metadata: {
description: "Gets the homeserver's supported login types to authenticate users. Clients should pick one of these and supply it as the type when logging in.",
method: GET,
name: "get_login_types",
r0_path: "/_matrix/client/r0/login",
stable_path: "/_matrix/client/v3/login",
rate_limited: true,
authentication: None,
added: 1.0,
}
#[derive(Default)]
request: {}
response: {
pub flows: Vec<LoginType>,
}
error: crate::Error
}
impl Request {
pub fn new() -> Self {
Self {}
}
}
impl Response {
pub fn new(flows: Vec<LoginType>) -> Self {
Self { flows }
}
}
#[derive(Clone, Debug, Serialize)]
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
#[serde(untagged)]
pub enum LoginType {
Password(PasswordLoginType),
Token(TokenLoginType),
Sso(SsoLoginType),
#[doc(hidden)]
_Custom(CustomLoginType),
}
impl LoginType {
pub fn new(login_type: &str, data: JsonObject) -> serde_json::Result<Self> {
fn from_json_object<T: DeserializeOwned>(obj: JsonObject) -> serde_json::Result<T> {
serde_json::from_value(JsonValue::Object(obj))
}
Ok(match login_type {
"m.login.password" => Self::Password(from_json_object(data)?),
"m.login.token" => Self::Token(from_json_object(data)?),
"m.login.sso" => Self::Sso(from_json_object(data)?),
_ => Self::_Custom(CustomLoginType { type_: login_type.to_owned(), data }),
})
}
pub fn login_type(&self) -> &str {
match self {
Self::Password(_) => "m.login.password",
Self::Token(_) => "m.login.token",
Self::Sso(_) => "m.login.sso",
Self::_Custom(c) => &c.type_,
}
}
pub fn data(&self) -> Cow<'_, JsonObject> {
fn serialize<T: Serialize>(obj: &T) -> JsonObject {
match serde_json::to_value(obj).expect("login type serialization to succeed") {
JsonValue::Object(obj) => obj,
_ => panic!("all login types must serialize to objects"),
}
}
match self {
Self::Password(d) => Cow::Owned(serialize(d)),
Self::Token(d) => Cow::Owned(serialize(d)),
Self::Sso(d) => Cow::Owned(serialize(d)),
Self::_Custom(c) => Cow::Borrowed(&c.data),
}
}
}
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
#[serde(tag = "type", rename = "m.login.password")]
pub struct PasswordLoginType {}
impl PasswordLoginType {
pub fn new() -> Self {
Self {}
}
}
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
#[serde(tag = "type", rename = "m.login.token")]
pub struct TokenLoginType {}
impl TokenLoginType {
pub fn new() -> Self {
Self {}
}
}
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
#[serde(tag = "type", rename = "m.login.sso")]
pub struct SsoLoginType {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub identity_providers: Vec<IdentityProvider>,
}
impl SsoLoginType {
pub fn new() -> Self {
Self::default()
}
}
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct IdentityProvider {
pub id: String,
pub name: String,
pub icon: Option<OwnedMxcUri>,
pub brand: Option<IdentityProviderBrand>,
}
impl IdentityProvider {
pub fn new(id: String, name: String) -> Self {
Self { id, name, icon: None, brand: None }
}
}
#[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))]
#[derive(Clone, Debug, PartialEq, Eq, StringEnum)]
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
pub enum IdentityProviderBrand {
#[ruma_enum(rename = "apple")]
Apple,
#[ruma_enum(rename = "facebook")]
Facebook,
#[ruma_enum(rename = "github")]
GitHub,
#[ruma_enum(rename = "gitlab")]
GitLab,
#[ruma_enum(rename = "google")]
Google,
#[ruma_enum(rename = "twitter")]
Twitter,
#[doc(hidden)]
_Custom(PrivOwnedStr),
}
#[doc(hidden)]
#[derive(Clone, Debug, Deserialize, Serialize)]
#[allow(clippy::exhaustive_structs)]
pub struct CustomLoginType {
#[serde(rename = "type")]
pub type_: String,
#[serde(flatten)]
pub data: JsonObject,
}
mod login_type_serde {
use ruma_common::serde::from_raw_json_value;
use serde::{de, Deserialize};
use serde_json::value::RawValue as RawJsonValue;
use super::LoginType;
#[derive(Debug, Deserialize)]
struct LoginTypeDeHelper {
#[serde(rename = "type")]
type_: String,
}
impl<'de> Deserialize<'de> for LoginType {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: de::Deserializer<'de>,
{
let json = Box::<RawJsonValue>::deserialize(deserializer)?;
let LoginTypeDeHelper { type_ } = from_raw_json_value(&json)?;
Ok(match type_.as_ref() {
"m.login.password" => Self::Password(from_raw_json_value(&json)?),
"m.login.token" => Self::Token(from_raw_json_value(&json)?),
"m.login.sso" => Self::Sso(from_raw_json_value(&json)?),
_ => Self::_Custom(from_raw_json_value(&json)?),
})
}
}
}
#[cfg(test)]
mod tests {
use assert_matches::assert_matches;
use ruma_common::mxc_uri;
use serde::{Deserialize, Serialize};
use serde_json::{
from_value as from_json_value, json, to_value as to_json_value, Value as JsonValue,
};
use super::{
IdentityProvider, IdentityProviderBrand, LoginType, SsoLoginType, TokenLoginType,
};
#[derive(Debug, Deserialize, Serialize)]
struct Wrapper {
pub flows: Vec<LoginType>,
}
#[test]
fn deserialize_password_login_type() {
let wrapper = from_json_value::<Wrapper>(json!({
"flows": [
{ "type": "m.login.password" }
],
}))
.unwrap();
assert_eq!(wrapper.flows.len(), 1);
assert_matches!(wrapper.flows[0], LoginType::Password(_));
}
#[test]
fn deserialize_custom_login_type() {
let wrapper = from_json_value::<Wrapper>(json!({
"flows": [
{
"type": "io.ruma.custom",
"color": "green",
}
],
}))
.unwrap();
assert_eq!(wrapper.flows.len(), 1);
let custom = assert_matches!(
&wrapper.flows[0],
LoginType::_Custom(custom) => custom
);
assert_eq!(custom.type_, "io.ruma.custom");
assert_eq!(custom.data.len(), 1);
assert_eq!(custom.data.get("color"), Some(&JsonValue::from("green")));
}
#[test]
fn deserialize_sso_login_type() {
let wrapper = from_json_value::<Wrapper>(json!({
"flows": [
{
"type": "m.login.sso",
"identity_providers": [
{
"id": "oidc-gitlab",
"name": "GitLab",
"icon": "mxc://localhost/gitlab-icon",
"brand": "gitlab"
},
{
"id": "custom",
"name": "Custom",
}
]
}
],
}))
.unwrap();
assert_eq!(wrapper.flows.len(), 1);
let flow = &wrapper.flows[0];
let identity_providers = assert_matches!(
flow,
LoginType::Sso(SsoLoginType { identity_providers }) => identity_providers
);
assert_eq!(identity_providers.len(), 2);
let provider = &identity_providers[0];
assert_eq!(provider.id, "oidc-gitlab");
assert_eq!(provider.name, "GitLab");
assert_eq!(provider.icon.as_deref(), Some(mxc_uri!("mxc://localhost/gitlab-icon")));
assert_eq!(provider.brand, Some(IdentityProviderBrand::GitLab));
let provider = &identity_providers[1];
assert_eq!(provider.id, "custom");
assert_eq!(provider.name, "Custom");
assert_eq!(provider.icon, None);
assert_eq!(provider.brand, None);
}
#[test]
fn serialize_sso_login_type() {
let wrapper = to_json_value(Wrapper {
flows: vec![
LoginType::Token(TokenLoginType {}),
LoginType::Sso(SsoLoginType {
identity_providers: vec![IdentityProvider {
id: "oidc-github".into(),
name: "GitHub".into(),
icon: Some("mxc://localhost/github-icon".into()),
brand: Some(IdentityProviderBrand::GitHub),
}],
}),
],
})
.unwrap();
assert_eq!(
wrapper,
json!({
"flows": [
{
"type": "m.login.token"
},
{
"type": "m.login.sso",
"identity_providers": [
{
"id": "oidc-github",
"name": "GitHub",
"icon": "mxc://localhost/github-icon",
"brand": "github"
},
]
}
],
})
);
}
}
}