1 use crate::ar_date_format;
2 use chrono::{DateTime, Utc};
3 use serde::{Deserialize, Serialize};
4 use tsync::tsync;
5
6 #[derive(Serialize, Deserialize, Debug, Clone)]
7 pub struct AuthApp {
8 pub id: String,
9 pub secret: Option<String>,
10 pub name: String,
11 // Serialized as a number by the API, like `Deployment`/`MailApp`. The
12 // create/update payloads take it as a String because it comes from a CLI
13 // arg — the two are intentionally different types.
14 pub project_id: Option<i32>,
15 pub support_email: Option<String>,
16 /// Whether Sign in with Apple is enabled for this auth app.
17 pub apple_oauth_enabled: Option<bool>,
18 /// The Services ID used as the Apple OAuth client identifier.
19 pub apple_oauth_client_id: Option<String>,
20 /// Whether the required Apple OAuth credentials are configured.
21 pub apple_oauth_configured: Option<bool>,
22 /// Whether an Apple OAuth client secret has been stored.
23 pub apple_oauth_secret_present: Option<bool>,
24 #[serde(with = "ar_date_format")]
25 pub created_at: DateTime<Utc>,
26 #[serde(with = "ar_date_format")]
27 pub updated_at: DateTime<Utc>,
28 }
29
30 #[derive(Serialize, Deserialize, Debug, Clone)]
31 pub struct AuthAppCreate {
32 pub name: String,
33 pub project_id: String,
34 #[serde(skip_serializing_if = "Option::is_none")]
35 pub support_email: Option<String>,
36 }
37
38 #[derive(Serialize, Deserialize, Debug, Clone, Default)]
39 pub struct AuthAppUpdate {
40 #[serde(skip_serializing_if = "Option::is_none")]
41 pub name: Option<String>,
42 #[serde(skip_serializing_if = "Option::is_none")]
43 pub support_email: Option<String>,
44 }
45
46 impl AuthAppUpdate {
47 pub fn is_empty(&self) -> bool {
48 self.name.is_none() && self.support_email.is_none()
49 }
50 }
51
52 /// A public OAuth client registered against an AuthApp for the hosted
53 /// Authorization Code + PKCE flow.
54 ///
55 /// These are public clients: no client secret is issued, so security rests on
56 /// PKCE plus the `redirect_uris` allowlist. `redirect_uris` is a newline-
57 /// separated list. `client_id` is the public identifier (prefixed `auc_`) sent
58 /// in the `/authorize` request.
59 #[derive(Serialize, Deserialize, Debug, Clone)]
60 #[tsync]
61 pub struct AuthAppClient {
62 pub id: i64,
63 pub client_id: String,
64 pub name: String,
65 pub redirect_uris: String,
66 pub confidential: bool,
67 pub auth_app_id: String,
68 #[serde(with = "ar_date_format")]
69 pub created_at: DateTime<Utc>,
70 #[serde(with = "ar_date_format")]
71 pub updated_at: DateTime<Utc>,
72 }
73
74 /// Request body for registering a new public OAuth client on an AuthApp.
75 /// `redirect_uris` is a newline-separated allowlist; each entry must be https,
76 /// a loopback http URL, or a custom scheme (native apps).
77 #[derive(Serialize, Deserialize, Debug, Clone)]
78 #[tsync]
79 pub struct AuthAppClientCreate {
80 pub name: String,
81 pub redirect_uris: String,
82 }
83
84 #[cfg(test)]
85 mod tests {
86 use super::*;
87 use serde_json::json;
88
89 #[test]
90 fn auth_app_apple_settings_are_optional_and_typed() -> Result<(), serde_json::Error> {
91 let mut payload = json!({
92 "id": "app-id",
93 "name": "Example",
94 "created_at": "2026-09-30T08:00:00.000+00:00",
95 "updated_at": "2026-09-30T08:00:00.000+00:00"
96 });
97 let legacy: AuthApp = serde_json::from_value(payload.clone())?;
98 assert_eq!(legacy.apple_oauth_enabled, None);
99 assert_eq!(legacy.apple_oauth_client_id, None);
100 assert_eq!(legacy.apple_oauth_configured, None);
101 assert_eq!(legacy.apple_oauth_secret_present, None);
102
103 payload["apple_oauth_enabled"] = json!(true);
104 payload["apple_oauth_client_id"] = json!("com.example.web");
105 payload["apple_oauth_configured"] = json!(true);
106 payload["apple_oauth_secret_present"] = json!(true);
107 let configured: AuthApp = serde_json::from_value(payload)?;
108 assert_eq!(configured.apple_oauth_enabled, Some(true));
109 assert_eq!(
110 configured.apple_oauth_client_id.as_deref(),
111 Some("com.example.web")
112 );
113 assert_eq!(configured.apple_oauth_configured, Some(true));
114 assert_eq!(configured.apple_oauth_secret_present, Some(true));
115
116 let serialized = serde_json::to_value(&configured)?;
117 assert_eq!(serialized["apple_oauth_enabled"], json!(true));
118 assert_eq!(
119 serialized["apple_oauth_client_id"],
120 json!("com.example.web")
121 );
122 assert_eq!(serialized["apple_oauth_configured"], json!(true));
123 assert_eq!(serialized["apple_oauth_secret_present"], json!(true));
124 Ok(())
125 }
126 #[test]
127 fn test_auth_app_create() {
128 let auth_app_create = AuthAppCreate {
129 name: "test".to_owned(),
130 project_id: "1".to_owned(),
131 support_email: None,
132 };
133 let json = json!({
134 "name": "test",
135 "project_id": "1",
136 });
137 assert_eq!(serde_json::to_value(auth_app_create).unwrap(), json);
138 }
139 }