mirror of
https://github.com/aliasvault/aliasvault.git
synced 2026-05-24 16:32:20 -04:00
Add attachment viewer to browser extension (#808)
This commit is contained in:
committed by
Leendert de Borst
parent
25acce3ae0
commit
ccb84780eb
@@ -0,0 +1,120 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { useDb } from '@/entrypoints/popup/context/DbContext';
|
||||
|
||||
import type { Attachment } from '@/utils/dist/shared/models/vault';
|
||||
|
||||
type AttachmentBlockProps = {
|
||||
credentialId: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* This component shows attachments for a credential.
|
||||
*/
|
||||
const AttachmentBlock: React.FC<AttachmentBlockProps> = ({ credentialId }) => {
|
||||
const { t } = useTranslation();
|
||||
const [attachments, setAttachments] = useState<Attachment[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const dbContext = useDb();
|
||||
|
||||
/**
|
||||
* Downloads an attachment file.
|
||||
*/
|
||||
const downloadAttachment = (attachment: Attachment): void => {
|
||||
try {
|
||||
// Convert Uint8Array or number[] to Uint8Array
|
||||
const byteArray = attachment.Blob instanceof Uint8Array
|
||||
? attachment.Blob
|
||||
: new Uint8Array(attachment.Blob);
|
||||
|
||||
// Create blob and download
|
||||
const blob = new Blob([byteArray as BlobPart]);
|
||||
const url = URL.createObjectURL(blob);
|
||||
|
||||
// Create temporary download link
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = attachment.Filename;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
|
||||
// Cleanup
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
} catch (error) {
|
||||
console.error('Error downloading attachment:', error);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
/**
|
||||
* Loads the attachments for the credential.
|
||||
*/
|
||||
const loadAttachments = async (): Promise<void> => {
|
||||
if (!dbContext?.sqliteClient) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const attachmentList = dbContext.sqliteClient.getAttachmentsForCredential(credentialId);
|
||||
setAttachments(attachmentList);
|
||||
} catch (error) {
|
||||
console.error('Error loading attachments:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
loadAttachments();
|
||||
}, [credentialId, dbContext?.sqliteClient]);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="text-gray-500 dark:text-gray-400 mb-4">
|
||||
<h2 className="text-lg font-semibold text-gray-900 dark:text-white mb-2">{t('common.attachments')}</h2>
|
||||
{t('common.loadingAttachments')}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (attachments.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mb-4">
|
||||
<div className="space-y-2">
|
||||
<h2 className="text-lg font-semibold text-gray-900 dark:text-white">{t('common.attachments')}</h2>
|
||||
<div className="grid grid-cols-1 gap-2">
|
||||
{attachments.map(attachment => (
|
||||
<button
|
||||
key={attachment.Id}
|
||||
className="w-full text-left p-2 ps-3 pe-3 rounded bg-white dark:bg-gray-800 shadow hover:shadow-md transition-all border border-gray-200 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-700"
|
||||
onClick={() => downloadAttachment(attachment)}
|
||||
aria-label={`Download ${attachment.Filename}`}
|
||||
>
|
||||
<div className="flex justify-between items-center gap-2">
|
||||
<div className="flex items-center flex-1">
|
||||
<div className="flex flex-col">
|
||||
<h4 className="text-sm font-medium text-gray-900 dark:text-white">{attachment.Filename}</h4>
|
||||
<span className="text-xs text-gray-500 dark:text-gray-400">
|
||||
{new Date(attachment.CreatedAt).toLocaleDateString()}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<svg className="w-4 h-4 text-gray-500 dark:text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16v2a2 2 0 002 2h12a2 2 0 002-2v-2M7 10l5 5m0 0l5-5m-5 5V4" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AttachmentBlock;
|
||||
@@ -1,4 +1,5 @@
|
||||
import AliasBlock from './AliasBlock';
|
||||
import AttachmentBlock from './AttachmentBlock';
|
||||
import EmailBlock from './EmailBlock';
|
||||
import HeaderBlock from './HeaderBlock';
|
||||
import LoginCredentialsBlock from './LoginCredentialsBlock';
|
||||
@@ -11,5 +12,6 @@ export {
|
||||
TotpBlock,
|
||||
LoginCredentialsBlock,
|
||||
AliasBlock,
|
||||
NotesBlock
|
||||
NotesBlock,
|
||||
AttachmentBlock
|
||||
};
|
||||
@@ -8,7 +8,8 @@ import {
|
||||
TotpBlock,
|
||||
LoginCredentialsBlock,
|
||||
AliasBlock,
|
||||
NotesBlock
|
||||
NotesBlock,
|
||||
AttachmentBlock
|
||||
} from '@/entrypoints/popup/components/CredentialDetails';
|
||||
import HeaderButton from '@/entrypoints/popup/components/HeaderButton';
|
||||
import { HeaderIconType } from '@/entrypoints/popup/components/Icons/HeaderIcons';
|
||||
@@ -114,6 +115,7 @@ const CredentialDetails: React.FC = (): React.ReactElement => {
|
||||
<LoginCredentialsBlock credential={credential} />
|
||||
<AliasBlock credential={credential} />
|
||||
<NotesBlock notes={credential.Notes} />
|
||||
<AttachmentBlock credentialId={credential.Id} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -64,6 +64,8 @@
|
||||
"copyToClipboard": "Copy to clipboard",
|
||||
"loadingEmails": "Loading emails...",
|
||||
"loadingTotpCodes": "Loading TOTP codes...",
|
||||
"attachments": "Attachments",
|
||||
"loadingAttachments": "Loading attachments...",
|
||||
"settings": "Settings",
|
||||
"recentEmails": "Recent emails",
|
||||
"loginCredentials": "Login credentials",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import initSqlJs, { Database } from 'sql.js';
|
||||
|
||||
import type { Credential, EncryptionKey, PasswordSettings, TotpCode } from '@/utils/dist/shared/models/vault';
|
||||
import type { Attachment, Credential, EncryptionKey, PasswordSettings, TotpCode } from '@/utils/dist/shared/models/vault';
|
||||
import type { VaultVersion } from '@/utils/dist/shared/vault-sql';
|
||||
import { VaultSqlGenerator } from '@/utils/dist/shared/vault-sql';
|
||||
|
||||
@@ -641,6 +641,39 @@ export class SqliteClient {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get attachments for a specific credential
|
||||
* @param credentialId - The ID of the credential
|
||||
* @returns Array of attachments for the credential
|
||||
*/
|
||||
public getAttachmentsForCredential(credentialId: string): Attachment[] {
|
||||
if (!this.db) {
|
||||
throw new Error('Database not initialized');
|
||||
}
|
||||
|
||||
try {
|
||||
if (!this.tableExists('Attachments')) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const query = `
|
||||
SELECT
|
||||
Id,
|
||||
Filename,
|
||||
Blob,
|
||||
CredentialId,
|
||||
CreatedAt,
|
||||
UpdatedAt,
|
||||
IsDeleted
|
||||
FROM Attachments
|
||||
WHERE CredentialId = ? AND IsDeleted = 0`;
|
||||
return this.executeQuery<Attachment>(query, [credentialId]);
|
||||
} catch (error) {
|
||||
console.error('Error getting attachments:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a credential by ID
|
||||
* @param credentialId - The ID of the credential to delete
|
||||
|
||||
@@ -77,4 +77,17 @@ type Alias = {
|
||||
Email?: string;
|
||||
};
|
||||
|
||||
export type { Alias, Credential, EncryptionKey, PasswordSettings, TotpCode };
|
||||
/**
|
||||
* Attachment SQLite database type.
|
||||
*/
|
||||
type Attachment = {
|
||||
Id: string;
|
||||
Filename: string;
|
||||
Blob: Uint8Array | number[];
|
||||
CredentialId: string;
|
||||
CreatedAt: string;
|
||||
UpdatedAt: string;
|
||||
IsDeleted?: boolean;
|
||||
};
|
||||
|
||||
export type { Alias, Attachment, Credential, EncryptionKey, PasswordSettings, TotpCode };
|
||||
|
||||
Reference in New Issue
Block a user