Search code examples
javascriptnode.jsgoogle-calendar-api

Google Calendar API client-side authorization using client-id, event creation


I am using node.js and google calendar API. I am trying to create an event on my calendar using this API. Here is my code, with the API key and CLIENT ID censored:

<!DOCTYPE html>
<html>

<head>
  <title>Google Calendar API Quickstart</title>
  <meta charset="utf-8" />
</head>

<body>
  <p>Google Calendar API Quickstart</p>

  <!--Add buttons to initiate auth sequence and sign out-->
  <button id="authorize_button" onclick="handleAuthClick()">Authorize</button>
  <button id="signout_button" onclick="handleSignoutClick()">Sign Out</button>

  <pre id="content" style="white-space: pre-wrap;"></pre>

  <script type="text/javascript">
    /* exported gapiLoaded */
    /* exported gisLoaded */
    /* exported handleAuthClick */
    /* exported handleSignoutClick */

    // TODO(developer): Set to client ID and API key from the Developer Console
    const CLIENT_ID = (CLIENT_ID);
    const API_KEY = null;

    // Discovery doc URL for APIs used by the quickstart
    const DISCOVERY_DOC = 'https://www.googleapis.com/discovery/v1/apis/calendar/v3/rest';

    
    // Authorization scopes required by the API; multiple scopes can be
    // included, separated by spaces.
    const SCOPES = 'https://www.googleapis.com/auth/calendar';

    let tokenClient;
    let gapiInited = false;
    let gisInited = false;

    document.getElementById('authorize_button').style.visibility = 'hidden';
    document.getElementById('signout_button').style.visibility = 'hidden';

    /**
     * Callback after api.js is loaded.
     */
    function gapiLoaded() {
      gapi.load('client', initializeGapiClient);
    }

    /**
     * Callback after the API client is loaded. Loads the
     * discovery doc to initialize the API.
     */
    async function initializeGapiClient() {
      await gapi.client.init({
        apiKey: API_KEY,
        discoveryDocs: [DISCOVERY_DOC],
      });
      gapiInited = true;
      maybeEnableButtons();
      createEvent(); // Call createEvent function after initialization
    }

    /**
     * Callback after Google Identity Services are loaded.
     */
    function gisLoaded() {
      tokenClient = google.accounts.oauth2.initTokenClient({
        client_id: CLIENT_ID,
        scope: SCOPES,
        callback: '', // defined later
      });
      gisInited = true;
      maybeEnableButtons();
    }

    /**
     * Enables user interaction after all libraries are loaded.
     */
    function maybeEnableButtons() {
      if (gapiInited && gisInited) {
        document.getElementById('authorize_button').style.visibility = 'visible';
      }
    }

    /**
     *  Sign in the user upon button click.
     */
    function handleAuthClick() {
      tokenClient.callback = async (resp) => {
        if (resp.error !== undefined) {
          throw (resp);
        }
        document.getElementById('signout_button').style.visibility = 'visible';
        document.getElementById('authorize_button').innerText = 'Refresh';
        await listUpcomingEvents();
      };

      if (gapi.client.getToken() === null) {
        // Prompt the user to select a Google Account and ask for consent to share their data
        // when establishing a new session.
        tokenClient.requestAccessToken({ prompt: 'consent' });
      } else {
        // Skip display of account chooser and consent dialog for an existing session.
        tokenClient.requestAccessToken({ prompt: '' });
      }
    }


    const event = {
      'summary': 'Google I/O 2015',
      'location': '800 Howard St., San Francisco, CA 94103',
      'description': 'A chance to hear more about Google\'s developer products.',
      'start': {
        'dateTime': '2023-06-30T09:00:00-07:00',
        'timeZone': 'America/Los_Angeles',
      },
      'end': {
        'dateTime': '2023-06-30T17:00:00-07:00',
        'timeZone': 'America/Los_Angeles',
      },
      'recurrence': [
        'RRULE:FREQ=DAILY;COUNT=2'
      ],
      'attendees': [
        { 'email': 'lpage@example.com' },
        { 'email': 'sbrin@example.com' },
      ],
      'reminders': {
        'useDefault': false,
        'overrides': [
          { 'method': 'email', 'minutes': 24 * 60 },
          { 'method': 'popup', 'minutes': 10 },
        ],
      },
    };


   

async function createEvent() {

    await gapi.client.calendar.events.insert({
      calendarId: 'primary',
      resource: event,
    }, function (err, event) {
      if (err) {
        console.log('There was an error contacting the Calendar service: ' + err);
        return;
      }
      console.log('Event created: %s', event.htmlLink);
    });
  }







    /**
     *  Sign out the user upon button click.
     */
    function handleSignoutClick() {
      const token = gapi.client.getToken();
      if (token !== null) {
        google.accounts.oauth2.revoke(token.access_token);
        gapi.client.setToken('');
        document.getElementById('content').innerText = '';
        document.getElementById('authorize_button').innerText = 'Authorize';
        document.getElementById('signout_button').style.visibility = 'hidden';
      }
    }

    /**
     * Print the summary and start datetime/date of the next ten events in
     * the authorized user's calendar. If no events are found an
     * appropriate message is printed.
     */
    async function listUpcomingEvents() {
      let response;
      try {
        const request = {
          'calendarId': 'primary',
          'timeMin': (new Date()).toISOString(),
          'showDeleted': false,
          'singleEvents': true,
          'maxResults': 10,
          'orderBy': 'startTime',
        };
        response = await gapi.client.calendar.events.list(request);
      } catch (err) {
        document.getElementById('content').innerText = err.message;
        return;
      }

      const events = response.result.items;
      if (!events || events.length == 0) {
        document.getElementById('content').innerText = 'No events found.';
        return;
      }
      // Flatten to string to display
      const output = events.reduce(
        (str, event) => `${str}${event.summary} (${event.start.dateTime || event.start.date})\n`,
        'Events:\n');
      document.getElementById('content').innerText = output;
    }
  </script>
  <script async defer src="https://apis.google.com/js/api.js" onload="gapiLoaded()"></script>
  <script async defer src="https://accounts.google.com/gsi/client" onload="gisLoaded()"></script>
</body>

</html>

I am having trouble specifically with my createEvent() function. I want to insert the event into my calendar, but when I run the HTML in my browser, I get the console error:

Uncaught (in promise) {result: {…}, body: '{\n  "error": {\n    "code": 401,\n    "message": "Re…on.googleapis.com"\n        }\n      }\n    ]\n  }\n}\n', headers: {…}, status: 401, statusText: null} 

Most of this code is example code from the google calendar documentation. But I wrote the createEvent() function myself and that is where the problem is. I was wondering if there is something I am missing or if there is an error in my code. Thank you.


Solution

  • I was able to replicate the error using your code. 401 is indicating that the API call is not authorized correctly, this is because the method createEvent() is being invoked before the authorization is performed.

    To make this work you can move the method to after the authorization is performed, preferably right after the example method listUpcommingEvents()

     function handleAuthClick() {
                tokenClient.callback = async (resp) => {
                    if (resp.error !== undefined) {
                        throw (resp);
                    }
                    document.getElementById('signout_button').style.visibility = 'visible';
                    document.getElementById('authorize_button').innerText = 'Refresh';
                    await listUpcomingEvents();
                    createEvent(); // <-- Here. Call createEvent function after initialization
                };
    
                if (gapi.client.getToken() === null) {
                    // Prompt the user to select a Google Account and ask for consent to share their data
                    // when establishing a new session.
                    tokenClient.requestAccessToken({ prompt: 'consent' });
                } else {
                    // Skip display of account chooser and consent dialog for an existing session.
                    tokenClient.requestAccessToken({ prompt: '' });
                }
            }
    

    This is the full updated code:

    <!DOCTYPE html>
    <html>
    
    <head>
        <title>Google Calendar API Quickstart</title>
        <meta charset="utf-8" />
    </head>
    
    <body>
        <p>Google Calendar API Quickstart</p>
    
        <!--Add buttons to initiate auth sequence and sign out-->
        <button id="authorize_button" onclick="handleAuthClick()">Authorize</button>
        <button id="signout_button" onclick="handleSignoutClick()">Sign Out</button>
    
        <pre id="content" style="white-space: pre-wrap;"></pre>
    
        <script type="text/javascript">
            /* exported gapiLoaded */
            /* exported gisLoaded */
            /* exported handleAuthClick */
            /* exported handleSignoutClick */
    
            // TODO(developer): Set to client ID and API key from the Developer Console
            const CLIENT_ID = (CLIENT_ID);
            const API_KEY = null;
    
            // Discovery doc URL for APIs used by the quickstart
            const DISCOVERY_DOC = 'https://www.googleapis.com/discovery/v1/apis/calendar/v3/rest';
    
            // Authorization scopes required by the API; multiple scopes can be
            // included, separated by spaces.
            const SCOPES = 'https://www.googleapis.com/auth/calendar';
    
            let tokenClient;
            let gapiInited = false;
            let gisInited = false;
    
            document.getElementById('authorize_button').style.visibility = 'hidden';
            document.getElementById('signout_button').style.visibility = 'hidden';
    
            /**
             * Callback after api.js is loaded.
             */
            function gapiLoaded() {
                gapi.load('client', initializeGapiClient);
            }
    
            /**
             * Callback after the API client is loaded. Loads the
             * discovery doc to initialize the API.
             */
            async function initializeGapiClient() {
                await gapi.client.init({
                    apiKey: API_KEY,
                    discoveryDocs: [DISCOVERY_DOC],
                });
                gapiInited = true;
                maybeEnableButtons();
            }
    
            /**
             * Callback after Google Identity Services are loaded.
             */
            function gisLoaded() {
                tokenClient = google.accounts.oauth2.initTokenClient({
                    client_id: CLIENT_ID,
                    scope: SCOPES,
                    callback: '', // defined later
                });
                gisInited = true;
                maybeEnableButtons();
            }
    
            /**
             * Enables user interaction after all libraries are loaded.
             */
            function maybeEnableButtons() {
                if (gapiInited && gisInited) {
                    document.getElementById('authorize_button').style.visibility = 'visible';
                }
            }
    
            /**
             *  Sign in the user upon button click.
             */
            function handleAuthClick() {
                tokenClient.callback = async (resp) => {
                    if (resp.error !== undefined) {
                        throw (resp);
                    }
                    document.getElementById('signout_button').style.visibility = 'visible';
                    document.getElementById('authorize_button').innerText = 'Refresh';
                    await listUpcomingEvents();
                    createEvent(); // Call createEvent function after initialization
                };
    
                if (gapi.client.getToken() === null) {
                    // Prompt the user to select a Google Account and ask for consent to share their data
                    // when establishing a new session.
                    tokenClient.requestAccessToken({ prompt: 'consent' });
                } else {
                    // Skip display of account chooser and consent dialog for an existing session.
                    tokenClient.requestAccessToken({ prompt: '' });
                }
            }
    
            const event = {
                'summary': 'Google I/O 2015',
                'location': '800 Howard St., San Francisco, CA 94103',
                'description': 'A chance to hear more about Google\'s developer products.',
                'start': {
                    'dateTime': '2023-06-30T09:00:00-07:00',
                    'timeZone': 'America/Los_Angeles',
                },
                'end': {
                    'dateTime': '2023-06-30T17:00:00-07:00',
                    'timeZone': 'America/Los_Angeles',
                },
                'recurrence': [
                    'RRULE:FREQ=DAILY;COUNT=2'
                ],
                'attendees': [
                    { 'email': 'lpage@example.com' },
                    { 'email': 'sbrin@example.com' },
                ],
                'reminders': {
                    'useDefault': false,
                    'overrides': [
                        { 'method': 'email', 'minutes': 24 * 60 },
                        { 'method': 'popup', 'minutes': 10 },
                    ],
                },
            };
    
            async function createEvent() {
    
                await gapi.client.calendar.events.insert({
                    calendarId: 'primary',
                    resource: event,
                }, function (err, event) {
                    if (err) {
                        console.log('There was an error contacting the Calendar service: ' + err);
                        return;
                    }
                    console.log('Event created: %s', event.htmlLink);
                });
            }
    
            /**
             *  Sign out the user upon button click.
             */
            function handleSignoutClick() {
                const token = gapi.client.getToken();
                if (token !== null) {
                    google.accounts.oauth2.revoke(token.access_token);
                    gapi.client.setToken('');
                    document.getElementById('content').innerText = '';
                    document.getElementById('authorize_button').innerText = 'Authorize';
                    document.getElementById('signout_button').style.visibility = 'hidden';
                }
            }
    
            /**
             * Print the summary and start datetime/date of the next ten events in
             * the authorized user's calendar. If no events are found an
             * appropriate message is printed.
             */
            async function listUpcomingEvents() {
                let response;
                try {
                    const request = {
                        'calendarId': 'primary',
                        'timeMin': (new Date()).toISOString(),
                        'showDeleted': false,
                        'singleEvents': true,
                        'maxResults': 10,
                        'orderBy': 'startTime',
                    };
                    response = await gapi.client.calendar.events.list(request);
                } catch (err) {
                    document.getElementById('content').innerText = err.message;
                    return;
                }
    
                const events = response.result.items;
                if (!events || events.length == 0) {
                    document.getElementById('content').innerText = 'No events found.';
                    return;
                }
                // Flatten to string to display
                const output = events.reduce(
                    (str, event) => `${str}${event.summary} (${event.start.dateTime || event.start.date})\n`,
                    'Events:\n');
                document.getElementById('content').innerText = output;
            }
        </script>
        <script async defer src="https://apis.google.com/js/api.js" onload="gapiLoaded()"></script>
        <script async defer src="https://accounts.google.com/gsi/client" onload="gisLoaded()"></script>
    </body>
    
    </html>