279 lines
9.4 KiB
TypeScript
279 lines
9.4 KiB
TypeScript
/**
|
|
* useVaultAssets: Encapsulates /assets/get and /assets/create for VaultScreen.
|
|
* - Fetches assets when vault is unlocked and token exists.
|
|
* - Exposes createAsset with 401/network error handling and list refresh on success.
|
|
*/
|
|
|
|
import { useState, useEffect, useCallback } from 'react';
|
|
import * as bip39 from 'bip39';
|
|
import AsyncStorage from '@react-native-async-storage/async-storage';
|
|
import { useAuth } from '../context/AuthContext';
|
|
import { assetsService } from '../services/assets.service';
|
|
import { getVaultStorageKeys, DEBUG_MODE } from '../config';
|
|
import { SentinelVault } from '../utils/crypto_core';
|
|
import { storageService } from '../services/storage.service';
|
|
import {
|
|
initialVaultAssets,
|
|
mapApiAssetsToVaultAssets,
|
|
type ApiAsset,
|
|
} from '../utils/vaultAssets';
|
|
import type { VaultAsset } from '../types';
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// Types
|
|
// -----------------------------------------------------------------------------
|
|
|
|
export interface CreateAssetResult {
|
|
success: boolean;
|
|
isUnauthorized?: boolean;
|
|
error?: string;
|
|
}
|
|
|
|
export interface UseVaultAssetsReturn {
|
|
/** Current list (mock until API succeeds) */
|
|
assets: VaultAsset[];
|
|
/** Replace list (e.g. after external refresh) */
|
|
setAssets: React.Dispatch<React.SetStateAction<VaultAsset[]>>;
|
|
/** Refetch from GET /assets/get */
|
|
refreshAssets: () => Promise<void>;
|
|
/** Create asset via POST /assets/create; on success refreshes list */
|
|
createAsset: (params: { title: string; content: string }) => Promise<CreateAssetResult>;
|
|
/** Delete asset via POST /assets/delete; on success refreshes list */
|
|
deleteAsset: (assetId: number) => Promise<CreateAssetResult>;
|
|
/** Assign asset to heir via POST /assets/assign */
|
|
assignAsset: (assetId: number, heirEmail: string) => Promise<CreateAssetResult>;
|
|
/** True while create request is in flight */
|
|
isSealing: boolean;
|
|
/** Error message from last create failure (non-401) */
|
|
createError: string | null;
|
|
/** Clear createError */
|
|
clearCreateError: () => void;
|
|
}
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// Hook
|
|
// -----------------------------------------------------------------------------
|
|
|
|
/**
|
|
* Vault assets list + create. Fetches on unlock when token exists; keeps mock on error.
|
|
*/
|
|
export function useVaultAssets(isUnlocked: boolean): UseVaultAssetsReturn {
|
|
const { user, token, signOut } = useAuth();
|
|
const [assets, setAssets] = useState<VaultAsset[]>(initialVaultAssets);
|
|
const [isSealing, setIsSealing] = useState(false);
|
|
const [createError, setCreateError] = useState<string | null>(null);
|
|
|
|
const refreshAssets = useCallback(async () => {
|
|
if (!token) return;
|
|
try {
|
|
const list = await assetsService.getMyAssets(token);
|
|
if (Array.isArray(list)) {
|
|
setAssets(mapApiAssetsToVaultAssets(list as ApiAsset[]));
|
|
}
|
|
} catch (err: unknown) {
|
|
const rawMessage = err instanceof Error ? err.message : String(err ?? '');
|
|
if (/Could not validate credentials/i.test(rawMessage)) {
|
|
signOut();
|
|
}
|
|
// Keep current assets (mock or previous fetch)
|
|
}
|
|
}, [token, signOut]);
|
|
|
|
// Fetch list when unlocked and token exists
|
|
useEffect(() => {
|
|
if (!isUnlocked || !token) return;
|
|
let cancelled = false;
|
|
assetsService
|
|
.getMyAssets(token)
|
|
.then((list) => {
|
|
if (!cancelled && Array.isArray(list)) {
|
|
setAssets(mapApiAssetsToVaultAssets(list as ApiAsset[]));
|
|
}
|
|
})
|
|
.catch((err) => {
|
|
if (!cancelled) {
|
|
const rawMessage = err instanceof Error ? err.message : String(err ?? '');
|
|
if (/Could not validate credentials/i.test(rawMessage)) {
|
|
signOut();
|
|
}
|
|
}
|
|
// Keep initial (mock) assets
|
|
});
|
|
return () => {
|
|
cancelled = true;
|
|
};
|
|
}, [isUnlocked, token]);
|
|
|
|
const createAsset = useCallback(
|
|
async ({
|
|
title,
|
|
content,
|
|
}: {
|
|
title: string;
|
|
content: string;
|
|
}): Promise<CreateAssetResult> => {
|
|
if (!token) {
|
|
return { success: false, error: 'Not logged in.' };
|
|
}
|
|
setIsSealing(true);
|
|
setCreateError(null);
|
|
try {
|
|
const vaultKeys = getVaultStorageKeys(user?.id ?? null);
|
|
const [s1Str, aesKeyHex, s0Str, s2Str] = await Promise.all([
|
|
AsyncStorage.getItem(vaultKeys.SHARE_SERVER),
|
|
AsyncStorage.getItem(vaultKeys.AES_KEY),
|
|
AsyncStorage.getItem(vaultKeys.SHARE_DEVICE),
|
|
AsyncStorage.getItem(vaultKeys.SHARE_HEIR),
|
|
]);
|
|
|
|
if (!s1Str || !aesKeyHex) {
|
|
throw new Error('Vault keys missing. Please re-unlock your vault.');
|
|
}
|
|
|
|
const vault = new SentinelVault();
|
|
const aesKey = Buffer.from(aesKeyHex, 'hex');
|
|
const encryptedBuffer = vault.encryptData(aesKey, content.trim());
|
|
const content_inner_encrypted = encryptedBuffer.toString('hex');
|
|
|
|
if (DEBUG_MODE) {
|
|
console.log('[DEBUG] Crypto Data during Asset Creation:');
|
|
console.log(' s0 (Device):', s0Str);
|
|
console.log(' s1 (Server):', s1Str);
|
|
console.log(' s2 (Heir): ', s2Str);
|
|
console.log(' AES Key: ', aesKeyHex);
|
|
console.log(' Encrypted: ', content_inner_encrypted);
|
|
}
|
|
|
|
const createdAsset = await assetsService.createAsset(
|
|
{
|
|
title: title.trim(),
|
|
private_key_shard: s1Str,
|
|
content_inner_encrypted,
|
|
},
|
|
token
|
|
);
|
|
|
|
// Backup plaintext content locally
|
|
if (createdAsset && createdAsset.id && user?.id) {
|
|
await storageService.saveAssetBackup(createdAsset.id, content, user.id);
|
|
}
|
|
await refreshAssets();
|
|
return { success: true };
|
|
} catch (err: unknown) {
|
|
const status =
|
|
err && typeof err === 'object' && 'status' in err
|
|
? (err as { status?: number }).status
|
|
: undefined;
|
|
const rawMessage =
|
|
err instanceof Error ? err.message : String(err ?? 'Failed to create.');
|
|
const isUnauthorized =
|
|
status === 401 || /401|Unauthorized/i.test(rawMessage);
|
|
|
|
if (isUnauthorized) {
|
|
signOut();
|
|
return { success: false, isUnauthorized: true };
|
|
}
|
|
|
|
const friendlyMessage = /failed to fetch|network error/i.test(rawMessage)
|
|
? 'Network error. Please check that the backend is running and reachable (see API_BASE_URL in config).'
|
|
: rawMessage;
|
|
setCreateError(friendlyMessage);
|
|
return { success: false, error: friendlyMessage };
|
|
} finally {
|
|
setIsSealing(false);
|
|
}
|
|
},
|
|
[token, user, refreshAssets, signOut]
|
|
);
|
|
|
|
const deleteAsset = useCallback(
|
|
async (assetId: number): Promise<CreateAssetResult> => {
|
|
if (!token) {
|
|
return { success: false, error: 'Not logged in.' };
|
|
}
|
|
setIsSealing(true);
|
|
setCreateError(null);
|
|
try {
|
|
await assetsService.deleteAsset(assetId, token);
|
|
await refreshAssets();
|
|
return { success: true };
|
|
} catch (err: unknown) {
|
|
const status =
|
|
err && typeof err === 'object' && 'status' in err
|
|
? (err as { status?: number }).status
|
|
: undefined;
|
|
const rawMessage =
|
|
err instanceof Error ? err.message : String(err ?? 'Failed to delete.');
|
|
const isUnauthorized =
|
|
status === 401 || /401|Unauthorized/i.test(rawMessage);
|
|
|
|
if (isUnauthorized) {
|
|
signOut();
|
|
return { success: false, isUnauthorized: true };
|
|
}
|
|
|
|
const friendlyMessage = /failed to fetch|network error/i.test(rawMessage)
|
|
? 'Network error. Please check that the backend is running and reachable.'
|
|
: rawMessage;
|
|
setCreateError(friendlyMessage);
|
|
return { success: false, error: friendlyMessage };
|
|
} finally {
|
|
setIsSealing(false);
|
|
}
|
|
},
|
|
[token, refreshAssets, signOut]
|
|
);
|
|
|
|
const assignAsset = useCallback(
|
|
async (assetId: number, heirEmail: string): Promise<CreateAssetResult> => {
|
|
if (!token) {
|
|
return { success: false, error: 'Not logged in.' };
|
|
}
|
|
setIsSealing(true);
|
|
setCreateError(null);
|
|
try {
|
|
await assetsService.assignAsset({ asset_id: assetId, heir_email: heirEmail }, token);
|
|
await refreshAssets();
|
|
return { success: true };
|
|
} catch (err: unknown) {
|
|
const status =
|
|
err && typeof err === 'object' && 'status' in err
|
|
? (err as { status?: number }).status
|
|
: undefined;
|
|
const rawMessage =
|
|
err instanceof Error ? err.message : String(err ?? 'Failed to assign.');
|
|
const isUnauthorized =
|
|
status === 401 || /401|Unauthorized/i.test(rawMessage);
|
|
|
|
if (isUnauthorized) {
|
|
signOut();
|
|
return { success: false, isUnauthorized: true };
|
|
}
|
|
|
|
const friendlyMessage = /failed to fetch|network error/i.test(rawMessage)
|
|
? 'Network error. Please check that the backend is running and reachable.'
|
|
: rawMessage;
|
|
setCreateError(friendlyMessage);
|
|
return { success: false, error: friendlyMessage };
|
|
} finally {
|
|
setIsSealing(false);
|
|
}
|
|
},
|
|
[token, signOut]
|
|
);
|
|
|
|
const clearCreateError = useCallback(() => setCreateError(null), []);
|
|
|
|
return {
|
|
assets,
|
|
setAssets,
|
|
refreshAssets,
|
|
createAsset,
|
|
deleteAsset,
|
|
assignAsset,
|
|
isSealing,
|
|
createError,
|
|
clearCreateError,
|
|
};
|
|
}
|