1use std::collections::BTreeMap;
4
5use rustauth_core::db::{Account, User};
6use serde::{Deserialize, Serialize};
7use time::OffsetDateTime;
8
9use crate::mappings::resource_url;
10use crate::metadata::{SCIM_GROUP_SCHEMA_ID, SCIM_USER_SCHEMA_ID};
11
12#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
13#[serde(rename_all = "camelCase")]
14pub struct ScimUserResource {
15 pub id: String,
16 #[serde(skip_serializing_if = "Option::is_none")]
17 pub external_id: Option<String>,
18 pub meta: ScimResourceMeta,
19 #[serde(rename = "userName")]
20 pub user_name: String,
21 pub name: ScimUserResourceName,
22 #[serde(rename = "displayName")]
23 pub display_name: String,
24 pub active: bool,
25 pub emails: Vec<ScimUserResourceEmail>,
26 #[serde(skip_serializing_if = "Vec::is_empty", default)]
27 pub groups: Vec<ScimUserResourceGroup>,
28 pub schemas: Vec<String>,
29 #[serde(flatten, skip_serializing_if = "BTreeMap::is_empty", default)]
30 pub additional_fields: BTreeMap<String, serde_json::Value>,
31}
32
33#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
34pub struct ScimUserResourceName {
35 pub formatted: String,
36}
37
38#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
39pub struct ScimUserResourceEmail {
40 pub primary: bool,
41 pub value: String,
42}
43
44#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
45pub struct ScimUserResourceGroup {
46 pub value: String,
47 #[serde(rename = "$ref")]
48 pub ref_: String,
49 #[serde(skip_serializing_if = "Option::is_none")]
50 pub display: Option<String>,
51}
52
53#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
54#[serde(rename_all = "camelCase")]
55pub struct ScimResourceMeta {
56 pub resource_type: String,
57 #[serde(with = "time::serde::rfc3339")]
58 pub created: OffsetDateTime,
59 #[serde(with = "time::serde::rfc3339")]
60 pub last_modified: OffsetDateTime,
61 pub location: String,
62 #[serde(skip_serializing_if = "Option::is_none")]
63 pub version: Option<String>,
64}
65
66#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
67#[serde(rename_all = "camelCase")]
68pub struct ScimGroupResource {
69 pub id: String,
70 #[serde(skip_serializing_if = "Option::is_none")]
71 pub external_id: Option<String>,
72 pub meta: ScimResourceMeta,
73 #[serde(rename = "displayName")]
74 pub display_name: String,
75 #[serde(skip_serializing_if = "Vec::is_empty", default)]
76 pub members: Vec<ScimGroupResourceMember>,
77 pub schemas: Vec<String>,
78}
79
80#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
81pub struct ScimGroupResourceMember {
82 pub value: String,
83 #[serde(rename = "$ref")]
84 pub ref_: String,
85 #[serde(skip_serializing_if = "Option::is_none")]
86 pub display: Option<String>,
87}
88
89pub fn user_resource(base_url: &str, user: &User, account: Option<&Account>) -> ScimUserResource {
90 ScimUserResource {
91 id: user.id.clone(),
92 external_id: account.map(|account| account.account_id.clone()),
93 meta: ScimResourceMeta {
94 resource_type: "User".to_owned(),
95 created: user.created_at,
96 last_modified: user.updated_at,
97 location: resource_url(base_url, &format!("/scim/v2/Users/{}", user.id)),
98 version: Some(resource_version(user.updated_at)),
99 },
100 user_name: user.email.clone(),
101 name: ScimUserResourceName {
102 formatted: user.name.clone(),
103 },
104 display_name: user.name.clone(),
105 active: true,
106 emails: vec![ScimUserResourceEmail {
107 primary: true,
108 value: user.email.clone(),
109 }],
110 groups: Vec::new(),
111 schemas: vec![SCIM_USER_SCHEMA_ID.to_owned()],
112 additional_fields: BTreeMap::new(),
113 }
114}
115
116pub fn group_resource(
117 base_url: &str,
118 group_id: &str,
119 external_id: Option<String>,
120 display_name: String,
121 created_at: OffsetDateTime,
122 updated_at: OffsetDateTime,
123 members: Vec<ScimGroupResourceMember>,
124) -> ScimGroupResource {
125 ScimGroupResource {
126 id: group_id.to_owned(),
127 external_id,
128 meta: ScimResourceMeta {
129 resource_type: "Group".to_owned(),
130 created: created_at,
131 last_modified: updated_at,
132 location: resource_url(base_url, &format!("/scim/v2/Groups/{group_id}")),
133 version: Some(resource_version(updated_at)),
134 },
135 display_name,
136 members,
137 schemas: vec![SCIM_GROUP_SCHEMA_ID.to_owned()],
138 }
139}
140
141pub fn group_member_resource(
142 base_url: &str,
143 user_id: &str,
144 display: Option<String>,
145) -> ScimGroupResourceMember {
146 ScimGroupResourceMember {
147 value: user_id.to_owned(),
148 ref_: resource_url(base_url, &format!("/scim/v2/Users/{user_id}")),
149 display,
150 }
151}
152
153pub fn resource_version(updated_at: OffsetDateTime) -> String {
154 format!(r#"W/"{}""#, updated_at.unix_timestamp_nanos())
155}