mirror of
https://github.com/aliasvault/aliasvault.git
synced 2026-05-18 21:40:41 -04:00
Show icon in react native app (#771)
This commit is contained in:
@@ -9,6 +9,7 @@ import { useDb } from '@/context/DbContext';
|
||||
import { Credential } from '@/utils/types/Credential';
|
||||
import SqliteClient from '@/utils/SqliteClient';
|
||||
import { ThemedScrollView } from '@/components/ThemedScrollView';
|
||||
import { CredentialIcon } from '@/components/CredentialIcon';
|
||||
|
||||
interface FormInputCopyToClipboardProps {
|
||||
label: string;
|
||||
@@ -89,7 +90,7 @@ export default function CredentialDetailsScreen() {
|
||||
useEffect(() => {
|
||||
const loadCredential = async () => {
|
||||
if (!dbContext.dbAvailable || !id) return;
|
||||
|
||||
|
||||
try {
|
||||
const cred = await dbContext.sqliteClient!.getCredentialById(id as string);
|
||||
if (cred?.Alias?.BirthDate) {
|
||||
@@ -132,12 +133,7 @@ export default function CredentialDetailsScreen() {
|
||||
return (
|
||||
<ThemedScrollView style={styles.container}>
|
||||
<ThemedView style={styles.header}>
|
||||
{credential.Logo && (
|
||||
<Image
|
||||
source={{ uri: SqliteClient.imgSrcFromBytes(credential.Logo) }}
|
||||
style={styles.logo}
|
||||
/>
|
||||
)}
|
||||
<CredentialIcon logo={credential.Logo} style={styles.logo} />
|
||||
<View style={styles.headerText}>
|
||||
<ThemedText type="title" style={styles.serviceName}>
|
||||
{credential.ServiceName}
|
||||
@@ -292,4 +288,4 @@ const styles = StyleSheet.create({
|
||||
notes: {
|
||||
fontSize: 14,
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,4 @@
|
||||
import { StyleSheet, View, Text, FlatList, ActivityIndicator, useColorScheme, TouchableOpacity, TextInput, Keyboard } from 'react-native';
|
||||
import { StyleSheet, View, Text, FlatList, ActivityIndicator, useColorScheme, TouchableOpacity, TextInput, Keyboard, Image } from 'react-native';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { router, Stack } from 'expo-router';
|
||||
|
||||
@@ -8,6 +8,7 @@ import { ThemedSafeAreaView } from '@/components/ThemedSafeAreaView';
|
||||
import { useDb } from '@/context/DbContext';
|
||||
import { useAuth } from '@/context/AuthContext';
|
||||
import { Credential } from '@/utils/types/Credential';
|
||||
import { CredentialIcon } from '@/components/CredentialIcon';
|
||||
|
||||
export default function CredentialsScreen() {
|
||||
const colorScheme = useColorScheme();
|
||||
@@ -109,19 +110,24 @@ export default function CredentialsScreen() {
|
||||
style={[styles.credentialItem, dynamicStyles.credentialItem]}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<Text style={[styles.serviceName, dynamicStyles.serviceName]}>
|
||||
{item.ServiceName ?? 'Unknown Service'}
|
||||
</Text>
|
||||
{item.Username && (
|
||||
<Text style={[styles.credentialText, dynamicStyles.credentialText]}>
|
||||
Username: {item.Username}
|
||||
</Text>
|
||||
)}
|
||||
{item.Alias?.Email && (
|
||||
<Text style={[styles.credentialText, dynamicStyles.credentialText]}>
|
||||
Email: {item.Alias.Email}
|
||||
</Text>
|
||||
)}
|
||||
<View style={styles.credentialContent}>
|
||||
<CredentialIcon logo={item.Logo} style={styles.logo} />
|
||||
<View style={styles.credentialInfo}>
|
||||
<Text style={[styles.serviceName, dynamicStyles.serviceName]}>
|
||||
{item.ServiceName ?? 'Unknown Service'}
|
||||
</Text>
|
||||
{item.Username && (
|
||||
<Text style={[styles.credentialText, dynamicStyles.credentialText]}>
|
||||
Username: {item.Username}
|
||||
</Text>
|
||||
)}
|
||||
{item.Alias?.Email && (
|
||||
<Text style={[styles.credentialText, dynamicStyles.credentialText]}>
|
||||
Email: {item.Alias.Email}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
ListEmptyComponent={
|
||||
@@ -157,11 +163,24 @@ const styles = StyleSheet.create({
|
||||
gap: 8,
|
||||
},
|
||||
credentialItem: {
|
||||
padding: 16,
|
||||
padding: 12,
|
||||
borderRadius: 8,
|
||||
marginBottom: 8,
|
||||
borderWidth: 1,
|
||||
},
|
||||
credentialContent: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
},
|
||||
logo: {
|
||||
width: 32,
|
||||
height: 32,
|
||||
borderRadius: 4,
|
||||
marginRight: 12,
|
||||
},
|
||||
credentialInfo: {
|
||||
flex: 1,
|
||||
},
|
||||
serviceName: {
|
||||
fontSize: 16,
|
||||
fontWeight: '600',
|
||||
@@ -181,4 +200,4 @@ const styles = StyleSheet.create({
|
||||
marginBottom: 16,
|
||||
fontSize: 16,
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -49,7 +49,6 @@ export default function TabTwoScreen() {
|
||||
<ThemedText type="defaultSemiBold">@3x</ThemedText> suffixes to provide files for
|
||||
different screen densities
|
||||
</ThemedText>
|
||||
<Image source={require('@/assets/images/react-logo.png')} style={{ alignSelf: 'center' }} />
|
||||
<ExternalLink href="https://reactnative.dev/docs/images">
|
||||
<ThemedText type="link">Learn more</ThemedText>
|
||||
</ExternalLink>
|
||||
|
||||
0
mobile-app/app/components/CredentialIcon.tsx
Normal file
0
mobile-app/app/components/CredentialIcon.tsx
Normal file
@@ -208,7 +208,7 @@ export default function LoginScreen() {
|
||||
<ThemedView style={styles.titleContainer}>
|
||||
<ThemedText type="title">AliasVault</ThemedText>
|
||||
</ThemedView>
|
||||
|
||||
|
||||
{error && (
|
||||
<View style={styles.errorContainer}>
|
||||
<Text style={styles.errorText}>{error}</Text>
|
||||
@@ -391,4 +391,4 @@ const styles = StyleSheet.create({
|
||||
textAlign: 'center',
|
||||
marginTop: 16,
|
||||
},
|
||||
});
|
||||
});
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 5.0 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 6.2 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 14 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 21 KiB |
BIN
mobile-app/assets/images/service-placeholder.webp
Normal file
BIN
mobile-app/assets/images/service-placeholder.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 115 KiB |
135
mobile-app/components/CredentialIcon.tsx
Normal file
135
mobile-app/components/CredentialIcon.tsx
Normal file
@@ -0,0 +1,135 @@
|
||||
import { Image, ImageStyle, StyleSheet } from 'react-native';
|
||||
import { Buffer } from 'buffer';
|
||||
import { SvgUri } from 'react-native-svg';
|
||||
|
||||
type CredentialIconProps = {
|
||||
logo?: Uint8Array | number[] | string | null;
|
||||
style?: ImageStyle;
|
||||
};
|
||||
|
||||
export function CredentialIcon({ logo, style }: CredentialIconProps) {
|
||||
const getLogoSource = (logoData: Uint8Array | number[] | string | null | undefined) => {
|
||||
if (!logoData) {
|
||||
return { type: 'image', source: require('@/assets/images/service-placeholder.webp') };
|
||||
}
|
||||
|
||||
try {
|
||||
// If logo is already a base64 string (from iOS SQLite query result)
|
||||
if (typeof logoData === 'string') {
|
||||
const mimeType = detectMimeTypeFromBase64(logoData);
|
||||
return {
|
||||
type: mimeType === 'image/svg+xml' ? 'svg' : 'image',
|
||||
source: `data:${mimeType};base64,${logoData}`
|
||||
};
|
||||
}
|
||||
|
||||
// Handle binary data (from Android or other sources)
|
||||
const logoBytes = toUint8Array(logoData);
|
||||
const base64Logo = Buffer.from(logoBytes).toString('base64');
|
||||
const mimeType = detectMimeType(logoBytes);
|
||||
return {
|
||||
type: mimeType === 'image/svg+xml' ? 'svg' : 'image',
|
||||
source: `data:${mimeType};base64,${base64Logo}`
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error converting logo:', error);
|
||||
return { type: 'image', source: require('@/assets/images/service-placeholder.webp') };
|
||||
}
|
||||
};
|
||||
|
||||
const logoSource = getLogoSource(logo);
|
||||
|
||||
if (logoSource.type === 'svg') {
|
||||
// SVGs are not supported in React Native Image component,
|
||||
// so we use SvgUri from react-native-svg.
|
||||
return (
|
||||
<SvgUri
|
||||
uri={logoSource.source}
|
||||
width={Number(style?.width ?? styles.logo.width)}
|
||||
height={Number(style?.height ?? styles.logo.height)}
|
||||
style={{
|
||||
borderRadius: styles.logo.borderRadius,
|
||||
...(style as any),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Image
|
||||
source={typeof logoSource.source === 'string' ? { uri: logoSource.source } : logoSource.source}
|
||||
style={[styles.logo, style]}
|
||||
defaultSource={require('@/assets/images/service-placeholder.webp')}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect MIME type from base64 string by decoding first few bytes
|
||||
*/
|
||||
function detectMimeTypeFromBase64(base64: string): string {
|
||||
try {
|
||||
const binaryString = atob(base64.slice(0, 8));
|
||||
const bytes = new Uint8Array(binaryString.length);
|
||||
for (let i = 0; i < binaryString.length; i++) {
|
||||
bytes[i] = binaryString.charCodeAt(i);
|
||||
}
|
||||
return detectMimeType(bytes);
|
||||
} catch (error) {
|
||||
console.warn('Error detecting mime type from base64:', error);
|
||||
return 'image/x-icon';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect MIME type from file signature (magic numbers)
|
||||
*/
|
||||
function detectMimeType(bytes: Uint8Array): string {
|
||||
const isSvg = (): boolean => {
|
||||
const header = new TextDecoder().decode(bytes.slice(0, 5)).toLowerCase();
|
||||
return header.includes('<?xml') || header.includes('<svg');
|
||||
};
|
||||
|
||||
const isIco = (): boolean => {
|
||||
return bytes[0] === 0x00 && bytes[1] === 0x00 && bytes[2] === 0x01 && bytes[3] === 0x00;
|
||||
};
|
||||
|
||||
const isPng = (): boolean => {
|
||||
return bytes[0] === 0x89 && bytes[1] === 0x50 && bytes[2] === 0x4E && bytes[3] === 0x47;
|
||||
};
|
||||
|
||||
if (isSvg()) return 'image/svg+xml';
|
||||
if (isIco()) return 'image/x-icon';
|
||||
if (isPng()) return 'image/png';
|
||||
|
||||
return 'image/x-icon';
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert various binary data formats to Uint8Array
|
||||
*/
|
||||
function toUint8Array(buffer: Uint8Array | number[] | {[key: number]: number}): Uint8Array {
|
||||
if (buffer instanceof Uint8Array) {
|
||||
return buffer;
|
||||
}
|
||||
|
||||
if (Array.isArray(buffer)) {
|
||||
return new Uint8Array(buffer);
|
||||
}
|
||||
|
||||
const length = Object.keys(buffer).length;
|
||||
const arr = new Uint8Array(length);
|
||||
for (let i = 0; i < length; i++) {
|
||||
arr[i] = buffer[i];
|
||||
}
|
||||
|
||||
return arr;
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
logo: {
|
||||
width: 32,
|
||||
height: 32,
|
||||
borderRadius: 4,
|
||||
},
|
||||
});
|
||||
@@ -13,17 +13,17 @@ class SharedCredentialStore {
|
||||
private let encryptedDbFileName = "encrypted_db.sqlite"
|
||||
private var db: Connection?
|
||||
private var encryptionKey: Data?
|
||||
|
||||
|
||||
public init() {}
|
||||
|
||||
// MARK: - Vault Status
|
||||
func isVaultInitialized() -> Bool {
|
||||
// Check if encrypted database file exists
|
||||
let hasDatabase = FileManager.default.fileExists(atPath: getEncryptedDbPath().path)
|
||||
|
||||
|
||||
return hasDatabase
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Encryption Key Management
|
||||
private func getEncryptionKey() throws -> Data {
|
||||
if let key = encryptionKey {
|
||||
@@ -31,32 +31,32 @@ class SharedCredentialStore {
|
||||
print("Key found in memory: \(key.base64EncodedString())")
|
||||
return key
|
||||
}
|
||||
|
||||
|
||||
guard let keyData = try? keychain
|
||||
.authenticationPrompt("Authenticate to unlock your vault")
|
||||
.getData(encryptionKeyKey) else {
|
||||
throw NSError(domain: "SharedCredentialStore", code: 2, userInfo: [NSLocalizedDescriptionKey: "No encryption key found"])
|
||||
}
|
||||
|
||||
|
||||
encryptionKey = keyData
|
||||
// print as base64 for debugging
|
||||
print("Key found in keychain: \(keyData.base64EncodedString())")
|
||||
return keyData
|
||||
}
|
||||
|
||||
|
||||
func storeEncryptionKey(_ base64Key: String) throws {
|
||||
print("Storing encryption key")
|
||||
|
||||
|
||||
// Convert base64 string to bytes
|
||||
guard let keyData = Data(base64Encoded: base64Key) else {
|
||||
throw NSError(domain: "SharedCredentialStore", code: 6, userInfo: [NSLocalizedDescriptionKey: "Invalid base64 key"])
|
||||
}
|
||||
|
||||
|
||||
// Validate key length (AES-256 requires 32 bytes)
|
||||
guard keyData.count == 32 else {
|
||||
throw NSError(domain: "SharedCredentialStore", code: 7, userInfo: [NSLocalizedDescriptionKey: "Invalid key length. Expected 32 bytes"])
|
||||
}
|
||||
|
||||
|
||||
do {
|
||||
try keychain
|
||||
.authenticationPrompt("Authenticate to unlock your vault")
|
||||
@@ -69,21 +69,21 @@ class SharedCredentialStore {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Database Management
|
||||
|
||||
|
||||
private func getEncryptedDbPath() -> URL {
|
||||
guard let containerURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.net.aliasvault.autofill") else {
|
||||
fatalError("Failed to get shared container URL")
|
||||
}
|
||||
return containerURL.appendingPathComponent(encryptedDbFileName)
|
||||
}
|
||||
|
||||
|
||||
func storeEncryptedDatabase(_ base64EncryptedDb: String) throws {
|
||||
// Store the encrypted database (base64 encoded) in the app's documents directory
|
||||
try base64EncryptedDb.write(to: getEncryptedDbPath(), atomically: true, encoding: .utf8)
|
||||
}
|
||||
|
||||
|
||||
// Get the encrypted database as a base64 encoded string
|
||||
func getEncryptedDatabase() -> String? {
|
||||
do {
|
||||
@@ -93,13 +93,13 @@ class SharedCredentialStore {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func initializeDatabase() throws {
|
||||
// Get the encrypted database
|
||||
guard let encryptedDbBase64 = getEncryptedDatabase() else {
|
||||
throw NSError(domain: "SharedCredentialStore", code: 1, userInfo: [NSLocalizedDescriptionKey: "No encrypted database found"])
|
||||
}
|
||||
|
||||
|
||||
let encryptedDbData = Data(base64Encoded: encryptedDbBase64)!
|
||||
|
||||
// Get the encryption key
|
||||
@@ -114,60 +114,60 @@ class SharedCredentialStore {
|
||||
// Create a temporary file for the decrypted database in the same directory as the encrypted one
|
||||
let tempDbPath = FileManager.default.temporaryDirectory.appendingPathComponent("temp_db.sqlite")
|
||||
try decryptedDbData.write(to: tempDbPath)
|
||||
|
||||
|
||||
// Create an in-memory database
|
||||
db = try Connection(":memory:")
|
||||
|
||||
|
||||
// Import the decrypted database into memory
|
||||
try db?.attach(.uri(tempDbPath.path, parameters: [.mode(.readOnly)]), as: "source")
|
||||
try db?.execute("BEGIN TRANSACTION")
|
||||
|
||||
|
||||
// Copy all tables from source to memory
|
||||
let tables = try db?.prepare("SELECT name FROM source.sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'")
|
||||
for table in tables! {
|
||||
let tableName = table[0] as! String
|
||||
try db?.execute("CREATE TABLE \(tableName) AS SELECT * FROM source.\(tableName)")
|
||||
}
|
||||
|
||||
|
||||
try db?.execute("COMMIT")
|
||||
try db?.execute("DETACH DATABASE source")
|
||||
|
||||
|
||||
// Clean up the temporary file
|
||||
try? FileManager.default.removeItem(at: tempDbPath)
|
||||
|
||||
|
||||
// Setup database pragmas
|
||||
try db?.execute("PRAGMA journal_mode = WAL")
|
||||
try db?.execute("PRAGMA synchronous = NORMAL")
|
||||
try db?.execute("PRAGMA foreign_keys = ON")
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Encryption/Decryption
|
||||
|
||||
|
||||
private func encrypt(data: Data, key: Data) throws -> Data {
|
||||
// TODO: make sure we encrypt the data the same as decryption works with combined iv/content/tag.
|
||||
let key = SymmetricKey(data: key)
|
||||
let sealedBox = try AES.GCM.seal(data, using: key)
|
||||
return sealedBox.combined!
|
||||
}
|
||||
|
||||
|
||||
private func decrypt(data: Data, key: Data) throws -> Data {
|
||||
let key = SymmetricKey(data: key)
|
||||
let sealedBox = try AES.GCM.SealedBox(combined: data)
|
||||
return try AES.GCM.open(sealedBox, using: key)
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Credential Operations
|
||||
|
||||
|
||||
func addCredential(_ credential: Credential) throws {
|
||||
if db == nil {
|
||||
try initializeDatabase()
|
||||
}
|
||||
|
||||
|
||||
// After initialization attempt, check if db is still nil
|
||||
guard let db = db else {
|
||||
throw NSError(domain: "SharedCredentialStore", code: 4, userInfo: [NSLocalizedDescriptionKey: "Database not initialized"])
|
||||
}
|
||||
|
||||
|
||||
// TODO: update this to use the actual database schema.
|
||||
// TODO: having the add logic here means we have duplicate code with the react native implementation.
|
||||
let credentials = Table("credentials")
|
||||
@@ -177,7 +177,7 @@ class SharedCredentialStore {
|
||||
let service = Expression<String>("service")
|
||||
let createdAt = Expression<Date>("created_at")
|
||||
let updatedAt = Expression<Date>("updated_at")
|
||||
|
||||
|
||||
try db.run(credentials.insert(
|
||||
id <- UUID().uuidString,
|
||||
username <- credential.username,
|
||||
@@ -187,17 +187,17 @@ class SharedCredentialStore {
|
||||
updatedAt <- Date()
|
||||
))
|
||||
}
|
||||
|
||||
|
||||
func getAllCredentials() throws -> [Credential] {
|
||||
if db == nil {
|
||||
try initializeDatabase()
|
||||
}
|
||||
|
||||
|
||||
// After initialization attempt, check if db is still nil
|
||||
guard let db = db else {
|
||||
throw NSError(domain: "SharedCredentialStore", code: 4, userInfo: [NSLocalizedDescriptionKey: "Database not initialized"])
|
||||
}
|
||||
|
||||
|
||||
let query = """
|
||||
SELECT DISTINCT
|
||||
c.Username,
|
||||
@@ -209,13 +209,13 @@ class SharedCredentialStore {
|
||||
WHERE c.IsDeleted = 0
|
||||
ORDER BY c.CreatedAt DESC
|
||||
"""
|
||||
|
||||
|
||||
var result: [Credential] = []
|
||||
for row in try db.prepare(query) {
|
||||
let username = row[0] as? String ?? ""
|
||||
let service = row[1] as? String ?? ""
|
||||
let password = row[2] as? String ?? ""
|
||||
|
||||
|
||||
result.append(Credential(
|
||||
username: username,
|
||||
password: password,
|
||||
@@ -229,7 +229,7 @@ class SharedCredentialStore {
|
||||
func clearCache() {
|
||||
// Clear the cached encryption key
|
||||
encryptionKey = nil
|
||||
|
||||
|
||||
// Clear the cached encrypted database
|
||||
db = nil
|
||||
}
|
||||
@@ -244,48 +244,67 @@ class SharedCredentialStore {
|
||||
|
||||
clearCache()
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Query Execution
|
||||
|
||||
|
||||
func executeQuery(_ query: String, params: [Binding?]) throws -> [[String: Any]] {
|
||||
guard let db = db else {
|
||||
throw NSError(domain: "SharedCredentialStore", code: 4, userInfo: [NSLocalizedDescriptionKey: "Database not initialized"])
|
||||
}
|
||||
|
||||
|
||||
let statement = try db.prepare(query)
|
||||
var results: [[String: Any]] = []
|
||||
|
||||
|
||||
for row in try statement.run(params) {
|
||||
var rowDict: [String: Any] = [:]
|
||||
for (index, column) in statement.columnNames.enumerated() {
|
||||
rowDict[column] = row[index]
|
||||
// Handle different SQLite data types appropriately
|
||||
let value = row[index]
|
||||
switch value {
|
||||
case let data as SQLite.Blob:
|
||||
// Convert SQLite blob to base64 string for React Native bridge
|
||||
let binaryData = Data(data.bytes)
|
||||
rowDict[column] = binaryData.base64EncodedString()
|
||||
case let date as Date:
|
||||
rowDict[column] = date
|
||||
case let number as Int64:
|
||||
rowDict[column] = number
|
||||
case let number as Double:
|
||||
rowDict[column] = number
|
||||
case let text as String:
|
||||
rowDict[column] = text
|
||||
case .none:
|
||||
rowDict[column] = NSNull()
|
||||
default:
|
||||
rowDict[column] = value
|
||||
}
|
||||
}
|
||||
results.append(rowDict)
|
||||
}
|
||||
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
|
||||
func executeUpdate(_ query: String, params: [Binding?]) throws -> Int {
|
||||
guard let db = db else {
|
||||
throw NSError(domain: "SharedCredentialStore", code: 4, userInfo: [NSLocalizedDescriptionKey: "Database not initialized"])
|
||||
}
|
||||
|
||||
|
||||
let statement = try db.prepare(query)
|
||||
try statement.run(params)
|
||||
return db.changes
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Biometric Authentication
|
||||
|
||||
|
||||
func authenticateWithBiometrics() async throws -> Bool {
|
||||
let context = LAContext()
|
||||
var error: NSError?
|
||||
|
||||
|
||||
guard context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error) else {
|
||||
throw error ?? NSError(domain: "SharedCredentialStore", code: 5, userInfo: [NSLocalizedDescriptionKey: "Biometrics not available"])
|
||||
}
|
||||
|
||||
|
||||
return try await withCheckedThrowingContinuation { continuation in
|
||||
context.evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics,
|
||||
localizedReason: "Authenticate to access your credentials") { success, error in
|
||||
|
||||
@@ -2095,6 +2095,49 @@ PODS:
|
||||
- ReactCommon/turbomodule/bridging
|
||||
- ReactCommon/turbomodule/core
|
||||
- Yoga
|
||||
- RNSVG (15.11.2):
|
||||
- DoubleConversion
|
||||
- glog
|
||||
- hermes-engine
|
||||
- RCT-Folly (= 2024.10.14.00)
|
||||
- RCTRequired
|
||||
- RCTTypeSafety
|
||||
- React-Core
|
||||
- React-debug
|
||||
- React-Fabric
|
||||
- React-featureflags
|
||||
- React-graphics
|
||||
- React-ImageManager
|
||||
- React-NativeModulesApple
|
||||
- React-RCTFabric
|
||||
- React-rendererdebug
|
||||
- React-utils
|
||||
- ReactCodegen
|
||||
- ReactCommon/turbomodule/bridging
|
||||
- ReactCommon/turbomodule/core
|
||||
- RNSVG/common (= 15.11.2)
|
||||
- Yoga
|
||||
- RNSVG/common (15.11.2):
|
||||
- DoubleConversion
|
||||
- glog
|
||||
- hermes-engine
|
||||
- RCT-Folly (= 2024.10.14.00)
|
||||
- RCTRequired
|
||||
- RCTTypeSafety
|
||||
- React-Core
|
||||
- React-debug
|
||||
- React-Fabric
|
||||
- React-featureflags
|
||||
- React-graphics
|
||||
- React-ImageManager
|
||||
- React-NativeModulesApple
|
||||
- React-RCTFabric
|
||||
- React-rendererdebug
|
||||
- React-utils
|
||||
- ReactCodegen
|
||||
- ReactCommon/turbomodule/bridging
|
||||
- ReactCommon/turbomodule/core
|
||||
- Yoga
|
||||
- SocketRocket (0.7.1)
|
||||
- SQLite.swift (0.14.1):
|
||||
- SQLite.swift/standard (= 0.14.1)
|
||||
@@ -2201,6 +2244,7 @@ DEPENDENCIES:
|
||||
- RNGestureHandler (from `../node_modules/react-native-gesture-handler`)
|
||||
- RNReanimated (from `../node_modules/react-native-reanimated`)
|
||||
- RNScreens (from `../node_modules/react-native-screens`)
|
||||
- RNSVG (from `../node_modules/react-native-svg`)
|
||||
- SQLite.swift (~> 0.14.0)
|
||||
- Yoga (from `../node_modules/react-native/ReactCommon/yoga`)
|
||||
|
||||
@@ -2405,6 +2449,8 @@ EXTERNAL SOURCES:
|
||||
:path: "../node_modules/react-native-reanimated"
|
||||
RNScreens:
|
||||
:path: "../node_modules/react-native-screens"
|
||||
RNSVG:
|
||||
:path: "../node_modules/react-native-svg"
|
||||
Yoga:
|
||||
:path: "../node_modules/react-native/ReactCommon/yoga"
|
||||
|
||||
@@ -2507,6 +2553,7 @@ SPEC CHECKSUMS:
|
||||
RNGestureHandler: fffddeb8af59709c6d8de11b6461a6af63cad532
|
||||
RNReanimated: 2e5069649cbab2c946652d3b97589b2ae0526220
|
||||
RNScreens: 362f4c861dd155f898908d5035d97b07a3f1a9d1
|
||||
RNSVG: 3a1cce2e940268a7d3554e3cf2bbd2195871f4fe
|
||||
SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748
|
||||
SQLite.swift: 2992550ebf3c5b268bf4352603e3df87d2a4ed72
|
||||
Yoga: feb4910aba9742cfedc059e2b2902e22ffe9954a
|
||||
|
||||
@@ -412,6 +412,7 @@
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/ExpoSystemUI/ExpoSystemUI_privacy.bundle",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/RCT-Folly/RCT-Folly_privacy.bundle",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/RNCAsyncStorage/RNCAsyncStorage_resources.bundle",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/RNSVG/RNSVGFilters.bundle",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/React-Core/React-Core_privacy.bundle",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/React-cxxreact/React-cxxreact_privacy.bundle",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/boost/boost_privacy.bundle",
|
||||
@@ -427,6 +428,7 @@
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoSystemUI_privacy.bundle",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RCT-Folly_privacy.bundle",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RNCAsyncStorage_resources.bundle",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RNSVGFilters.bundle",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/React-Core_privacy.bundle",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/React-cxxreact_privacy.bundle",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/boost_privacy.bundle",
|
||||
|
||||
137
mobile-app/package-lock.json
generated
137
mobile-app/package-lock.json
generated
@@ -37,6 +37,7 @@
|
||||
"react-native-reanimated": "~3.16.1",
|
||||
"react-native-safe-area-context": "4.12.0",
|
||||
"react-native-screens": "~4.4.0",
|
||||
"react-native-svg": "^15.11.2",
|
||||
"react-native-toast-message": "^2.2.1",
|
||||
"react-native-web": "~0.19.13",
|
||||
"react-native-webview": "13.12.5",
|
||||
@@ -5128,6 +5129,12 @@
|
||||
"node": ">=0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/boolbase": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
|
||||
"integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/bplist-creator": {
|
||||
"version": "0.0.7",
|
||||
"resolved": "https://registry.npmjs.org/bplist-creator/-/bplist-creator-0.0.7.tgz",
|
||||
@@ -5935,6 +5942,47 @@
|
||||
"hyphenate-style-name": "^1.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/css-select": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz",
|
||||
"integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==",
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"boolbase": "^1.0.0",
|
||||
"css-what": "^6.1.0",
|
||||
"domhandler": "^5.0.2",
|
||||
"domutils": "^3.0.1",
|
||||
"nth-check": "^2.0.1"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/fb55"
|
||||
}
|
||||
},
|
||||
"node_modules/css-tree": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.3.tgz",
|
||||
"integrity": "sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mdn-data": "2.0.14",
|
||||
"source-map": "^0.6.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/css-what": {
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz",
|
||||
"integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==",
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/fb55"
|
||||
}
|
||||
},
|
||||
"node_modules/cssom": {
|
||||
"version": "0.5.0",
|
||||
"resolved": "https://registry.npmjs.org/cssom/-/cssom-0.5.0.tgz",
|
||||
@@ -6178,6 +6226,32 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/dom-serializer": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
|
||||
"integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"domelementtype": "^2.3.0",
|
||||
"domhandler": "^5.0.2",
|
||||
"entities": "^4.2.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/domelementtype": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz",
|
||||
"integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/fb55"
|
||||
}
|
||||
],
|
||||
"license": "BSD-2-Clause"
|
||||
},
|
||||
"node_modules/domexception": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/domexception/-/domexception-4.0.0.tgz",
|
||||
@@ -6192,6 +6266,35 @@
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/domhandler": {
|
||||
"version": "5.0.3",
|
||||
"resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz",
|
||||
"integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==",
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"domelementtype": "^2.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/fb55/domhandler?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/domutils": {
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz",
|
||||
"integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==",
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"dom-serializer": "^2.0.0",
|
||||
"domelementtype": "^2.3.0",
|
||||
"domhandler": "^5.0.3"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/fb55/domutils?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/dotenv": {
|
||||
"version": "16.4.7",
|
||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz",
|
||||
@@ -6313,7 +6416,6 @@
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
|
||||
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.12"
|
||||
@@ -10079,6 +10181,12 @@
|
||||
"node": ">=0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/mdn-data": {
|
||||
"version": "2.0.14",
|
||||
"resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz",
|
||||
"integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==",
|
||||
"license": "CC0-1.0"
|
||||
},
|
||||
"node_modules/memoize-one": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz",
|
||||
@@ -10916,6 +11024,18 @@
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/nth-check": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz",
|
||||
"integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==",
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"boolbase": "^1.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/fb55/nth-check?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/nullthrows": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/nullthrows/-/nullthrows-1.1.1.tgz",
|
||||
@@ -12013,6 +12133,21 @@
|
||||
"react-native": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/react-native-svg": {
|
||||
"version": "15.11.2",
|
||||
"resolved": "https://registry.npmjs.org/react-native-svg/-/react-native-svg-15.11.2.tgz",
|
||||
"integrity": "sha512-+YfF72IbWQUKzCIydlijV1fLuBsQNGMT6Da2kFlo1sh+LE3BIm/2Q7AR1zAAR6L0BFLi1WaQPLfFUC9bNZpOmw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"css-select": "^5.1.0",
|
||||
"css-tree": "^1.1.3",
|
||||
"warn-once": "0.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "*",
|
||||
"react-native": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/react-native-toast-message": {
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/react-native-toast-message/-/react-native-toast-message-2.2.1.tgz",
|
||||
|
||||
@@ -44,6 +44,7 @@
|
||||
"react-native-reanimated": "~3.16.1",
|
||||
"react-native-safe-area-context": "4.12.0",
|
||||
"react-native-screens": "~4.4.0",
|
||||
"react-native-svg": "^15.11.2",
|
||||
"react-native-toast-message": "^2.2.1",
|
||||
"react-native-web": "~0.19.13",
|
||||
"react-native-webview": "13.12.5",
|
||||
|
||||
@@ -4,11 +4,6 @@ import { EncryptionKey } from './types/EncryptionKey';
|
||||
import { TotpCode } from './types/TotpCode';
|
||||
import { PasswordSettings } from './types/PasswordSettings';
|
||||
|
||||
/**
|
||||
* Placeholder base64 image for credentials without a logo.
|
||||
*/
|
||||
const placeholderBase64 = 'UklGRjoEAABXRUJQVlA4IC4EAAAwFwCdASqAAIAAPpFCm0olo6Ihp5IraLASCWUA0eb/0s56RrLtCnYfLPiBshdXWMx8j1Ez65f169iA4xUDBTEV6ylMQeCIj2b7RngGi7gKZ9WjKdSoy9R8JcgOmjCMlDmLG20KhNo/i/Dc/Ah5GAvGfm8kfniV3AkR6fxN6eKwjDc6xrDgSfS48G5uGV6WzQt24YAVlLSK9BMwndzfHnePK1KFchFrL7O3ulB8cGNCeomu4o+l0SrS/JKblJ4WTzj0DAD++lCUEouSfgRKdiV2TiYCD+H+l3tANKSPQFPQuzi7rbvxqGeRmXB9kDwURaoSTTpYjA9REMUi9uA6aV7PWtBNXgUzMLowYMZeos6Xvyhb34GmufswMHA5ZyYpxzjTphOak4ZjNOiz8aScO5ygiTx99SqwX/uL+HSeVOSraHw8IymrMwm+jLxqN8BS8dGcItLlm/ioulqH2j4V8glDgSut+ExkxiD7m8TGPrrjCQNJbRDzpOFsyCyfBZupvp8QjGKW2KGziSZeIWes4aTB9tRmeEBhnUrmTDZQuXcc67Fg82KHrSfaeeOEq6jjuUjQ8wUnzM4Zz3dhrwSyslVz/WvnKqYkr4V/TTXPFF5EjF4rM1bHZ8bK63EfTnK41+n3n4gEFoYP4mXkNH0hntnYcdTqiE7Gn+q0BpRRxnkpBSZlA6Wa70jpW0FGqkw5e591A5/H+OV+60WAo+4Mi+NlsKrvLZ9EiVaPnoEFZlJQx1fA777AJ2MjXJ4KSsrWDWJi1lE8yPs8V6XvcC0chDTYt8456sKXAagCZyY+fzQriFMaddXyKQdG8qBqcdYjAsiIcjzaRFBBoOK9sU+sFY7N6B6+xtrlu3c37rQKkI3O2EoiJOris54EjJ5OFuumA0M6riNUuBf/MEPFBVx1JRcUEs+upEBsCnwYski7FT3TTqHrx7v5AjgFN97xhPTkmVpu6sxRnWBi1fxIRp8eWZeFM6mUcGgVk1WeVb1yhdV9hoMo2TsNEPE0tHo/wvuSJSzbZo7wibeXM9v/rRfKcx7X93rfiXVnyQ9f/5CaAQ4lxedPp/6uzLtOS4FyL0bCNeZ6L5w+AiuyWCTDFIYaUzhwfG+/YTQpWyeZCdQIKzhV+3GeXI2cxoP0ER/DlOKymf1gm+zRU3sqf1lBVQ0y+mK/Awl9bS3uaaQmI0FUyUwHUKP7PKuXnO+LcwDv4OfPT6hph8smc1EtMe5ib/apar/qZ9dyaEaElALJ1KKxnHziuvVl8atk1fINSQh7OtXDyqbPw9o/nGIpTnv5iFmwmWJLis2oyEgPkJqyx0vYI8rjkVEzKc8eQavAJBYSpjMwM193Swt+yJyjvaGYWPnqExxKiNarpB2WSO7soCAZXhS1uEYHryrK47BH6W1dRiruqT0xpLih3MXiwU3VDwAAAA==';
|
||||
|
||||
type SQLiteBindValue = string | number | null | Uint8Array;
|
||||
|
||||
/**
|
||||
@@ -106,12 +101,9 @@ class SqliteClient {
|
||||
const results = await this.executeQuery(query, [credentialId]);
|
||||
|
||||
if (results.length === 0) {
|
||||
console.log('No results found for credential:', credentialId);
|
||||
return null;
|
||||
}
|
||||
|
||||
console.log('Results found for credential:', results);
|
||||
|
||||
// Convert the first row to a Credential object
|
||||
const row = results[0] as any;
|
||||
return {
|
||||
@@ -161,9 +153,9 @@ class SqliteClient {
|
||||
WHERE c.IsDeleted = 0
|
||||
ORDER BY c.CreatedAt DESC`;
|
||||
|
||||
const results = await this.executeQuery(query);
|
||||
const results = await this.executeQuery<any>(query);
|
||||
|
||||
return results.map((row: any) => ({
|
||||
return results.map((row) => ({
|
||||
Id: row.Id,
|
||||
Username: row.Username,
|
||||
Password: row.Password,
|
||||
@@ -243,7 +235,7 @@ class SqliteClient {
|
||||
*/
|
||||
public async getPasswordSettings(): Promise<PasswordSettings> {
|
||||
const settingsJson = await this.getSetting('PasswordGenerationSettings');
|
||||
|
||||
|
||||
// Default settings if none found or parsing fails
|
||||
const defaultSettings: PasswordSettings = {
|
||||
Length: 18,
|
||||
@@ -253,7 +245,7 @@ class SqliteClient {
|
||||
UseSpecialChars: true,
|
||||
UseNonAmbiguousChars: false
|
||||
};
|
||||
|
||||
|
||||
try {
|
||||
if (settingsJson) {
|
||||
return { ...defaultSettings, ...JSON.parse(settingsJson) };
|
||||
@@ -261,7 +253,7 @@ class SqliteClient {
|
||||
} catch (error) {
|
||||
console.warn('Failed to parse password settings:', error);
|
||||
}
|
||||
|
||||
|
||||
return defaultSettings;
|
||||
}
|
||||
|
||||
@@ -445,100 +437,6 @@ class SqliteClient {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert binary data to a base64 encoded image source.
|
||||
*/
|
||||
public static imgSrcFromBytes(bytes: Uint8Array | number[] | undefined): string {
|
||||
// Handle base64 image data
|
||||
if (bytes) {
|
||||
try {
|
||||
const logoBytes = this.toUint8Array(bytes);
|
||||
const base64Logo = this.base64Encode(logoBytes);
|
||||
// Detect image type from first few bytes
|
||||
const mimeType = this.detectMimeType(logoBytes);
|
||||
return `data:${mimeType};base64,${base64Logo}`;
|
||||
} catch (error) {
|
||||
console.error('Error setting logo:', error);
|
||||
return `data:image/x-icon;base64,${placeholderBase64}`;
|
||||
}
|
||||
} else {
|
||||
return `data:image/x-icon;base64,${placeholderBase64}`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect MIME type from file signature (magic numbers)
|
||||
*/
|
||||
private static detectMimeType(bytes: Uint8Array): string {
|
||||
/**
|
||||
* Check if the file is an SVG file.
|
||||
*/
|
||||
const isSvg = () : boolean => {
|
||||
const header = new TextDecoder().decode(bytes.slice(0, 5)).toLowerCase();
|
||||
return header.includes('<?xml') || header.includes('<svg');
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if the file is an ICO file.
|
||||
*/
|
||||
const isIco = () : boolean => {
|
||||
return bytes[0] === 0x00 && bytes[1] === 0x00 && bytes[2] === 0x01 && bytes[3] === 0x00;
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if the file is an PNG file.
|
||||
*/
|
||||
const isPng = () : boolean => {
|
||||
return bytes[0] === 0x89 && bytes[1] === 0x50 && bytes[2] === 0x4E && bytes[3] === 0x47;
|
||||
};
|
||||
|
||||
if (isSvg()) {
|
||||
return 'image/svg+xml';
|
||||
}
|
||||
if (isIco()) {
|
||||
return 'image/x-icon';
|
||||
}
|
||||
if (isPng()) {
|
||||
return 'image/png';
|
||||
}
|
||||
|
||||
return 'image/x-icon';
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert various binary data formats to Uint8Array
|
||||
*/
|
||||
private static toUint8Array(buffer: Uint8Array | number[] | {[key: number]: number}): Uint8Array {
|
||||
if (buffer instanceof Uint8Array) {
|
||||
return buffer;
|
||||
}
|
||||
|
||||
if (Array.isArray(buffer)) {
|
||||
return new Uint8Array(buffer);
|
||||
}
|
||||
|
||||
const length = Object.keys(buffer).length;
|
||||
const arr = new Uint8Array(length);
|
||||
for (let i = 0; i < length; i++) {
|
||||
arr[i] = buffer[i];
|
||||
}
|
||||
|
||||
return arr;
|
||||
}
|
||||
|
||||
/**
|
||||
* Base64 encode binary data.
|
||||
*/
|
||||
private static base64Encode(buffer: Uint8Array | number[] | {[key: number]: number}): string | null {
|
||||
try {
|
||||
const arr = Array.from(this.toUint8Array(buffer));
|
||||
return btoa(arr.reduce((data, byte) => data + String.fromCharCode(byte), ''));
|
||||
} catch (error) {
|
||||
console.error('Error encoding to base64:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a table exists in the database
|
||||
* @param tableName - The name of the table to check
|
||||
|
||||
Reference in New Issue
Block a user