tenseleyflow/rcal / 18935ad

Browse files

Prepare official Google OAuth setup

Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
18935adf25ac285d7472deab91667e72536cec81
Parents
97c7de7
Tree
7aa9fee

4 changed files

StatusFile+-
M README.md 19 9
M src/cli.rs 52 6
M src/config.rs 21 13
M src/providers.rs 26 1
README.mdmodified
@@ -34,7 +34,7 @@ rcal providers google auth login --account ID
3434
 rcal providers google auth logout --account ID
3535
 rcal providers google auth inspect --account ID
3636
 rcal providers google calendars list --account ID
37
-rcal providers google setup --account ID --client-id ID [--client-secret SECRET] [--calendar ID]
37
+rcal providers google setup --account ID [--client-id ID] [--client-secret SECRET] [--calendar ID]
3838
 rcal providers google sync [--account ID]
3939
 rcal providers google status
4040
 rcal reminders run [--events-file PATH] [--state-file PATH] [--once]
@@ -204,13 +204,12 @@ daemon does not sync remote calendars itself.
204204
 Google Calendar support is also cache-first. You refresh remote data explicitly:
205205
 
206206
 ```sh
207
-rcal providers google setup --account personal --client-id GOOGLE_CLIENT_ID
207
+rcal providers google setup --account personal
208208
 rcal
209209
 ```
210210
 
211
-Google setup currently needs your own Google OAuth Desktop client ID. If Google
212
-also gives you a client secret, pass `--client-secret GOOGLE_CLIENT_SECRET`.
213
-The setup flow opens browser auth, selects your default editable calendar,
211
+The setup flow uses rcal's official Google OAuth Desktop client when the build
212
+includes one. It opens browser auth, selects your default editable calendar,
214213
 writes `~/.config/rcal/config.toml`, performs the first sync, and sets new
215214
 event creation to Google by default. User tokens are stored in the OS keychain.
216215
 
@@ -242,12 +241,22 @@ sync_future_days = 365
242241
 
243242
 [[providers.google.accounts]]
244243
 id = "personal"
245
-client_id = "GOOGLE_CLIENT_ID"
246
-# client_secret = "GOOGLE_CLIENT_SECRET"
244
+# client_id = "GOOGLE_CLIENT_ID"         # optional custom OAuth client
245
+# client_secret = "GOOGLE_CLIENT_SECRET" # optional custom OAuth client secret
247246
 redirect_port = 8766
248247
 calendars = ["primary"]
249248
 ```
250249
 
250
+Advanced custom-client setup remains available for development builds or for
251
+users who want their own Google OAuth quota/project:
252
+
253
+```sh
254
+rcal providers google setup \
255
+  --account personal \
256
+  --client-id GOOGLE_CLIENT_ID \
257
+  --client-secret GOOGLE_CLIENT_SECRET
258
+```
259
+
251260
 The Google provider syncs configured calendars through the Calendar API,
252261
 caches selected events separately from the local events JSON, and routes
253262
 create/edit/delete/copy operations for Google events back through Google
@@ -259,8 +268,9 @@ Calendar. Provider reminders fire from cached Google events after a sync.
259268
 - CalDAV and other non-Microsoft/non-Google providers are not implemented yet.
260269
 - Provider sync is manual and cache-first; there is no background provider
261270
   sync daemon yet.
262
-- Google currently requires a user-supplied OAuth Desktop client ID; rcal does
263
-  not yet ship an official Google OAuth client.
271
+- Google official OAuth client registration and verification are still in
272
+  progress; until those constants are filled for a release build, use the
273
+  advanced custom-client setup above.
264274
 
265275
 ## Development
266276
 
src/cli.rsmodified
@@ -67,7 +67,7 @@ const HELP: &str = concat!(
6767
     "  rcal providers google auth logout --account ID\n",
6868
     "  rcal providers google auth inspect --account ID\n",
6969
     "  rcal providers google calendars list --account ID\n",
70
-    "  rcal providers google setup --account ID --client-id ID [--client-secret SECRET] [--calendar ID]\n",
70
+    "  rcal providers google setup --account ID [--client-id ID] [--client-secret SECRET] [--calendar ID]\n",
7171
     "  rcal providers google sync [--account ID]\n",
7272
     "  rcal providers google status\n\n",
7373
     "  rcal reminders run [--events-file PATH] [--state-file PATH] [--once]\n",
@@ -195,7 +195,7 @@ pub enum MicrosoftCliAction {
195195
 pub enum GoogleCliAction {
196196
     Setup {
197197
         account: String,
198
-        client_id: String,
198
+        client_id: Option<String>,
199199
         client_secret: Option<String>,
200200
         calendar: Option<String>,
201201
         config_path: PathBuf,
@@ -1210,7 +1210,7 @@ where
12101210
     Ok(CliAction::Providers(ProviderCliAction::Google(
12111211
         GoogleCliAction::Setup {
12121212
             account: account.ok_or(CliError::MissingProviderAccount)?,
1213
-            client_id: client_id.ok_or(CliError::MissingProviderClientId)?,
1213
+            client_id,
12141214
             client_secret,
12151215
             calendar,
12161216
             config_path,
@@ -1816,8 +1816,20 @@ fn run_google_action(
18161816
             config_path,
18171817
             config,
18181818
         } => (|| {
1819
-            let account_config =
1820
-                GoogleAccountConfig::new(account.clone(), client_id.clone(), client_secret.clone());
1819
+            let account_config = match &client_id {
1820
+                Some(client_id) => GoogleAccountConfig::new(
1821
+                    account.clone(),
1822
+                    client_id.clone(),
1823
+                    client_secret.clone(),
1824
+                ),
1825
+                None => {
1826
+                    let mut account_config = GoogleAccountConfig::new_official(account.clone())?;
1827
+                    if client_secret.is_some() {
1828
+                        account_config.client_secret = client_secret.clone();
1829
+                    }
1830
+                    account_config
1831
+                }
1832
+            };
18211833
             login_google_browser(&account_config, &http, &token_store, stdout)?;
18221834
             let calendars = list_google_calendars(&account_config, &http, &token_store)?;
18231835
             let calendar = choose_google_setup_calendar(&calendars, calendar.as_deref())?;
@@ -3165,7 +3177,7 @@ create_event = ["n"]
31653177
             action,
31663178
             CliAction::Providers(ProviderCliAction::Google(GoogleCliAction::Setup {
31673179
                 account: "personal".to_string(),
3168
-                client_id: "google-client".to_string(),
3180
+                client_id: Some("google-client".to_string()),
31693181
                 client_secret: Some("google-secret".to_string()),
31703182
                 calendar: Some("primary".to_string()),
31713183
                 config_path,
@@ -3174,6 +3186,40 @@ create_event = ["n"]
31743186
         );
31753187
     }
31763188
 
3189
+    #[test]
3190
+    fn google_setup_command_allows_official_client_defaults() {
3191
+        let today = date(2026, Month::April, 23);
3192
+        let user_config = UserConfig::empty();
3193
+        let google = user_config.providers.google.clone();
3194
+        let config_path = PathBuf::from("/tmp/rcal/google-official-setup-config.toml");
3195
+
3196
+        let action = parse_args_with_config(
3197
+            [
3198
+                arg("providers"),
3199
+                arg("google"),
3200
+                arg("setup"),
3201
+                arg("--account"),
3202
+                arg("personal"),
3203
+            ],
3204
+            today.into(),
3205
+            user_config,
3206
+            Some(config_path.clone()),
3207
+        )
3208
+        .expect("setup parses");
3209
+
3210
+        assert_eq!(
3211
+            action,
3212
+            CliAction::Providers(ProviderCliAction::Google(GoogleCliAction::Setup {
3213
+                account: "personal".to_string(),
3214
+                client_id: None,
3215
+                client_secret: None,
3216
+                calendar: None,
3217
+                config_path,
3218
+                config: google,
3219
+            }))
3220
+        );
3221
+    }
3222
+
31773223
     #[test]
31783224
     fn microsoft_setup_command_parses_with_calendar_and_config_path() {
31793225
         let today = date(2026, Month::April, 23);
src/config.rsmodified
@@ -12,7 +12,7 @@ use crate::{
1212
     providers::{
1313
         GoogleAccountConfig, GoogleProviderConfig, MICROSOFT_DEFAULT_TENANT,
1414
         MICROSOFT_OFFICIAL_CLIENT_ID, MicrosoftAccountConfig, MicrosoftProviderConfig,
15
-        ProviderConfig, ProviderCreateTarget,
15
+        ProviderConfig, ProviderCreateTarget, google_official_client_config,
1616
     },
1717
 };
1818
 
@@ -56,9 +56,8 @@ calendars = []
5656
 
5757
 [providers.google]
5858
 # Google Calendar provider.
59
-# For now, create a Google OAuth Desktop client and paste its client_id here.
60
-# Some Google clients also provide a client_secret; include it in the account
61
-# block if token exchange requires it.
59
+# rcal release builds can ship an official Google OAuth Desktop client.
60
+# Advanced users may override client_id/client_secret inside an account block.
6261
 enabled = false
6362
 default_account = "personal"
6463
 sync_past_days = 30
@@ -68,8 +67,8 @@ sync_future_days = 365
6867
 
6968
 [[providers.google.accounts]]
7069
 id = "personal"
71
-client_id = ""
72
-# client_secret = ""
70
+# client_id = "GOOGLE_CLIENT_ID"
71
+# client_secret = "GOOGLE_CLIENT_SECRET"
7372
 redirect_port = 8766
7473
 calendars = []
7574
 
@@ -215,7 +214,7 @@ pub struct MicrosoftSetupConfig {
215214
 #[derive(Debug, Clone, PartialEq, Eq)]
216215
 pub struct GoogleSetupConfig {
217216
     pub account_id: String,
218
-    pub client_id: String,
217
+    pub client_id: Option<String>,
219218
     pub client_secret: Option<String>,
220219
     pub calendar_id: String,
221220
     pub calendar_name: String,
@@ -360,14 +359,15 @@ fn google_setup_config_body(existing: &str, setup: &GoogleSetupConfig) -> String
360359
             "sync_future_days = {sync_future_days}\n\n",
361360
             "[[providers.google.accounts]]\n",
362361
             "id = {account}\n",
363
-            "client_id = {client_id}\n",
364362
         ),
365363
         account = toml_string(&setup.account_id),
366364
         calendar = toml_string(&setup.calendar_id),
367
-        client_id = toml_string(&setup.client_id),
368365
         sync_past_days = setup.sync_past_days.max(0),
369366
         sync_future_days = setup.sync_future_days.max(1),
370367
     ));
368
+    if let Some(client_id) = &setup.client_id {
369
+        body.push_str(&format!("client_id = {}\n", toml_string(client_id)));
370
+    }
371371
     if let Some(client_secret) = &setup.client_secret {
372372
         body.push_str(&format!("client_secret = {}\n", toml_string(client_secret)));
373373
     }
@@ -620,7 +620,7 @@ struct RawGoogleProviderConfig {
620620
 #[serde(deny_unknown_fields)]
621621
 struct RawGoogleAccountConfig {
622622
     id: String,
623
-    client_id: String,
623
+    client_id: Option<String>,
624624
     client_secret: Option<String>,
625625
     redirect_port: Option<u16>,
626626
     calendars: Option<Vec<String>>,
@@ -799,10 +799,18 @@ impl RawGoogleProviderConfig {
799799
 
800800
 impl RawGoogleAccountConfig {
801801
     fn into_config(self) -> GoogleAccountConfig {
802
+        let uses_official_client = self.client_id.is_none();
803
+        let official_client = uses_official_client
804
+            .then(google_official_client_config)
805
+            .flatten();
806
+        let official_client_id = official_client
807
+            .as_ref()
808
+            .map(|(client_id, _)| client_id.clone());
809
+        let official_client_secret = official_client.and_then(|(_, client_secret)| client_secret);
802810
         GoogleAccountConfig {
803811
             id: self.id,
804
-            client_id: self.client_id,
805
-            client_secret: self.client_secret,
812
+            client_id: self.client_id.or(official_client_id).unwrap_or_default(),
813
+            client_secret: self.client_secret.or(official_client_secret),
806814
             redirect_port: self.redirect_port.unwrap_or(8766),
807815
             calendars: self.calendars.unwrap_or_default(),
808816
         }
@@ -1176,7 +1184,7 @@ enabled = false
11761184
             Some(path.clone()),
11771185
             &GoogleSetupConfig {
11781186
                 account_id: "personal".to_string(),
1179
-                client_id: "google-client".to_string(),
1187
+                client_id: Some("google-client".to_string()),
11801188
                 client_secret: Some("google-secret".to_string()),
11811189
                 calendar_id: "primary".to_string(),
11821190
                 calendar_name: "Calendar".to_string(),
src/providers.rsmodified
@@ -36,10 +36,16 @@ const KEYRING_SERVICE: &str = "rcal.microsoft";
3636
 const GOOGLE_CALENDAR_BASE_URL: &str = "https://www.googleapis.com/calendar/v3";
3737
 const GOOGLE_AUTH_URL: &str = "https://accounts.google.com/o/oauth2/v2/auth";
3838
 const GOOGLE_TOKEN_URL: &str = "https://oauth2.googleapis.com/token";
39
-const GOOGLE_SCOPES: &str = "https://www.googleapis.com/auth/calendar";
39
+const GOOGLE_SCOPES: &str = concat!(
40
+    "https://www.googleapis.com/auth/calendar.calendarlist.readonly",
41
+    " ",
42
+    "https://www.googleapis.com/auth/calendar.events"
43
+);
4044
 const GOOGLE_KEYRING_SERVICE: &str = "rcal.google";
4145
 pub const MICROSOFT_OFFICIAL_CLIENT_ID: &str = "9a49eaac-422b-4192-a65d-82dc8f43c11d";
4246
 pub const MICROSOFT_DEFAULT_TENANT: &str = "common";
47
+pub const GOOGLE_OFFICIAL_CLIENT_ID: &str = "";
48
+pub const GOOGLE_OFFICIAL_CLIENT_SECRET: &str = "";
4349
 
4450
 #[derive(Debug, Clone, PartialEq, Eq)]
4551
 pub struct ProviderConfig {
@@ -360,11 +366,30 @@ impl GoogleAccountConfig {
360366
         }
361367
     }
362368
 
369
+    pub fn new_official(id: impl Into<String>) -> Result<Self, ProviderError> {
370
+        let Some((client_id, client_secret)) = google_official_client_config() else {
371
+            return Err(ProviderError::Config(
372
+                "this rcal build does not include an official Google OAuth client yet; pass --client-id and --client-secret or finish the official Google client registration".to_string(),
373
+            ));
374
+        };
375
+        Ok(Self::new(id, client_id, client_secret))
376
+    }
377
+
363378
     fn redirect_uri(&self) -> String {
364379
         format!("http://127.0.0.1:{}/callback", self.redirect_port)
365380
     }
366381
 }
367382
 
383
+pub fn google_official_client_config() -> Option<(String, Option<String>)> {
384
+    let client_id = GOOGLE_OFFICIAL_CLIENT_ID.trim();
385
+    if client_id.is_empty() {
386
+        return None;
387
+    }
388
+    let client_secret = (!GOOGLE_OFFICIAL_CLIENT_SECRET.trim().is_empty())
389
+        .then(|| GOOGLE_OFFICIAL_CLIENT_SECRET.to_string());
390
+    Some((GOOGLE_OFFICIAL_CLIENT_ID.to_string(), client_secret))
391
+}
392
+
368393
 pub fn default_microsoft_cache_file() -> PathBuf {
369394
     if let Some(cache_home) = env::var_os("XDG_CACHE_HOME") {
370395
         return PathBuf::from(cache_home)