slight refactor of all the code, made it so that login and onboarding works, need to add system to validate connections to google photos + NAS before just blindly accepting

Added library home page and navigation bar at the bottom, using emojis as icon placeholders, album page also works.

elipses for settings and icon also work.
This commit is contained in:
bobbert committed 2026-02-21 13:52:28 +00:00
1 parent 37511780b7
commit c1d7139055
17 files changed
+1431 -10040

No files matched your search

+31 -54
View File
@@ -1,93 +1,70 @@
import * as Crypto from 'expo-crypto';
/**
* Credential Encryption Service
*
* Encrypts sensitive credentials before storing in database
* Uses base64 encoding (React Native compatible)
* For production, use a proper encryption library like react-native-aes-crypto
* Uses expo-crypto for password hashing (SHA-256)
* Uses base64 encoding for storing account data objects in the database
*/
class CredentialEncryption {
constructor() {
// Simple key for demo - in production, use proper key management
this.encryptionKey = 'photo-app-secret-key-2026';
}
/**
* Base64 encode a string (React Native compatible)
* Uses btoa which is available in React Native
* Hash a password using SHA-256 via expo-crypto
* Returns a hex digest string
*/
base64Encode(str) {
async hashPassword(password) {
try {
// btoa is available in React Native
return btoa(unescape(encodeURIComponent(str)));
if (!password) throw new Error('Password is required');
const digest = await Crypto.digestStringAsync(
Crypto.CryptoDigestAlgorithm.SHA256,
password
);
return digest;
} catch (error) {
console.error('Base64 encode error:', error);
throw new Error('Failed to encode data');
console.error('Password hashing error:', error);
throw new Error('Failed to hash password: ' + error.message);
}
}
/**
* Base64 decode a string (React Native compatible)
* Uses atob which is available in React Native
* Verify a password against a stored hash
*/
base64Decode(str) {
async verifyPassword(password, storedHash) {
try {
// atob is available in React Native
return decodeURIComponent(escape(atob(str)));
const hash = await this.hashPassword(password);
return hash === storedHash;
} catch (error) {
console.error('Base64 decode error:', error);
throw new Error('Failed to decode data');
console.error('Password verification error:', error);
throw new Error('Failed to verify password: ' + error.message);
}
}
/**
* Encrypt credentials before storing
* Converts object to JSON string, then base64 encodes it
* Encrypt an object for database storage (base64-encoded JSON)
*/
encrypt(data) {
try {
if (!data) {
throw new Error('No data provided to encrypt');
}
if (!data) throw new Error('No data provided to encrypt');
const jsonString = JSON.stringify(data);
const encoded = this.base64Encode(jsonString);
console.log('✓ Credentials encrypted successfully');
// btoa is available in React Native / Hermes
const encoded = btoa(unescape(encodeURIComponent(jsonString)));
return encoded;
} catch (error) {
console.error('Encryption error:', error);
throw new Error('Failed to encrypt credentials: ' + error.message);
throw new Error('Failed to encrypt data: ' + error.message);
}
}
/**
* Decrypt credentials after retrieving
* Base64 decodes, then parses JSON back to object
* Decrypt a base64-encoded JSON string back to an object
*/
decrypt(encryptedData) {
try {
if (!encryptedData) {
return null;
}
const jsonString = this.base64Decode(encryptedData);
const data = JSON.parse(jsonString);
console.log('✓ Credentials decrypted successfully');
return data;
if (!encryptedData) return null;
const jsonString = decodeURIComponent(escape(atob(encryptedData)));
return JSON.parse(jsonString);
} catch (error) {
console.error('Decryption error:', error);
throw new Error('Failed to decrypt credentials: ' + error.message);
}
}
/**
* Check if data is already encrypted (base64)
*/
isEncrypted(data) {
if (typeof data !== 'string') return false;
try {
this.base64Decode(data);
return true;
} catch {
return false;
throw new Error('Failed to decrypt data: ' + error.message);
}
}
}
+74 -188
View File
@@ -6,37 +6,34 @@ let initPromise = null;
const DatabaseService = {
/**
* Initialize the database and create tables if they don't exist
* Initialize the database and create tables
*/
initDatabase: async () => {
if (initPromise) {
return initPromise;
}
if (initPromise) return initPromise;
initPromise = (async () => {
try {
if (db) {
return true;
}
if (db) return true;
if (typeof SQLite.openDatabaseAsync !== 'function') {
throw new Error('SQLite openDatabaseAsync API not available');
}
db = await SQLite.openDatabaseAsync('PhotosApp.db');
if (!db) {
throw new Error('SQLite openDatabaseAsync returned null');
}
await db.execAsync(
`CREATE TABLE IF NOT EXISTS users (
db = await SQLite.openDatabaseAsync('PhotosApp.db');
if (!db) throw new Error('SQLite openDatabaseAsync returned null');
await db.execAsync(`
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT UNIQUE NOT NULL,
email TEXT UNIQUE NOT NULL,
password TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
)`
);
)
`);
await db.execAsync(
`CREATE TABLE IF NOT EXISTS linked_accounts (
await db.execAsync(`
CREATE TABLE IF NOT EXISTS linked_accounts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
service_type TEXT NOT NULL,
@@ -45,11 +42,11 @@ const DatabaseService = {
is_default INTEGER DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY(user_id) REFERENCES users(id)
)`
);
)
`);
await db.execAsync(
`CREATE TABLE IF NOT EXISTS media (
await db.execAsync(`
CREATE TABLE IF NOT EXISTS media (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
source_service TEXT NOT NULL,
@@ -60,54 +57,40 @@ const DatabaseService = {
remote_path TEXT,
media_type TEXT,
created_date DATETIME,
uploaded_to_default INTEGER DEFAULT 0,
synced INTEGER DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY(user_id) REFERENCES users(id)
)`
);
await db.execAsync(
`CREATE TABLE IF NOT EXISTS sync_logs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
service_type TEXT NOT NULL,
status TEXT,
message TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY(user_id) REFERENCES users(id)
)`
);
)
`);
console.log('Database initialized successfully');
return true;
} catch (error) {
console.error('Database initialization error:', error);
db = null;
initPromise = null;
throw error;
}
})();
return initPromise;
},
/**
* Ensure the database is initialized before use
*/
ensureDb: async () => {
await DatabaseService.initDatabase();
if (!db) {
throw new Error('Database not initialized');
}
if (!db) throw new Error('Database not initialized');
},
/**
* Create a new user account
* Create a new user with hashed password
*/
createUser: async (username, email, password) => {
try {
await DatabaseService.ensureDb();
const hashedPassword = await CredentialEncryption.hashPassword(password);
const result = await db.runAsync(
'INSERT INTO users (username, email, password) VALUES (?, ?, ?)',
[username, email, password]
[username, email, hashedPassword]
);
return result.lastInsertRowId;
} catch (error) {
@@ -117,16 +100,21 @@ const DatabaseService = {
},
/**
* Get user by email and password
* Login user by username or email, verify hashed password
*/
loginUser: async (email, password) => {
loginUser: async (identifier, password) => {
try {
await DatabaseService.ensureDb();
const row = await db.getFirstAsync(
'SELECT * FROM users WHERE email = ? AND password = ?',
[email, password]
'SELECT * FROM users WHERE username = ? OR email = ?',
[identifier, identifier]
);
return row || null;
if (!row) return null;
const valid = await CredentialEncryption.verifyPassword(password, row.password);
if (!valid) return null;
return row;
} catch (error) {
console.error('Error logging in user:', error);
throw error;
@@ -134,14 +122,14 @@ const DatabaseService = {
},
/**
* Check if user exists by email
* Check if username or email already exists
*/
userExists: async (email) => {
userExists: async (username, email) => {
try {
await DatabaseService.ensureDb();
const row = await db.getFirstAsync(
'SELECT id FROM users WHERE email = ?',
[email]
'SELECT id FROM users WHERE username = ? OR email = ?',
[username, email]
);
return !!row;
} catch (error) {
@@ -150,19 +138,11 @@ const DatabaseService = {
}
},
/**
* Get user by ID
*/
getUserById: async (userId) => {
try {
await DatabaseService.ensureDb();
if (!userId && userId !== 0) {
throw new Error('User ID is required and cannot be null');
}
const row = await db.getFirstAsync(
'SELECT * FROM users WHERE id = ?',
[userId]
);
if (!userId && userId !== 0) throw new Error('User ID is required');
const row = await db.getFirstAsync('SELECT * FROM users WHERE id = ?', [userId]);
return row || null;
} catch (error) {
console.error('Error getting user:', error);
@@ -171,58 +151,41 @@ const DatabaseService = {
},
/**
* Link an external account to user
* Link an external service account to a user
*/
linkAccount: async (userId, serviceType, accountData, isDefault = false) => {
try {
await DatabaseService.ensureDb();
// Validate inputs
if (!userId) {
throw new Error('User ID is required');
}
if (!serviceType) {
throw new Error('Service type is required');
}
if (!userId) throw new Error('User ID is required');
if (!serviceType) throw new Error('Service type is required');
if (!accountData || typeof accountData !== 'object') {
throw new Error('Account data must be a valid object');
}
console.log('📝 Linking account:', { userId, serviceType, dataKeys: Object.keys(accountData) });
// Encrypt account data
const encryptedData = CredentialEncryption.encrypt(accountData);
if (!encryptedData) throw new Error('Encryption returned empty data');
if (!encryptedData) {
throw new Error('Encryption returned empty data');
}
console.log('✓ Account data encrypted, storing in database...');
const result = await db.runAsync(
`INSERT INTO linked_accounts (user_id, service_type, account_data, is_default)
VALUES (?, ?, ?, ?)`,
[userId, serviceType, encryptedData, isDefault ? 1 : 0]
// Remove existing account of same type first
await db.runAsync(
'DELETE FROM linked_accounts WHERE user_id = ? AND service_type = ?',
[userId, serviceType]
);
console.log('✓ Account linked successfully, ID:', result.lastInsertRowId);
const result = await db.runAsync(
'INSERT INTO linked_accounts (user_id, service_type, account_data, is_default) VALUES (?, ?, ?, ?)',
[userId, serviceType, encryptedData, isDefault ? 1 : 0]
);
return result.lastInsertRowId;
} catch (error) {
console.error('Error linking account:', error);
throw new Error(`Failed to link account: ${error.message}`);
throw new Error('Failed to link account: ' + error.message);
}
},
/**
* Get all linked accounts for user
*/
getLinkedAccounts: async (userId) => {
try {
await DatabaseService.ensureDb();
if (!userId && userId !== 0) {
throw new Error('User ID is required and cannot be null');
}
if (!userId && userId !== 0) throw new Error('User ID is required');
const rows = await db.getAllAsync(
'SELECT * FROM linked_accounts WHERE user_id = ? AND is_enabled = 1',
[userId]
@@ -237,9 +200,6 @@ const DatabaseService = {
}
},
/**
* Get linked account by service type
*/
getLinkedAccountByService: async (userId, serviceType) => {
try {
await DatabaseService.ensureDb();
@@ -248,39 +208,24 @@ const DatabaseService = {
[userId, serviceType]
);
if (!row) return null;
return {
...row,
account_data: CredentialEncryption.decrypt(row.account_data),
};
return { ...row, account_data: CredentialEncryption.decrypt(row.account_data) };
} catch (error) {
console.error('Error getting linked account:', error);
throw error;
}
},
/**
* Set default service for uploads
*/
setDefaultService: async (userId, accountId) => {
try {
await DatabaseService.ensureDb();
await db.runAsync(
'UPDATE linked_accounts SET is_default = 0 WHERE user_id = ?',
[userId]
);
await db.runAsync(
'UPDATE linked_accounts SET is_default = 1 WHERE id = ? AND user_id = ?',
[accountId, userId]
);
await db.runAsync('UPDATE linked_accounts SET is_default = 0 WHERE user_id = ?', [userId]);
await db.runAsync('UPDATE linked_accounts SET is_default = 1 WHERE id = ? AND user_id = ?', [accountId, userId]);
} catch (error) {
console.error('Error setting default service:', error);
throw error;
}
},
/**
* Get default service for user
*/
getDefaultService: async (userId) => {
try {
await DatabaseService.ensureDb();
@@ -289,50 +234,22 @@ const DatabaseService = {
[userId]
);
if (!row) return null;
return {
...row,
account_data: CredentialEncryption.decrypt(row.account_data),
};
return { ...row, account_data: CredentialEncryption.decrypt(row.account_data) };
} catch (error) {
console.error('Error getting default service:', error);
throw error;
}
},
/**
* Add media to database
*/
addMedia: async (
userId,
sourceService,
sourceId,
title,
description,
localPath,
remotePath,
mediaType,
createdDate
) => {
addMedia: async (userId, sourceService, sourceId, title, description, localPath, remotePath, mediaType, createdDate) => {
try {
await DatabaseService.ensureDb();
if (!userId || !sourceService) {
throw new Error('userId and sourceService are required');
}
if (!userId || !sourceService) throw new Error('userId and sourceService are required');
const result = await db.runAsync(
`INSERT INTO media
(user_id, source_service, source_id, title, description, local_path, remote_path, media_type, created_date)
`INSERT INTO media (user_id, source_service, source_id, title, description, local_path, remote_path, media_type, created_date)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[
userId,
sourceService,
sourceId || null,
title || null,
description || null,
localPath || null,
remotePath || null,
mediaType || null,
createdDate || null,
]
[userId, sourceService, sourceId || null, title || null, description || null,
localPath || null, remotePath || null, mediaType || null, createdDate || null]
);
return result.lastInsertRowId;
} catch (error) {
@@ -341,15 +258,10 @@ const DatabaseService = {
}
},
/**
* Get all media for user
*/
getMediaByUser: async (userId) => {
try {
await DatabaseService.ensureDb();
if (!userId && userId !== 0) {
throw new Error('User ID is required and cannot be null');
}
if (!userId && userId !== 0) throw new Error('User ID is required');
return await db.getAllAsync(
'SELECT * FROM media WHERE user_id = ? ORDER BY created_date DESC',
[userId]
@@ -360,9 +272,6 @@ const DatabaseService = {
}
},
/**
* Get media by service
*/
getMediaByService: async (userId, serviceType) => {
try {
await DatabaseService.ensureDb();
@@ -376,46 +285,23 @@ const DatabaseService = {
}
},
/**
* Update media sync status
*/
updateMediaSyncStatus: async (mediaId, synced) => {
searchMedia: async (userId, query) => {
try {
await DatabaseService.ensureDb();
await db.runAsync(
'UPDATE media SET synced = ? WHERE id = ?',
[synced ? 1 : 0, mediaId]
const searchTerm = `%${query}%`;
return await db.getAllAsync(
'SELECT * FROM media WHERE user_id = ? AND (title LIKE ? OR description LIKE ?) ORDER BY created_date DESC',
[userId, searchTerm, searchTerm]
);
} catch (error) {
console.error('Error updating media sync status:', error);
console.error('Error searching media:', error);
throw error;
}
},
/**
* Log sync operation
*/
logSync: async (userId, serviceType, status, message) => {
try {
await DatabaseService.ensureDb();
await db.runAsync(
'INSERT INTO sync_logs (user_id, service_type, status, message) VALUES (?, ?, ?, ?)',
[userId, serviceType, status, message]
);
} catch (error) {
console.error('Error logging sync:', error);
throw error;
}
},
/**
* Close database connection
*/
closeDatabase: async () => {
try {
if (db?.closeAsync) {
await db.closeAsync();
}
if (db?.closeAsync) await db.closeAsync();
db = null;
initPromise = null;
} catch (error) {