Skip to content

Commit 58e1937

Browse files
authored
Merge pull request #67 from Ziqian-Huang0607/feature/google-drive-v2
feat: integrate Google Drive file loading and saving into Notes apphu…
2 parents b2fc160 + dd2b6b9 commit 58e1937

File tree

15 files changed

+946
-221
lines changed

15 files changed

+946
-221
lines changed

package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,16 @@
1515
"dependencies": {
1616
"@heroicons/react": "^2.2.0",
1717
"bcryptjs": "^3.0.2",
18+
"cookie": "^1.0.2",
1819
"cookie-parser": "^1.4.7",
1920
"cors": "^2.8.5",
2021
"express": "^4.18.0",
2122
"express-rate-limit": "^8.1.0",
2223
"express-validator": "^7.2.1",
2324
"framer-motion": "^12.23.16",
25+
"googleapis": "^162.0.0",
2426
"helmet": "^8.1.0",
27+
"idb": "^8.0.3",
2528
"jsonwebtoken": "^9.0.2",
2629
"mongoose": "^8.18.2",
2730
"multer": "2.0.2",

public/icons/gdrive.png

6.54 KB
Loading

src/context/DriveContext.js

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
// src/context/DriveContext.js
2+
3+
import { createContext, useContext, useState, useEffect } from 'react';
4+
import {
5+
cacheDriveFiles,
6+
getCachedDriveFiles,
7+
clearDriveCache,
8+
} from '@/lib/driveCache';
9+
10+
const DriveContext = createContext();
11+
12+
export function DriveProvider({ children }) {
13+
const [isConnected, setIsConnected] = useState(false);
14+
const [files, setFiles] = useState([]);
15+
const [isLoading, setIsLoading] = useState(true);
16+
const [userEmail, setUserEmail] = useState(null);
17+
18+
const syncFiles = async (showNotification = false) => {
19+
setIsLoading(true);
20+
try {
21+
// First, check if we are authenticated and get the user's email
22+
const authCheckResponse = await fetch('/api/auth/google/me');
23+
if (!authCheckResponse.ok) {
24+
throw new Error('Not authenticated');
25+
}
26+
const userData = await authCheckResponse.json();
27+
setUserEmail(userData.email);
28+
setIsConnected(true);
29+
30+
// If authenticated, then fetch the files
31+
const filesResponse = await fetch('/api/files/gdrive');
32+
if (!filesResponse.ok) throw new Error('Failed to fetch files');
33+
const driveFiles = await filesResponse.json();
34+
setFiles(driveFiles);
35+
await cacheDriveFiles(driveFiles);
36+
} catch (error) {
37+
// If any step fails, we are not connected
38+
setIsConnected(false);
39+
setUserEmail(null);
40+
// Try to load from cache as a fallback
41+
const cachedFiles = await getCachedDriveFiles();
42+
setFiles(cachedFiles); // Show cached files even if disconnected
43+
} finally {
44+
setIsLoading(false);
45+
}
46+
};
47+
48+
useEffect(() => {
49+
syncFiles();
50+
}, []);
51+
52+
const connectToDrive = () => {
53+
window.location.href = '/api/auth/google/login';
54+
};
55+
56+
const disconnectFromDrive = async () => {
57+
await fetch('/api/auth/google/logout');
58+
await clearDriveCache();
59+
setIsConnected(false);
60+
setUserEmail(null);
61+
setFiles([]);
62+
};
63+
64+
const value = {
65+
isConnected,
66+
files,
67+
isLoading,
68+
userEmail,
69+
connectToDrive,
70+
disconnectFromDrive,
71+
syncFiles,
72+
};
73+
74+
return (
75+
<DriveContext.Provider value={value}>{children}</DriveContext.Provider>
76+
);
77+
}
78+
79+
export function useDrive() {
80+
return useContext(DriveContext);
81+
}

src/lib/driveCache.js

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
// src/lib/driveCache.js
2+
3+
import { openDB } from 'idb';
4+
5+
const DB_NAME = 'orbitos-drive-cache';
6+
const STORE_NAME = 'files';
7+
8+
let dbPromise; // We will not initialize this immediately.
9+
10+
/**
11+
* A server-safe function to initialize the IndexedDB connection.
12+
* It only runs in the browser and only creates the connection once.
13+
*/
14+
const getDb = () => {
15+
// If we are on the server, 'window' will be undefined.
16+
// In that case, we can't do anything, so we return null.
17+
if (typeof window === 'undefined') {
18+
return null;
19+
}
20+
21+
// If the dbPromise hasn't been created yet, create it.
22+
if (!dbPromise) {
23+
dbPromise = openDB(DB_NAME, 1, {
24+
upgrade(db) {
25+
db.createObjectStore(STORE_NAME, { keyPath: 'id' });
26+
},
27+
});
28+
}
29+
30+
return dbPromise;
31+
};
32+
33+
export async function cacheDriveFiles(files) {
34+
const db = getDb();
35+
if (!db) return; // Do nothing if we're on the server.
36+
37+
const dbInstance = await db;
38+
const tx = dbInstance.transaction(STORE_NAME, 'readwrite');
39+
await Promise.all(files.map((file) => tx.store.put(file)));
40+
await tx.done;
41+
console.log('Google Drive files cached successfully.');
42+
}
43+
44+
export async function getCachedDriveFiles() {
45+
const db = getDb();
46+
if (!db) return []; // Return an empty array if on the server.
47+
48+
const dbInstance = await db;
49+
const files = await dbInstance.getAll(STORE_NAME);
50+
console.log('Retrieved files from offline cache.');
51+
return files;
52+
}
53+
54+
export async function clearDriveCache() {
55+
const db = getDb();
56+
if (!db) return; // Do nothing on the server.
57+
58+
const dbInstance = await db;
59+
await dbInstance.clear(STORE_NAME);
60+
console.log('Google Drive offline cache cleared.');
61+
}

src/pages/_app.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { ThemeProvider } from '@/context/ThemeContext';
66
import { NotificationProvider } from '@/system/services/NotificationRegistry';
77
import { SettingsProvider } from '@/context/SettingsContext';
88
import { AuthProvider } from '@/context/AuthContext';
9+
import { DriveProvider } from '@/context/DriveContext';
910

1011
// We will use one function for our app wrapper
1112
export default function MyApp({ Component, pageProps }) {
@@ -15,11 +16,13 @@ export default function MyApp({ Component, pageProps }) {
1516
<AppProvider>
1617
<NotificationProvider>
1718
<SettingsProvider>
18-
<Component {...pageProps} />
19+
<DriveProvider>
20+
<Component {...pageProps} />
21+
</DriveProvider>
1922
</SettingsProvider>
2023
</NotificationProvider>
2124
</AppProvider>
2225
</AuthProvider>
2326
</ThemeProvider>
2427
);
25-
}
28+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { google } from 'googleapis';
2+
import * as cookie from 'cookie';
3+
4+
const oauth2Client = new google.auth.OAuth2(
5+
process.env.GOOGLE_CLIENT_ID,
6+
process.env.GOOGLE_CLIENT_SECRET,
7+
'http://localhost:3000/api/auth/google/callback',
8+
);
9+
10+
export default async function handler(req, res) {
11+
const { code } = req.query;
12+
try {
13+
const { tokens } = await oauth2Client.getToken(code);
14+
res.setHeader(
15+
'Set-Cookie',
16+
cookie.serialize('gdrive_token', tokens.access_token, {
17+
httpOnly: true,
18+
secure: process.env.NODE_ENV !== 'development',
19+
maxAge: 3600, // 1 hour
20+
sameSite: 'lax',
21+
path: '/',
22+
}),
23+
);
24+
res.redirect('/');
25+
} catch (error) {
26+
console.error('Error authenticating with Google:', error);
27+
res.redirect('/?error=google_auth_failed');
28+
}
29+
}

src/pages/api/auth/google/login.js

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
// src/pages/api/auth/google/login.js
2+
3+
import { google } from 'googleapis';
4+
5+
const oauth2Client = new google.auth.OAuth2(
6+
process.env.GOOGLE_CLIENT_ID,
7+
process.env.GOOGLE_CLIENT_SECRET,
8+
'http://localhost:3000/api/auth/google/callback',
9+
);
10+
11+
export default function handler(req, res) {
12+
const scopes = [
13+
'https://www.googleapis.com/auth/drive.file',
14+
'https://www.googleapis.com/auth/userinfo.email',
15+
];
16+
17+
const url = oauth2Client.generateAuthUrl({
18+
access_type: 'offline',
19+
scope: scopes,
20+
});
21+
22+
res.redirect(url);
23+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import * as cookie from 'cookie';
2+
3+
export default function handler(req, res) {
4+
res.setHeader(
5+
'Set-Cookie',
6+
cookie.serialize('gdrive_token', '', {
7+
httpOnly: true,
8+
secure: process.env.NODE_ENV !== 'development',
9+
maxAge: -1,
10+
sameSite: 'lax',
11+
path: '/',
12+
}),
13+
);
14+
res.status(200).json({ message: 'Logged out successfully' });
15+
}

src/pages/api/auth/google/me.js

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
// src/pages/api/auth/google/me.js
2+
3+
import { google } from 'googleapis';
4+
import * as cookie from 'cookie';
5+
6+
export default async function handler(req, res) {
7+
try {
8+
const oauth2Client = new google.auth.OAuth2(
9+
process.env.GOOGLE_CLIENT_ID,
10+
process.env.GOOGLE_CLIENT_SECRET,
11+
'http://localhost:3000/api/auth/google/callback',
12+
);
13+
14+
const cookies = cookie.parse(req.headers.cookie || '');
15+
const accessToken = cookies.gdrive_token;
16+
17+
if (!accessToken) {
18+
return res.status(401).json({ error: 'Not authenticated with Google' });
19+
}
20+
21+
oauth2Client.setCredentials({ access_token: accessToken });
22+
const oauth2 = google.oauth2({ version: 'v2', auth: oauth2Client });
23+
24+
// Fetch the user's profile information
25+
const userInfo = await oauth2.userinfo.get();
26+
27+
res.status(200).json({ email: userInfo.data.email });
28+
} catch (error) {
29+
res.status(500).json({ error: 'Failed to get user info from Google' });
30+
}
31+
}

src/pages/api/files/gdrive.js

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
// src/pages/api/files/gdrive.js
2+
3+
import { google } from 'googleapis';
4+
import * as cookie from 'cookie';
5+
6+
export default async function handler(req, res) {
7+
try {
8+
// --- THIS IS THE FIX ---
9+
// Create a new, clean OAuth2 client for EVERY request.
10+
const oauth2Client = new google.auth.OAuth2(
11+
process.env.GOOGLE_CLIENT_ID,
12+
process.env.GOOGLE_CLIENT_SECRET,
13+
'http://localhost:3000/api/auth/google/callback',
14+
);
15+
16+
const cookies = cookie.parse(req.headers.cookie || '');
17+
const accessToken = cookies.gdrive_token;
18+
19+
if (!accessToken) {
20+
return res.status(401).json({ error: 'Not authenticated with Google' });
21+
}
22+
23+
// Set the credentials for this specific request's client instance.
24+
oauth2Client.setCredentials({ access_token: accessToken });
25+
const drive = google.drive({ version: 'v3', auth: oauth2Client });
26+
27+
const response = await drive.files.list({
28+
pageSize: 100,
29+
fields: 'files(id, name, mimeType, modifiedTime, iconLink, webViewLink)',
30+
q: "'root' in parents and trashed = false",
31+
});
32+
33+
res.status(200).json(response.data.files);
34+
} catch (error) {
35+
// Add a log so we can see any future errors in the terminal
36+
console.error('Error in /api/files/gdrive:', error);
37+
res.status(500).json({ error: 'Failed to list files from Google Drive' });
38+
}
39+
}

0 commit comments

Comments
 (0)