Skip to content
Merged
Show file tree
Hide file tree
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 Aug 20, 2025
1957f56
Merge branch 'main' of liblab.github.com:liblaber/ai into dr/ENG-769
Davidrl1000 Aug 20, 2025
7f4ce86
Updated selector view and added more options to support more cases in…
davidrojasliblab Aug 21, 2025
323ecb0
Updated functions to save data
davidrojasliblab Aug 21, 2025
703f209
Updated functions to support public documents
davidrojasliblab Aug 26, 2025
231ff65
Merge branch 'main' of liblab.github.com:liblaber/ai into dr/ENG-769
Davidrl1000 Aug 26, 2025
4fe2b9c
Updated copy
davidrojasliblab Aug 26, 2025
fa02477
Fixed Gemini comments
davidrojasliblab Aug 26, 2025
248e701
Updated code to support more update and delete cases
davidrojasliblab Aug 26, 2025
266ae97
Hide docs and fixed cookie error
davidrojasliblab Aug 27, 2025
af1b1ae
Fixed small issues and sheet name
davidrojasliblab Aug 28, 2025
d3fc2e1
Renamed files
davidrojasliblab Aug 28, 2025
d441a2c
Added logic to prevent loops and memory issues
davidrojasliblab Aug 29, 2025
ce00a0a
Merge dev branch into dr/ENG-769
Davidrl1000 Aug 29, 2025
b729477
Fixed comments on the PR and merge issues
davidrojasliblab Aug 29, 2025
1c156c7
Fixed large documents load
davidrojasliblab Aug 30, 2025
fd512e7
Merge remote-tracking branch 'origin/dev' into dr/ENG-769
Davidrl1000 Aug 30, 2025
d062bde
Fixed PR comments
davidrojasliblab Sep 1, 2025
fe19093
Updated console logs to logger
davidrojasliblab Sep 1, 2025
a80c4c5
Fixed test issues
davidrojasliblab Sep 1, 2025
b7ad53d
Fixed test issues
davidrojasliblab Sep 1, 2025
7ed799d
Fixed test issues
davidrojasliblab Sep 1, 2025
dfb4c1c
Fixed test issues
davidrojasliblab Sep 1, 2025
177f45e
Fixed Milan comments
davidrojasliblab Sep 2, 2025
fc5e1b0
Fixed tests
davidrojasliblab Sep 2, 2025
97b1449
Merge branch 'dev' of liblab.github.com:liblaber/ai into dr/ENG-769
Davidrl1000 Sep 2, 2025
c6ab4dd
Fixed tests
davidrojasliblab Sep 3, 2025
6521fa6
Fixed tests
davidrojasliblab Sep 3, 2025
6fc2ff3
Fixed tests
davidrojasliblab Sep 3, 2025
869aa53
Fixed tests
davidrojasliblab Sep 3, 2025
3d01daf
Fixed zod and env
davidrojasliblab Sep 4, 2025
7577fa4
Create a simple redirect window
davidrojasliblab Sep 4, 2025
eafc96d
Merge dev branch and resolve conflicts
Davidrl1000 Sep 4, 2025
0000bf8
Merge branch 'dev' into dr/ENG-769
davidrojasliblab Sep 5, 2025
c7b8d8b
Updated pnpm
davidrojasliblab Sep 5, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 66 additions & 0 deletions app/api/auth/google-workspace/authorize/route.ts
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 });
}
}
127 changes: 127 additions & 0 deletions app/api/auth/google-workspace/callback/route.ts
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' },
},
);
}
}
37 changes: 37 additions & 0 deletions app/api/auth/google-workspace/config/route.ts
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,
Comment thread
davidrojasliblab marked this conversation as resolved.
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 },
);
}
}
25 changes: 25 additions & 0 deletions app/api/auth/google-workspace/logout/route.ts
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 });
}
}
99 changes: 99 additions & 0 deletions app/api/auth/google-workspace/refresh/route.ts
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 },
);
}
}
Loading