Google Slides Integration
One-Line Summary: Global, user-scoped Google OAuth2 flow with encrypted credential storage and LLM-powered HTML-to-Google-Slides conversion.
1. Overview
The Google Slides integration allows users to export their AI-generated slide decks directly to Google Slides presentations. It extends the existing PPTX export with a cloud-native alternative that produces editable Google Slides.
Key design decisions:
- Global credentials — A single
credentials.jsonis stored app-wide in thegoogle_global_credentialstable, encrypted at rest. Admins upload via the/adminpage. - Per-user tokens — OAuth tokens are scoped to
user_identityonly. Each user authorizes once; tokens are stored ingoogle_oauth_tokens. - DB-backed storage — No files on disk; all secrets live in PostgreSQL/Lakebase, encrypted with Fernet symmetric encryption.
- LLM code-gen approach — Same architecture as PPTX export: an LLM generates Python code that calls the Google Slides API, which is then executed server-side.
2. Architecture
Frontend (Admin page → GoogleSlidesAuthForm)
│
├─ Upload credentials.json ──► POST /api/admin/google-credentials
│ └─ validates → encrypts → stores in google_global_credentials
│
├─ Authorize (popup) ────────► GET /api/export/google-slides/auth/url
│ └─ builds Flow from decrypted creds → returns consent URL
│ Google consent ──────────► GET /api/export/google-slides/auth/callback
│ └─ exchanges code → encrypts token → stores in google_oauth_tokens
│
└─ Export ───────────────────► POST /api/export/google-slides
│ └─ validates auth → fetches slides → enqueues async job → returns job_id
└─ Poll ────────────────────► GET /api/export/google-slides/poll/{job_id}
└─ returns progress, status, and presentation URL on completion
Data Flow
- Admin uploads
credentials.jsonvia the Google Slides tab on the/adminpage. - Each user clicks "Authorize" to complete the OAuth consent flow in a popup. The resulting token is encrypted and stored per-user (by
user_identity). - Export validates auth, fetches slides, and enqueues an async background job. The frontend polls for progress until the job completes with
presentation_idandpresentation_url.
3. Database Schema
Table: google_global_credentials
CREATE TABLE google_global_credentials (
id SERIAL PRIMARY KEY,
credentials_encrypted TEXT NOT NULL,
uploaded_by VARCHAR(255),
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
);
Stores the Fernet-encrypted contents of credentials.json app-wide. Single-row table; upsert on upload.
Table: google_oauth_tokens
CREATE TABLE google_oauth_tokens (
id SERIAL PRIMARY KEY,
user_identity VARCHAR(255) NOT NULL,
token_encrypted TEXT NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW(),
UNIQUE (user_identity)
);
| Column | Description |
|---|---|
user_identity | Databricks username (email) or "local_dev" |
token_encrypted | Fernet-encrypted JSON token (access, refresh, expiry) |
Unique on user_identity only — one token per user across the app.
Migration
run_migrations() in src/core/database.py migrates any existing config_profiles.google_credentials_encrypted data into google_global_credentials on startup, then nulls out the profile column. See Database Configuration.
4. Encryption
Module: src/core/encryption.py
Uses Fernet symmetric encryption from the cryptography library.
| Function | Purpose |
|---|---|
get_encryption_key() | Returns the 32-byte Fernet key (cached). Reads from GOOGLE_OAUTH_ENCRYPTION_KEY env var, falls back to .encryption_key file, or generates and persists a new key. |
encrypt_data(plaintext) | Encrypts a string and returns a base64-encoded ciphertext string. |
decrypt_data(ciphertext) | Decrypts. Raises InvalidToken if the key doesn't match. |
Key management:
- Production: Set
GOOGLE_OAUTH_ENCRYPTION_KEYenvironment variable (base64-encoded 32-byte key generated byFernet.generate_key()). If this variable is missing in production (ENVIRONMENT=production), aRuntimeErroris raised at first use ofencrypt_data/decrypt_data— not at startup. - Local dev: The key is auto-generated and persisted to
.encryption_key(gitignored).
Generate a production key:
python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"
Stale key handling: If a token or credential cannot be decrypted (e.g., after key rotation), the system silently deletes the stale record and prompts the user to re-authorize.
5. API Endpoints
Credential Management (src/api/routes/admin.py)
| Method | Path | Description |
|---|---|---|
POST | /api/admin/google-credentials | Upload credentials.json file. Validates structure, encrypts, upserts into google_global_credentials. |
GET | /api/admin/google-credentials/status | Returns {"has_credentials": bool}. Attempts decryption; clears stale data on failure. |
DELETE | /api/admin/google-credentials | Removes stored credentials. Returns 204. |
Validation: The uploaded JSON must contain either an "installed" or "web" top-level key with client_id and client_secret.
OAuth Flow (/api/export/google-slides/auth)
| Method | Path | Description |
|---|---|---|
GET | /auth/status | Returns {"authorized": bool}. Gracefully returns false on any error. |
GET | /auth/url | Generates and returns the Google OAuth consent URL. |
GET | /auth/callback?code=... | Exchanges auth code for tokens, encrypts, stores. Returns HTML that notifies the opener window. |
Export (/api/export/google-slides)
| Method | Path | Description |
|---|---|---|
POST | /api/export/google-slides | Starts an async Google Slides export job. Returns {job_id, status, total_slides}. |
GET | /api/export/google-slides/poll/{job_id} | Polls export progress. Returns {job_id, status, progress, total_slides}. When status is completed, also includes presentation_id and presentation_url. |
Export is asynchronous. The POST validates auth, fetches slides, builds HTML, and enqueues a background job for the slow LLM conversion. The frontend polls the GET endpoint until status reaches completed or error.
Re-export: If a session has already been exported to Google Slides, the endpoint looks up the existing_presentation_id via the session manager and overwrites the existing presentation instead of creating a new one.
Request body:
{
"session_id": "abc123",
"chart_images": [[{"canvas_id": "chart_0", "base64_data": "data:image/png;base64,..."}]]
}
Response body (POST):
{
"job_id": "abc-123",
"status": "pending",
"total_slides": 5
}
Response body (GET poll, completed):
{
"job_id": "abc-123",
"status": "completed",
"progress": 5,
"total_slides": 5,
"presentation_id": "1A2B3C...",
"presentation_url": "https://docs.google.com/presentation/d/1A2B3C.../edit"
}
Auth uses global credentials and user-scoped token.
6. Backend Services
GoogleSlidesAuth (src/services/google_slides_auth.py)
Manages OAuth2 credentials and token lifecycle. Supports two modes:
| Mode | Constructor | Persistence |
|---|---|---|
| DB-backed | GoogleSlidesAuth.from_global(user_identity, db_session) | Encrypted in PostgreSQL |
| File-backed | GoogleSlidesAuth(credentials_path=..., token_path=...) | JSON files on disk |
Key methods:
is_authorized()— Checks for valid (or refreshable) token.get_auth_url(redirect_uri)— Generates consent URL.authorize(code, redirect_uri)— Exchanges code for tokens, persists.build_slides_service()/build_drive_service()— Returns authenticated Google API clients.
HtmlToGoogleSlidesConverter (src/services/html_to_google_slides.py)
LLM-powered converter following the same pattern as HtmlToPptxConverterV3:
- Creates a blank Google Slides presentation via the API.
- Extracts base64-embedded content images from HTML, converting SVGs to PNG via
_svg_to_png()(svgpathtools+Pillow). - For each slide, sends the HTML to the LLM with a detailed system prompt.
- LLM generates Python code that calls
slides_service.presentations().batchUpdate(). - Content images are uploaded to Google Drive and embedded via
createImagerequests. - Code is executed server-side with sanitization (smart quote replacement, function wrapping, import injection).
- On failure, retries once with error context. Falls back to a placeholder text box.
Prompts (src/services/google_slides_prompts_defaults.py)
Shared rules cover:
- Slide dimensions (16:9 widescreen, 10" x 5.625")
- Overflow prevention with strict bounds
- Font size maximums per element type
- Metric card, table, chart, and layout patterns
- Google Slides API patterns (batchUpdate nesting, textRange, shapeProperties)
7. Frontend Components
GoogleSlidesAuthForm (frontend/src/components/config/GoogleSlidesAuthForm.tsx)
Rendered on the /admin page. Provides:
- Drag-and-drop file upload for
credentials.json - Status indicators (uploaded / not configured)
- Upload / replace / remove actions
- "Authorize with Google" button (opens popup)
- Authorization status (authorized / not authorized)
- Help text with instructions for obtaining credentials from Google Cloud Console
SlidePanel (frontend/src/components/SlidePanel/SlidePanel.tsx)
handleExportGoogleSlides calls exportToGoogleSlides(sessionId, chartImages) to export the current session's deck.
API Services
frontend/src/api/config.ts—uploadGoogleCredentials(),getGoogleCredentialsStatus(),deleteGoogleCredentials()(admin endpoints)frontend/src/services/api.ts—checkGoogleSlidesAuth(),getGoogleSlidesAuthUrl(),exportToGoogleSlides(sessionId, chartImages)
8. Test Coverage
| Test File | Tests | Coverage |
|---|---|---|
tests/unit/test_encryption.py | 5 | Encrypt/decrypt roundtrip, wrong-key rejection, key sources |
tests/unit/config/test_google_oauth.py | 16 | Models, credentials API, from_global, auth service, auth endpoint |
tests/unit/config/test_admin_routes.py | — | Admin credential upload, status, delete |
tests/unit/test_database_migrations.py | 4 | Migration from profile credentials to global |
tests/unit/test_google_slides_converter.py | 19 | Static methods: extract, strip fences, chart notes, code prep, image save, SVG-to-PNG, content image extraction |
tests/unit/test_prompts_defaults.py | 7 | PPTX + Google Slides prompt constant validation |
tests/unit/test_app_wiring.py | 7 | Model registration, router exports, route registration |
tests/unit/test_google_slides_routes.py | 12 | Auth endpoints, export endpoint, poll endpoint, helper functions |
Run all tests:
pytest tests/unit/ -v --ignore=tests/unit/test_chart_persistence.py \
--ignore=tests/unit/test_deck_integrity.py \
--ignore=tests/unit/test_llm_edit_responses.py
9. Configuration & Setup
Google Cloud Console Setup
- Go to Google Cloud Console.
- Create a project (or use an existing one).
- Enable the Google Slides API and Google Drive API.
- Go to Credentials → Create OAuth 2.0 Client ID (Desktop app).
- Download the
credentials.jsonfile. - Upload it on the admin page (Google Slides tab).
Environment Variables
| Variable | Required | Description |
|---|---|---|
GOOGLE_OAUTH_ENCRYPTION_KEY | Yes (production) | Fernet key for encrypting credentials/tokens. A RuntimeError is raised at first use of encrypt_data/decrypt_data if missing in production. |
Dependencies
Added to requirements.txt:
google-api-python-client>=2.100.0
google-auth-oauthlib>=1.2.0
google-auth-httplib2>=0.2.0
cryptography>=42.0.0
svgpathtools>=1.6.0 # Pure-Python SVG-to-PNG conversion for image library assets
Pillow>=10.0.0 # Image rasterization (also used by PPTX export)
10. Cross-References
- Export Features — PDF and PPTX export details
- Backend Overview — FastAPI architecture and API surface
- Database Configuration — Schema details
- Frontend Overview — React components and state management