added NAS configuration file; updated MediaViewer to support thumbnail carousel; improved error handling in NASAPI and NASAUTH; refactored various comments for clarity

This commit is contained in:
bobbert committed 2026-02-22 19:19:55 +00:00
1 parent 9519b32d68
commit f8e11fc368
27 files changed
+1036 -558

No files matched your search

+6 -24
View File
@@ -1,7 +1,4 @@
/**
* Abstract base class for authentication services
* This provides a common interface for different authentication providers
*/
// Abstract base class for authentication services — provides a common interface for all providers
export class AuthServiceBase {
constructor() {
if (new.target === AuthServiceBase) {
@@ -9,26 +6,11 @@ export class AuthServiceBase {
}
}
async authenticate() {
throw new Error('authenticate() must be implemented');
}
async getAccessToken() {
throw new Error('getAccessToken() must be implemented');
}
async refreshToken() {
throw new Error('refreshToken() must be implemented');
}
async revokeAccess() {
throw new Error('revokeAccess() must be implemented');
}
async getUserInfo() {
throw new Error('getUserInfo() must be implemented');
}
async authenticate() { throw new Error('authenticate() must be implemented'); }
async getAccessToken() { throw new Error('getAccessToken() must be implemented'); }
async refreshToken() { throw new Error('refreshToken() must be implemented'); }
async revokeAccess() { throw new Error('revokeAccess() must be implemented'); }
async getUserInfo() { throw new Error('getUserInfo() must be implemented'); }
}
export default AuthServiceBase;
+12 -19
View File
@@ -4,7 +4,7 @@ import { AuthServiceBase } from './AuthServiceBase';
WebBrowser.maybeCompleteAuthSession();
// TODO: Replace with your real Google Cloud OAuth Client ID
// Replace with your real Google Cloud OAuth Client ID (see ignorefiles/GOOGLE_OAUTH_SETUP.md)
const GOOGLE_CLIENT_ID = 'YOUR_GOOGLE_CLIENT_ID.apps.googleusercontent.com';
const GOOGLE_DISCOVERY = {
@@ -13,7 +13,7 @@ const GOOGLE_DISCOVERY = {
revocationEndpoint: 'https://oauth2.googleapis.com/revoke',
};
// Google Photos Library API scopes - read and write
// Google Photos Library API scopes — read, write, and user profile
const SCOPES = [
'https://www.googleapis.com/auth/photoslibrary',
'https://www.googleapis.com/auth/photoslibrary.appendonly',
@@ -31,19 +31,15 @@ class GoogleAUTH extends AuthServiceBase {
this.idToken = null;
}
/**
* Authenticate with Google using expo-auth-session
* Falls back to placeholder tokens when no real client ID is set
*/
// Authenticate via Google OAuth; uses placeholder tokens when no real client ID is configured
async authenticate() {
try {
if (GOOGLE_CLIENT_ID === 'YOUR_GOOGLE_CLIENT_ID.apps.googleusercontent.com') {
console.log('Google Auth: Using placeholder mode (no client ID configured)');
console.log('Google Auth: 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@placeholder.com';
return {
success: true,
accessToken: this.accessToken,
@@ -52,8 +48,10 @@ class GoogleAUTH extends AuthServiceBase {
};
}
// Real OAuth flow
const redirectUri = AuthSession.makeRedirectUri({ preferLocalhost: true });
const redirectUri = AuthSession.makeRedirectUri({
scheme: 'media-aggregation',
preferLocalhost: true,
});
const authRequest = new AuthSession.AuthRequest({
clientId: GOOGLE_CLIENT_ID,
@@ -114,6 +112,7 @@ class GoogleAUTH extends AuthServiceBase {
}
}
// Return a valid access token, refreshing if expired
async getAccessToken() {
if (this.accessToken && this.expiresAt > Date.now()) return this.accessToken;
if (this.refreshToken) return await this.refreshAccessToken();
@@ -165,9 +164,7 @@ class GoogleAUTH extends AuthServiceBase {
return info || { id: 'google_user', email: this.email || 'unknown', name: 'Google User' };
}
/**
* Fetch media items from Google Photos Library API
*/
// Fetch media items from the Google Photos Library API
async getPhotos(pageSize = 50, pageToken = null) {
try {
const token = await this.getAccessToken();
@@ -204,9 +201,7 @@ class GoogleAUTH extends AuthServiceBase {
}
}
/**
* Fetch albums from Google Photos Library API
*/
// Fetch album list from the Google Photos Library API
async getAlbums(pageSize = 50, pageToken = null) {
try {
const token = await this.getAccessToken();
@@ -227,9 +222,7 @@ class GoogleAUTH extends AuthServiceBase {
}
}
/**
* Upload media to Google Photos Library
*/
// Upload a photo to Google Photos Library
async uploadPhoto(filePath, filename, mimeType = 'image/jpeg') {
try {
const token = await this.getAccessToken();
+6 -25
View File
@@ -1,10 +1,6 @@
import { AuthServiceBase } from './AuthServiceBase';
/**
* iCloud Photos Authentication Service
* Placeholder - Apple does not provide a public iCloud Photos API.
* This is kept for future integration if/when Apple provides API access.
*/
// iCloud Photos placeholder — Apple does not provide a public iCloud Photos API
class IOSAUTH extends AuthServiceBase {
constructor() {
super();
@@ -12,13 +8,8 @@ class IOSAUTH extends AuthServiceBase {
}
async authenticate() {
console.log('iCloud Auth: Placeholder - no Apple API available');
this.accessToken = 'icloud_placeholder_' + Date.now();
return {
success: true,
accessToken: this.accessToken,
message: 'iCloud placeholder - full integration pending',
};
return { success: true, accessToken: this.accessToken, message: 'iCloud placeholder — full integration pending' };
}
async getAccessToken() {
@@ -26,26 +17,16 @@ class IOSAUTH extends AuthServiceBase {
throw new Error('Not authenticated with iCloud');
}
async refreshToken() {
return this.authenticate();
}
async refreshToken() { return this.authenticate(); }
async revokeAccess() {
this.accessToken = null;
return { success: true };
}
async getUserInfo() {
return { id: 'icloud_placeholder', name: 'iCloud User' };
}
async getPhotos() {
return { mediaItems: [], nextPageToken: null };
}
async getAlbums() {
return { albums: [], nextPageToken: null };
}
async getUserInfo() { return { id: 'icloud_placeholder', name: 'iCloud User' }; }
async getPhotos() { return { mediaItems: [], nextPageToken: null }; }
async getAlbums() { return { albums: [], nextPageToken: null }; }
}
export default IOSAUTH;
+34 -154
View File
@@ -1,12 +1,7 @@
import { AuthServiceBase } from './AuthServiceBase';
import NASAPI from '../API/NASAPI';
/**
* NAS SFTP Authentication Service
*
* Uses NASAPI for direct NAS connection management
* No backend server required
*/
// NAS SFTP authentication — connects to the backend server via NASAPI
class NASAUTH extends AuthServiceBase {
constructor() {
super();
@@ -19,17 +14,9 @@ class NASAUTH extends AuthServiceBase {
this.isConnected = false;
}
/**
* Authenticate with NAS/SFTP server
*/
async authenticate(host, port = 22, username, password, photoFolder = '/Photos') {
try {
// Validate input
if (!host || !username || !password) {
throw new Error('Host, username, and password are required');
}
console.log(`Connecting to NAS at ${host}:${port}...`);
if (!host || !username || !password) throw new Error('Host, username, and password are required');
this.host = host;
this.port = port || 22;
@@ -37,17 +24,10 @@ class NASAUTH extends AuthServiceBase {
this.password = password;
this.photoFolder = photoFolder;
// Connect via NASAPI
const result = await this.nasAPI.connect(host, this.port, username, password, photoFolder);
await this.nasAPI.connect(host, this.port, username, password, photoFolder);
this.isConnected = true;
return {
success: true,
message: `NAS configured: ${host}`,
host: this.host,
username: this.username,
};
return { success: true, message: `NAS configured: ${host}`, host: this.host, username: this.username };
} catch (error) {
console.error('NAS authentication error:', error.message);
this.isConnected = false;
@@ -55,167 +35,67 @@ class NASAUTH extends AuthServiceBase {
}
}
/**
* Get current access/connection status
*/
async getAccessToken() {
if (!this.isConnected) {
throw new Error('Not connected to NAS');
}
return {
host: this.host,
username: this.username,
isConnected: this.isConnected,
};
if (!this.isConnected) throw new Error('Not connected to NAS');
return { host: this.host, username: this.username, isConnected: this.isConnected };
}
/**
* Refresh connection (reconnect if needed)
*/
async refreshToken() {
try {
if (this.host && this.username && this.password) {
return await this.authenticate(
this.host,
this.port,
this.username,
this.password,
this.photoFolder
);
}
throw new Error('No NAS credentials available for reconnection');
} catch (error) {
console.error('Connection refresh error:', error);
throw error;
if (this.host && this.username && this.password) {
return await this.authenticate(this.host, this.port, this.username, this.password, this.photoFolder);
}
throw new Error('No NAS credentials available for reconnection');
}
/**
* Disconnect from NAS
*/
async revokeAccess() {
try {
await this.nasAPI.disconnect();
this.isConnected = false;
this.host = null;
this.username = null;
this.password = null;
this.photoFolder = null;
return {
success: true,
message: 'NAS connection closed',
};
} catch (error) {
console.error('Disconnect error:', error.message);
this.isConnected = false;
throw error;
}
this.isConnected = false;
this.host = null;
this.username = null;
this.password = null;
this.photoFolder = null;
return { success: true, message: 'NAS connection closed' };
}
/**
* Get NAS server information
*/
async getUserInfo() {
try {
if (!this.isConnected) {
throw new Error('Not connected to NAS');
}
return {
host: this.host,
username: this.username,
port: this.port,
photoFolder: this.photoFolder,
connectedAt: new Date().toISOString(),
serverType: 'NAS SFTP',
};
} catch (error) {
console.error('Get user info error:', error);
throw error;
}
if (!this.isConnected) throw new Error('Not connected to NAS');
return {
host: this.host,
username: this.username,
port: this.port,
photoFolder: this.photoFolder,
connectedAt: new Date().toISOString(),
serverType: 'NAS SFTP',
};
}
/**
* List directories on NAS
*/
async listDirectory(remotePath = '/') {
try {
if (!this.isConnected) {
throw new Error('Not connected to NAS');
}
return await this.nasAPI.listDirectory(remotePath);
} catch (error) {
console.error('List directory error:', error);
throw error;
}
if (!this.isConnected) throw new Error('Not connected to NAS');
return await this.nasAPI.listDirectory(remotePath);
}
/**
* Upload file to NAS
*/
async uploadFile(localPath, remotePath) {
try {
if (!this.isConnected) {
throw new Error('Not connected to NAS');
}
return await this.nasAPI.uploadFile(localPath, remotePath);
} catch (error) {
console.error('Upload error:', error);
throw error;
}
if (!this.isConnected) throw new Error('Not connected to NAS');
return await this.nasAPI.uploadFile(localPath, remotePath);
}
/**
* Get photos from designated NAS photo folder
*/
async getPhotos(photoFolder = null) {
try {
if (!this.isConnected) {
throw new Error('Not connected to NAS');
}
const folder = photoFolder || this.photoFolder || '/Photos';
return await this.nasAPI.getPhotos(folder);
} catch (error) {
console.error('Get photos error:', error);
throw error;
}
if (!this.isConnected) throw new Error('Not connected to NAS');
return await this.nasAPI.getPhotos(photoFolder || this.photoFolder || '/Photos');
}
/**
* Create directory on NAS
*/
async createDirectory(remotePath) {
try {
if (!this.isConnected) {
throw new Error('Not connected to NAS');
}
return await this.nasAPI.createDirectory(remotePath);
} catch (error) {
console.error('Create directory error:', error);
throw error;
}
if (!this.isConnected) throw new Error('Not connected to NAS');
return await this.nasAPI.createDirectory(remotePath);
}
/**
* Delete file from NAS
*/
async deleteFile(remotePath) {
try {
if (!this.isConnected) {
throw new Error('Not connected to NAS');
}
return await this.nasAPI.deleteFile(remotePath);
} catch (error) {
console.error('Delete file error:', error);
throw error;
}
if (!this.isConnected) throw new Error('Not connected to NAS');
return await this.nasAPI.deleteFile(remotePath);
}
}