1pub mod v3 {
7 use std::borrow::Cow;
12
13 use ruma_common::{
14 OwnedMxcUri,
15 api::{auth_scheme::NoAuthentication, request, response},
16 metadata,
17 serde::{JsonObject, StringEnum},
18 };
19 use serde::{Deserialize, Serialize, de::DeserializeOwned};
20 use serde_json::Value as JsonValue;
21
22 use crate::PrivOwnedStr;
23
24 metadata! {
25 method: GET,
26 rate_limited: true,
27 authentication: NoAuthentication,
28 history: {
29 1.0 => "/_matrix/client/r0/login",
30 1.1 => "/_matrix/client/v3/login",
31 }
32 }
33
34 #[request(error = crate::Error)]
36 #[derive(Default)]
37 pub struct Request {}
38
39 #[response(error = crate::Error)]
41 pub struct Response {
42 pub flows: Vec<LoginType>,
44 }
45
46 impl Request {
47 pub fn new() -> Self {
49 Self {}
50 }
51 }
52
53 impl Response {
54 pub fn new(flows: Vec<LoginType>) -> Self {
56 Self { flows }
57 }
58 }
59
60 #[derive(Clone, Debug, Serialize)]
62 #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
63 #[serde(untagged)]
64 pub enum LoginType {
65 Password(PasswordLoginType),
67
68 Token(TokenLoginType),
70
71 Sso(SsoLoginType),
73
74 ApplicationService(ApplicationServiceLoginType),
76
77 #[doc(hidden)]
79 _Custom(Box<CustomLoginType>),
80 }
81
82 impl LoginType {
83 pub fn new(login_type: &str, data: JsonObject) -> serde_json::Result<Self> {
89 fn from_json_object<T: DeserializeOwned>(obj: JsonObject) -> serde_json::Result<T> {
90 serde_json::from_value(JsonValue::Object(obj))
91 }
92
93 Ok(match login_type {
94 "m.login.password" => Self::Password(from_json_object(data)?),
95 "m.login.token" => Self::Token(from_json_object(data)?),
96 "m.login.sso" => Self::Sso(from_json_object(data)?),
97 "m.login.application_service" => Self::ApplicationService(from_json_object(data)?),
98 _ => {
99 Self::_Custom(Box::new(CustomLoginType { type_: login_type.to_owned(), data }))
100 }
101 })
102 }
103
104 pub fn login_type(&self) -> &str {
106 match self {
107 Self::Password(_) => "m.login.password",
108 Self::Token(_) => "m.login.token",
109 Self::Sso(_) => "m.login.sso",
110 Self::ApplicationService(_) => "m.login.application_service",
111 Self::_Custom(c) => &c.type_,
112 }
113 }
114
115 pub fn data(&self) -> Cow<'_, JsonObject> {
120 fn serialize<T: Serialize>(obj: &T) -> JsonObject {
121 match serde_json::to_value(obj).expect("login type serialization to succeed") {
122 JsonValue::Object(obj) => obj,
123 _ => panic!("all login types must serialize to objects"),
124 }
125 }
126
127 match self {
128 Self::Password(d) => Cow::Owned(serialize(d)),
129 Self::Token(d) => Cow::Owned(serialize(d)),
130 Self::Sso(d) => Cow::Owned(serialize(d)),
131 Self::ApplicationService(d) => Cow::Owned(serialize(d)),
132 Self::_Custom(c) => Cow::Borrowed(&c.data),
133 }
134 }
135 }
136
137 #[derive(Clone, Debug, Default, Deserialize, Serialize)]
139 #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
140 #[serde(tag = "type", rename = "m.login.password")]
141 pub struct PasswordLoginType {}
142
143 impl PasswordLoginType {
144 pub fn new() -> Self {
146 Self {}
147 }
148 }
149
150 #[derive(Clone, Debug, Default, Deserialize, Serialize)]
152 #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
153 #[serde(tag = "type", rename = "m.login.token")]
154 pub struct TokenLoginType {
155 #[serde(default, skip_serializing_if = "ruma_common::serde::is_default")]
157 pub get_login_token: bool,
158 }
159
160 impl TokenLoginType {
161 pub fn new() -> Self {
163 Self { get_login_token: false }
164 }
165 }
166
167 #[derive(Clone, Debug, Default, Deserialize, Serialize)]
169 #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
170 #[serde(tag = "type", rename = "m.login.sso")]
171 pub struct SsoLoginType {
172 #[serde(default, skip_serializing_if = "Vec::is_empty")]
174 pub identity_providers: Vec<IdentityProvider>,
175
176 #[cfg(feature = "unstable-msc3824")]
182 #[serde(
183 default,
184 skip_serializing_if = "ruma_common::serde::is_default",
185 rename = "org.matrix.msc3824.delegated_oidc_compatibility"
186 )]
187 pub delegated_oidc_compatibility: bool,
188 }
189
190 impl SsoLoginType {
191 pub fn new() -> Self {
193 Self::default()
194 }
195 }
196
197 #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
199 #[derive(Clone, Debug, Deserialize, Serialize)]
200 pub struct IdentityProvider {
201 pub id: String,
203
204 pub name: String,
206
207 pub icon: Option<OwnedMxcUri>,
209
210 pub brand: Option<IdentityProviderBrand>,
212 }
213
214 impl IdentityProvider {
215 pub fn new(id: String, name: String) -> Self {
217 Self { id, name, icon: None, brand: None }
218 }
219 }
220
221 #[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))]
228 #[derive(Clone, StringEnum)]
229 #[ruma_enum(rename_all = "lowercase")]
230 #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
231 pub enum IdentityProviderBrand {
232 Apple,
236
237 Facebook,
239
240 GitHub,
242
243 GitLab,
245
246 Google,
248
249 Twitter,
253
254 #[doc(hidden)]
256 _Custom(PrivOwnedStr),
257 }
258
259 #[derive(Clone, Debug, Default, Deserialize, Serialize)]
261 #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
262 #[serde(tag = "type", rename = "m.login.application_service")]
263 pub struct ApplicationServiceLoginType {}
264
265 impl ApplicationServiceLoginType {
266 pub fn new() -> Self {
268 Self::default()
269 }
270 }
271
272 #[doc(hidden)]
274 #[derive(Clone, Debug, Deserialize, Serialize)]
275 #[allow(clippy::exhaustive_structs)]
276 pub struct CustomLoginType {
277 #[serde(rename = "type")]
282 pub type_: String,
283
284 #[serde(flatten)]
286 pub data: JsonObject,
287 }
288
289 mod login_type_serde {
290 use ruma_common::serde::from_raw_json_value;
291 use serde::{Deserialize, de};
292 use serde_json::value::RawValue as RawJsonValue;
293
294 use super::LoginType;
295
296 #[derive(Debug, Deserialize)]
298 struct LoginTypeDeHelper {
299 #[serde(rename = "type")]
301 type_: String,
302 }
303
304 impl<'de> Deserialize<'de> for LoginType {
305 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
306 where
307 D: de::Deserializer<'de>,
308 {
309 let json = Box::<RawJsonValue>::deserialize(deserializer)?;
310 let LoginTypeDeHelper { type_ } = from_raw_json_value(&json)?;
311
312 Ok(match type_.as_ref() {
313 "m.login.password" => Self::Password(from_raw_json_value(&json)?),
314 "m.login.token" => Self::Token(from_raw_json_value(&json)?),
315 "m.login.sso" => Self::Sso(from_raw_json_value(&json)?),
316 "m.login.application_service" => {
317 Self::ApplicationService(from_raw_json_value(&json)?)
318 }
319 _ => Self::_Custom(from_raw_json_value(&json)?),
320 })
321 }
322 }
323 }
324
325 #[cfg(test)]
326 mod tests {
327 use assert_matches2::assert_matches;
328 use ruma_common::{canonical_json::assert_to_canonical_json_eq, mxc_uri};
329 use serde::{Deserialize, Serialize};
330 use serde_json::{Value as JsonValue, from_value as from_json_value, json};
331
332 use super::{
333 IdentityProvider, IdentityProviderBrand, LoginType, SsoLoginType, TokenLoginType,
334 };
335
336 #[derive(Debug, Deserialize, Serialize)]
337 struct Wrapper {
338 flows: Vec<LoginType>,
339 }
340
341 #[test]
342 fn deserialize_password_login_type() {
343 let wrapper = from_json_value::<Wrapper>(json!({
344 "flows": [
345 { "type": "m.login.password" }
346 ],
347 }))
348 .unwrap();
349 assert_eq!(wrapper.flows.len(), 1);
350 assert_matches!(&wrapper.flows[0], LoginType::Password(_));
351 }
352
353 #[test]
354 fn deserialize_custom_login_type() {
355 let wrapper = from_json_value::<Wrapper>(json!({
356 "flows": [
357 {
358 "type": "io.ruma.custom",
359 "color": "green",
360 }
361 ],
362 }))
363 .unwrap();
364 assert_eq!(wrapper.flows.len(), 1);
365 assert_matches!(&wrapper.flows[0], LoginType::_Custom(custom));
366 assert_eq!(custom.type_, "io.ruma.custom");
367 assert_eq!(custom.data.len(), 1);
368 assert_eq!(custom.data.get("color"), Some(&JsonValue::from("green")));
369 }
370
371 #[test]
372 fn deserialize_sso_login_type() {
373 let wrapper = from_json_value::<Wrapper>(json!({
374 "flows": [
375 {
376 "type": "m.login.sso",
377 "identity_providers": [
378 {
379 "id": "oidc-gitlab",
380 "name": "GitLab",
381 "icon": "mxc://localhost/gitlab-icon",
382 "brand": "gitlab"
383 },
384 {
385 "id": "custom",
386 "name": "Custom",
387 }
388 ]
389 }
390 ],
391 }))
392 .unwrap();
393 assert_eq!(wrapper.flows.len(), 1);
394 let flow = &wrapper.flows[0];
395
396 assert_matches!(
397 flow,
398 LoginType::Sso(SsoLoginType {
399 identity_providers,
400 #[cfg(feature = "unstable-msc3824")]
401 delegated_oidc_compatibility: false
402 })
403 );
404 assert_eq!(identity_providers.len(), 2);
405
406 let provider = &identity_providers[0];
407 assert_eq!(provider.id, "oidc-gitlab");
408 assert_eq!(provider.name, "GitLab");
409 assert_eq!(provider.icon.as_deref(), Some(mxc_uri!("mxc://localhost/gitlab-icon")));
410 assert_eq!(provider.brand, Some(IdentityProviderBrand::GitLab));
411
412 let provider = &identity_providers[1];
413 assert_eq!(provider.id, "custom");
414 assert_eq!(provider.name, "Custom");
415 assert_eq!(provider.icon, None);
416 assert_eq!(provider.brand, None);
417 }
418
419 #[test]
420 fn serialize_sso_login_type() {
421 let wrapper = Wrapper {
422 flows: vec![
423 LoginType::Token(TokenLoginType::new()),
424 LoginType::Sso(SsoLoginType {
425 identity_providers: vec![IdentityProvider {
426 id: "oidc-github".into(),
427 name: "GitHub".into(),
428 icon: Some("mxc://localhost/github-icon".into()),
429 brand: Some(IdentityProviderBrand::GitHub),
430 }],
431 #[cfg(feature = "unstable-msc3824")]
432 delegated_oidc_compatibility: false,
433 }),
434 ],
435 };
436
437 assert_to_canonical_json_eq!(
438 wrapper,
439 json!({
440 "flows": [
441 {
442 "type": "m.login.token"
443 },
444 {
445 "type": "m.login.sso",
446 "identity_providers": [
447 {
448 "id": "oidc-github",
449 "name": "GitHub",
450 "icon": "mxc://localhost/github-icon",
451 "brand": "github"
452 },
453 ]
454 }
455 ],
456 })
457 );
458 }
459 }
460}