mirror of
https://github.com/shuaiplus/nodewarden.git
synced 2026-06-20 13:00:39 +00:00
Polish public Send pages
This commit is contained in:
@@ -1,8 +1,10 @@
|
|||||||
import { useEffect, useRef, useState } from 'preact/hooks';
|
import { useEffect, useRef, useState } from 'preact/hooks';
|
||||||
import { Download, Eye, Lock } from 'lucide-preact';
|
import { Clipboard, Download, Eye, Lock } from 'lucide-preact';
|
||||||
import { accessPublicSend, accessPublicSendFile, decryptPublicSend, decryptPublicSendFileBytes } from '@/lib/api/send';
|
import { accessPublicSend, accessPublicSendFile, decryptPublicSend, decryptPublicSendFileBytes } from '@/lib/api/send';
|
||||||
|
import { copyTextToClipboard } from '@/lib/clipboard';
|
||||||
import { toBufferSource } from '@/lib/crypto';
|
import { toBufferSource } from '@/lib/crypto';
|
||||||
import { downloadBytesAsFile, readResponseBytesWithProgress } from '@/lib/download';
|
import { downloadBytesAsFile, readResponseBytesWithProgress } from '@/lib/download';
|
||||||
|
import NotFoundPage from '@/components/NotFoundPage';
|
||||||
import StandalonePageFrame from '@/components/StandalonePageFrame';
|
import StandalonePageFrame from '@/components/StandalonePageFrame';
|
||||||
import { t } from '@/lib/i18n';
|
import { t } from '@/lib/i18n';
|
||||||
|
|
||||||
@@ -27,6 +29,25 @@ interface PublicSendData {
|
|||||||
file?: PublicSendFileData | null;
|
file?: PublicSendFileData | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function decodeBase64Url(value: string): Uint8Array | null {
|
||||||
|
try {
|
||||||
|
const raw = value.replace(/-/g, '+').replace(/_/g, '/');
|
||||||
|
const padded = raw + '='.repeat((4 - (raw.length % 4)) % 4);
|
||||||
|
const decoded = atob(padded);
|
||||||
|
const out = new Uint8Array(decoded.length);
|
||||||
|
for (let i = 0; i < decoded.length; i += 1) out[i] = decoded.charCodeAt(i);
|
||||||
|
return out;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasUsableSendKey(keyPart: string | null): boolean {
|
||||||
|
if (!keyPart) return false;
|
||||||
|
const bytes = decodeBase64Url(keyPart);
|
||||||
|
return !!bytes && bytes.length >= 16;
|
||||||
|
}
|
||||||
|
|
||||||
function asRecord(value: unknown): Record<string, unknown> | null {
|
function asRecord(value: unknown): Record<string, unknown> | null {
|
||||||
return value && typeof value === 'object' ? value as Record<string, unknown> : null;
|
return value && typeof value === 'object' ? value as Record<string, unknown> : null;
|
||||||
}
|
}
|
||||||
@@ -69,6 +90,7 @@ export default function PublicSendPage(props: PublicSendPageProps) {
|
|||||||
const [password, setPassword] = useState('');
|
const [password, setPassword] = useState('');
|
||||||
const [needPassword, setNeedPassword] = useState(false);
|
const [needPassword, setNeedPassword] = useState(false);
|
||||||
const [error, setError] = useState('');
|
const [error, setError] = useState('');
|
||||||
|
const [notFound, setNotFound] = useState(false);
|
||||||
const [sendData, setSendData] = useState<PublicSendData | null>(null);
|
const [sendData, setSendData] = useState<PublicSendData | null>(null);
|
||||||
const [busy, setBusy] = useState(false);
|
const [busy, setBusy] = useState(false);
|
||||||
const [downloadPercent, setDownloadPercent] = useState<number | null>(null);
|
const [downloadPercent, setDownloadPercent] = useState<number | null>(null);
|
||||||
@@ -83,8 +105,14 @@ export default function PublicSendPage(props: PublicSendPageProps) {
|
|||||||
loadAbortRef.current = controller;
|
loadAbortRef.current = controller;
|
||||||
setBusy(true);
|
setBusy(true);
|
||||||
setError('');
|
setError('');
|
||||||
|
setNotFound(false);
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
|
if (!hasUsableSendKey(props.keyPart)) {
|
||||||
|
setNotFound(true);
|
||||||
|
setSendData(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
const data = await accessPublicSend(props.accessId, props.keyPart, pass, { signal: controller.signal });
|
const data = await accessPublicSend(props.accessId, props.keyPart, pass, { signal: controller.signal });
|
||||||
if (controller.signal.aborted || requestId !== loadRequestRef.current) return;
|
if (controller.signal.aborted || requestId !== loadRequestRef.current) return;
|
||||||
if (!props.keyPart) {
|
if (!props.keyPart) {
|
||||||
@@ -104,6 +132,10 @@ export default function PublicSendPage(props: PublicSendPageProps) {
|
|||||||
if (err.status === 401) {
|
if (err.status === 401) {
|
||||||
setNeedPassword(true);
|
setNeedPassword(true);
|
||||||
setError(t('txt_this_send_is_password_protected'));
|
setError(t('txt_this_send_is_password_protected'));
|
||||||
|
} else if (err.status === 404) {
|
||||||
|
setNeedPassword(false);
|
||||||
|
setNotFound(true);
|
||||||
|
setError('');
|
||||||
} else {
|
} else {
|
||||||
setError(err.message || t('txt_failed_to_open_send'));
|
setError(err.message || t('txt_failed_to_open_send'));
|
||||||
}
|
}
|
||||||
@@ -158,9 +190,16 @@ export default function PublicSendPage(props: PublicSendPageProps) {
|
|||||||
};
|
};
|
||||||
}, [props.accessId, props.keyPart]);
|
}, [props.accessId, props.keyPart]);
|
||||||
|
|
||||||
|
if (!loading && notFound) {
|
||||||
|
return <NotFoundPage title={t('txt_page_not_found')} message={t('txt_send_unavailable')} />;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="auth-page public-send-page">
|
<div className="auth-page public-send-page">
|
||||||
<StandalonePageFrame title={t('txt_nodewarden_send')}>
|
<StandalonePageFrame
|
||||||
|
title={sendData ? (sendData.decName || t('txt_no_name')) : t('txt_nodewarden_send')}
|
||||||
|
eyebrow={sendData ? t('txt_nodewarden_send') : undefined}
|
||||||
|
>
|
||||||
{loading && <p className="muted">{t('txt_loading')}</p>}
|
{loading && <p className="muted">{t('txt_loading')}</p>}
|
||||||
|
|
||||||
{!loading && needPassword && (
|
{!loading && needPassword && (
|
||||||
@@ -190,9 +229,20 @@ export default function PublicSendPage(props: PublicSendPageProps) {
|
|||||||
|
|
||||||
{!loading && sendData && (
|
{!loading && sendData && (
|
||||||
<>
|
<>
|
||||||
<h2 className="public-send-title">{sendData.decName || t('txt_no_name')}</h2>
|
|
||||||
{sendData.type === 0 ? (
|
{sendData.type === 0 ? (
|
||||||
<div className="card public-send-card">
|
<div className="card public-send-card">
|
||||||
|
<div className="public-send-card-head">
|
||||||
|
<span>{t('txt_text_send')}</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-secondary small public-send-copy-btn"
|
||||||
|
disabled={!sendData.decText}
|
||||||
|
onClick={() => void copyTextToClipboard(sendData.decText || '')}
|
||||||
|
>
|
||||||
|
<Clipboard size={14} className="btn-icon" />
|
||||||
|
{t('txt_copy')}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<div className="notes">{sendData.decText || ''}</div>
|
<div className="notes">{sendData.decText || ''}</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { APP_VERSION } from '@shared/app-version';
|
|||||||
|
|
||||||
interface StandalonePageFrameProps {
|
interface StandalonePageFrameProps {
|
||||||
title: string;
|
title: string;
|
||||||
|
eyebrow?: ComponentChildren;
|
||||||
children: ComponentChildren;
|
children: ComponentChildren;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -17,6 +18,7 @@ export default function StandalonePageFrame(props: StandalonePageFrameProps) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="auth-card">
|
<div className="auth-card">
|
||||||
|
{props.eyebrow && <div className="standalone-eyebrow">{props.eyebrow}</div>}
|
||||||
<h1 className="standalone-title">{props.title}</h1>
|
<h1 className="standalone-title">{props.title}</h1>
|
||||||
{props.children}
|
{props.children}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user