mirror of
https://github.com/mudabbir-ahmad/Media-Aggregation-and-Sorting-System.git
synced 2026-10-07 19:40:20 +00:00
291 lines
10 KiB
JavaScript
291 lines
10 KiB
JavaScript
import * as AuthSession from 'expo-auth-session';
|
|
import * as WebBrowser from 'expo-web-browser';
|
|
import * as Application from 'expo-application';
|
|
import { AuthServiceBase } from './AuthServiceBase';
|
|
|
|
WebBrowser.maybeCompleteAuthSession();
|
|
|
|
// Web client ID — used for token exchange (authorization code → access token).
|
|
// Created in Google Cloud Console as "Web application" type.
|
|
// No redirect URIs need to be registered for this client.
|
|
const WEB_CLIENT_ID = '313090284964-rgq1u7np6ogucu9o97s134n5nc6nj7kf.apps.googleusercontent.com';
|
|
// Android client ID — used for the authorization request on Android.
|
|
// Created in Google Cloud Console as "Android" type with:
|
|
// Package name: com.bobthebob.massphotoapp
|
|
// SHA-1 fingerprint:
|
|
//REMOVE LATER. 59:1C:1A:B2:61:C9:8D:80:C8:E1:96:FE:CA:44:18:CE:91:5E:38:63 IS THE SHA-1 FINGERPRINT FOR THE APP
|
|
// Android clients verify by package + SHA-1, no redirect URI registration needed.
|
|
const ANDROID_CLIENT_ID = '313090284964-pa9p2rs7g60l9t8hr6haee3qbn2a7vl9.apps.googleusercontent.com';
|
|
|
|
const GOOGLE_DISCOVERY = {
|
|
authorizationEndpoint: 'https://accounts.google.com/o/oauth2/v2/auth',
|
|
tokenEndpoint: 'https://oauth2.googleapis.com/token',
|
|
revocationEndpoint: 'https://oauth2.googleapis.com/revoke',
|
|
};
|
|
|
|
const SCOPES = [
|
|
'openid',
|
|
'https://www.googleapis.com/auth/photoslibrary',
|
|
'https://www.googleapis.com/auth/photoslibrary.appendonly',
|
|
'https://www.googleapis.com/auth/userinfo.email',
|
|
'https://www.googleapis.com/auth/userinfo.profile',
|
|
];
|
|
|
|
// Redirect URI uses the Android package name as scheme, matching what
|
|
// expo-auth-session/providers/google generates for installed Android apps.
|
|
// Result: com.bobthebob.massphotoapp:/oauthredirect
|
|
const REDIRECT_URI = `${Application.applicationId}:/oauthredirect`;
|
|
|
|
class GoogleAUTH extends AuthServiceBase {
|
|
constructor() {
|
|
super();
|
|
this.accessToken = null;
|
|
this.refreshToken = null;
|
|
this.expiresAt = null;
|
|
this.email = null;
|
|
this.idToken = null;
|
|
}
|
|
|
|
async authenticate() {
|
|
try {
|
|
if (ANDROID_CLIENT_ID === 'REPLACE_WITH_ANDROID_CLIENT_ID.apps.googleusercontent.com') {
|
|
console.warn('Google Auth: no Android client ID — using placeholder mode');
|
|
this.accessToken = 'placeholder_google_token_' + Date.now();
|
|
this.refreshToken = 'placeholder_refresh_' + Date.now();
|
|
this.expiresAt = Date.now() + 3600000;
|
|
this.email = 'google-user@placeholder.com';
|
|
return {
|
|
success: true,
|
|
accessToken: this.accessToken,
|
|
refreshToken: this.refreshToken,
|
|
email: this.email,
|
|
};
|
|
}
|
|
|
|
console.log('OAuth redirect URI:', REDIRECT_URI);
|
|
|
|
// Auth request uses the Android client ID.
|
|
// Android OAuth clients are verified by package name + SHA-1 signing cert,
|
|
// so Google accepts the package-name scheme redirect without URI registration.
|
|
const authRequest = new AuthSession.AuthRequest({
|
|
clientId: ANDROID_CLIENT_ID,
|
|
scopes: SCOPES,
|
|
redirectUri: REDIRECT_URI,
|
|
responseType: AuthSession.ResponseType.Code,
|
|
usePKCE: true,
|
|
extraParams: { access_type: 'offline', prompt: 'consent' },
|
|
});
|
|
|
|
const result = await authRequest.promptAsync(GOOGLE_DISCOVERY);
|
|
|
|
if (result.type !== 'success') {
|
|
throw new Error('Google authentication was cancelled or failed');
|
|
}
|
|
|
|
// Exchange authorization code for tokens using the Web client ID.
|
|
// Android public clients don't have a client secret — the code_verifier
|
|
// from PKCE is used to verify the exchange instead.
|
|
const tokenBody = new URLSearchParams({
|
|
client_id: WEB_CLIENT_ID,
|
|
code: result.params.code,
|
|
code_verifier: authRequest.codeVerifier,
|
|
grant_type: 'authorization_code',
|
|
redirect_uri: REDIRECT_URI,
|
|
});
|
|
|
|
const tokenResponse = await fetch(GOOGLE_DISCOVERY.tokenEndpoint, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
body: tokenBody.toString(),
|
|
});
|
|
|
|
const tokenData = await tokenResponse.json();
|
|
|
|
if (tokenData.error) {
|
|
throw new Error(tokenData.error_description || tokenData.error);
|
|
}
|
|
|
|
this.accessToken = tokenData.access_token;
|
|
this.refreshToken = tokenData.refresh_token || null;
|
|
this.idToken = tokenData.id_token || null;
|
|
this.expiresAt = Date.now() + (tokenData.expires_in || 3600) * 1000;
|
|
|
|
const userInfo = await this.fetchUserInfo();
|
|
this.email = userInfo?.email || 'google-user';
|
|
|
|
return {
|
|
success: true,
|
|
accessToken: this.accessToken,
|
|
refreshToken: this.refreshToken,
|
|
email: this.email,
|
|
};
|
|
} catch (error) {
|
|
console.error('Google authentication error:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
async fetchUserInfo() {
|
|
try {
|
|
const response = await fetch('https://www.googleapis.com/oauth2/v2/userinfo', {
|
|
headers: { Authorization: `Bearer ${this.accessToken}` },
|
|
});
|
|
if (!response.ok) return null;
|
|
return await response.json();
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
async getAccessToken() {
|
|
if (this.accessToken && this.expiresAt > Date.now()) return this.accessToken;
|
|
if (this.refreshToken) return await this.refreshAccessToken();
|
|
throw new Error('No valid Google access token');
|
|
}
|
|
|
|
async refreshAccessToken() {
|
|
try {
|
|
const body = new URLSearchParams({
|
|
client_id: WEB_CLIENT_ID,
|
|
refresh_token: this.refreshToken,
|
|
grant_type: 'refresh_token',
|
|
});
|
|
|
|
const response = await fetch(GOOGLE_DISCOVERY.tokenEndpoint, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
body: body.toString(),
|
|
});
|
|
|
|
const data = await response.json();
|
|
if (data.error) throw new Error(data.error_description || data.error);
|
|
|
|
this.accessToken = data.access_token;
|
|
this.expiresAt = Date.now() + (data.expires_in || 3600) * 1000;
|
|
return this.accessToken;
|
|
} catch (error) {
|
|
console.error('Token refresh error:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
async revokeAccess() {
|
|
try {
|
|
if (this.accessToken) {
|
|
await fetch(`${GOOGLE_DISCOVERY.revocationEndpoint}?token=${this.accessToken}`, {
|
|
method: 'POST',
|
|
});
|
|
}
|
|
} catch (err) {
|
|
console.error('Revoke error:', err);
|
|
}
|
|
this.accessToken = null;
|
|
this.refreshToken = null;
|
|
this.expiresAt = null;
|
|
this.email = null;
|
|
this.idToken = null;
|
|
return { success: true };
|
|
}
|
|
|
|
async getUserInfo() {
|
|
const info = await this.fetchUserInfo();
|
|
return info || { id: 'google_user', email: this.email || 'unknown', name: 'Google User' };
|
|
}
|
|
|
|
async getPhotos(pageSize = 50, pageToken = null) {
|
|
try {
|
|
const token = await this.getAccessToken();
|
|
const params = new URLSearchParams({ pageSize: String(pageSize) });
|
|
if (pageToken) params.append('pageToken', pageToken);
|
|
|
|
const response = await fetch(
|
|
`https://photoslibrary.googleapis.com/v1/mediaItems?${params.toString()}`,
|
|
{ headers: { Authorization: `Bearer ${token}` } }
|
|
);
|
|
|
|
if (!response.ok) {
|
|
console.error('Google Photos API error:', response.status);
|
|
return { mediaItems: [], nextPageToken: null };
|
|
}
|
|
|
|
const data = await response.json();
|
|
return {
|
|
mediaItems: (data.mediaItems || []).map((item) => ({
|
|
source_service: 'GOOGLE_PHOTOS',
|
|
source_id: item.id,
|
|
title: item.filename || 'Untitled',
|
|
description: item.description || '',
|
|
local_path: item.baseUrl ? `${item.baseUrl}=w400-h400` : null,
|
|
remote_path: item.productUrl || '',
|
|
media_type: item.mimeType?.startsWith('video') ? 'video' : 'photo',
|
|
created_date: item.mediaMetadata?.creationTime || null,
|
|
})),
|
|
nextPageToken: data.nextPageToken || null,
|
|
};
|
|
} catch (error) {
|
|
console.error('Get photos error:', error);
|
|
return { mediaItems: [], nextPageToken: null };
|
|
}
|
|
}
|
|
|
|
async getAlbums(pageSize = 50, pageToken = null) {
|
|
try {
|
|
const token = await this.getAccessToken();
|
|
const params = new URLSearchParams({ pageSize: String(pageSize) });
|
|
if (pageToken) params.append('pageToken', pageToken);
|
|
|
|
const response = await fetch(
|
|
`https://photoslibrary.googleapis.com/v1/albums?${params.toString()}`,
|
|
{ headers: { Authorization: `Bearer ${token}` } }
|
|
);
|
|
|
|
if (!response.ok) return { albums: [], nextPageToken: null };
|
|
const data = await response.json();
|
|
return { albums: data.albums || [], nextPageToken: data.nextPageToken || null };
|
|
} catch (error) {
|
|
console.error('Get albums error:', error);
|
|
return { albums: [], nextPageToken: null };
|
|
}
|
|
}
|
|
|
|
async uploadPhoto(filePath, filename, mimeType = 'image/jpeg') {
|
|
try {
|
|
const token = await this.getAccessToken();
|
|
|
|
const uploadResponse = await fetch('https://photoslibrary.googleapis.com/v1/uploads', {
|
|
method: 'POST',
|
|
headers: {
|
|
Authorization: `Bearer ${token}`,
|
|
'Content-Type': 'application/octet-stream',
|
|
'X-Goog-Upload-File-Name': filename,
|
|
'X-Goog-Upload-Protocol': 'raw',
|
|
},
|
|
body: await fetch(filePath).then((r) => r.blob()),
|
|
});
|
|
|
|
if (!uploadResponse.ok) throw new Error('Upload bytes failed');
|
|
const uploadToken = await uploadResponse.text();
|
|
|
|
const createResponse = await fetch(
|
|
'https://photoslibrary.googleapis.com/v1/mediaItems:batchCreate',
|
|
{
|
|
method: 'POST',
|
|
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
newMediaItems: [{ simpleMediaItem: { uploadToken, fileName: filename } }],
|
|
}),
|
|
}
|
|
);
|
|
|
|
if (!createResponse.ok) throw new Error('Create media item failed');
|
|
return await createResponse.json();
|
|
} catch (error) {
|
|
console.error('Upload photo error:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
}
|
|
|
|
export default GoogleAUTH;
|
|
|