From 76ca21e14f8e6918e4e3a0d8b75906cb7ffb76d7 Mon Sep 17 00:00:00 2001 From: cyph3rasi Date: Sat, 7 Mar 2026 18:29:18 -0800 Subject: [PATCH] Prompt for upload re-auth on demand --- src/app/admin/page.tsx | 122 ++++++++++++--- src/components/StorageSessionPrompt.tsx | 97 ++++++++++++ src/components/UserStorageImageUpload.tsx | 173 ++++++++++++++-------- src/lib/storage/client.ts | 13 ++ 4 files changed, 328 insertions(+), 77 deletions(-) create mode 100644 src/components/StorageSessionPrompt.tsx create mode 100644 src/lib/storage/client.ts diff --git a/src/app/admin/page.tsx b/src/app/admin/page.tsx index a1f9600..5845e27 100644 --- a/src/app/admin/page.tsx +++ b/src/app/admin/page.tsx @@ -3,8 +3,10 @@ import { useEffect, useState } from 'react'; import Link from 'next/link'; import AutoTextarea from '@/components/AutoTextarea'; +import { StorageSessionPrompt } from '@/components/StorageSessionPrompt'; import { useToast } from '@/lib/contexts/ToastContext'; import { useAccentColor } from '@/lib/contexts/AccentColorContext'; +import { refreshStorageSession } from '@/lib/storage/client'; export default function AdminPage() { const { showToast } = useToast(); @@ -27,6 +29,11 @@ export default function AdminPage() { const [savingSettings, setSavingSettings] = useState(false); const [isUploadingBanner, setIsUploadingBanner] = useState(false); const [bannerUploadError, setBannerUploadError] = useState(null); + const [showBannerSessionPrompt, setShowBannerSessionPrompt] = useState(false); + const [pendingBannerFile, setPendingBannerFile] = useState(null); + const [bannerPassword, setBannerPassword] = useState(''); + const [bannerPromptError, setBannerPromptError] = useState(''); + const [isRefreshingBannerSession, setIsRefreshingBannerSession] = useState(false); const [isUploadingLogo, setIsUploadingLogo] = useState(false); const [logoUploadError, setLogoUploadError] = useState(null); const [isUploadingFavicon, setIsUploadingFavicon] = useState(false); @@ -92,11 +99,7 @@ export default function AdminPage() { } }; - const handleBannerUpload = async (event: React.ChangeEvent) => { - const file = event.target.files?.[0]; - event.target.value = ''; - if (!file) return; - + const uploadBannerFile = async (file: File, allowPrompt = true) => { setBannerUploadError(null); setIsUploadingBanner(true); @@ -110,6 +113,13 @@ export default function AdminPage() { const data = await res.json(); if (!res.ok || !data.url) { + if (res.status === 401 && allowPrompt) { + setPendingBannerFile(file); + setBannerPromptError(''); + setShowBannerSessionPrompt(true); + return; + } + throw new Error(data.error || 'Upload failed'); } @@ -119,6 +129,10 @@ export default function AdminPage() { }; setNodeSettings(nextSettings); await handleSaveSettings(nextSettings); + setPendingBannerFile(null); + setShowBannerSessionPrompt(false); + setBannerPassword(''); + setBannerPromptError(''); } catch (error) { console.error('Banner upload failed', error); setBannerUploadError(error instanceof Error ? error.message : 'Upload failed. Please try again.'); @@ -127,6 +141,47 @@ export default function AdminPage() { } }; + const handleBannerUpload = async (event: React.ChangeEvent) => { + const file = event.target.files?.[0]; + event.target.value = ''; + if (!file) return; + + await uploadBannerFile(file); + }; + + const handleBannerSessionSubmit = async (event: React.FormEvent) => { + event.preventDefault(); + + if (!pendingBannerFile) { + setShowBannerSessionPrompt(false); + return; + } + + if (!bannerPassword.trim()) { + setBannerPromptError('Please enter your password'); + return; + } + + setIsRefreshingBannerSession(true); + setBannerPromptError(''); + + try { + await refreshStorageSession(bannerPassword.trim()); + await uploadBannerFile(pendingBannerFile, false); + } catch (error) { + setBannerPromptError(error instanceof Error ? error.message : 'Failed to confirm password'); + } finally { + setIsRefreshingBannerSession(false); + } + }; + + const handleBannerSessionCancel = () => { + setShowBannerSessionPrompt(false); + setPendingBannerFile(null); + setBannerPassword(''); + setBannerPromptError(''); + }; + const handleLogoUpload = async (event: React.ChangeEvent) => { const file = event.target.files?.[0]; event.target.value = ''; @@ -382,15 +437,34 @@ export default function AdminPage() { )} {nodeSettings.bannerUrl && ( -
-
-
+
+
+ Sidebar preview +
+
+
+
+

+ Welcome to {nodeSettings.name || 'Your Node'} +

+

+ {nodeSettings.description || 'A brief tagline for your node.'} +

+
+
)}
@@ -530,9 +604,23 @@ export default function AdminPage() { -
-
- )} +
+ + )} + + { + setBannerPassword(nextPassword); + setBannerPromptError(''); + }} + onSubmit={handleBannerSessionSubmit} + onCancel={handleBannerSessionCancel} + /> ); } diff --git a/src/components/StorageSessionPrompt.tsx b/src/components/StorageSessionPrompt.tsx new file mode 100644 index 0000000..7335a17 --- /dev/null +++ b/src/components/StorageSessionPrompt.tsx @@ -0,0 +1,97 @@ +'use client'; + +interface StorageSessionPromptProps { + open: boolean; + isSubmitting: boolean; + password: string; + error: string; + title?: string; + description?: string; + onPasswordChange: (password: string) => void; + onSubmit: (event: React.FormEvent) => void; + onCancel: () => void; +} + +export function StorageSessionPrompt({ + open, + isSubmitting, + password, + error, + title = 'Confirm your password', + description = 'Please confirm your password to continue uploading to your storage.', + onPasswordChange, + onSubmit, + onCancel, +}: StorageSessionPromptProps) { + if (!open) { + return null; + } + + return ( +
+
event.stopPropagation()} + > +

+ {title} +

+

+ {description} +

+ +
+
+ + onPasswordChange(event.target.value)} + autoFocus + /> +
+ + {error && ( +
+ {error} +
+ )} + +
+ + +
+
+
+
+ ); +} diff --git a/src/components/UserStorageImageUpload.tsx b/src/components/UserStorageImageUpload.tsx index b70295a..99a678a 100644 --- a/src/components/UserStorageImageUpload.tsx +++ b/src/components/UserStorageImageUpload.tsx @@ -1,6 +1,8 @@ 'use client'; import { useRef, useState } from 'react'; +import { StorageSessionPrompt } from '@/components/StorageSessionPrompt'; +import { refreshStorageSession } from '@/lib/storage/client'; interface UserStorageImageUploadProps { label: string; @@ -25,6 +27,11 @@ export function UserStorageImageUpload({ }: UserStorageImageUploadProps) { const inputRef = useRef(null); const [isUploading, setIsUploading] = useState(false); + const [showSessionPrompt, setShowSessionPrompt] = useState(false); + const [pendingFile, setPendingFile] = useState(null); + const [password, setPassword] = useState(''); + const [promptError, setPromptError] = useState(''); + const [isRefreshingSession, setIsRefreshingSession] = useState(false); const resetFileInput = () => { if (inputRef.current) { @@ -32,7 +39,7 @@ export function UserStorageImageUpload({ } }; - const uploadFile = async (file: File) => { + const uploadFile = async (file: File, allowPrompt = true) => { setIsUploading(true); try { @@ -48,8 +55,11 @@ export function UserStorageImageUpload({ if (!res.ok || !data.url) { const message = data.error || 'Upload failed'; - if (res.status === 401) { - throw new Error(message || 'Upload session expired. Please sign in again.'); + if (res.status === 401 && allowPrompt) { + setPendingFile(file); + setPromptError(''); + setShowSessionPrompt(true); + return; } throw new Error(message); @@ -57,6 +67,10 @@ export function UserStorageImageUpload({ onChange(data.media?.url || data.url); onError?.(''); + setPendingFile(null); + setShowSessionPrompt(false); + setPassword(''); + setPromptError(''); } catch (error) { onError?.(error instanceof Error ? error.message : 'Upload failed'); } finally { @@ -71,70 +85,109 @@ export function UserStorageImageUpload({ await uploadFile(file); }; + const handlePromptSubmit = async (event: React.FormEvent) => { + event.preventDefault(); + + if (!pendingFile) { + setShowSessionPrompt(false); + return; + } + + if (!password.trim()) { + setPromptError('Please enter your password'); + return; + } + + setIsRefreshingSession(true); + setPromptError(''); + + try { + await refreshStorageSession(password.trim()); + await uploadFile(pendingFile, false); + } catch (error) { + setPromptError(error instanceof Error ? error.message : 'Failed to confirm password'); + } finally { + setIsRefreshingSession(false); + } + }; + + const handlePromptCancel = () => { + setShowSessionPrompt(false); + setPendingFile(null); + setPassword(''); + setPromptError(''); + resetFileInput(); + }; + return ( -
- -
-
- {helperText && ( -

- {helperText} -

- )} - -

{ + setPassword(nextPassword); + setPromptError(''); }} - > - If uploads stop working after a long session, sign in again to refresh your upload access. -

-
+ onSubmit={handlePromptSubmit} + onCancel={handlePromptCancel} + /> + ); } diff --git a/src/lib/storage/client.ts b/src/lib/storage/client.ts new file mode 100644 index 0000000..256d21a --- /dev/null +++ b/src/lib/storage/client.ts @@ -0,0 +1,13 @@ +export async function refreshStorageSession(password: string): Promise { + const res = await fetch('/api/storage/session', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ password }), + }); + + const data = await res.json().catch(() => ({})); + + if (!res.ok) { + throw new Error(data.error || 'Failed to confirm password'); + } +}