1use crate::{AuthApiKey, AuthInvitation, AuthOrganization, AuthPasskey, InvitationStatus};
2use chrono::{DateTime, Utc};
3use serde::{Deserialize, Serialize, Serializer};
4#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
6pub struct OrganizationView {
7 #[serde(default, flatten)]
8 pub additional_fields: std::collections::BTreeMap<String, serde_json::Value>,
9 pub id: String,
10 pub name: String,
11 pub slug: String,
12 pub logo: Option<String>,
13 #[serde(
14 serialize_with = "serialize_json_option_as_string",
15 skip_serializing_if = "Option::is_none"
16 )]
17 #[serde(
18 default,
19 deserialize_with = "crate::utils::json::deserialize_optional_value"
20 )]
21 pub metadata: Option<serde_json::Value>,
22 #[serde(rename = "createdAt")]
23 #[serde(serialize_with = "crate::utils::datetime::serialize")]
24 pub created_at: DateTime<Utc>,
25}
26
27impl<T: AuthOrganization> From<&T> for OrganizationView {
28 fn from(org: &T) -> Self {
29 Self {
30 additional_fields: org.additional_fields(),
31 id: org.id().into_owned(),
32 name: org.name().to_owned(),
33 slug: org.slug().to_owned(),
34 logo: org.logo().map(str::to_owned),
35 metadata: org.metadata().cloned(),
36 created_at: org.created_at(),
37 }
38 }
39}
40
41#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
43pub struct InvitationView {
44 pub id: String,
45 #[serde(rename = "organizationId")]
46 pub organization_id: String,
47 pub email: String,
48 pub role: Option<String>,
49 pub status: InvitationStatus,
50 #[serde(rename = "inviterId")]
51 pub inviter_id: String,
52 #[serde(rename = "expiresAt")]
53 #[serde(serialize_with = "crate::utils::datetime::serialize")]
54 pub expires_at: DateTime<Utc>,
55 #[serde(rename = "createdAt")]
56 #[serde(serialize_with = "crate::utils::datetime::serialize")]
57 pub created_at: DateTime<Utc>,
58 #[serde(rename = "teamId", default, skip_serializing_if = "Option::is_none")]
59 pub team_id: Option<String>,
60 #[serde(
61 flatten,
62 default,
63 deserialize_with = "crate::utils::json::deserialize_btree_map"
64 )]
65 pub extension_fields: std::collections::BTreeMap<String, serde_json::Value>,
66}
67
68impl<T: AuthInvitation> From<&T> for InvitationView {
69 fn from(inv: &T) -> Self {
70 Self {
71 id: inv.id().into_owned(),
72 organization_id: inv.organization_id().into_owned(),
73 email: inv.email().to_owned(),
74 role: inv.optional_role().map(str::to_owned),
75 status: inv.status().clone(),
76 inviter_id: inv.inviter_id().into_owned(),
77 expires_at: inv.expires_at(),
78 created_at: inv.created_at(),
79 team_id: inv.team_id().map(str::to_owned),
80 extension_fields: std::collections::BTreeMap::default(),
81 }
82 }
83}
84
85#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
87pub struct PasskeyView {
88 pub id: String,
89 #[serde(skip_serializing_if = "Option::is_none")]
90 pub name: Option<String>,
91 #[serde(rename = "credentialID")]
92 pub credential_id: String,
93 #[serde(rename = "userId")]
94 pub user_id: String,
95 #[serde(rename = "publicKey")]
96 pub public_key: String,
97 pub counter: u64,
98 #[serde(rename = "deviceType")]
99 pub device_type: String,
100 #[serde(rename = "backedUp")]
101 pub backed_up: bool,
102 #[serde(skip_serializing_if = "Option::is_none")]
103 pub transports: Option<String>,
104 #[serde(rename = "createdAt")]
105 pub created_at: Option<String>,
106 #[serde(skip_serializing_if = "Option::is_none")]
107 pub aaguid: Option<String>,
108}
109
110impl<T: AuthPasskey> From<&T> for PasskeyView {
111 fn from(pk: &T) -> Self {
112 Self {
113 id: pk.id().into_owned(),
114 name: pk.name().map(str::to_owned),
115 credential_id: pk.credential_id().to_owned(),
116 user_id: pk.user_id().into_owned(),
117 public_key: pk.public_key().to_owned(),
118 counter: pk.counter(),
119 device_type: pk.device_type().to_owned(),
120 backed_up: pk.backed_up(),
121 transports: pk.transports().map(str::to_owned),
122 created_at: pk
123 .created_at()
124 .map(|time| time.to_rfc3339_opts(chrono::SecondsFormat::Millis, true)),
125 aaguid: pk.aaguid().map(str::to_owned),
126 }
127 }
128}
129
130#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
135pub struct ApiKeyView {
136 pub id: String,
137 pub name: Option<String>,
138 pub start: Option<String>,
139 pub prefix: Option<String>,
140 #[serde(rename = "referenceId")]
141 pub reference_id: String,
142 #[serde(rename = "configId")]
143 pub config_id: String,
144 #[serde(rename = "refillInterval")]
145 #[serde(serialize_with = "serialize_api_key_number")]
146 pub refill_interval: Option<f64>,
147 #[serde(rename = "refillAmount")]
148 #[serde(serialize_with = "serialize_api_key_number")]
149 pub refill_amount: Option<f64>,
150 #[serde(rename = "lastRefillAt")]
151 pub last_refill_at: Option<String>,
152 pub enabled: bool,
153 #[serde(rename = "rateLimitEnabled")]
154 pub rate_limit_enabled: bool,
155 #[serde(rename = "rateLimitTimeWindow")]
156 #[serde(serialize_with = "serialize_api_key_number")]
157 pub rate_limit_time_window: Option<f64>,
158 #[serde(rename = "rateLimitMax")]
159 #[serde(serialize_with = "serialize_api_key_number")]
160 pub rate_limit_max: Option<f64>,
161 #[serde(rename = "requestCount")]
162 #[serde(serialize_with = "serialize_api_key_number")]
163 pub request_count: Option<f64>,
164 #[serde(serialize_with = "serialize_api_key_number")]
165 pub remaining: Option<f64>,
166 #[serde(rename = "lastRequest")]
167 pub last_request: Option<String>,
168 #[serde(rename = "expiresAt")]
169 pub expires_at: Option<String>,
170 #[serde(rename = "createdAt")]
171 pub created_at: String,
172 #[serde(rename = "updatedAt")]
173 pub updated_at: String,
174 #[serde(
175 default,
176 deserialize_with = "crate::utils::json::deserialize_optional_value"
177 )]
178 pub permissions: Option<serde_json::Value>,
179 #[serde(
180 default,
181 deserialize_with = "crate::utils::json::deserialize_optional_value"
182 )]
183 pub metadata: Option<serde_json::Value>,
184}
185
186impl<T: AuthApiKey> From<&T> for ApiKeyView {
187 fn from(ak: &T) -> Self {
188 Self {
189 id: ak.id().into_owned(),
190 name: ak.name().map(str::to_owned),
191 start: ak.start().map(str::to_owned),
192 prefix: ak.prefix().map(str::to_owned),
193 reference_id: ak.reference_id().into_owned(),
194 config_id: ak.config_id().into_owned(),
195 refill_interval: ak.refill_interval(),
196 refill_amount: ak.refill_amount(),
197 last_refill_at: ak.last_refill_at().map(api_key_wire_date),
198 enabled: ak.enabled(),
199 rate_limit_enabled: ak.rate_limit_enabled(),
200 rate_limit_time_window: ak.rate_limit_time_window(),
201 rate_limit_max: ak.rate_limit_max(),
202 request_count: ak.request_count(),
203 remaining: ak.remaining(),
204 last_request: ak.last_request().map(api_key_wire_date),
205 expires_at: ak.expires_at().map(api_key_wire_date),
206 created_at: api_key_wire_date(ak.created_at()),
207 updated_at: api_key_wire_date(ak.updated_at()),
208 permissions: ak
209 .permissions()
210 .and_then(|s| crate::utils::json::from_slice(s.as_bytes()).ok())
211 .map(|mut value| {
212 normalize_api_key_permission_dates(&mut value);
213 value
214 }),
215 metadata: ak.metadata().and_then(|s| {
216 let value = crate::utils::json::parse_value(s).ok()?;
217 if let crate::utils::json::JsValue::String(encoded) = value {
218 let mut value = crate::utils::json::parse_value(&encoded)
219 .ok()?
220 .to_json_value()
221 .ok()?;
222 normalize_api_key_permission_dates(&mut value);
223 Some(value)
224 } else {
225 value.to_json_value().ok()
226 }
227 }),
228 }
229 }
230}
231
232pub(in crate::wire) fn normalize_api_key_permission_dates(value: &mut serde_json::Value) {
233 match value {
234 serde_json::Value::String(text) => {
235 if let Some(date) = crate::utils::datetime::normalize_json_date(text) {
236 *text = date;
237 }
238 }
239 serde_json::Value::Array(values) => {
240 for value in values {
241 normalize_api_key_permission_dates(value);
242 }
243 }
244 serde_json::Value::Object(values) => {
245 for value in values.values_mut() {
246 normalize_api_key_permission_dates(value);
247 }
248 }
249 _ => {}
250 }
251}
252
253#[expect(
256 clippy::ref_option,
257 reason = "Serde field serializers receive a reference to the declared Option field type"
258)]
259pub(in crate::wire) fn serialize_json_option_as_string<S>(
260 value: &Option<serde_json::Value>,
261 serializer: S,
262) -> Result<S::Ok, S::Error>
263where
264 S: Serializer,
265{
266 match value {
267 Some(inner) => serializer
268 .serialize_some(&serde_json::to_string(inner).map_err(serde::ser::Error::custom)?),
269 None => serializer.serialize_none(),
270 }
271}
272
273#[expect(
275 clippy::as_conversions,
276 clippy::cast_possible_truncation,
277 reason = "JavaScript-compatible numbers deliberately retain IEEE754 rounding and guarded integer coercion at the wire or adapter boundary"
278)]
279#[expect(
280 clippy::ref_option,
281 reason = "Serde field serializers receive a reference to the declared Option field type"
282)]
283pub(in crate::wire) fn serialize_api_key_number<S: Serializer>(
284 value: &Option<f64>,
285 serializer: S,
286) -> Result<S::Ok, S::Error> {
287 match value {
288 Some(value) if value.fract() == 0.0 && value.abs() <= 9_007_199_254_740_991.0 => {
289 serializer.serialize_i64(*value as i64)
290 }
291 value => value.serialize(serializer),
292 }
293}
294
295pub(in crate::wire) fn api_key_wire_date(value: &str) -> String {
298 DateTime::parse_from_rfc3339(value).map_or_else(
299 |_| value.to_owned(),
300 |date| {
301 date.with_timezone(&Utc)
302 .to_rfc3339_opts(chrono::SecondsFormat::Millis, true)
303 },
304 )
305}