mirror of
https://github.com/mudabbir-ahmad/Media-Aggregation-and-Sorting-System.git
synced 2026-10-07 19:40:20 +00:00
add centralized error handling and improve Google Auth error logging
This commit is contained in:
1 parent
0ef59ba5d3
commit
f6c61ab829
5 files changed
+82
-68
No files matched your search
@@ -42,9 +42,12 @@ async function main() {
|
|||||||
console.log('[1] Fetching user info...');
|
console.log('[1] Fetching user info...');
|
||||||
try {
|
try {
|
||||||
const res = await fetch(USERINFO_URL, { headers });
|
const res = await fetch(USERINFO_URL, { headers });
|
||||||
if (!res.ok) throw new Error(`HTTP ${res.status}: ${await res.text()}`);
|
if (!res.ok) {
|
||||||
|
console.log(` Failed: HTTP ${res.status}: ${await res.text()}\n`);
|
||||||
|
} else {
|
||||||
const user = await res.json();
|
const user = await res.json();
|
||||||
console.log(` Authenticated as: ${user.email} (${user.name})\n`);
|
console.log(` Authenticated as: ${user.email} (${user.name})\n`);
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log(` Failed: ${err.message}\n`);
|
console.log(` Failed: ${err.message}\n`);
|
||||||
}
|
}
|
||||||
@@ -57,7 +60,9 @@ async function main() {
|
|||||||
headers: { ...headers, 'Content-Type': 'application/json' },
|
headers: { ...headers, 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ pageSize: 10 }),
|
body: JSON.stringify({ pageSize: 10 }),
|
||||||
});
|
});
|
||||||
if (!res.ok) throw new Error(`HTTP ${res.status}: ${await res.text()}`);
|
if (!res.ok) {
|
||||||
|
console.log(` Failed: HTTP ${res.status}: ${await res.text()}\n`);
|
||||||
|
} else {
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
const items = data.mediaItems || [];
|
const items = data.mediaItems || [];
|
||||||
console.log(` Found ${items.length} items`);
|
console.log(` Found ${items.length} items`);
|
||||||
@@ -67,6 +72,7 @@ async function main() {
|
|||||||
if (items.length > 5) console.log(` ... and ${items.length - 5} more`);
|
if (items.length > 5) console.log(` ... and ${items.length - 5} more`);
|
||||||
if (data.nextPageToken) console.log(' More pages available');
|
if (data.nextPageToken) console.log(' More pages available');
|
||||||
console.log();
|
console.log();
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log(` Failed: ${err.message}\n`);
|
console.log(` Failed: ${err.message}\n`);
|
||||||
}
|
}
|
||||||
@@ -75,7 +81,9 @@ async function main() {
|
|||||||
console.log('[3] Listing albums...');
|
console.log('[3] Listing albums...');
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${API_BASE}/albums?pageSize=10`, { headers });
|
const res = await fetch(`${API_BASE}/albums?pageSize=10`, { headers });
|
||||||
if (!res.ok) throw new Error(`HTTP ${res.status}: ${await res.text()}`);
|
if (!res.ok) {
|
||||||
|
console.log(` Failed: HTTP ${res.status}: ${await res.text()}\n`);
|
||||||
|
} else {
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
const albums = data.albums || [];
|
const albums = data.albums || [];
|
||||||
console.log(` Found ${albums.length} albums`);
|
console.log(` Found ${albums.length} albums`);
|
||||||
@@ -83,6 +91,7 @@ async function main() {
|
|||||||
console.log(` ${i + 1}. "${album.title}" - ${album.mediaItemsCount || 0} items`);
|
console.log(` ${i + 1}. "${album.title}" - ${album.mediaItemsCount || 0} items`);
|
||||||
});
|
});
|
||||||
console.log();
|
console.log();
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log(` Failed: ${err.message}\n`);
|
console.log(` Failed: ${err.message}\n`);
|
||||||
}
|
}
|
||||||
|
|||||||
+32
-63
@@ -2,6 +2,7 @@ import * as AuthSession from 'expo-auth-session';
|
|||||||
import * as WebBrowser from 'expo-web-browser';
|
import * as WebBrowser from 'expo-web-browser';
|
||||||
import * as Application from 'expo-application';
|
import * as Application from 'expo-application';
|
||||||
import { AuthServiceBase } from './AuthServiceBase';
|
import { AuthServiceBase } from './AuthServiceBase';
|
||||||
|
import { logError } from '../services/ErrorHandler';
|
||||||
|
|
||||||
WebBrowser.maybeCompleteAuthSession();
|
WebBrowser.maybeCompleteAuthSession();
|
||||||
|
|
||||||
@@ -24,7 +25,6 @@ const SCOPES = [
|
|||||||
'https://www.googleapis.com/auth/photoslibrary',
|
'https://www.googleapis.com/auth/photoslibrary',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
||||||
// Redirect URI uses the Android package name as scheme, matching what
|
// Redirect URI uses the Android package name as scheme, matching what
|
||||||
// expo-auth-session/providers/google generates for installed Android apps.
|
// expo-auth-session/providers/google generates for installed Android apps.
|
||||||
// Result: com.bobthebob.massphotoapp:/oauthredirect
|
// Result: com.bobthebob.massphotoapp:/oauthredirect
|
||||||
@@ -37,11 +37,9 @@ class GoogleAUTH extends AuthServiceBase {
|
|||||||
this.refreshToken = null;
|
this.refreshToken = null;
|
||||||
this.expiresAt = null;
|
this.expiresAt = null;
|
||||||
this.email = null;
|
this.email = null;
|
||||||
this.idToken = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async authenticate() {
|
async authenticate() {
|
||||||
try {
|
|
||||||
if (ANDROID_CLIENT_ID === 'REPLACE_WITH_ANDROID_CLIENT_ID.apps.googleusercontent.com') {
|
if (ANDROID_CLIENT_ID === 'REPLACE_WITH_ANDROID_CLIENT_ID.apps.googleusercontent.com') {
|
||||||
console.warn('Google Auth: no Android client ID — using placeholder mode');
|
console.warn('Google Auth: no Android client ID — using placeholder mode');
|
||||||
this.accessToken = 'placeholder_google_token_' + Date.now();
|
this.accessToken = 'placeholder_google_token_' + Date.now();
|
||||||
@@ -59,14 +57,6 @@ class GoogleAUTH extends AuthServiceBase {
|
|||||||
console.log('OAuth redirect URI:', REDIRECT_URI);
|
console.log('OAuth redirect URI:', REDIRECT_URI);
|
||||||
console.log('Requesting scopes:', SCOPES.join(' '));
|
console.log('Requesting scopes:', SCOPES.join(' '));
|
||||||
|
|
||||||
// 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.
|
|
||||||
//
|
|
||||||
// NOTE: Do NOT use include_granted_scopes — we need a clean token with
|
|
||||||
// exactly the scopes listed above. Old tokens may carry stale scopes
|
|
||||||
// (e.g. photoslibrary instead of photoslibrary.readonly) that no longer
|
|
||||||
// work with the Photos Library API.
|
|
||||||
const authRequest = new AuthSession.AuthRequest({
|
const authRequest = new AuthSession.AuthRequest({
|
||||||
clientId: ANDROID_CLIENT_ID,
|
clientId: ANDROID_CLIENT_ID,
|
||||||
scopes: SCOPES,
|
scopes: SCOPES,
|
||||||
@@ -82,12 +72,13 @@ class GoogleAUTH extends AuthServiceBase {
|
|||||||
const result = await authRequest.promptAsync(GOOGLE_DISCOVERY);
|
const result = await authRequest.promptAsync(GOOGLE_DISCOVERY);
|
||||||
|
|
||||||
if (result.type !== 'success') {
|
if (result.type !== 'success') {
|
||||||
throw new Error('Google authentication was cancelled or failed');
|
const err = new Error('Google authentication was cancelled or failed');
|
||||||
|
logError('Google Auth', err);
|
||||||
|
throw err;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Exchange authorization code for tokens using the Android client ID.
|
// Exchange authorization code for tokens.
|
||||||
// Android clients are public (no client secret required) — the PKCE
|
// Android clients are public — PKCE code_verifier secures the exchange.
|
||||||
// code_verifier is used to verify the exchange instead.
|
|
||||||
const tokenBody = new URLSearchParams({
|
const tokenBody = new URLSearchParams({
|
||||||
client_id: ANDROID_CLIENT_ID,
|
client_id: ANDROID_CLIENT_ID,
|
||||||
code: result.params.code,
|
code: result.params.code,
|
||||||
@@ -105,29 +96,17 @@ class GoogleAUTH extends AuthServiceBase {
|
|||||||
const tokenData = await tokenResponse.json();
|
const tokenData = await tokenResponse.json();
|
||||||
|
|
||||||
if (tokenData.error) {
|
if (tokenData.error) {
|
||||||
throw new Error(tokenData.error_description || tokenData.error);
|
const err = new Error(tokenData.error_description || tokenData.error);
|
||||||
|
logError('Google Token Exchange', err);
|
||||||
|
throw err;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.accessToken = tokenData.access_token;
|
this.accessToken = tokenData.access_token;
|
||||||
this.refreshToken = tokenData.refresh_token || null;
|
this.refreshToken = tokenData.refresh_token || null;
|
||||||
this.idToken = tokenData.id_token || null;
|
|
||||||
this.expiresAt = Date.now() + (tokenData.expires_in || 3600) * 1000;
|
this.expiresAt = Date.now() + (tokenData.expires_in || 3600) * 1000;
|
||||||
|
|
||||||
// Log the scopes Google actually granted (the token response includes a `scope` field)
|
|
||||||
console.log('Google token granted scopes:', tokenData.scope || '(none returned)');
|
console.log('Google token granted scopes:', tokenData.scope || '(none returned)');
|
||||||
|
|
||||||
// Also verify via tokeninfo endpoint for a definitive answer
|
|
||||||
try {
|
|
||||||
const infoRes = await fetch(
|
|
||||||
`https://oauth2.googleapis.com/tokeninfo?access_token=${this.accessToken}`
|
|
||||||
);
|
|
||||||
const infoData = await infoRes.json();
|
|
||||||
console.log('Google tokeninfo scopes:', infoData.scope || '(none)');
|
|
||||||
console.log('Google tokeninfo audience:', infoData.aud || '(none)');
|
|
||||||
} catch (e) {
|
|
||||||
console.log('Could not fetch tokeninfo:', e.message);
|
|
||||||
}
|
|
||||||
|
|
||||||
const userInfo = await this.fetchUserInfo();
|
const userInfo = await this.fetchUserInfo();
|
||||||
this.email = userInfo?.email || 'google-user';
|
this.email = userInfo?.email || 'google-user';
|
||||||
|
|
||||||
@@ -137,10 +116,6 @@ class GoogleAUTH extends AuthServiceBase {
|
|||||||
refreshToken: this.refreshToken,
|
refreshToken: this.refreshToken,
|
||||||
email: this.email,
|
email: this.email,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
|
||||||
console.error('Google authentication error:', error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fetchUserInfo() {
|
async fetchUserInfo() {
|
||||||
@@ -162,7 +137,6 @@ class GoogleAUTH extends AuthServiceBase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async refreshAccessToken() {
|
async refreshAccessToken() {
|
||||||
try {
|
|
||||||
const body = new URLSearchParams({
|
const body = new URLSearchParams({
|
||||||
client_id: ANDROID_CLIENT_ID,
|
client_id: ANDROID_CLIENT_ID,
|
||||||
refresh_token: this.refreshToken,
|
refresh_token: this.refreshToken,
|
||||||
@@ -176,15 +150,15 @@ class GoogleAUTH extends AuthServiceBase {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
if (data.error) throw new Error(data.error_description || data.error);
|
if (data.error) {
|
||||||
|
const err = new Error(data.error_description || data.error);
|
||||||
|
logError('Token Refresh', err);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
|
||||||
this.accessToken = data.access_token;
|
this.accessToken = data.access_token;
|
||||||
this.expiresAt = Date.now() + (data.expires_in || 3600) * 1000;
|
this.expiresAt = Date.now() + (data.expires_in || 3600) * 1000;
|
||||||
return this.accessToken;
|
return this.accessToken;
|
||||||
} catch (error) {
|
|
||||||
console.error('Token refresh error:', error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async revokeAccess() {
|
async revokeAccess() {
|
||||||
@@ -195,13 +169,12 @@ class GoogleAUTH extends AuthServiceBase {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Revoke error:', err);
|
logError('Revoke', err);
|
||||||
}
|
}
|
||||||
this.accessToken = null;
|
this.accessToken = null;
|
||||||
this.refreshToken = null;
|
this.refreshToken = null;
|
||||||
this.expiresAt = null;
|
this.expiresAt = null;
|
||||||
this.email = null;
|
this.email = null;
|
||||||
this.idToken = null;
|
|
||||||
return { success: true };
|
return { success: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -216,7 +189,6 @@ class GoogleAUTH extends AuthServiceBase {
|
|||||||
|
|
||||||
// Use the mediaItems:search endpoint (POST) instead of the deprecated
|
// Use the mediaItems:search endpoint (POST) instead of the deprecated
|
||||||
// GET /v1/mediaItems which Google shut down in 2025.
|
// GET /v1/mediaItems which Google shut down in 2025.
|
||||||
// An empty filters body returns ALL photos, ordered by creation time.
|
|
||||||
const body = { pageSize };
|
const body = { pageSize };
|
||||||
if (pageToken) body.pageToken = pageToken;
|
if (pageToken) body.pageToken = pageToken;
|
||||||
|
|
||||||
@@ -242,23 +214,16 @@ class GoogleAUTH extends AuthServiceBase {
|
|||||||
errorJson?.error?.message || errorBody || `HTTP ${response.status}`;
|
errorJson?.error?.message || errorBody || `HTTP ${response.status}`;
|
||||||
const googleStatus = errorJson?.error?.status || '';
|
const googleStatus = errorJson?.error?.status || '';
|
||||||
|
|
||||||
console.error(
|
logError('Google Photos API', `${response.status} (${googleStatus}): ${googleMessage}`);
|
||||||
`Google Photos API ${response.status} (${googleStatus}):`,
|
|
||||||
googleMessage,
|
|
||||||
);
|
|
||||||
|
|
||||||
// 401 → token expired, try one refresh
|
|
||||||
if (response.status === 401 && this.refreshToken && !pageToken) {
|
if (response.status === 401 && this.refreshToken && !pageToken) {
|
||||||
console.log('Token expired, refreshing and retrying…');
|
console.log('Token expired, refreshing and retrying…');
|
||||||
await this.refreshAccessToken();
|
await this.refreshAccessToken();
|
||||||
return this.getPhotos(pageSize, pageToken);
|
return this.getPhotos(pageSize, pageToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 403 → scope or API issue
|
|
||||||
if (response.status === 403) {
|
if (response.status === 403) {
|
||||||
const isScopeProblem =
|
const isScopeProblem = googleMessage.toLowerCase().includes('scope');
|
||||||
googleMessage.toLowerCase().includes('scope');
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
mediaItems: [],
|
mediaItems: [],
|
||||||
nextPageToken: null,
|
nextPageToken: null,
|
||||||
@@ -292,7 +257,7 @@ class GoogleAUTH extends AuthServiceBase {
|
|||||||
nextPageToken: data.nextPageToken || null,
|
nextPageToken: data.nextPageToken || null,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Get photos error:', error);
|
logError('Get Photos', error);
|
||||||
return { mediaItems: [], nextPageToken: null };
|
return { mediaItems: [], nextPageToken: null };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -310,21 +275,19 @@ class GoogleAUTH extends AuthServiceBase {
|
|||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const errorBody = await response.text();
|
const errorBody = await response.text();
|
||||||
console.error(`Google Albums API error ${response.status}:`, errorBody);
|
logError('Google Albums API', `${response.status}: ${errorBody}`);
|
||||||
return { albums: [], nextPageToken: null };
|
return { albums: [], nextPageToken: null };
|
||||||
}
|
}
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
return { albums: data.albums || [], nextPageToken: data.nextPageToken || null };
|
return { albums: data.albums || [], nextPageToken: data.nextPageToken || null };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Get albums error:', error);
|
logError('Get Albums', error);
|
||||||
return { albums: [], nextPageToken: null };
|
return { albums: [], nextPageToken: null };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// NOTE: Upload requires the `photoslibrary.appendonly` scope which is not
|
// NOTE: Upload requires the `photoslibrary.appendonly` scope.
|
||||||
// requested by default (read-only mode). Re-authenticate with upload scopes
|
async uploadPhoto(filePath, filename) {
|
||||||
// before calling this method.
|
|
||||||
async uploadPhoto(filePath, filename, mimeType = 'image/jpeg') {
|
|
||||||
try {
|
try {
|
||||||
const token = await this.getAccessToken();
|
const token = await this.getAccessToken();
|
||||||
|
|
||||||
@@ -339,7 +302,10 @@ class GoogleAUTH extends AuthServiceBase {
|
|||||||
body: await fetch(filePath).then((r) => r.blob()),
|
body: await fetch(filePath).then((r) => r.blob()),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!uploadResponse.ok) throw new Error('Upload bytes failed');
|
if (!uploadResponse.ok) {
|
||||||
|
logError('Upload Photo', 'Upload bytes failed: HTTP ' + uploadResponse.status);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
const uploadToken = await uploadResponse.text();
|
const uploadToken = await uploadResponse.text();
|
||||||
|
|
||||||
const createResponse = await fetch(
|
const createResponse = await fetch(
|
||||||
@@ -353,11 +319,14 @@ class GoogleAUTH extends AuthServiceBase {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!createResponse.ok) throw new Error('Create media item failed');
|
if (!createResponse.ok) {
|
||||||
|
logError('Upload Photo', 'Create media item failed: HTTP ' + createResponse.status);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
return await createResponse.json();
|
return await createResponse.json();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Upload photo error:', error);
|
logError('Upload Photo', error);
|
||||||
throw error;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ const MediaViewer = ({ visible, media, allMedia, onClose }) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[activeIndex, mediaList]
|
[activeIndex, mediaList, thumbListRef]
|
||||||
);
|
);
|
||||||
|
|
||||||
const onThumbPress = (index) => {
|
const onThumbPress = (index) => {
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ const ALBUM_COLUMNS = 2;
|
|||||||
const ALBUM_GAP = 12;
|
const ALBUM_GAP = 12;
|
||||||
const albumSize = (width - ALBUM_GAP * (ALBUM_COLUMNS + 1)) / ALBUM_COLUMNS;
|
const albumSize = (width - ALBUM_GAP * (ALBUM_COLUMNS + 1)) / ALBUM_COLUMNS;
|
||||||
|
|
||||||
const CollectionsScreen = ({ route, navigation }) => {
|
const CollectionsScreen = ({ route }) => {
|
||||||
const userId = route?.params?.userId;
|
const userId = route?.params?.userId;
|
||||||
const { theme } = useTheme();
|
const { theme } = useTheme();
|
||||||
const [sections, setSections] = useState([]);
|
const [sections, setSections] = useState([]);
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import { Alert } from 'react-native';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Centralised error handler for the app.
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* import { logError, alertError } from '../services/ErrorHandler';
|
||||||
|
*
|
||||||
|
* logError('Google Auth', error); // console.error only
|
||||||
|
* alertError('Upload Failed', error); // Alert + console.error
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log an error to the console with a contextual tag.
|
||||||
|
* @param {string} tag – short label, e.g. 'Google Auth', 'NAS'
|
||||||
|
* @param {Error|string} error
|
||||||
|
*/
|
||||||
|
export const logError = (tag, error) => {
|
||||||
|
const message = error instanceof Error ? error.message : String(error);
|
||||||
|
console.error(`[${tag}] ${message}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show a user-facing Alert AND log to the console.
|
||||||
|
* @param {string} title – Alert title
|
||||||
|
* @param {Error|string} error
|
||||||
|
* @param {Array} [buttons] – optional Alert button array
|
||||||
|
*/
|
||||||
|
export const alertError = (title, error, buttons) => {
|
||||||
|
const message = error instanceof Error ? error.message : String(error);
|
||||||
|
console.error(`[${title}] ${message}`);
|
||||||
|
Alert.alert(title, message, buttons || [{ text: 'OK' }]);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default { logError, alertError };
|
||||||
|
|
||||||
Reference in new issue
Block a user