refactored code into components (major cleanup for reused code) added email for local acccount creation and changes to how nas and google photo connections work and added themes

This commit is contained in:
bobbert committed 2026-02-21 15:04:58 +00:00
1 parent c1d7139055
commit 6f4ec66e2e
17 files changed
+1761 -1018

No files matched your search

+192 -34
View File
@@ -1,18 +1,26 @@
import * as AuthSession from 'expo-auth-session';
import * as WebBrowser from 'expo-web-browser';
import { AuthServiceBase } from './AuthServiceBase';
/**
* Google Photos Authentication Service
*
* Uses placeholder tokens for now.
* To enable real Google OAuth, set up a Google Cloud project with Photos Library API,
* create OAuth 2.0 credentials, and replace GOOGLE_CLIENT_ID below.
*
* Real OAuth flow uses expo-auth-session + expo-web-browser.
*/
WebBrowser.maybeCompleteAuthSession();
// TODO: Replace with your real Google Cloud OAuth Client ID
const GOOGLE_CLIENT_ID = 'YOUR_GOOGLE_CLIENT_ID.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',
};
// Google Photos Library API scopes - read and write
const SCOPES = [
'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',
];
class GoogleAUTH extends AuthServiceBase {
constructor() {
super();
@@ -20,21 +28,21 @@ class GoogleAUTH extends AuthServiceBase {
this.refreshToken = null;
this.expiresAt = null;
this.email = null;
this.idToken = null;
}
/**
* Authenticate with Google
* Currently uses placeholder tokens. Replace with real OAuth when client ID is configured.
* Authenticate with Google using expo-auth-session
* Falls back to placeholder tokens when no real client ID is set
*/
async authenticate() {
try {
if (GOOGLE_CLIENT_ID === 'YOUR_GOOGLE_CLIENT_ID.apps.googleusercontent.com') {
// Placeholder mode - no real client ID configured
console.log('Google Auth: Using placeholder mode (no client ID configured)');
this.accessToken = 'placeholder_google_token_' + Date.now();
this.refreshToken = 'placeholder_refresh_' + Date.now();
this.expiresAt = Date.now() + 3600000;
this.email = 'google-user';
this.email = 'google-user@placeholder.com';
return {
success: true,
@@ -44,26 +52,92 @@ class GoogleAUTH extends AuthServiceBase {
};
}
// Real OAuth flow would go here using expo-auth-session
// const discovery = { authorizationEndpoint: 'https://accounts.google.com/o/oauth2/v2/auth', tokenEndpoint: 'https://oauth2.googleapis.com/token' };
// Use AuthSession.makeRedirectUri() and AuthSession.useAuthRequest()
throw new Error('Google OAuth client ID configured but real OAuth flow not yet implemented');
// Real OAuth flow
const redirectUri = AuthSession.makeRedirectUri({ preferLocalhost: true });
const authRequest = new AuthSession.AuthRequest({
clientId: GOOGLE_CLIENT_ID,
scopes: SCOPES,
redirectUri,
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');
}
const tokenResponse = await AuthSession.exchangeCodeAsync(
{
clientId: GOOGLE_CLIENT_ID,
code: result.params.code,
redirectUri,
extraParams: { code_verifier: authRequest.codeVerifier },
},
GOOGLE_DISCOVERY
);
this.accessToken = tokenResponse.accessToken;
this.refreshToken = tokenResponse.refreshToken;
this.idToken = tokenResponse.idToken;
this.expiresAt = tokenResponse.issuedAt
? (tokenResponse.issuedAt + (tokenResponse.expiresIn || 3600)) * 1000
: Date.now() + 3600000;
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 {
console.log('Refreshing Google access token...');
this.accessToken = 'refreshed_google_token_' + Date.now();
this.expiresAt = Date.now() + 3600000;
if (GOOGLE_CLIENT_ID === 'YOUR_GOOGLE_CLIENT_ID.apps.googleusercontent.com') {
this.accessToken = 'refreshed_placeholder_' + Date.now();
this.expiresAt = Date.now() + 3600000;
return this.accessToken;
}
const tokenResponse = await AuthSession.refreshAsync(
{ clientId: GOOGLE_CLIENT_ID, refreshToken: this.refreshToken },
GOOGLE_DISCOVERY
);
this.accessToken = tokenResponse.accessToken;
this.expiresAt = tokenResponse.issuedAt
? (tokenResponse.issuedAt + (tokenResponse.expiresIn || 3600)) * 1000
: Date.now() + 3600000;
return this.accessToken;
} catch (error) {
console.error('Token refresh error:', error);
@@ -72,6 +146,13 @@ class GoogleAUTH extends AuthServiceBase {
}
async revokeAccess() {
try {
if (this.accessToken && GOOGLE_CLIENT_ID !== 'YOUR_GOOGLE_CLIENT_ID.apps.googleusercontent.com') {
await AuthSession.revokeAsync({ token: this.accessToken }, GOOGLE_DISCOVERY);
}
} catch (err) {
console.error('Revoke error:', err);
}
this.accessToken = null;
this.refreshToken = null;
this.expiresAt = null;
@@ -80,31 +161,108 @@ class GoogleAUTH extends AuthServiceBase {
}
async getUserInfo() {
return {
id: 'google_user',
email: this.email || 'unknown',
name: 'Google User',
};
const info = await this.fetchUserInfo();
return info || { id: 'google_user', email: this.email || 'unknown', name: 'Google User' };
}
async getPhotos(pageSize = 50) {
/**
* Fetch media items from Google Photos Library API
*/
async getPhotos(pageSize = 50, pageToken = null) {
try {
await this.getAccessToken();
// In production: call Google Photos API
// https://photoslibrary.googleapis.com/v1/mediaItems
return { mediaItems: [], nextPageToken: null };
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);
throw error;
return { mediaItems: [], nextPageToken: null };
}
}
async getAlbums() {
/**
* Fetch albums from Google Photos Library API
*/
async getAlbums(pageSize = 50, pageToken = null) {
try {
await this.getAccessToken();
return { albums: [], nextPageToken: null };
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 };
}
}
/**
* Upload media to Google Photos Library
*/
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;
}
}