feat: implement direct file upload for sends with JWT token validation

- Added `processSendFileUpload` function to handle file uploads for sends.
- Integrated JWT token creation and verification for secure file uploads.
- Updated `handleCreateFileSendV2` and `handleGetSendFileUpload` to use new upload URL generation.
- Refactored upload handling in `handleUploadSendFile` and `handlePublicUploadSendFile` to utilize the new upload process.
- Introduced `uploadDirectEncryptedPayload` for handling direct uploads with progress tracking.
- Enhanced API routes to support both POST and PUT methods for attachment uploads.
- Added localization strings for upload progress messages.
- Created utility functions for direct upload URL building and payload parsing.
This commit is contained in:
shuaiplus
2026-03-18 02:26:10 +08:00
parent 3204eeb9ab
commit bb3fe41330
17 changed files with 666 additions and 127 deletions
+1 -1
View File
@@ -11,7 +11,7 @@ interface AdminPageProps {
onRefresh: () => void;
onCreateInvite: (hours: number) => Promise<void>;
onDeleteAllInvites: () => Promise<void>;
onToggleUserStatus: (userId: string, currentStatus: string) => Promise<void>;
onToggleUserStatus: (userId: string, currentStatus: 'active' | 'banned') => Promise<void>;
onDeleteUser: (userId: string) => Promise<void>;
onRevokeInvite: (code: string) => Promise<void>;
}
+8
View File
@@ -67,11 +67,15 @@ export interface AppMainRoutesProps {
onDownloadVaultAttachment: (cipher: Cipher, attachmentId: string) => Promise<void>;
downloadingAttachmentKey: string;
attachmentDownloadPercent: number | null;
uploadingAttachmentName: string;
attachmentUploadPercent: number | null;
onRefreshVault: () => Promise<void>;
onCreateSend: (draft: SendDraft, autoCopyLink: boolean) => Promise<void>;
onUpdateSend: (send: Send, draft: SendDraft, autoCopyLink: boolean) => Promise<void>;
onDeleteSend: (send: Send) => Promise<void>;
onBulkDeleteSends: (ids: string[]) => Promise<void>;
uploadingSendFileName: string;
sendUploadPercent: number | null;
onChangePassword: (currentPassword: string, nextPassword: string, nextPassword2: string) => Promise<void>;
onEnableTotp: (secret: string, token: string) => Promise<void>;
onOpenDisableTotp: () => void;
@@ -139,6 +143,8 @@ export default function AppMainRoutes(props: AppMainRoutesProps) {
onUpdate={props.onUpdateSend}
onDelete={props.onDeleteSend}
onBulkDelete={props.onBulkDeleteSends}
uploadingSendFileName={props.uploadingSendFileName}
sendUploadPercent={props.sendUploadPercent}
onNotify={props.onNotify}
/>
</Suspense>
@@ -171,6 +177,8 @@ export default function AppMainRoutes(props: AppMainRoutesProps) {
onDownloadAttachment={props.onDownloadVaultAttachment}
downloadingAttachmentKey={props.downloadingAttachmentKey}
attachmentDownloadPercent={props.attachmentDownloadPercent}
uploadingAttachmentName={props.uploadingAttachmentName}
attachmentUploadPercent={props.attachmentUploadPercent}
/>
</Suspense>
</Route>
+10
View File
@@ -12,6 +12,8 @@ interface SendsPageProps {
onUpdate: (send: Send, draft: SendDraft, autoCopyLink: boolean) => Promise<void>;
onDelete: (send: Send) => Promise<void>;
onBulkDelete: (ids: string[]) => Promise<void>;
uploadingSendFileName: string;
sendUploadPercent: number | null;
onNotify: (type: 'success' | 'error', text: string) => void;
}
@@ -79,6 +81,13 @@ export default function SendsPage(props: SendsPageProps) {
return false;
}
});
const sendUploadLabel =
props.sendUploadPercent == null
? t('txt_uploading_file_named', { name: props.uploadingSendFileName || t('txt_file') })
: t('txt_uploading_file_named_percent', {
name: props.uploadingSendFileName || t('txt_file'),
percent: props.sendUploadPercent,
});
useEffect(() => {
if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') return;
@@ -370,6 +379,7 @@ export default function SendsPage(props: SendsPageProps) {
{isEditing && draft && (
<div className="card">
<h3 className="detail-title">{isCreating ? t('txt_new_send') : t('txt_edit_send')}</h3>
{!!props.uploadingSendFileName && <div className="detail-sub">{sendUploadLabel}</div>}
<div className="field-grid">
<label className="field field-span-2">
<span>{t('txt_name')}</span>
+4
View File
@@ -48,6 +48,8 @@ interface VaultPageProps {
onDownloadAttachment: (cipher: Cipher, attachmentId: string) => Promise<void>;
downloadingAttachmentKey: string;
attachmentDownloadPercent: number | null;
uploadingAttachmentName: string;
attachmentUploadPercent: number | null;
}
@@ -821,6 +823,8 @@ function folderName(id: string | null | undefined): string {
onDownloadAttachment={(cipher, attachmentId) => void props.onDownloadAttachment(cipher, attachmentId)}
downloadingAttachmentKey={props.downloadingAttachmentKey}
attachmentDownloadPercent={props.attachmentDownloadPercent}
uploadingAttachmentName={props.uploadingAttachmentName}
attachmentUploadPercent={props.attachmentUploadPercent}
onPatchDraftCustomField={patchDraftCustomField}
onUpdateDraftCustomFields={updateDraftCustomFields}
onOpenFieldModal={() => setFieldModalOpen(true)}
@@ -18,6 +18,8 @@ interface VaultEditorProps {
localError: string;
downloadingAttachmentKey: string;
attachmentDownloadPercent: number | null;
uploadingAttachmentName: string;
attachmentUploadPercent: number | null;
onUpdateDraft: (patch: Partial<VaultDraft>) => void;
onSeedSshDefaults: (force?: boolean) => void;
onUpdateSshPublicKey: (value: string) => void;
@@ -42,6 +44,13 @@ export default function VaultEditor(props: VaultEditorProps) {
? t('txt_downloading')
: t('txt_downloading_percent', { percent: props.attachmentDownloadPercent });
};
const uploadLabel =
props.attachmentUploadPercent == null
? t('txt_uploading_attachment_named', { name: props.uploadingAttachmentName || t('txt_attachment') })
: t('txt_uploading_attachment_named_percent', {
name: props.uploadingAttachmentName || t('txt_attachment'),
percent: props.attachmentUploadPercent,
});
return (
<>
@@ -220,6 +229,7 @@ export default function VaultEditor(props: VaultEditorProps) {
<Plus size={14} className="btn-icon" />
</button>
</div>
{!!props.uploadingAttachmentName && <div className="detail-sub">{uploadLabel}</div>}
{!props.isCreating && props.selectedCipher && props.editExistingAttachments.length > 0 && (
<div className="attachment-list">
{props.editExistingAttachments.map((attachment) => {