-
Notifications
You must be signed in to change notification settings - Fork 13
Add Google Sheets integration with OAuth and Apps Script support #94
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
35 commits
Select commit
Hold shift + click to select a range
9614058
Added based code for google sheets first iteration
davidrojasliblab 1957f56
Merge branch 'main' of liblab.github.com:liblaber/ai into dr/ENG-769
Davidrl1000 7f4ce86
Updated selector view and added more options to support more cases in…
davidrojasliblab 323ecb0
Updated functions to save data
davidrojasliblab 703f209
Updated functions to support public documents
davidrojasliblab 231ff65
Merge branch 'main' of liblab.github.com:liblaber/ai into dr/ENG-769
Davidrl1000 4fe2b9c
Updated copy
davidrojasliblab fa02477
Fixed Gemini comments
davidrojasliblab 248e701
Updated code to support more update and delete cases
davidrojasliblab 266ae97
Hide docs and fixed cookie error
davidrojasliblab af1b1ae
Fixed small issues and sheet name
davidrojasliblab d3fc2e1
Renamed files
davidrojasliblab d441a2c
Added logic to prevent loops and memory issues
davidrojasliblab ce00a0a
Merge dev branch into dr/ENG-769
Davidrl1000 b729477
Fixed comments on the PR and merge issues
davidrojasliblab 1c156c7
Fixed large documents load
davidrojasliblab fd512e7
Merge remote-tracking branch 'origin/dev' into dr/ENG-769
Davidrl1000 d062bde
Fixed PR comments
davidrojasliblab fe19093
Updated console logs to logger
davidrojasliblab a80c4c5
Fixed test issues
davidrojasliblab b7ad53d
Fixed test issues
davidrojasliblab 7ed799d
Fixed test issues
davidrojasliblab dfb4c1c
Fixed test issues
davidrojasliblab 177f45e
Fixed Milan comments
davidrojasliblab fc5e1b0
Fixed tests
davidrojasliblab 97b1449
Merge branch 'dev' of liblab.github.com:liblaber/ai into dr/ENG-769
Davidrl1000 c6ab4dd
Fixed tests
davidrojasliblab 6521fa6
Fixed tests
davidrojasliblab 6fc2ff3
Fixed tests
davidrojasliblab 869aa53
Fixed tests
davidrojasliblab 3d01daf
Fixed zod and env
davidrojasliblab 7577fa4
Create a simple redirect window
davidrojasliblab eafc96d
Merge dev branch and resolve conflicts
Davidrl1000 0000bf8
Merge branch 'dev' into dr/ENG-769
davidrojasliblab c7b8d8b
Updated pnpm
davidrojasliblab File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,66 @@ | ||
| import { NextRequest, NextResponse } from 'next/server'; | ||
| import { requireUserAbility } from '~/auth/session'; | ||
| import { GoogleWorkspaceAuthManager } from '@liblab/data-access/accessors/google-workspace/auth-manager'; | ||
| import { GOOGLE_WORKSPACE_SCOPES } from '@liblab/data-access/accessors/google-workspace/types'; | ||
| import { env } from '~/env/server'; | ||
| import { createScopedLogger } from '~/utils/logger'; | ||
| import { z } from 'zod'; | ||
|
|
||
| const logger = createScopedLogger('google-workspace-auth'); | ||
|
|
||
| const requestSchema = z.object({ | ||
| type: z.enum(['docs', 'sheets']), | ||
| scopes: z.array(z.string()).optional(), | ||
| }); | ||
|
|
||
| export async function POST(request: NextRequest) { | ||
| try { | ||
| // Validate required environment variables | ||
| if (!env.GOOGLE_CLIENT_ID || !env.GOOGLE_CLIENT_SECRET || !env.GOOGLE_AUTH_ENCRYPTION_KEY) { | ||
| logger.error('Google Workspace auth is not configured. Missing required environment variables.'); | ||
| return NextResponse.json( | ||
| { | ||
| success: false, | ||
| error: 'Google Workspace authentication is not configured.', | ||
| }, | ||
| { status: 500 }, | ||
| ); | ||
| } | ||
|
|
||
| const { userId } = await requireUserAbility(request); | ||
|
|
||
| // Validate request body with Zod | ||
| const body = await request.json(); | ||
| const { type, scopes } = requestSchema.parse(body); | ||
|
|
||
| // Initialize auth manager | ||
| const authManager = new GoogleWorkspaceAuthManager(env.GOOGLE_AUTH_ENCRYPTION_KEY!); | ||
|
|
||
| // Determine scopes based on type | ||
| const requestedScopes = | ||
| scopes || (type === 'docs' ? [GOOGLE_WORKSPACE_SCOPES.DOCS.READONLY] : [GOOGLE_WORKSPACE_SCOPES.SHEETS.READONLY]); | ||
|
|
||
| // Add Drive scope for file listing and ensure we get offline access | ||
| const allScopes = [ | ||
| ...requestedScopes, | ||
| 'https://www.googleapis.com/auth/drive.readonly', // For listing files | ||
| 'https://www.googleapis.com/auth/userinfo.email', // For user identification | ||
| ]; | ||
|
|
||
| await authManager.initialize({ | ||
| type: 'oauth2', | ||
| clientId: env.GOOGLE_CLIENT_ID!, | ||
| clientSecret: env.GOOGLE_CLIENT_SECRET!, | ||
| redirectUri: `${request.nextUrl.origin}/api/auth/google-workspace/callback`, | ||
| scopes: allScopes, | ||
| }); | ||
|
|
||
| // Generate auth URL with user ID in state | ||
| const authUrl = authManager.generateAuthUrl(allScopes, JSON.stringify({ userId, type })); | ||
|
|
||
| return NextResponse.json({ success: true, authUrl }); | ||
| } catch (error) { | ||
| logger.error('Google Workspace auth error:', error); | ||
| return NextResponse.json({ success: false, error: 'Failed to initialize Google authentication' }, { status: 500 }); | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,127 @@ | ||
| import { NextRequest, NextResponse } from 'next/server'; | ||
| import { GoogleWorkspaceAuthManager } from '@liblab/data-access/accessors/google-workspace/auth-manager'; | ||
| import { env } from '~/env/server'; | ||
| import { createScopedLogger } from '~/utils/logger'; | ||
| import { generateOAuthCallbackHTML } from '~/lib/oauth-templates'; | ||
|
|
||
| const logger = createScopedLogger('google-workspace-callback'); | ||
|
|
||
| export async function GET(request: NextRequest) { | ||
| // Validate required environment variables | ||
| if (!env.GOOGLE_CLIENT_ID || !env.GOOGLE_CLIENT_SECRET || !env.GOOGLE_AUTH_ENCRYPTION_KEY) { | ||
| logger.error('Google Workspace auth is not configured. Missing required environment variables.'); | ||
| return new NextResponse( | ||
| generateOAuthCallbackHTML({ | ||
| type: 'error', | ||
| error: 'Google Workspace authentication is not configured', | ||
| }), | ||
| { | ||
| status: 500, | ||
| headers: { 'Content-Type': 'text/html' }, | ||
| }, | ||
| ); | ||
| } | ||
|
|
||
| const searchParams = request.nextUrl.searchParams; | ||
| const code = searchParams.get('code'); | ||
| const state = searchParams.get('state'); | ||
| const error = searchParams.get('error'); | ||
|
|
||
| // Handle OAuth errors | ||
| if (error) { | ||
| return new NextResponse( | ||
| generateOAuthCallbackHTML({ | ||
| type: 'error', | ||
| error: 'Authentication was cancelled or failed', | ||
| }), | ||
| { | ||
| status: 200, | ||
| headers: { 'Content-Type': 'text/html' }, | ||
| }, | ||
| ); | ||
| } | ||
|
|
||
| if (!code || !state) { | ||
| return new NextResponse( | ||
| generateOAuthCallbackHTML({ | ||
| type: 'error', | ||
| error: 'Missing authorization code or state', | ||
| }), | ||
| { | ||
| status: 400, | ||
| headers: { 'Content-Type': 'text/html' }, | ||
| }, | ||
| ); | ||
| } | ||
|
|
||
| try { | ||
| // Parse state to get user info | ||
| const stateData = JSON.parse(state); | ||
| const { userId, type } = stateData; | ||
|
|
||
| // Initialize auth manager | ||
| const authManager = new GoogleWorkspaceAuthManager(env.GOOGLE_AUTH_ENCRYPTION_KEY!); | ||
|
|
||
| await authManager.initialize({ | ||
| type: 'oauth2', | ||
| clientId: env.GOOGLE_CLIENT_ID!, | ||
| clientSecret: env.GOOGLE_CLIENT_SECRET!, | ||
| redirectUri: `${request.nextUrl.origin}/api/auth/google-workspace/callback`, | ||
| scopes: [], // Will be populated from the auth flow | ||
| }); | ||
|
|
||
| // Exchange code for tokens | ||
| const credentials = await authManager.exchangeCodeForTokens(code); | ||
|
|
||
| // Return success HTML page | ||
| const successData = { | ||
| tokens: { | ||
| access_token: credentials.access_token, | ||
| refresh_token: credentials.refresh_token, | ||
| expiry_date: credentials.expiry_date, | ||
| }, | ||
| userId, | ||
| workspaceType: type, | ||
| }; | ||
|
|
||
| const response = new NextResponse( | ||
| generateOAuthCallbackHTML({ | ||
| type: 'success', | ||
| data: successData, | ||
| }), | ||
| { | ||
| status: 200, | ||
| headers: { 'Content-Type': 'text/html' }, | ||
| }, | ||
| ); | ||
|
|
||
| // Set secure cookies for the tokens with extended lifetime | ||
| // Google refresh tokens are long-lived, so we can set a longer cookie expiration | ||
| const cookieMaxAge = 30 * 24 * 60 * 60; // 30 days in seconds | ||
| const encryptedCredentials = authManager.encryptCredentials(credentials); | ||
|
|
||
| // Try multiple approaches to ensure the cookie is set | ||
| response.cookies.set('google_workspace_auth', encryptedCredentials, { | ||
| httpOnly: true, | ||
| secure: env.NODE_ENV === 'production', | ||
| sameSite: 'lax', | ||
| maxAge: cookieMaxAge, | ||
| path: '/', | ||
| }); | ||
|
|
||
| return response; | ||
| } catch (error) { | ||
| logger.error('Google Workspace callback error:', error); | ||
|
|
||
| return new NextResponse( | ||
| generateOAuthCallbackHTML({ | ||
| type: 'error', | ||
| error: 'Failed to complete authentication', | ||
| }), | ||
| { | ||
| status: 500, | ||
| headers: { 'Content-Type': 'text/html' }, | ||
| }, | ||
| ); | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| import { NextResponse } from 'next/server'; | ||
| import { createScopedLogger } from '~/utils/logger'; | ||
| import { env } from '~/env/server'; | ||
|
|
||
| const logger = createScopedLogger('google-workspace-config'); | ||
|
|
||
| export async function GET() { | ||
| try { | ||
| // Check if required OAuth environment variables are configured | ||
| // Note: GOOGLE_REDIRECT_URI is dynamically constructed, not required as env var | ||
| const hasClientId = !!env.GOOGLE_CLIENT_ID; | ||
| const hasClientSecret = !!env.GOOGLE_CLIENT_SECRET; | ||
| const hasEncryptionKey = !!env.GOOGLE_AUTH_ENCRYPTION_KEY; | ||
|
|
||
| const configured = hasClientId && hasClientSecret && hasEncryptionKey; | ||
|
|
||
| return NextResponse.json({ | ||
| success: true, | ||
| configured, | ||
| missing: { | ||
| ...(!hasClientId && { GOOGLE_CLIENT_ID: 'Google OAuth Client ID is required' }), | ||
| ...(!hasClientSecret && { GOOGLE_CLIENT_SECRET: 'Google OAuth Client Secret is required' }), | ||
| ...(!hasEncryptionKey && { GOOGLE_AUTH_ENCRYPTION_KEY: 'Google Auth Encryption Key is required' }), | ||
| }, | ||
| }); | ||
| } catch (error) { | ||
| logger.error('OAuth config check error:', error); | ||
| return NextResponse.json( | ||
| { | ||
| success: false, | ||
| configured: false, | ||
| error: 'Failed to check OAuth configuration', | ||
| }, | ||
| { status: 500 }, | ||
| ); | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| import { NextRequest, NextResponse } from 'next/server'; | ||
| import { createScopedLogger } from '~/utils/logger'; | ||
| import { env } from '~/env/server'; | ||
|
|
||
| const logger = createScopedLogger('google-workspace-logout'); | ||
|
|
||
| export async function POST(_request: NextRequest) { | ||
| try { | ||
| // Clear the authentication cookie | ||
| const response = NextResponse.json({ success: true }); | ||
|
|
||
| response.cookies.set('google_workspace_auth', '', { | ||
| httpOnly: true, | ||
| secure: env.NODE_ENV === 'production', | ||
| sameSite: 'lax', | ||
| maxAge: 0, // Expire immediately | ||
| path: '/', | ||
| }); | ||
|
|
||
| return response; | ||
| } catch (error) { | ||
| logger.error('Google Workspace logout error:', error); | ||
| return NextResponse.json({ success: false, error: 'Failed to logout' }, { status: 500 }); | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,99 @@ | ||
| import { NextRequest, NextResponse } from 'next/server'; | ||
| import { requireUserAbility } from '~/auth/session'; | ||
| import { GoogleWorkspaceAuthManager } from '@liblab/data-access/accessors/google-workspace/auth-manager'; | ||
| import { createScopedLogger } from '~/utils/logger'; | ||
| import { env } from '~/env/server'; | ||
|
|
||
| const logger = createScopedLogger('google-workspace-refresh'); | ||
|
|
||
| export async function POST(request: NextRequest) { | ||
| try { | ||
| await requireUserAbility(request); | ||
|
|
||
| // Check if user has stored Google Workspace auth | ||
| const authCookie = request.cookies.get('google_workspace_auth'); | ||
|
|
||
| if (!authCookie?.value) { | ||
| return NextResponse.json( | ||
| { | ||
| success: false, | ||
| error: 'No authentication found', | ||
| }, | ||
| { status: 401 }, | ||
| ); | ||
| } | ||
|
|
||
| try { | ||
| // Decrypt existing tokens | ||
| const authManager = new GoogleWorkspaceAuthManager(env.GOOGLE_AUTH_ENCRYPTION_KEY); | ||
| const credentials = authManager.decryptCredentials(authCookie.value); | ||
|
|
||
| if (!credentials.refresh_token) { | ||
| return NextResponse.json( | ||
| { | ||
| success: false, | ||
| error: 'No refresh token available', | ||
| }, | ||
| { status: 400 }, | ||
| ); | ||
| } | ||
|
|
||
| // Initialize auth manager with existing credentials | ||
| await authManager.initialize({ | ||
| type: 'oauth2', | ||
| clientId: env.GOOGLE_CLIENT_ID, | ||
| clientSecret: env.GOOGLE_CLIENT_SECRET, | ||
| credentials, | ||
| scopes: [], | ||
| }); | ||
|
|
||
| // Force refresh tokens | ||
| const refreshedCredentials = await authManager.refreshTokens(); | ||
|
|
||
| // Update cookie with new tokens | ||
| const cookieMaxAge = 30 * 24 * 60 * 60; // 30 days | ||
| const response = NextResponse.json({ | ||
| success: true, | ||
| tokens: { | ||
| access_token: refreshedCredentials.access_token, | ||
| refresh_token: refreshedCredentials.refresh_token, | ||
| expiry_date: refreshedCredentials.expiry_date, | ||
| }, | ||
| }); | ||
|
|
||
| response.cookies.set('google_workspace_auth', authManager.encryptCredentials(refreshedCredentials), { | ||
| httpOnly: true, | ||
| secure: env.NODE_ENV === 'production', | ||
| sameSite: 'lax', | ||
| maxAge: cookieMaxAge, | ||
| path: '/', | ||
| }); | ||
|
|
||
| return response; | ||
| } catch (error) { | ||
| logger.error('Token refresh failed:', error); | ||
|
|
||
| // Clear invalid cookie | ||
| const response = NextResponse.json( | ||
| { | ||
| success: false, | ||
| error: 'Token refresh failed', | ||
| }, | ||
| { status: 401 }, | ||
| ); | ||
|
|
||
| response.cookies.delete('google_workspace_auth'); | ||
|
|
||
| return response; | ||
| } | ||
| } catch (error) { | ||
| logger.error('Google Workspace token refresh error:', error); | ||
| return NextResponse.json( | ||
| { | ||
| success: false, | ||
| error: 'Failed to refresh tokens', | ||
| }, | ||
| { status: 500 }, | ||
| ); | ||
| } | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.