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

+7 -15
View File
@@ -1,24 +1,19 @@
import * as Crypto from 'expo-crypto';
/**
* Credential Encryption Service
*
* Uses expo-crypto for password hashing (SHA-256)
* Uses base64 encoding for storing account data objects in the database
* Handles password hashing (SHA-256) and base64 encoding of account data objects
*/
class CredentialEncryption {
/**
* Hash a password using SHA-256 via expo-crypto
* Returns a hex digest string
* Hash a password with SHA-256 and return a hex digest
*/
async hashPassword(password) {
try {
if (!password) throw new Error('Password is required');
const digest = await Crypto.digestStringAsync(
return await Crypto.digestStringAsync(
Crypto.CryptoDigestAlgorithm.SHA256,
password
);
return digest;
} catch (error) {
console.error('Password hashing error:', error);
throw new Error('Failed to hash password: ' + error.message);
@@ -26,7 +21,7 @@ class CredentialEncryption {
}
/**
* Verify a password against a stored hash
* Compare a plaintext password against a stored hash
*/
async verifyPassword(password, storedHash) {
try {
@@ -39,15 +34,13 @@ class CredentialEncryption {
}
/**
* Encrypt an object for database storage (base64-encoded JSON)
* Encode an object as a base64 JSON string for database storage
*/
encrypt(data) {
try {
if (!data) throw new Error('No data provided to encrypt');
const jsonString = JSON.stringify(data);
// btoa is available in React Native / Hermes
const encoded = btoa(unescape(encodeURIComponent(jsonString)));
return encoded;
return btoa(unescape(encodeURIComponent(jsonString)));
} catch (error) {
console.error('Encryption error:', error);
throw new Error('Failed to encrypt data: ' + error.message);
@@ -55,7 +48,7 @@ class CredentialEncryption {
}
/**
* Decrypt a base64-encoded JSON string back to an object
* Decode a base64 JSON string back to an object
*/
decrypt(encryptedData) {
try {
@@ -70,4 +63,3 @@ class CredentialEncryption {
}
export default new CredentialEncryption();
+16 -52
View File
@@ -5,9 +5,7 @@ let db = null;
let initPromise = null;
const DatabaseService = {
/**
* Initialize the database and create tables
*/
// Open the database and create all tables
initDatabase: async () => {
if (initPromise) return initPromise;
@@ -63,13 +61,9 @@ const DatabaseService = {
)
`);
// Migration: add columns if they don't exist
try {
await db.execAsync(`ALTER TABLE users ADD COLUMN auto_backup_services TEXT DEFAULT '[]'`);
} catch (e) { /* column already exists */ }
try {
await db.execAsync(`ALTER TABLE users ADD COLUMN theme_preference TEXT DEFAULT 'light'`);
} catch (e) { /* column already exists */ }
// Add optional columns (safe migration — errors ignored if columns already exist)
try { await db.execAsync(`ALTER TABLE users ADD COLUMN auto_backup_services TEXT DEFAULT '[]'`); } catch (_) {}
try { await db.execAsync(`ALTER TABLE users ADD COLUMN theme_preference TEXT DEFAULT 'light'`); } catch (_) {}
console.log('Database initialized successfully');
return true;
@@ -89,9 +83,6 @@ const DatabaseService = {
if (!db) throw new Error('Database not initialized');
},
/**
* Create a new user with hashed password
*/
createUser: async (username, email, password) => {
try {
await DatabaseService.ensureDb();
@@ -107,9 +98,7 @@ const DatabaseService = {
}
},
/**
* Login user by username or email, verify hashed password
*/
// Authenticate by username or email
loginUser: async (identifier, password) => {
try {
await DatabaseService.ensureDb();
@@ -129,9 +118,6 @@ const DatabaseService = {
}
},
/**
* Check if username or email already exists
*/
userExists: async (username, email) => {
try {
await DatabaseService.ensureDb();
@@ -150,34 +136,26 @@ const DatabaseService = {
try {
await DatabaseService.ensureDb();
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;
return (await db.getFirstAsync('SELECT * FROM users WHERE id = ?', [userId])) || null;
} catch (error) {
console.error('Error getting user:', error);
throw error;
}
},
/**
* Link an external service account to a user
*/
// Link an external service account (Google, iCloud, NAS) to a local user
linkAccount: async (userId, serviceType, accountData, isDefault = false) => {
try {
await DatabaseService.ensureDb();
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');
}
if (!accountData || typeof accountData !== 'object') throw new Error('Account data must be a valid object');
const encryptedData = CredentialEncryption.encrypt(accountData);
if (!encryptedData) throw new Error('Encryption returned empty data');
// Remove existing account of same type first
await db.runAsync(
'DELETE FROM linked_accounts WHERE user_id = ? AND service_type = ?',
[userId, serviceType]
);
// Replace any existing link for the same service type
await db.runAsync('DELETE FROM linked_accounts WHERE user_id = ? AND service_type = ?', [userId, serviceType]);
const result = await db.runAsync(
'INSERT INTO linked_accounts (user_id, service_type, account_data, is_default) VALUES (?, ?, ?, ?)',
@@ -195,13 +173,9 @@ const DatabaseService = {
await DatabaseService.ensureDb();
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]
'SELECT * FROM linked_accounts WHERE user_id = ? AND is_enabled = 1', [userId]
);
return rows.map((row) => ({
...row,
account_data: CredentialEncryption.decrypt(row.account_data),
}));
return rows.map((row) => ({ ...row, account_data: CredentialEncryption.decrypt(row.account_data) }));
} catch (error) {
console.error('Error getting linked accounts:', error);
throw error;
@@ -238,8 +212,7 @@ const DatabaseService = {
try {
await DatabaseService.ensureDb();
const row = await db.getFirstAsync(
'SELECT * FROM linked_accounts WHERE user_id = ? AND is_default = 1',
[userId]
'SELECT * FROM linked_accounts WHERE user_id = ? AND is_default = 1', [userId]
);
if (!row) return null;
return { ...row, account_data: CredentialEncryption.decrypt(row.account_data) };
@@ -270,10 +243,7 @@ const DatabaseService = {
try {
await DatabaseService.ensureDb();
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]
);
return await db.getAllAsync('SELECT * FROM media WHERE user_id = ? ORDER BY created_date DESC', [userId]);
} catch (error) {
console.error('Error getting media:', error);
throw error;
@@ -352,14 +322,8 @@ const DatabaseService = {
getAllLinkedAccounts: async (userId) => {
try {
await DatabaseService.ensureDb();
const rows = await db.getAllAsync(
'SELECT * FROM linked_accounts WHERE user_id = ?',
[userId]
);
return rows.map((row) => ({
...row,
account_data: CredentialEncryption.decrypt(row.account_data),
}));
const rows = await db.getAllAsync('SELECT * FROM linked_accounts WHERE user_id = ?', [userId]);
return rows.map((row) => ({ ...row, account_data: CredentialEncryption.decrypt(row.account_data) }));
} catch (error) {
console.error('Error getting all linked accounts:', error);
throw error;
+1
View File
@@ -3,6 +3,7 @@ import * as SecureStore from 'expo-secure-store';
const THEME_KEY = 'app_theme_preference';
// Available theme definitions
const lightTheme = {
name: 'light',
background: '#ffffff',