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_matches2::{assert_let, assert_matches};
326 use ruma_common::{canonical_json::assert_to_canonical_json_eq, mxc_uri};
327 use serde::{Deserialize, Serialize};
328 use serde_json::{Value as JsonValue, from_value as from_json_value, json};
329
330 use super::{
331 IdentityProvider, IdentityProviderBrand, LoginType, SsoLoginType, TokenLoginType,
332 };
333
334 #[derive(Debug, Deserialize, Serialize)]
335 struct Wrapper {
336 flows: Vec<LoginType>,
337 }
338
339 #[test]
340 fn deserialize_password_login_type() {
341 let wrapper = from_json_value::<Wrapper>(json!({
342 "flows": [
343 { "type": "m.login.password" }
344 ],
345 }))
346 .unwrap();
347 assert_eq!(wrapper.flows.len(), 1);
348 assert_matches!(&wrapper.flows[0], LoginType::Password(_));
349 }
350
351 #[test]
352 fn custom_login_type_serialize_roundtrip() {
353 let json = json!({
354 "flows": [
355 {
356 "type": "io.ruma.custom",
357 "color": "green",
358 }
359 ],
360 });
361
362 let wrapper = from_json_value::<Wrapper>(json.clone()).unwrap();
363 assert_eq!(wrapper.flows.len(), 1);
364 assert_let!(LoginType::_Custom(custom) = &wrapper.flows[0]);
365 assert_eq!(custom.login_type, "io.ruma.custom");
366 assert_eq!(custom.data.len(), 1);
367 assert_eq!(custom.data.get("color"), Some(&JsonValue::from("green")));
368
369 assert_to_canonical_json_eq!(wrapper, json);
370 }
371
372 #[test]
373 fn deserialize_sso_login_type() {
374 let wrapper = from_json_value::<Wrapper>(json!({
375 "flows": [
376 {
377 "type": "m.login.sso",
378 "identity_providers": [
379 {
380 "id": "oidc-gitlab",
381 "name": "GitLab",
382 "icon": "mxc://localhost/gitlab-icon",
383 "brand": "gitlab"
384 },
385 {
386 "id": "custom",
387 "name": "Custom",
388 }
389 ]
390 }
391 ],
392 }))
393 .unwrap();
394 assert_eq!(wrapper.flows.len(), 1);
395 let flow = &wrapper.flows[0];
396
397 assert_let!(
398 LoginType::Sso(SsoLoginType { identity_providers, oauth_aware_preferred: false }) =
399 flow
400 );
401 assert_eq!(identity_providers.len(), 2);
402
403 let provider = &identity_providers[0];
404 assert_eq!(provider.id, "oidc-gitlab");
405 assert_eq!(provider.name, "GitLab");
406 assert_eq!(provider.icon.as_deref(), Some(mxc_uri!("mxc://localhost/gitlab-icon")));
407 assert_eq!(provider.brand, Some(IdentityProviderBrand::GitLab));
408
409 let provider = &identity_providers[1];
410 assert_eq!(provider.id, "custom");
411 assert_eq!(provider.name, "Custom");
412 assert_eq!(provider.icon, None);
413 assert_eq!(provider.brand, None);
414 }
415
416 #[test]
417 fn serialize_sso_login_type() {
418 let wrapper = Wrapper {
419 flows: vec![
420 LoginType::Token(TokenLoginType::new()),
421 LoginType::Sso(SsoLoginType {
422 identity_providers: vec![IdentityProvider {
423 id: "oidc-github".into(),
424 name: "GitHub".into(),
425 icon: Some("mxc://localhost/github-icon".into()),
426 brand: Some(IdentityProviderBrand::GitHub),
427 }],
428 oauth_aware_preferred: false,
429 }),
430 ],
431 };
432
433 assert_to_canonical_json_eq!(
434 wrapper,
435 json!({
436 "flows": [
437 {
438 "type": "m.login.token"
439 },
440 {
441 "type": "m.login.sso",
442 "identity_providers": [
443 {
444 "id": "oidc-github",
445 "name": "GitHub",
446 "icon": "mxc://localhost/github-icon",
447 "brand": "github"
448 },
449 ]
450 }
451 ],
452 })
453 );
454 }
455 }
456}