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 #[serde(default, skip_serializing_if = "ruma_common::serde::is_default")]
182 pub oauth_aware_preferred: bool,
183 }
184
185 impl SsoLoginType {
186 pub fn new() -> Self {
188 Self::default()
189 }
190 }
191
192 #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
194 #[derive(Clone, Debug, Deserialize, Serialize)]
195 pub struct IdentityProvider {
196 pub id: String,
198
199 pub name: String,
201
202 pub icon: Option<OwnedMxcUri>,
204
205 pub brand: Option<IdentityProviderBrand>,
207 }
208
209 impl IdentityProvider {
210 pub fn new(id: String, name: String) -> Self {
212 Self { id, name, icon: None, brand: None }
213 }
214 }
215
216 #[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))]
223 #[derive(Clone, StringEnum)]
224 #[ruma_enum(rename_all = "lowercase")]
225 #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
226 pub enum IdentityProviderBrand {
227 Apple,
231
232 Facebook,
234
235 GitHub,
237
238 GitLab,
240
241 Google,
243
244 Twitter,
248
249 #[doc(hidden)]
251 _Custom(PrivOwnedStr),
252 }
253
254 #[derive(Clone, Debug, Default, Deserialize, Serialize)]
256 #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
257 #[serde(tag = "type", rename = "m.login.application_service")]
258 pub struct ApplicationServiceLoginType {}
259
260 impl ApplicationServiceLoginType {
261 pub fn new() -> Self {
263 Self::default()
264 }
265 }
266
267 #[doc(hidden)]
269 #[derive(Clone, Debug, Deserialize, Serialize)]
270 #[allow(clippy::exhaustive_structs)]
271 pub struct CustomLoginType {
272 #[serde(rename = "type")]
277 pub type_: String,
278
279 #[serde(flatten)]
281 pub data: JsonObject,
282 }
283
284 mod login_type_serde {
285 use ruma_common::serde::from_raw_json_value;
286 use serde::{Deserialize, de};
287 use serde_json::value::RawValue as RawJsonValue;
288
289 use super::LoginType;
290
291 #[derive(Debug, Deserialize)]
293 struct LoginTypeDeHelper {
294 #[serde(rename = "type")]
296 type_: String,
297 }
298
299 impl<'de> Deserialize<'de> for LoginType {
300 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
301 where
302 D: de::Deserializer<'de>,
303 {
304 let json = Box::<RawJsonValue>::deserialize(deserializer)?;
305 let LoginTypeDeHelper { type_ } = from_raw_json_value(&json)?;
306
307 Ok(match type_.as_ref() {
308 "m.login.password" => Self::Password(from_raw_json_value(&json)?),
309 "m.login.token" => Self::Token(from_raw_json_value(&json)?),
310 "m.login.sso" => Self::Sso(from_raw_json_value(&json)?),
311 "m.login.application_service" => {
312 Self::ApplicationService(from_raw_json_value(&json)?)
313 }
314 _ => Self::_Custom(from_raw_json_value(&json)?),
315 })
316 }
317 }
318 }
319
320 #[cfg(test)]
321 mod tests {
322 use assert_matches2::assert_matches;
323 use ruma_common::{canonical_json::assert_to_canonical_json_eq, mxc_uri};
324 use serde::{Deserialize, Serialize};
325 use serde_json::{Value as JsonValue, from_value as from_json_value, json};
326
327 use super::{
328 IdentityProvider, IdentityProviderBrand, LoginType, SsoLoginType, TokenLoginType,
329 };
330
331 #[derive(Debug, Deserialize, Serialize)]
332 struct Wrapper {
333 flows: Vec<LoginType>,
334 }
335
336 #[test]
337 fn deserialize_password_login_type() {
338 let wrapper = from_json_value::<Wrapper>(json!({
339 "flows": [
340 { "type": "m.login.password" }
341 ],
342 }))
343 .unwrap();
344 assert_eq!(wrapper.flows.len(), 1);
345 assert_matches!(&wrapper.flows[0], LoginType::Password(_));
346 }
347
348 #[test]
349 fn deserialize_custom_login_type() {
350 let wrapper = from_json_value::<Wrapper>(json!({
351 "flows": [
352 {
353 "type": "io.ruma.custom",
354 "color": "green",
355 }
356 ],
357 }))
358 .unwrap();
359 assert_eq!(wrapper.flows.len(), 1);
360 assert_matches!(&wrapper.flows[0], LoginType::_Custom(custom));
361 assert_eq!(custom.type_, "io.ruma.custom");
362 assert_eq!(custom.data.len(), 1);
363 assert_eq!(custom.data.get("color"), Some(&JsonValue::from("green")));
364 }
365
366 #[test]
367 fn deserialize_sso_login_type() {
368 let wrapper = from_json_value::<Wrapper>(json!({
369 "flows": [
370 {
371 "type": "m.login.sso",
372 "identity_providers": [
373 {
374 "id": "oidc-gitlab",
375 "name": "GitLab",
376 "icon": "mxc://localhost/gitlab-icon",
377 "brand": "gitlab"
378 },
379 {
380 "id": "custom",
381 "name": "Custom",
382 }
383 ]
384 }
385 ],
386 }))
387 .unwrap();
388 assert_eq!(wrapper.flows.len(), 1);
389 let flow = &wrapper.flows[0];
390
391 assert_matches!(
392 flow,
393 LoginType::Sso(SsoLoginType { identity_providers, oauth_aware_preferred: false })
394 );
395 assert_eq!(identity_providers.len(), 2);
396
397 let provider = &identity_providers[0];
398 assert_eq!(provider.id, "oidc-gitlab");
399 assert_eq!(provider.name, "GitLab");
400 assert_eq!(provider.icon.as_deref(), Some(mxc_uri!("mxc://localhost/gitlab-icon")));
401 assert_eq!(provider.brand, Some(IdentityProviderBrand::GitLab));
402
403 let provider = &identity_providers[1];
404 assert_eq!(provider.id, "custom");
405 assert_eq!(provider.name, "Custom");
406 assert_eq!(provider.icon, None);
407 assert_eq!(provider.brand, None);
408 }
409
410 #[test]
411 fn serialize_sso_login_type() {
412 let wrapper = Wrapper {
413 flows: vec![
414 LoginType::Token(TokenLoginType::new()),
415 LoginType::Sso(SsoLoginType {
416 identity_providers: vec![IdentityProvider {
417 id: "oidc-github".into(),
418 name: "GitHub".into(),
419 icon: Some("mxc://localhost/github-icon".into()),
420 brand: Some(IdentityProviderBrand::GitHub),
421 }],
422 oauth_aware_preferred: false,
423 }),
424 ],
425 };
426
427 assert_to_canonical_json_eq!(
428 wrapper,
429 json!({
430 "flows": [
431 {
432 "type": "m.login.token"
433 },
434 {
435 "type": "m.login.sso",
436 "identity_providers": [
437 {
438 "id": "oidc-github",
439 "name": "GitHub",
440 "icon": "mxc://localhost/github-icon",
441 "brand": "github"
442 },
443 ]
444 }
445 ],
446 })
447 );
448 }
449 }
450}