1pub mod v3 {
7 use std::borrow::Cow;
12
13 use ruma_common::{
14 OwnedMxcUri,
15 api::{auth_scheme::NoAccessToken, 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: NoAccessToken,
28 history: {
29 1.0 => "/_matrix/client/r0/login",
30 1.1 => "/_matrix/client/v3/login",
31 }
32 }
33
34 #[request]
36 #[derive(Default)]
37 pub struct Request {}
38
39 #[response]
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 _ => Self::_Custom(Box::new(CustomLoginType {
99 login_type: login_type.to_owned(),
100 data,
101 })),
102 })
103 }
104
105 pub fn login_type(&self) -> &str {
107 match self {
108 Self::Password(_) => "m.login.password",
109 Self::Token(_) => "m.login.token",
110 Self::Sso(_) => "m.login.sso",
111 Self::ApplicationService(_) => "m.login.application_service",
112 Self::_Custom(c) => &c.login_type,
113 }
114 }
115
116 pub fn data(&self) -> Cow<'_, JsonObject> {
121 fn serialize<T: Serialize>(obj: &T) -> JsonObject {
122 match serde_json::to_value(obj).expect("login type serialization to succeed") {
123 JsonValue::Object(obj) => obj,
124 _ => panic!("all login types must serialize to objects"),
125 }
126 }
127
128 match self {
129 Self::Password(d) => Cow::Owned(serialize(d)),
130 Self::Token(d) => Cow::Owned(serialize(d)),
131 Self::Sso(d) => Cow::Owned(serialize(d)),
132 Self::ApplicationService(d) => Cow::Owned(serialize(d)),
133 Self::_Custom(c) => Cow::Borrowed(&c.data),
134 }
135 }
136 }
137
138 #[derive(Clone, Debug, Default, Deserialize, Serialize)]
140 #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
141 #[serde(tag = "type", rename = "m.login.password")]
142 pub struct PasswordLoginType {}
143
144 impl PasswordLoginType {
145 pub fn new() -> Self {
147 Self {}
148 }
149 }
150
151 #[derive(Clone, Debug, Default, Deserialize, Serialize)]
153 #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
154 #[serde(tag = "type", rename = "m.login.token")]
155 pub struct TokenLoginType {
156 #[serde(default, skip_serializing_if = "ruma_common::serde::is_default")]
158 pub get_login_token: bool,
159 }
160
161 impl TokenLoginType {
162 pub fn new() -> Self {
164 Self { get_login_token: false }
165 }
166 }
167
168 #[derive(Clone, Debug, Default, Deserialize, Serialize)]
170 #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
171 #[serde(tag = "type", rename = "m.login.sso")]
172 pub struct SsoLoginType {
173 #[serde(default, skip_serializing_if = "Vec::is_empty")]
175 pub identity_providers: Vec<IdentityProvider>,
176
177 #[serde(default, skip_serializing_if = "ruma_common::serde::is_default")]
183 pub oauth_aware_preferred: bool,
184 }
185
186 impl SsoLoginType {
187 pub fn new() -> Self {
189 Self::default()
190 }
191 }
192
193 #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
195 #[derive(Clone, Debug, Deserialize, Serialize)]
196 pub struct IdentityProvider {
197 pub id: String,
199
200 pub name: String,
202
203 pub icon: Option<OwnedMxcUri>,
205
206 pub brand: Option<IdentityProviderBrand>,
208 }
209
210 impl IdentityProvider {
211 pub fn new(id: String, name: String) -> Self {
213 Self { id, name, icon: None, brand: None }
214 }
215 }
216
217 #[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))]
224 #[derive(Clone, StringEnum)]
225 #[ruma_enum(rename_all = "lowercase")]
226 #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
227 pub enum IdentityProviderBrand {
228 Apple,
232
233 Facebook,
235
236 GitHub,
238
239 GitLab,
241
242 Google,
244
245 Twitter,
249
250 #[doc(hidden)]
252 _Custom(PrivOwnedStr),
253 }
254
255 #[derive(Clone, Debug, Default, Deserialize, Serialize)]
257 #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
258 #[serde(tag = "type", rename = "m.login.application_service")]
259 pub struct ApplicationServiceLoginType {}
260
261 impl ApplicationServiceLoginType {
262 pub fn new() -> Self {
264 Self::default()
265 }
266 }
267
268 #[doc(hidden)]
270 #[derive(Clone, Debug, Serialize)]
271 #[allow(clippy::exhaustive_structs)]
272 pub struct CustomLoginType {
273 #[serde(rename = "type")]
275 login_type: String,
276
277 #[serde(flatten)]
279 data: JsonObject,
280 }
281
282 mod login_type_serde {
283 use ruma_common::serde::{JsonObject, from_raw_json_value};
284 use serde::{Deserialize, de};
285 use serde_json::value::RawValue as RawJsonValue;
286
287 use super::{CustomLoginType, LoginType};
288
289 #[derive(Debug, Deserialize)]
291 struct LoginTypeDeHelper {
292 #[serde(rename = "type")]
294 login_type: String,
295 }
296
297 impl<'de> Deserialize<'de> for LoginType {
298 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
299 where
300 D: de::Deserializer<'de>,
301 {
302 let json = Box::<RawJsonValue>::deserialize(deserializer)?;
303 let LoginTypeDeHelper { login_type } = from_raw_json_value(&json)?;
304
305 Ok(match login_type.as_ref() {
306 "m.login.password" => Self::Password(from_raw_json_value(&json)?),
307 "m.login.token" => Self::Token(from_raw_json_value(&json)?),
308 "m.login.sso" => Self::Sso(from_raw_json_value(&json)?),
309 "m.login.application_service" => {
310 Self::ApplicationService(from_raw_json_value(&json)?)
311 }
312 _ => {
313 let mut data = from_raw_json_value::<JsonObject, _>(&json)?;
314 data.remove("type");
315
316 Self::_Custom(CustomLoginType { login_type, data }.into())
317 }
318 })
319 }
320 }
321 }
322
323 #[cfg(test)]
324 mod tests {
325 use assert_matches::assert_matches;
326 use ruma_common::canonical_json::assert_to_canonical_json_eq;
327 use serde::{Deserialize, Serialize};
328 use serde_json::{Value as JsonValue, from_value as from_json_value, json};
329 use strass::{assert_let, assert_variant_eq};
330
331 use super::{
332 IdentityProvider, IdentityProviderBrand, LoginType, SsoLoginType, TokenLoginType,
333 };
334
335 #[derive(Debug, Deserialize, Serialize)]
336 struct Wrapper {
337 flows: Vec<LoginType>,
338 }
339
340 #[test]
341 fn deserialize_password_login_type() {
342 let wrapper = from_json_value::<Wrapper>(json!({
343 "flows": [
344 { "type": "m.login.password" }
345 ],
346 }))
347 .unwrap();
348 assert_eq!(wrapper.flows.len(), 1);
349 assert_matches!(&wrapper.flows[0], LoginType::Password(_));
350 }
351
352 #[test]
353 fn custom_login_type_serialize_roundtrip() {
354 let json = json!({
355 "flows": [
356 {
357 "type": "io.ruma.custom",
358 "color": "green",
359 }
360 ],
361 });
362
363 let wrapper = from_json_value::<Wrapper>(json.clone()).unwrap();
364 assert_eq!(wrapper.flows.len(), 1);
365 assert_let!(LoginType::_Custom(custom) = &wrapper.flows[0]);
366 assert_eq!(custom.login_type, "io.ruma.custom");
367 assert_eq!(custom.data.len(), 1);
368 assert_eq!(custom.data.get("color"), Some(&JsonValue::from("green")));
369
370 assert_to_canonical_json_eq!(wrapper, json);
371 }
372
373 #[test]
374 fn deserialize_sso_login_type() {
375 let wrapper = from_json_value::<Wrapper>(json!({
376 "flows": [
377 {
378 "type": "m.login.sso",
379 "identity_providers": [
380 {
381 "id": "oidc-gitlab",
382 "name": "GitLab",
383 "icon": "mxc://localhost/gitlab-icon",
384 "brand": "gitlab"
385 },
386 {
387 "id": "custom",
388 "name": "Custom",
389 }
390 ]
391 }
392 ],
393 }))
394 .unwrap();
395 assert_eq!(wrapper.flows.len(), 1);
396 let flow = &wrapper.flows[0];
397
398 assert_let!(
399 LoginType::Sso(SsoLoginType { identity_providers, oauth_aware_preferred: false }) =
400 flow
401 );
402 assert_eq!(identity_providers.len(), 2);
403
404 let provider = &identity_providers[0];
405 assert_eq!(provider.id, "oidc-gitlab");
406 assert_eq!(provider.name, "GitLab");
407 assert_variant_eq!(provider.icon, Some("mxc://localhost/gitlab-icon"));
408 assert_eq!(provider.brand, Some(IdentityProviderBrand::GitLab));
409
410 let provider = &identity_providers[1];
411 assert_eq!(provider.id, "custom");
412 assert_eq!(provider.name, "Custom");
413 assert_eq!(provider.icon, None);
414 assert_eq!(provider.brand, None);
415 }
416
417 #[test]
418 fn serialize_sso_login_type() {
419 let wrapper = Wrapper {
420 flows: vec![
421 LoginType::Token(TokenLoginType::new()),
422 LoginType::Sso(SsoLoginType {
423 identity_providers: vec![IdentityProvider {
424 id: "oidc-github".into(),
425 name: "GitHub".into(),
426 icon: Some("mxc://localhost/github-icon".into()),
427 brand: Some(IdentityProviderBrand::GitHub),
428 }],
429 oauth_aware_preferred: false,
430 }),
431 ],
432 };
433
434 assert_to_canonical_json_eq!(
435 wrapper,
436 json!({
437 "flows": [
438 {
439 "type": "m.login.token"
440 },
441 {
442 "type": "m.login.sso",
443 "identity_providers": [
444 {
445 "id": "oidc-github",
446 "name": "GitHub",
447 "icon": "mxc://localhost/github-icon",
448 "brand": "github"
449 },
450 ]
451 }
452 ],
453 })
454 );
455 }
456 }
457}