Google Drive Integration¶
Overview¶
Vitara uses Google Drive API v3 with OAuth 2.0 Authorization Code flow (offline access) to allow Care Recipients to upload clinical Excel reports directly to a Vitara folder in their personal Google Drive. Credentials are persisted server-side, so uploads can happen without user interaction after the initial one-time consent.
How It Works — End-to-End Flow¶
User (Profile page)
│
├─ 1. Click "Connect Google Drive"
│ │
│ └─► GET /api/google-drive/connect
│ └─ API builds OAuth2 consent URL
│ └─ Stores state → userId in IMemoryCache (15-min TTL)
│ └─ Returns { url: "https://accounts.google.com/o/oauth2/auth?..." }
│
├─ 2. Browser redirects to Google consent screen
│ └─ User grants Drive access
│
└─ 3. Google redirects back to
GET /api/google-drive/callback?code=...&state=...
└─ API validates state from cache
└─ Exchanges code for access + refresh tokens
└─ Upserts UserGoogleDriveToken row in database
└─ Redirects browser to /profile?drive=connected
After connection, any export action from the Incidents or Export screens can push an Excel file to Drive via POST /api/export/upload-to-drive.
Prerequisites — Google Cloud Console Setup¶
Before the integration works, an administrator must register a Google Cloud project and populate three AppConfig keys.
Step 1 — Create a Google Cloud Project¶
- Go to console.cloud.google.com.
- Click Select a project → New Project.
- Give it a name (e.g.
Vitara) and create it.
Step 2 — Enable the Google Drive API¶
- In the project, navigate to APIs & Services → Library.
- Search for Google Drive API and click Enable.
Step 3 — Configure the OAuth Consent Screen¶
- Navigate to APIs & Services → OAuth consent screen.
- Choose External (or Internal if using a Google Workspace org).
- Fill in the required app name, support email, and developer contact.
- Under Scopes, add
https://www.googleapis.com/auth/drive(ordrive.filefor narrower access). - Add test users if the app is still in "Testing" mode.
- Publish the app once ready for production.
Step 4 — Create OAuth 2.0 Credentials¶
- Navigate to APIs & Services → Credentials → Create Credentials → OAuth client ID.
- Application type: Web application.
- Under Authorised redirect URIs, add:
This must exactly match the
GoogleDriveRedirectUrivalue in App Config. - Click Create. Copy the Client ID and Client Secret.
Step 5 — Configure App Config¶
Log in as SuperAdmin or Administrator and set these three keys under Management → App Configuration:
| Key | Description | Example |
|---|---|---|
GoogleDriveClientId |
OAuth 2.0 Client ID from Google Cloud | 123456789-abc.apps.googleusercontent.com |
GoogleDriveClientSecret |
OAuth 2.0 Client Secret | GOCSPX-... |
GoogleDriveRedirectUri |
Redirect URI registered in Google Cloud | https://vitarapi.elroitec.com/api/google-drive/callback |
Security:
ClientIdandClientSecretare stored asIsSecret = true— their values are masked in the UI and never returned in API responses.
Backend¶
Controller — GoogleDriveAuthController¶
File: server/src/Vitara.Api/Controllers/GoogleDriveAuthController.cs
Base route: /api/google-drive
| Method | Route | Auth | Description |
|---|---|---|---|
| GET | /connect |
Authorized |
Generates and returns the Google OAuth2 consent URL |
| GET | /callback |
Anonymous | Exchanges auth code for tokens; redirects to frontend |
| GET | /status |
Authorized |
Returns { connected: bool } for the calling user |
| DELETE | /disconnect |
Authorized |
Revokes and deletes stored Drive tokens for the calling user |
GET /connect¶
- Reads
GoogleDriveClientId,GoogleDriveClientSecret, andGoogleDriveRedirectUrifromAppConfigService. - Creates a
GoogleAuthorizationCodeFlowwith scopehttps://www.googleapis.com/auth/drive. - Generates a CSRF-protection
stateUUID and caches it against the currentuserIdinIMemoryCachefor 15 minutes. - Builds and returns the authorization URL.
GET /callback¶
- Validates the
stateparameter against the cache. - If
erroris present (user denied), redirects to/profile?drive=error. - Exchanges the
codefor token response usingGoogleAuthorizationCodeFlow.ExchangeCodeForTokenAsync. - Upserts a
UserGoogleDriveTokenrow: UserId— from the cached state mappingAccessToken— short-lived access tokenRefreshToken— long-lived refresh tokenTokenExpiry— calculated fromexpires_in- Redirects to
{GoogleDriveFrontendUrl}/profile?drive=connected.
Token Refresh¶
The Google client library (Google.Apis.Auth) automatically refreshes the access token using the stored RefreshToken when it expires. ExportService reads the persisted token, passes it to GoogleCredential, and the library handles refresh transparently. The updated token is then saved back to the database.
Database — UserGoogleDriveTokens Table¶
Migration: 20260306013021_AddUserGoogleDriveToken
| Column | Type | Notes |
|---|---|---|
id |
integer |
PK, auto-increment |
user_id |
integer |
FK → Users.id |
access_token |
varchar(2048) |
Current OAuth access token |
refresh_token |
varchar(512) |
Long-lived refresh token |
token_expiry |
timestamp |
UTC expiry of the access token |
created_at |
timestamp |
Row insertion time |
updated_at |
timestamp |
Last upsert time |
One row per user. Upserted on each successful OAuth callback so re-authorisation always refreshes all fields.
App Config Keys¶
File: server/src/Vitara.Api/Models/AppConfigKeys.cs
| Constant | Key String | Category |
|---|---|---|
AppConfigKeys.GoogleDriveClientId |
GoogleDriveClientId |
GoogleDrive |
AppConfigKeys.GoogleDriveClientSecret |
GoogleDriveClientSecret |
GoogleDrive |
AppConfigKeys.GoogleDriveRedirectUri |
GoogleDriveRedirectUri |
GoogleDrive |
AppConfigKeys.GoogleDriveFrontendUrl |
GoogleDriveFrontendUrl |
GoogleDrive |
Service — ExportService (Drive Upload)¶
File: server/src/Vitara.Api/Services/ExportService.cs
UploadToGoogleDriveAsync(byte[] excelData, int careRecipientId, string fileName)¶
- Loads
UserGoogleDriveTokenfor thecareRecipientId. Throws if not found. - Reads
GoogleDriveClientIdandGoogleDriveClientSecretfromAppConfigService. - Constructs a
GoogleCredentialfrom the stored tokens. - Initialises
Google.Apis.Drive.v3.DriveService. - Searches for an existing folder named
"Vitara"in the user's Drive. - If not found, creates a new folder with
mimeType = "application/vnd.google-apps.folder". - Checks for an existing file with the same
fileNamein theVitarafolder. - If found, updates (replaces) the file content.
- If not found, creates a new file.
- Returns the file name on success.
GetCareRecipientDriveStatusAsync(int careRecipientId)¶
Returns true if a UserGoogleDriveToken row exists for the given Care Recipient.
Frontend¶
Service — GoogleDriveService¶
File: client/src/app/core/services/google-drive.service.ts
| Method | HTTP | Endpoint | Description |
|---|---|---|---|
getConnectUrl() |
GET | /api/google-drive/connect |
Returns OAuth consent URL |
getOwnStatus() |
GET | /api/google-drive/status |
Returns connection status for calling user |
getCareRecipientDriveStatus(id) |
GET | /api/export/drive-status/{id} |
Returns connection status for a CR |
disconnect() |
DELETE | /api/google-drive/disconnect |
Removes Drive tokens |
All calls include the JWT Bearer token in the Authorization header.
Profile Page Integration¶
File: client/src/app/features/profile/components/profile.component.ts
The Google Drive section is visible only when the logged-in user has the CareRecipient role.
| Action | Method | Behaviour |
|---|---|---|
| Page load | getOwnStatus() |
Sets driveConnected flag; shows "Connected" badge or "Not connected" |
| Connect button | getConnectUrl() |
Redirects window.location.href to Google consent screen |
| Disconnect button | disconnect() |
Removes tokens; sets driveConnected = false |
| Return from Google | Router query params | Reads ?drive=connected or ?drive=error; shows snackbar message |
Incidents / Export Page Integration¶
The Incidents and Export pages check getCareRecipientDriveStatus(careRecipientId) after a Care Recipient is selected. If connected, a "Upload to Drive" button is shown alongside the download button. Clicking it calls POST /api/export/upload-to-drive.
Security Notes¶
- OAuth
stateparameter is generated as a random UUID and validated on callback to prevent CSRF attacks. ClientIdandClientSecretare never returned in API responses (IsSecret = true).- Tokens are stored per-user; no token is shared between users.
- Disconnect (
DELETE /google-drive/disconnect) deletes the stored tokens immediately, preventing further uploads. - The Drive scope (
https://www.googleapis.com/auth/drive) grants access to the user's full Drive. For tighter isolation, considerdrive.filescope (access only to files created by the app) when re-registering the consent screen.